Skip to content

Commit 940c684

Browse files
committed
[fix] more robust web container init
guard against people switching the tutorial chapter while the web container boots
1 parent 2df261f commit 940c684

File tree

1 file changed

+124
-104
lines changed
  • src/lib/client/adapters/webcontainer

1 file changed

+124
-104
lines changed

src/lib/client/adapters/webcontainer/index.js

Lines changed: 124 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@ import { load } from '@webcontainer/api';
22
import base64 from 'base64-js';
33
import { ready } from '../common/index.js';
44

5-
/** @type {import('@webcontainer/api').WebContainer} */
5+
/** @type {import('@webcontainer/api').WebContainer} Web container singleton */
66
let vm;
77
/** Keep track of startup progress, so we don't repeat previous steps in case of a timeout */
88
let step = 0;
9-
/** @type {string} */
9+
/** @type {string} path to the web container server instance */
1010
let base;
11+
/**
12+
* Keeps track of the latest create/reset to ensure things are not processed in parallel.
13+
* (if this turns out to be insufficient, we can use a queue)
14+
* @type {Promise<any> | undefined}
15+
*/
16+
let running;
17+
/** @type {Set<string>} Paths of the currently loaded file stubs */
18+
let current = new Set();
1119

1220
/**
1321
* @param {import('$lib/types').Stub[]} stubs
@@ -28,7 +36,9 @@ export async function create(stubs) {
2836
throw new Error('WebContainers are not supported by Safari');
2937
}
3038

31-
base = await new Promise(async (fulfil, reject) => {
39+
await running; // wait for any previous create to finish
40+
41+
const init = new Promise(async (fulfil, reject) => {
3242
if (base) {
3343
// startup was successful in the meantime
3444
fulfil(base);
@@ -75,6 +85,8 @@ export async function create(stubs) {
7585
console.log('loading files');
7686

7787
await vm.loadFiles(tree);
88+
89+
current = new Set(stub_filenames(stubs));
7890
}
7991

8092
if (step < 2) {
@@ -120,131 +132,131 @@ export async function create(stubs) {
120132
}
121133
});
122134

123-
/**
124-
* Paths of the currently loaded file stubs
125-
*/
126-
let current = new Set(stubs.filter((stub) => stub.type === 'file').map((stub) => stub.name));
127-
/**
128-
* Keeps track of the latest create/reset to ensure things are not processed in parallel.
129-
* (if this turns out to be insufficient, we can use a queue)
130-
*/
131-
let running = Promise.resolve();
135+
running = init.catch(() => {});
136+
base = await init;
132137

