Skip to content

Commit bdc6acd

Browse files
Module recover fixes (codesandbox#3716)
* a bit further but do not know why it is not working * hmf * better, but out of sync when saving * fixed * fix double message and want second button on toast * infinite and better text * allow multiple primary and secondary on toast * apply all as thrid choirce * fix type issues * update from comments * fix unique recover * fix recover only to run on crash * remove clearing recover * bring back module recover * fix typing * now only does it when connected, meaning offline mode also gives reciver * Fix resetting code on save * Fix reopening a room * Nullcheck * Undo a file that was not changed Co-authored-by: Ives van Hoorne <[email protected]>
1 parent 8f682ca commit bdc6acd

File tree

13 files changed

+292
-134
lines changed

13 files changed

+292
-134
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default {
3030
return confirm(message); // eslint-disable-line no-alert,no-restricted-globals
3131
},
3232
onUnload(cb) {
33-
window.onbeforeunload = cb;
33+
window.addEventListener('beforeunload', cb);
3434
},
3535
openWindow(url) {
3636
window.open(url, '_blank');

packages/app/src/app/overmind/effects/live/clients.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,24 +155,17 @@ export class CodeSandboxOTClient extends OTClient {
155155
export default (
156156
sendOperation: SendOperation,
157157
applyOperation: ApplyOperation
158-
): {
159-
getAll(): CodeSandboxOTClient[];
160-
get(
161-
moduleShortid: string,
162-
revision?: number,
163-
force?: boolean
164-
): CodeSandboxOTClient;
165-
create(moduleShortid: string, revision: number): CodeSandboxOTClient;
166-
clear(): void;
167-
reset(moduleShortid: string, revision: number): void;
168-
} => {
158+
) => {
169159
const modules = new Map<string, CodeSandboxOTClient>();
170160

171161
return {
172162
getAll() {
173163
return Array.from(modules.values());
174164
},
175-
get(moduleShortid, revision = 0, force = false) {
165+
has(moduleShortid: string) {
166+
return modules.has(moduleShortid);
167+
},
168+
get(moduleShortid: string, revision = 0, force = false) {
176169
let client = modules.get(moduleShortid);
177170

178171
if (!client || force) {
@@ -181,7 +174,7 @@ export default (
181174

182175
return client!;
183176
},
184-
create(moduleShortid, initialRevision) {
177+
create(moduleShortid: string, initialRevision: number) {
185178
const client = new CodeSandboxOTClient(
186179
initialRevision || 0,
187180
moduleShortid,
@@ -195,7 +188,7 @@ export default (
195188

196189
return client;
197190
},
198-
reset(moduleShortid, revision) {
191+
reset(moduleShortid: string, revision: number) {
199192
modules.delete(moduleShortid);
200193
this.create(moduleShortid, revision);
201194
},

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { SerializedTextOperation, TextOperation } from 'ot';
1717
import { Channel, Presence, Socket } from 'phoenix';
1818
import uuid from 'uuid';
1919

20+
import { IModuleStateModule } from 'app/overmind/namespaces/live/types';
2021
import { OPTIMISTIC_ID_PREFIX } from '../utils';
2122
import clientsFactory from './clients';
2223

@@ -39,6 +40,13 @@ type JoinChannelResponse = {
3940
liveUserId: string;
4041
reconnectToken: string;
4142
roomInfo: RoomInfo;
43+
moduleState: {
44+
[moduleId: string]: IModuleStateModule;
45+
};
46+
};
47+
48+
type JoinChannelErrorResponse = {
49+
reason: 'room not found' | string;
4250
};
4351

4452
declare global {
@@ -229,7 +237,10 @@ class Live {
229237
});
230238
}
231239

232-
joinChannel(roomId: string): Promise<JoinChannelResponse> {
240+
joinChannel(
241+
roomId: string,
242+
onError: (reason: string) => void
243+
): Promise<JoinChannelResponse> {
233244
return new Promise((resolve, reject) => {
234245
this.channel = this.getSocket().channel(`live:${roomId}`, { version: 2 });
235246

@@ -257,6 +268,7 @@ class Live {
257268
.join()
258269
.receive('ok', resp => {
259270
const result = camelizeKeys(resp) as JoinChannelResponse;
271+
result.moduleState = resp.module_state; // Don't camelize this!!
260272

261273
// We rewrite what our reconnect params are by adding the reconnect token.
262274
// This token makes sure that you can retain state between reconnects and restarts
@@ -269,7 +281,15 @@ class Live {
269281

270282
resolve(result);
271283
})
272-
.receive('error', resp => reject(camelizeKeys(resp)));
284+
.receive('error', (resp: JoinChannelErrorResponse) => {
285+
if (resp.reason === 'room not found') {
286+
if (this.channel) {
287+
this.channel.leave();
288+
}
289+
onError(resp.reason);
290+
}
291+
reject(camelizeKeys(resp));
292+
});
273293
});
274294
}
275295

@@ -517,6 +537,10 @@ class Live {
517537
this.clients.reset(moduleShortid, revision);
518538
}
519539

540+
hasClient(moduleShortid: string) {
541+
return this.clients.has(moduleShortid);
542+
}
543+
520544
getAllClients() {
521545
return this.clients.getAll();
522546
}
Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,53 @@
11
import { Module } from '@codesandbox/common/lib/types';
22

3-
const getKey = (id, moduleShortid) => `recover:${id}:${moduleShortid}:code`;
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+
};
412

513
export default {
6-
save(
7-
currentId: string,
8-
version: number,
9-
module: Module,
10-
code: string,
11-
savedCode: string | null
12-
) {
14+
save(sandboxId: string, version: number, module: Module) {
1315
try {
1416
localStorage.setItem(
15-
getKey(currentId, module.shortid),
17+
getKey(sandboxId, module.shortid),
1618
JSON.stringify({
17-
code,
18-
savedCode,
19+
code: module.code,
1920
version,
2021
timestamp: new Date().getTime(),
21-
sandboxId: currentId,
22+
sandboxId,
2223
})
2324
);
2425
} catch (e) {
2526
// Too bad
2627
}
2728
},
2829

29-
remove(currentId: string, module: Module) {
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) {
3037
try {
31-
localStorage.removeItem(getKey(currentId, module.shortid));
38+
const recoverData = this.get(sandboxId, module.shortid);
39+
if (recoverData && recoverData.code === module.code) {
40+
localStorage.removeItem(getKey(sandboxId, module.shortid));
41+
}
3242
} catch (e) {
3343
// Too bad
3444
}
3545
},
3646

37-
clearSandbox(currentId: string) {
47+
clearSandbox(sandboxId: string) {
3848
try {
3949
Object.keys(localStorage)
40-
.filter(key => key.startsWith(`recover:${currentId}`))
50+
.filter(key => key.startsWith(`recover:${sandboxId}`))
4151
.forEach(key => {
4252
localStorage.removeItem(key);
4353
});
@@ -46,19 +56,22 @@ export default {
4656
}
4757
},
4858

49-
getRecoverList(currentId: string, modules: Module[]) {
59+
getRecoverList(sandboxId: string, modules: Module[]) {
5060
const localKeys = Object.keys(localStorage).filter(key =>
51-
key.startsWith(`recover:${currentId}`)
61+
key.startsWith(`recover:${sandboxId}`)
5262
);
5363

5464
return modules
55-
.filter(m => localKeys.includes(getKey(currentId, m.shortid)))
65+
.filter(m => localKeys.includes(getKey(sandboxId, m.shortid)))
5666
.map(module => {
57-
const key = getKey(currentId, module.shortid);
67+
const key = getKey(sandboxId, module.shortid);
5868

5969
try {
60-
const recoverData = JSON.parse(localStorage.getItem(key) || 'null');
61-
if (recoverData && recoverData.code !== module.code) {
70+
const recoverData: RecoverData = JSON.parse(
71+
localStorage.getItem(key) || 'null'
72+
);
73+
74+
if (recoverData) {
6275
return { recoverData, module };
6376
}
6477
} catch (e) {
@@ -67,6 +80,6 @@ export default {
6780

6881
return null;
6982
})
70-
.filter(Boolean);
83+
.filter(Boolean) as Array<{ recoverData: RecoverData; module: Module }>;
7184
},
7285
};

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,40 @@ export class VSCodeEffect {
568568
}
569569
};
570570

571+
public async openDiff(sandboxId: string, module: Module, oldCode: string) {
572+
if (!module.path) {
573+
return;
574+
}
575+
576+
const recoverPath = `/recover/${sandboxId}/recover-${module.path.replace(
577+
/\//g,
578+
' '
579+
)}`;
580+
const filePath = `/sandbox${module.path}`;
581+
const fileSystem = window.BrowserFS.BFSRequire('fs');
582+
583+
// We have to write a recover file to the filesystem, we save it behind
584+
// the sandboxId
585+
if (!fileSystem.existsSync(`/recover/${sandboxId}`)) {
586+
fileSystem.mkdirSync(`/recover/${sandboxId}`);
587+
}
588+
// We write the recover file with the old code, as the new code is already applied
589+
fileSystem.writeFileSync(recoverPath, oldCode);
590+
591+
// We open a conflict resolution editor for the files
592+
this.editorApi.editorService.openEditor({
593+
leftResource: this.monaco.Uri.from({
594+
scheme: 'conflictResolution',
595+
path: recoverPath,
596+
}),
597+
rightResource: this.monaco.Uri.file(filePath),
598+
label: `Recover - ${module.path}`,
599+
options: {
600+
pinned: true,
601+
},
602+
});
603+
}
604+
571605
public setCorrections = (corrections: ModuleCorrection[]) => {
572606
const activeEditor = this.editorApi.getActiveCodeEditor();
573607
if (activeEditor) {
@@ -785,9 +819,18 @@ export class VSCodeEffect {
785819
)
786820
),
787821
this.createFileSystem('InMemory', {}),
822+
this.createFileSystem('InMemory', {}),
788823
]);
789824

790-
const [root, sandbox, vscode, home, extensions, customTheme] = fileSystems;
825+
const [
826+
root,
827+
sandbox,
828+
vscode,
829+
home,
830+
extensions,
831+
customTheme,
832+
recover,
833+
] = fileSystems;
791834

792835
const mfs = await this.createFileSystem('MountableFileSystem', {
793836
'/': root,
@@ -796,6 +839,7 @@ export class VSCodeEffect {
796839
'/home': home,
797840
'/extensions': extensions,
798841
'/extensions/custom-theme': customTheme,
842+
'/recover': recover,
799843
});
800844

801845
window.BrowserFS.initialize(mfs);

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

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
} from 'app/graphql/types';
2525
import { Action, AsyncAction } from 'app/overmind';
2626
import { withLoadApp, withOwnedSandbox } from 'app/overmind/factories';
27-
import { getSavedCode } from 'app/overmind/utils/sandbox';
2827
import {
2928
addDevToolsTab as addDevToolsTabUtil,
3029
closeDevToolsTab as closeDevToolsTabUtil,
@@ -91,6 +90,10 @@ export const loadCursorFromUrl: AsyncAction = async ({
9190

9291
await actions.editor.moduleSelected({ id: module.id });
9392

93+
if (!selection) {
94+
return;
95+
}
96+
9497
const [parsedHead, parsedAnchor] = selection.split('-').map(Number);
9598
if (!isNaN(parsedHead) && !isNaN(parsedAnchor)) {
9699
effects.vscode.setSelection(parsedHead, parsedAnchor);
@@ -143,20 +146,6 @@ export const sandboxChanged: AsyncAction<{ id: string }> = withLoadApp<{
143146
state.editor.currentSandbox
144147
);
145148

146-
// We now need to send all dirty files that came over from the last sandbox.
147-
// There is the scenario where you edit a file and press fork. Then the server
148-
// doesn't know about how you got to that dirty state.
149-
const changedModules = state.editor.currentSandbox.modules.filter(
150-
m => getSavedCode(m.code, m.savedCode) !== m.code
151-
);
152-
changedModules.forEach(m => {
153-
// Update server with latest data
154-
effects.live.sendCodeUpdate(
155-
m.shortid,
156-
getTextOperation(m.savedCode || '', m.code || '')
157-
);
158-
});
159-
160149
state.editor.isForkingSandbox = false;
161150
return;
162151
}
@@ -263,13 +252,13 @@ export const sandboxChanged: AsyncAction<{ id: string }> = withLoadApp<{
263252

264253
await actions.editor.internal.initializeSandbox(sandbox);
265254

255+
// We only recover files at this point if we are not live. When live we recover them
256+
// when the module_state is received
266257
if (
267-
hasPermission(sandbox.authorization, 'write_code') &&
268-
!state.live.isLive
258+
!state.live.isLive &&
259+
hasPermission(sandbox.authorization, 'write_code')
269260
) {
270261
actions.files.internal.recoverFiles();
271-
} else if (state.live.isLive) {
272-
await effects.live.sendModuleStateSyncRequest();
273262
}
274263

275264
effects.vscode.openModule(state.editor.currentModule);
@@ -348,13 +337,14 @@ export const onOperationApplied: Action<{
348337
return;
349338
}
350339

351-
actions.editor.internal.setStateModuleCode({
340+
actions.editor.internal.updateModuleCode({
352341
module,
353342
code,
354343
});
355344

356345
actions.editor.internal.updatePreviewCode();
357346

347+
// If we are in a state of sync, we set "revertModule" to set it as saved
358348
if (module.savedCode !== null && module.code === module.savedCode) {
359349
effects.vscode.syncModule(module);
360350
}
@@ -416,10 +406,11 @@ export const codeChanged: Action<{
416406
});
417407
}
418408

419-
actions.editor.internal.setStateModuleCode({
409+
actions.editor.internal.updateModuleCode({
420410
module,
421411
code,
422412
});
413+
423414
if (module.savedCode !== null && module.code === module.savedCode) {
424415
effects.vscode.syncModule(module);
425416
}
@@ -469,6 +460,7 @@ export const saveClicked: AsyncAction = withOwnedSandbox(
469460
state.editor.modulesByPath,
470461
module
471462
);
463+
472464
effects.moduleRecover.remove(sandbox.id, module);
473465
} else {
474466
// We might not have the module, as it was created by the server. In

0 commit comments

Comments
 (0)