Skip to content

Commit 6a82061

Browse files
authored
API Caching (codesandbox#800)
* API Caching * Remove latestSha system * Use root domain for sending cache * Add nullcheck for contentDocument * Debug if query param is set * Show more debug statements on prod * Catch save cache error * Add debug messages * Only cache 30% of the sandboxes for testing
1 parent 15df6d2 commit 6a82061

File tree

11 files changed

+253
-90
lines changed

11 files changed

+253
-90
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"no-plusplus": 0,
2626
"no-underscore-dangle": "off",
2727
"no-nested-ternary": "warn",
28-
"react/require-default-props": "off"
28+
"react/require-default-props": "off",
29+
"import/no-named-default": 0
2930
}
3031
}

packages/app/src/app/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render } from 'react-dom';
33
import { ThemeProvider } from 'styled-components';
44
import { Router } from 'react-router-dom';
55
import history from 'app/utils/history';
6+
import _debug from 'app/utils/debug';
67
import VERSION from 'common/version';
78
import registerServiceWorker from 'common/registerServiceWorker';
89
import requirePolyfills from 'common/load-dynamic-polyfills';
@@ -16,6 +17,8 @@ import App from './pages/index';
1617
import './split-pane.css';
1718
import logError from './utils/error';
1819

20+
const debug = _debug('cs:app');
21+
1922
if (process.env.NODE_ENV === 'production') {
2023
try {
2124
Raven.config('https://[email protected]/155188', {
@@ -80,9 +83,11 @@ requirePolyfills().then(() => {
8083

8184
registerServiceWorker('/service-worker.js', {
8285
onUpdated: () => {
86+
debug('Updated SW');
8387
controller.getSignal('setUpdateStatus')({ status: 'available' });
8488
},
8589
onInstalled: () => {
90+
debug('Installed SW');
8691
showNotification(
8792
'CodeSandbox has been installed, it now works offline!',
8893
'success'

packages/app/src/app/utils/debug.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
const getDebugger = () => {
2-
if (process.env.NODE_ENV === 'production') {
2+
if (
3+
process.env.NODE_ENV === 'production' &&
4+
document.location.search.indexOf('debug') === -1
5+
) {
36
// Return a debugger that will log to sentry
47
return (key: string) => (message: string) => {
58
if (typeof window.Raven === 'object') {

packages/app/src/sandbox/compile.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from './boilerplates';
2323

2424
import loadDependencies from './npm';
25+
import { consumeCache, saveCache } from './eval/cache';
2526
import getDefinition from '../../../common/templates/index';
2627

2728
let initializedResizeListener = false;
@@ -246,8 +247,7 @@ async function updateManager(
246247
if (firstLoad && newManager) {
247248
// We save the state of transpiled modules, and load it here again. Gives
248249
// faster initial loads.
249-
250-
await manager.load();
250+
await consumeCache(manager);
251251
}
252252

253253
manager.updateConfigurations(configurations);
@@ -395,16 +395,18 @@ async function compile({
395395

396396
dispatch({ type: 'status', status: 'transpiling' });
397397

398+
await manager.verifyTreeTranspiled();
398399
await manager.preset.setup(manager);
399400
await manager.transpileModules(managerModuleToTranspile);
400401

402+
const managerTranspiledModuleToTranspile = manager.getTranspiledModule(
403+
managerModuleToTranspile
404+
);
405+
401406
debug(`Transpilation time ${Date.now() - t}ms`);
402407

403408
dispatch({ type: 'status', status: 'evaluating' });
404409

405-
const managerTranspiledModuleToTranspile = manager.getTranspiledModule(
406-
managerModuleToTranspile
407-
);
408410
if (!skipEval) {
409411
resetScreen();
410412

@@ -533,7 +535,7 @@ async function compile({
533535
type: 'success',
534536
});
535537

536-
manager.save();
538+
saveCache(sandboxId, managerModuleToTranspile, manager, firstLoad);
537539
} catch (e) {
538540
console.log('Error in sandbox:');
539541
console.error(e);
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Responsible for consuming and syncing with the server/local cache
2+
import localforage from 'localforage';
3+
import _debug from 'app/utils/debug';
4+
import type { default as Manager } from './manager';
5+
6+
import { SCRIPT_VERSION } from '../';
7+
8+
const debug = _debug('cs:compiler:cache');
9+
10+
const host = process.env.CODESANDBOX_HOST;
11+
12+
localforage.config({
13+
name: 'CodeSandboxApp',
14+
storeName: 'sandboxes', // Should be alphanumeric, with underscores.
15+
description:
16+
'Cached transpilations of the sandboxes, for faster initialization time.',
17+
});
18+
19+
// Prewarm store
20+
localforage.keys();
21+
22+
function shouldSaveOnlineCache(firstRun: boolean, sandboxId: string) {
23+
if (!firstRun) {
24+
return false;
25+
}
26+
27+
if (!window.__SANDBOX_DATA__) {
28+
// TODO remove this check
29+
return sandboxId.charCodeAt(0) < 105;
30+
}
31+
32+
return false;
33+
}
34+
35+
export async function saveCache(
36+
sandboxId: string,
37+
managerModuleToTranspile: any,
38+
manager: Manager,
39+
firstRun: boolean
40+
) {
41+
const managerState = {
42+
...manager.serialize(),
43+
};
44+
managerState.entry = managerModuleToTranspile
45+
? managerModuleToTranspile.path
46+
: null;
47+
48+
try {
49+
if (process.env.NODE_ENV === 'development') {
50+
debug(
51+
'Saving cache of ' +
52+
(JSON.stringify(managerState).length / 1024).toFixed(2) +
53+
'kb to localStorage'
54+
);
55+
}
56+
localforage.setItem(manager.id, managerState);
57+
} catch (e) {
58+
if (process.env.NODE_ENV === 'development') {
59+
console.error(e);
60+
}
61+
this.clearCache();
62+
}
63+
64+
if (shouldSaveOnlineCache(firstRun, sandboxId)) {
65+
const stringifiedManagerState = JSON.stringify(managerState);
66+
67+
debug(
68+
'Saving cache of ' +
69+
(stringifiedManagerState.length / 1024).toFixed(2) +
70+
'kb to CodeSandbox API'
71+
);
72+
73+
return window
74+
.fetch(`${host}/api/v1/sandboxes/${sandboxId}/cache`, {
75+
method: 'POST',
76+
body: JSON.stringify({
77+
version: SCRIPT_VERSION,
78+
data: stringifiedManagerState,
79+
}),
80+
headers: {
81+
'Content-Type': 'application/json',
82+
},
83+
})
84+
.then(x => x.json())
85+
.catch(e => {
86+
console.error('Something went wrong while saving cache.');
87+
console.error(e);
88+
});
89+
}
90+
91+
return Promise.resolve(false);
92+
}
93+
94+
function findCacheToUse(cache1, cache2) {
95+
if (!cache1 && !cache2) {
96+
return null;
97+
}
98+
99+
if (cache1 && !cache2) {
100+
return cache1;
101+
}
102+
103+
if (cache2 && !cache1) {
104+
return cache2;
105+
}
106+
107+
return cache2.timestamp > cache1.timestamp ? cache2 : cache1;
108+
}
109+
110+
export async function consumeCache(manager: Manager) {
111+
const cacheData = window.__SANDBOX_DATA__;
112+
const localData = await localforage.getItem(manager.id);
113+
114+
const cache = findCacheToUse(cacheData && cacheData.data, localData);
115+
if (cache) {
116+
const version = SCRIPT_VERSION;
117+
118+
if (cache.version === version) {
119+
debug(
120+
`Loading cache from ${cache === localData ? 'localStorage' : 'API'}`,
121+
cache
122+
);
123+
124+
await manager.load(cache);
125+
126+
return true;
127+
}
128+
}
129+
130+
return false;
131+
}

0 commit comments

Comments
 (0)