Skip to content

Commit 0752669

Browse files
authored
re-add module recovery logic (#6846)
Added back in order to only enable live by default for PRO users to ease server load
1 parent 6d402b9 commit 0752669

File tree

11 files changed

+266
-0
lines changed

11 files changed

+266
-0
lines changed

packages/app/src/app/overmind/actions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export const onInitializeOvermind = async (
3434

3535
effects.flows.initialize(overmindInstance.reaction);
3636

37+
// We consider recover mode something to be done when browser actually crashes, meaning there is no unmount
38+
effects.browser.onUnload(() => {
39+
if (state.editor.currentSandbox && state.connected) {
40+
effects.moduleRecover.clearSandbox(state.editor.currentSandbox.id);
41+
}
42+
});
43+
3744
effects.api.initialize({
3845
getParsedConfigurations() {
3946
return state.editor.parsedConfigurations;

packages/app/src/app/overmind/effects/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { default as browser } from './browser';
55
export { default as connection } from './connection';
66
export { default as jsZip } from './jsZip';
77
export { default as live } from './live';
8+
export { default as moduleRecover } from './moduleRecover';
89
export { default as notifications } from './notifications';
910
export { default as router } from './router';
1011
export { default as settingsStore } from './settingsStore';
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Module } from '@codesandbox/common/lib/types';
2+
3+
const getKey = (id: string, moduleShortid: string) =>
4+
`recover:${id}:${moduleShortid}:code`;
5+
6+
export type RecoverData = {
7+
code: string;
8+
version: number;
9+
timestamp: number;
10+
sandboxId: string;
11+
};
12+
13+
export default {
14+
save(sandboxId: string, version: number, module: Module) {
15+
try {
16+
localStorage.setItem(
17+
getKey(sandboxId, module.shortid),
18+
JSON.stringify({
19+
code: module.code,
20+
version,
21+
timestamp: new Date().getTime(),
22+
sandboxId,
23+
})
24+
);
25+
} catch (e) {
26+
// Too bad
27+
}
28+
},
29+
30+
get(sandboxId: string, moduleShortid: string): RecoverData | null {
31+
return JSON.parse(
32+
localStorage.getItem(getKey(sandboxId, moduleShortid)) || 'null'
33+
);
34+
},
35+
36+
remove(sandboxId: string, module: Module) {
37+
try {
38+
const recoverData = this.get(sandboxId, module.shortid);
39+
if (recoverData && recoverData.code === module.code) {
40+
localStorage.removeItem(getKey(sandboxId, module.shortid));
41+
}
42+
} catch (e) {
43+
// Too bad
44+
}
45+
},
46+
47+
clearSandbox(sandboxId: string) {
48+
try {
49+
Object.keys(localStorage)
50+
.filter(key => key.startsWith(`recover:${sandboxId}`))
51+
.forEach(key => {
52+
localStorage.removeItem(key);
53+
});
54+
} catch (e) {
55+
// Too bad
56+
}
57+
},
58+
59+
getRecoverList(sandboxId: string, modules: Module[]) {
60+
const localKeys = Object.keys(localStorage).filter(key =>
61+
key.startsWith(`recover:${sandboxId}`)
62+
);
63+
64+
return modules
65+
.filter(m => localKeys.includes(getKey(sandboxId, m.shortid)))
66+
.map(module => {
67+
const key = getKey(sandboxId, module.shortid);
68+
69+
try {
70+
const recoverData: RecoverData = JSON.parse(
71+
localStorage.getItem(key) || 'null'
72+
);
73+
74+
if (recoverData) {
75+
return { recoverData, module };
76+
}
77+
} catch (e) {
78+
// Too bad
79+
}
80+
81+
return null;
82+
})
83+
.filter(Boolean) as Array<{ recoverData: RecoverData; module: Module }>;
84+
},
85+
};

packages/app/src/app/overmind/namespaces/editor/actions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,15 @@ export const sandboxChanged = withLoadApp<{
352352

353353
await actions.editor.internal.initializeSandbox(sandbox);
354354

355+
// We only recover files at this point if we are not live. When live we recover them
356+
// when the module_state is received
357+
if (
358+
!state.live.isLive &&
359+
hasPermission(sandbox.authorization, 'write_code')
360+
) {
361+
actions.files.internal.recoverFiles();
362+
}
363+
355364
if (state.editor.currentModule.id) {
356365
effects.vscode.openModule(state.editor.currentModule);
357366
} else {
@@ -657,6 +666,7 @@ export const saveClicked = withOwnedSandbox(
657666
state.editor.modulesByPath,
658667
module
659668
);
669+
effects.moduleRecover.remove(sandbox.id, module);
660670
} else {
661671
// We might not have the module, as it was created by the server. In
662672
// this case we put it in. There is an edge case here where the user

packages/app/src/app/overmind/namespaces/editor/internalActions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export const saveCode = async (
187187

188188
if (savedCode === null) {
189189
// If the savedCode is also module.code
190+
effects.moduleRecover.remove(sandbox.id, module);
190191
effects.vscode.syncModule(module);
191192
}
192193

@@ -402,6 +403,8 @@ export const updateModuleCode = (
402403
}
403404

404405
module.code = code;
406+
// Save the code to localStorage so we can recover in case of a crash
407+
effects.moduleRecover.save(currentSandbox.id, currentSandbox.version, module);
405408
};
406409

407410
export const forkSandbox = async (

packages/app/src/app/overmind/namespaces/editor/state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@codesandbox/common/lib/types';
2121
import { getSandboxOptions } from '@codesandbox/common/lib/url';
2222
import { CollaboratorFragment, InvitationFragment } from 'app/graphql/types';
23+
import { RecoverData } from 'app/overmind/effects/moduleRecover';
2324
import immer from 'immer';
2425
import { derived } from 'overmind';
2526

@@ -85,6 +86,7 @@ type State = {
8586
currentDevToolsPosition: DevToolsTabPosition;
8687
sessionFrozen: boolean;
8788
hasLoadedInitialModule: boolean;
89+
recoveredFiles: Array<{ recoverData: RecoverData; module: Module }>;
8890
};
8991

9092
export const state: State = {
@@ -125,6 +127,7 @@ export const state: State = {
125127
quickActionsOpen: false,
126128
previewWindowVisible: true,
127129
statusBar: true,
130+
recoveredFiles: [],
128131
previewWindowOrientation:
129132
window.innerHeight / window.innerWidth > 0.9
130133
? WindowOrientation.HORIZONTAL

packages/app/src/app/overmind/namespaces/files/actions.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Directory, Module, UploadFile } from '@codesandbox/common/lib/types';
88
import { getTextOperation } from '@codesandbox/common/lib/utils/diff';
99
import { hasPermission } from '@codesandbox/common/lib/utils/permission';
1010
import { Context } from 'app/overmind';
11+
import { RecoverData } from 'app/overmind/effects/moduleRecover';
1112
import { withOwnedSandbox } from 'app/overmind/factories';
1213
import { createOptimisticModule } from 'app/overmind/utils/common';
1314
import { INormalizedModules } from 'codesandbox-import-util-types';
@@ -21,6 +22,64 @@ import * as internalActions from './internalActions';
2122

2223
export const internal = internalActions;
2324

25+
export const applyRecover = (
26+
{ state, effects, actions }: Context,
27+
recoveredList: Array<{
28+
module: Module;
29+
recoverData: RecoverData;
30+
}>
31+
) => {
32+
if (!state.editor.currentSandbox) {
33+
return;
34+
}
35+
36+
effects.moduleRecover.clearSandbox(state.editor.currentSandbox.id);
37+
recoveredList.forEach(({ recoverData, module }) => {
38+
actions.editor.codeChanged({
39+
moduleShortid: module.shortid,
40+
code: recoverData.code,
41+
});
42+
effects.vscode.setModuleCode(module);
43+
});
44+
45+
effects.analytics.track('Files Recovered', {
46+
fileCount: recoveredList.length,
47+
});
48+
};
49+
50+
export const createRecoverDiffs = (
51+
{ state, effects, actions }: Context,
52+
recoveredList: Array<{
53+
module: Module;
54+
recoverData: RecoverData;
55+
}>
56+
) => {
57+
const sandbox = state.editor.currentSandbox;
58+
if (!sandbox) {
59+
return;
60+
}
61+
effects.moduleRecover.clearSandbox(sandbox.id);
62+
recoveredList.forEach(({ recoverData, module }) => {
63+
const oldCode = module.code;
64+
actions.editor.codeChanged({
65+
moduleShortid: module.shortid,
66+
code: recoverData.code,
67+
});
68+
effects.vscode.openDiff(sandbox.id, module, oldCode);
69+
});
70+
71+
effects.analytics.track('Files Recovered', {
72+
fileCount: recoveredList.length,
73+
});
74+
};
75+
76+
export const discardRecover = ({ effects, state }: Context) => {
77+
if (!state.editor.currentSandbox) {
78+
return;
79+
}
80+
effects.moduleRecover.clearSandbox(state.editor.currentSandbox.id);
81+
};
82+
2483
export const moduleRenamed = withOwnedSandbox(
2584
async (
2685
{ state, actions, effects }: Context,

packages/app/src/app/overmind/namespaces/files/internalActions.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,37 @@ function b64DecodeUnicode(file: string) {
1616
);
1717
}
1818

19+
export const recoverFiles = ({ effects, state }: Context) => {
20+
const sandbox = state.editor.currentSandbox;
21+
22+
if (!sandbox) {
23+
return;
24+
}
25+
26+
const recoverList = effects.moduleRecover.getRecoverList(
27+
sandbox.id,
28+
sandbox.modules
29+
);
30+
31+
const recoveredList = recoverList.reduce((aggr, item) => {
32+
if (!item) {
33+
return aggr;
34+
}
35+
const { recoverData, module } = item;
36+
37+
if (module.code !== recoverData.code) {
38+
return aggr.concat(item);
39+
}
40+
41+
return aggr;
42+
}, [] as typeof recoverList);
43+
44+
if (recoveredList.length > 0) {
45+
state.editor.recoveredFiles = recoveredList;
46+
state.currentModal = 'recoveredFiles';
47+
}
48+
};
49+
1950
export const uploadFiles = async (
2051
{ effects }: Context,
2152
{

packages/app/src/app/overmind/namespaces/live/internalActions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ export const initializeModuleState = (
191191
moduleInfo,
192192
});
193193
});
194+
// TODO: enable once we know exactly when we want to recover
195+
// actions.files.internal.recoverFiles();
194196
actions.editor.internal.updatePreviewCode();
195197
};
196198

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, { FunctionComponent } from 'react';
2+
3+
import { Button, Stack } from '@codesandbox/components';
4+
import { useAppState, useActions } from 'app/overmind';
5+
import css from '@styled-system/css';
6+
import { Alert } from '../Common/Alert';
7+
8+
export const RecoverFilesModal: FunctionComponent = () => {
9+
const { files, modalClosed } = useActions();
10+
const { recoveredFiles } = useAppState().editor;
11+
12+
return (
13+
<Alert
14+
title="Recovered Files"
15+
description={`We recovered ${recoveredFiles.length} unsaved ${
16+
recoveredFiles.length > 1 ? 'files' : 'file'
17+
} from a previous session, what do you want to do?`}
18+
>
19+
<Stack justify="flex-end" gap={2}>
20+
<Button
21+
variant="secondary"
22+
css={css({
23+
width: 'auto',
24+
})}
25+
onClick={() => {
26+
files.applyRecover(recoveredFiles);
27+
modalClosed();
28+
}}
29+
>
30+
Apply Changes
31+
</Button>
32+
<Button
33+
variant="secondary"
34+
onClick={() => {
35+
files.createRecoverDiffs(recoveredFiles);
36+
modalClosed();
37+
}}
38+
type="submit"
39+
css={css({
40+
width: 'auto',
41+
})}
42+
>
43+
Compare
44+
</Button>
45+
<Button
46+
onClick={() => {
47+
files.discardRecover();
48+
modalClosed();
49+
}}
50+
type="submit"
51+
css={css({
52+
width: 'auto',
53+
})}
54+
>
55+
Discard
56+
</Button>
57+
</Stack>
58+
</Alert>
59+
);
60+
};

0 commit comments

Comments
 (0)