133-
return {
134-
base,
138+
if (stub_filenames(stubs).some((name) => !current.has(name))) {
139+
await reset(stubs);
140+
}
135141

136-
/**
137-
* Deletes old files and adds new ones
138-
* @param {import('$lib/types').Stub[]} stubs
139-
*/
140-
async reset(stubs) {
141-
await running;
142-
/** @type {Function} */
143-
let resolve = () => {};
144-
running = new Promise((fulfil) => (resolve = fulfil));
145-
146-
const old = current;
147-
current = new Set(stubs.filter((stub) => stub.type === 'file').map((stub) => stub.name));
148-
149-
for (const stub of stubs) {
150-
if (stub.type === 'file') {
151-
old.delete(stub.name);
152-
}
142+
/**
143+
* Deletes old files and adds new ones
144+
* @param {import('$lib/types').Stub[]} stubs
145+
*/
146+
async function reset(stubs) {
147+
await running;
148+
/** @type {Function} */
149+
let resolve = () => {};
150+
running = new Promise((fulfil) => (resolve = fulfil));
151+
152+
const old = current;
153+
current = new Set(stub_filenames(stubs));
154+
155+
for (const stub of stubs) {
156+
if (stub.type === 'file') {
157+
old.delete(stub.name);
153158
}
159+
}
154160

155-
// For some reason, server-ready is fired again on resetting the files here.
156-
// We need to wait for it to finish before we can continue, else we might
157-
// request files from Vite before it's ready, leading to a timeout.
158-
const promise = new Promise((fulfil, reject) => {
159-
const error_unsub = vm.on('error', (error) => {
160-
error_unsub();
161-
resolve();
162-
reject(new Error(error.message));
163-
});
164-
165-
const ready_unsub = vm.on('server-ready', (port, base) => {
166-
ready_unsub();
167-
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
168-
resolve();
169-
fulfil(undefined);
170-
});
161+
// For some reason, server-ready is fired again on resetting the files here.
162+
// We need to wait for it to finish before we can continue, else we might
163+
// request files from Vite before it's ready, leading to a timeout.
164+
const promise = new Promise((fulfil, reject) => {
165+
const error_unsub = vm.on('error', (error) => {
166+
error_unsub();
167+
resolve();
168+
reject(new Error(error.message));
169+
});
171170

172-
setTimeout(() => {
173-
resolve();
174-
reject(new Error('Timed out resetting WebContainer'));
175-
}, 10000);
171+
const ready_unsub = vm.on('server-ready', (port, base) => {
172+
ready_unsub();
173+
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
174+
resolve();
175+
fulfil(undefined);
176176
});
177177

178-
for (const file of old) {
179-
// TODO this fails with a cryptic error
180-
// index.svelte:155 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'rmSync')
181-
// at Object.rm (webcontainer.e2e246a845f9e80283581d6b944116e399af6950.js:6:121171)
182-
// at MessagePort._0x4ec3f4 (webcontainer.e2e246a845f9e80283581d6b944116e399af6950.js:6:110957)
183-
// at MessagePort.nrWrapper (headless:5:29785)
184-
// await vm.fs.rm(file);
185-
186-
// temporary workaround
187-
try {
188-
await vm.run({
189-
command: 'node',
190-
args: ['-e', `fs.rmSync('${file.slice(1)}')`]
191-
});
192-
} catch (e) {
193-
console.error(e);
194-
}
195-
}
178+
setTimeout(() => {
179+
resolve();
180+
reject(new Error('Timed out resetting WebContainer'));
181+
}, 10000);
182+
});
196183

197-
await vm.loadFiles(convert_stubs_to_tree(stubs));
184+
for (const file of old) {
185+
// TODO this fails with a cryptic error
186+
// index.svelte:155 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'rmSync')
187+
// at Object.rm (webcontainer.e2e246a845f9e80283581d6b944116e399af6950.js:6:121171)
188+
// at MessagePort._0x4ec3f4 (webcontainer.e2e246a845f9e80283581d6b944116e399af6950.js:6:110957)
189+
// at MessagePort.nrWrapper (headless:5:29785)
190+
// await vm.fs.rm(file);
191+
192+
// temporary workaround
193+
try {
194+
await vm.run({
195+
command: 'node',
196+
args: ['-e', `fs.rmSync('${file.slice(1)}')`]
197+
});
198+
} catch (e) {
199+
console.error(e);
200+
}
201+
}
198202

199-
await promise;
203+
await vm.loadFiles(convert_stubs_to_tree(stubs));
200204

201-
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
205+
await promise;
202206

203-
resolve();
204-
},
207+
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
205208

206-
/**
207-
* Loads new files but keeps the old ones
208-
* @param {import('$lib/types').FileStub[]} stubs
209-
*/
210-
async update(stubs) {
211-
await running;
209+
resolve();
210+
}
212211

213-
/** @type {import('@webcontainer/api').FileSystemTree} */
214-
const root = {};
212+
/**
213+
* Loads new files but keeps the old ones
214+
* @param {import('$lib/types').FileStub[]} stubs
215+
*/
216+
async function update(stubs) {
217+
await running;
215218

216-
for (const stub of stubs) {
217-
let tree = root;
219+
/** @type {import('@webcontainer/api').FileSystemTree} */
220+
const root = {};
218221

219-
const path = stub.name.split('/').slice(1);
220-
const basename = /** @type {string} */ (path.pop());
222+
for (const stub of stubs) {
223+
let tree = root;
221224

222-
for (const part of path) {
223-
if (!tree[part]) {
224-
/** @type {import('@webcontainer/api').FileSystemTree} */
225-
const directory = {};
225+
const path = stub.name.split('/').slice(1);
226+
const basename = /** @type {string} */ (path.pop());
226227

227-
tree[part] = {
228-
directory
229-
};
230-
}
228+
for (const part of path) {
229+
if (!tree[part]) {
230+
/** @type {import('@webcontainer/api').FileSystemTree} */
231+
const directory = {};
231232

232-
tree = /** @type {import('@webcontainer/api').DirectoryEntry} */ (tree[part]).directory;
233+
tree[part] = {
234+
directory
235+
};
233236
}
234237

235-
tree[basename] = to_file(stub);
238+
tree = /** @type {import('@webcontainer/api').DirectoryEntry} */ (tree[part]).directory;
236239
}
237240

238-
await vm.loadFiles(root);
241+
tree[basename] = to_file(stub);
242+
}
239243

240-
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
241-
},
244+
await vm.loadFiles(root);
242245

243-
async destroy() {
244-
vm.teardown();
245-
// @ts-ignore
246-
vm = null;
247-
}
246+
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
247+
}
248+
249+
async function destroy() {
250+
vm.teardown();
251+
// @ts-ignore
252+
vm = null;
253+
}
254+
255+
return {
256+
base,
257+
reset,
258+
update,
259+
destroy
248260
};
249261
}
250262

@@ -293,3 +305,11 @@ function to_file(stub) {
293305
file: { contents }
294306
};
295307
}
308+
309+
/**
310+
*
311+
* @param {import('$lib/types').Stub[]} stubs
312+
*/
313+
function stub_filenames(stubs) {
314+
return stubs.filter((stub) => stub.type === 'file').map((stub) => stub.name);
315+
}

0 commit comments

Comments
 (0)