Skip to content

Commit c6439ae

Browse files
authored
[fix] make webcontainer integration more robust (sveltejs#109)
- some console logs to help debugging if needed - add the wait time with the "reset iframe" hack for each time we (re)set the adapter - listen to server-ready event during reset, because for some reason it's fired then, too. This avoid a race condition where Vite could be requested too early, resulting in a timeout
1 parent 9f22fb4 commit c6439ae

File tree

2 files changed

+82
-43
lines changed

2 files changed

+82
-43
lines changed

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,34 @@ export async function create(stubs) {
2626

2727
const base = await new Promise(async (fulfil, reject) => {
2828
setTimeout(() => {
29-
reject(new Error('Timed out'));
29+
reject(new Error('Timed out starting WebContainer'));
3030
}, 15000);
3131

32+
console.log('loading webcontainer');
33+
3234
const WebContainer = await load();
3335

36+
console.log('booting webcontainer');
37+
3438
vm = await WebContainer.boot();
3539

36-
vm.on('error', (error) => {
40+
const error_unsub = vm.on('error', (error) => {
41+
error_unsub();
3742
reject(new Error(error.message));
3843
});
3944

40-
vm.on('server-ready', (port, base) => {
45+
const ready_unsub = vm.on('server-ready', (port, base) => {
46+
ready_unsub();
4147
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
4248
fulfil(base);
4349
});
4450

51+
console.log('loading files');
52+
4553
await vm.loadFiles(tree);
4654

55+
console.log('unpacking modules');
56+
4757
const unzip = await vm.run(
4858
{
4959
command: 'node',
@@ -60,6 +70,8 @@ export async function create(stubs) {
6070
reject(new Error('Failed to initialize WebContainer'));
6171
}
6272

73+
console.log('starting dev server');
74+
6375
await vm.run({ command: 'chmod', args: ['a+x', 'node_modules/vite/bin/vite.js'] });
6476

6577
await vm.run(
@@ -86,6 +98,24 @@ export async function create(stubs) {
8698
}
8799
}
88100

101+
// For some reason, server-ready is fired again on resetting the files here.
102+
// We need to wait for it to finish before we can continue, else we might
103+
// request files from Vite before it's ready, leading to a timeout.
104+
const promise = new Promise((fulfil, reject) => {
105+
const error_unsub = vm.on('error', (error) => {
106+
error_unsub();
107+
reject(new Error(error.message));
108+
});
109+
110+
const ready_unsub = vm.on('server-ready', (port, base) => {
111+
ready_unsub();
112+
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
113+
fulfil(undefined);
114+
});
115+
116+
setTimeout(() => reject(new Error('Timed out resetting WebContainer')), 10000);
117+
});
118+
89119
for (const file of old) {
90120
// TODO this fails with a cryptic error
91121
// index.svelte:155 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'rmSync')
@@ -107,6 +137,8 @@ export async function create(stubs) {
107137

108138
await vm.loadFiles(convert_stubs_to_tree(stubs));
109139

140+
await promise;
141+
110142
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
111143
},
112144

src/routes/tutorial/[slug]/+page.svelte

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -120,62 +120,68 @@
120120
121121
completed = false;
122122
123-
load_adapter();
123+
load_exercise();
124124
});
125125
126-
async function load_adapter() {
127-
clearTimeout(timeout);
128-
loading = true;
129-
126+
/**
127+
* Loads the adapter initially or resets it. This method can throw.
128+
* @param {import('$lib/types').Stub[]} stubs
129+
*/
130+
async function reset_adapter(stubs) {
130131
if (adapter) {
131-
await adapter.reset(Object.values(b));
132+
await adapter.reset(stubs);
133+
return adapter;
132134
} else {
133135
const module = import.meta.env.VITE_USE_FILESYSTEM
134136
? await import('$lib/client/adapters/filesystem/index.js')
135137
: await import('$lib/client/adapters/webcontainer/index.js');
136138
137-
try {
138-
adapter = await module.create(Object.values(b));
139-
} catch (e) {
140-
error = /** @type {Error} */ (e);
141-
return;
142-
}
139+
adapter = await module.create(stubs);
143140
}
144141
145142
set_iframe_src(adapter.base);
146143
147-
try {
148-
await new Promise((fulfil, reject) => {
149-
let called = false;
150-
151-
window.addEventListener('message', function handler(e) {
152-
if (e.origin !== adapter?.base) return;
153-
if (e.data.type === 'ping') {
154-
window.removeEventListener('message', handler);
155-
called = true;
156-
fulfil(undefined);
157-
}
158-
});
159-
160-
setTimeout(() => {
161-
if (!called && adapter) {
162-
// Updating the iframe too soon sometimes results in a blank screen,
163-
// so we try again after a short delay if we haven't heard back
164-
set_iframe_src(adapter.base);
165-
}
166-
}, 5000);
144+
await new Promise((fulfil, reject) => {
145+
let called = false;
167146
168-
setTimeout(() => {
169-
if (!called) {
170-
reject(new Error('Timed out'));
171-
}
172-
}, 10000);
147+
window.addEventListener('message', function handler(e) {
148+
if (e.origin !== adapter?.base) return;
149+
if (e.data.type === 'ping') {
150+
window.removeEventListener('message', handler);
151+
called = true;
152+
fulfil(undefined);
153+
}
173154
});
174155
156+
setTimeout(() => {
157+
if (!called && adapter) {
158+
// Updating the iframe too soon sometimes results in a blank screen,
159+
// so we try again after a short delay if we haven't heard back
160+
set_iframe_src(adapter.base);
161+
}
162+
}, 5000);
163+
164+
setTimeout(() => {
165+
if (!called) {
166+
reject(new Error('Timed out (re)setting adapter'));
167+
}
168+
}, 10000);
169+
});
170+
171+
return adapter;
172+
}
173+
174+
async function load_exercise() {
175+
try {
176+
clearTimeout(timeout);
177+
loading = true;
178+
179+
// Load expected output first so we can compare it to the actual output to determine when it's completed
180+
let adapter = await reset_adapter(Object.values(b));
175181
expected = await get_transformed_modules(data.section.scope.prefix, Object.values(b));
176182
177183
const stubs = Object.values(data.section.a);
178-
await adapter.reset(stubs);
184+
adapter = await reset_adapter(stubs);
179185
const actual = await get_transformed_modules(data.section.scope.prefix, stubs);
180186
181187
for (const [name, transformed] of expected.entries()) {
@@ -187,6 +193,7 @@
187193
loading = false;
188194
initial = false;
189195
} catch (e) {
196+
loading = false;
190197
error = /** @type {Error} */ (e);
191198
console.error(e);
192199
}
@@ -241,7 +248,7 @@
241248
});
242249
243250
setTimeout(() => {
244-
reject(new Error('Timed out'));
251+
reject(new Error('Timed out fetching files from Vite'));
245252
}, 5000);
246253
});
247254
}
@@ -426,7 +433,7 @@
426433
{error}
427434
on:reload={async () => {
428435
error = null;
429-
load_adapter();
436+
load_exercise();
430437
}}
431438
/>
432439
{/if}

0 commit comments

Comments
 (0)