Skip to content

Commit 9c705fb

Browse files
committed
chore: make adapter interaction more robust
..to timing specific things by keeping track of current and next task
1 parent 450199e commit 9c705fb

File tree

5 files changed

+111
-44
lines changed

5 files changed

+111
-44
lines changed

Diff for: src/lib/client/adapters/filesystem/index.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
22
* @param {import('$lib/types').Stub[]} stubs
3-
* @returns {Promise<import('$lib/types').Adapter>}
3+
* @param {(progress: number, status: string) => void} cb
4+
* @returns {Promise<import('$lib/types').AdapterInternal>}
45
*/
5-
export async function create(stubs) {
6+
export async function create(stubs, cb) {
67
const res = await fetch('/backend', {
78
method: 'post',
89
headers: {
@@ -22,6 +23,8 @@ export async function create(stubs) {
2223
}
2324
});
2425

26+
cb(100, 'Ready');
27+
2528
return {
2629
base: `http://localhost:${port}`,
2730

Diff for: src/lib/client/adapters/webcontainer/index.js

+1-20
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ import { ready } from '../common/index.js';
66
/** @type {import('@webcontainer/api').WebContainer} Web container singleton */
77
let vm;
88

9-
/** @type {Promise<import('$lib/types').Adapter>} */
10-
let promise;
11-
129
/**
1310
* @param {import('$lib/types').Stub[]} stubs
1411
* @param {(progress: number, status: string) => void} callback
15-
* @returns {Promise<import('$lib/types').Adapter>}
12+
* @returns {Promise<import('$lib/types').AdapterInternal>}
1613
*/
1714
export async function create(stubs, callback) {
1815
if (/safari/i.test(navigator.userAgent) && !/chrome/i.test(navigator.userAgent)) {
@@ -21,22 +18,6 @@ export async function create(stubs, callback) {
2118

2219
callback(0, 'loading files');
2320

24-
if (!promise) {
25-
promise = _create(stubs, callback);
26-
} else {
27-
const adapter = await promise;
28-
await adapter.reset(stubs);
29-
}
30-
31-
return promise;
32-
}
33-
34-
/**
35-
* @param {import('$lib/types').Stub[]} stubs
36-
* @param {(progress: number, status: string) => void} callback
37-
* @returns {Promise<import('$lib/types').Adapter>}
38-
*/
39-
async function _create(stubs, callback) {
4021
/**
4122
* Keeps track of the latest create/reset to ensure things are not processed in parallel.
4223
* (if this turns out to be insufficient, we can use a queue)

Diff for: src/lib/types/index.d.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@ export interface DirectoryStub {
1616

1717
export type Stub = FileStub | DirectoryStub;
1818

19-
export interface Adapter {
19+
export interface AdapterInternal {
2020
base: string;
2121
/** Returns `false` if the reset was in such a way that a reload of the iframe isn't needed */
2222
reset(files: Array<Stub>): Promise<boolean>;
2323
update(file: Array<FileStub>): Promise<boolean>;
2424
destroy(): Promise<void>;
2525
}
2626

27+
export interface Adapter extends AdapterInternal {
28+
reset(files: Array<Stub>): Promise<boolean | 'cancelled'>;
29+
update(file: Array<FileStub>): Promise<boolean | 'cancelled'>;
30+
init: Promise<void>;
31+
}
32+
2733
export interface Scope {
2834
prefix: string;
2935
depth: number;

Diff for: src/routes/tutorial/[slug]/+page.svelte

+21-21
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import Loading from './Loading.svelte';
1414
import ScreenToggle from './ScreenToggle.svelte';
1515
import Filetree from '$lib/components/filetree/Filetree.svelte';
16+
import { create_adapter } from './adapter';
1617
1718
/** @type {import('./$types').PageData} */
1819
export let data;
@@ -93,7 +94,7 @@
9394
}
9495
}
9596
96-
/** @type {import('$lib/types').Adapter | undefined} */
97+
/** @type {import('$lib/types').Adapter} Will be defined after first afterNavigate */
9798
let adapter;
9899
/** @type {string[]} */
99100
let history_bwd = [];
@@ -141,7 +142,7 @@
141142
142143
await reset_adapter($files);
143144
144-
if (adapter && path !== data.exercise.path) {
145+
if (path !== data.exercise.path) {
145146
path = data.exercise.path;
146147
set_iframe_src(adapter.base + path);
147148
}
@@ -162,14 +163,19 @@
162163
async function reset_adapter(stubs) {
163164
let reload_iframe = true;
164165
if (adapter) {
165-
reload_iframe = await adapter.reset(stubs);
166+
const result = await adapter.reset(stubs);
167+
if (result === 'cancelled') {
168+
return;
169+
} else {
170+
reload_iframe = result;
171+
}
166172
} else {
167-
const module = await import('$lib/client/adapters/webcontainer/index.js');
168-
169-
adapter = await module.create(stubs, (p, s) => {
173+
const _adapter = create_adapter(stubs, (p, s) => {
170174
progress = p;
171175
status = s;
172176
});
177+
adapter = _adapter;
178+
await _adapter.init;
173179
174180
set_iframe_src(adapter.base + path);
175181
}
@@ -178,7 +184,7 @@
178184
let called = false;
179185
180186
window.addEventListener('message', function handler(e) {
181-
if (e.origin !== adapter?.base) return;
187+
if (e.origin !== adapter.base) return;
182188
if (e.data.type === 'ping') {
183189
window.removeEventListener('message', handler);
184190
called = true;
@@ -187,7 +193,7 @@
187193
});
188194
189195
setTimeout(() => {
190-
if (!called && adapter) {
196+
if (!called) {
191197
// Updating the iframe too soon sometimes results in a blank screen,
192198
// so we try again after a short delay if we haven't heard back
193199
set_iframe_src(adapter.base + path);
@@ -216,7 +222,7 @@
216222
const stub = event.detail;
217223
const index = $files.findIndex((s) => s.name === stub.name);
218224
$files[index] = stub;
219-
adapter?.update([stub]).then((reload) => {
225+
adapter.update([stub]).then((reload) => {
220226
if (reload) {
221227
schedule_iframe_reload();
222228
}
@@ -229,9 +235,7 @@
229235
function schedule_iframe_reload() {
230236
clearTimeout(reload_timeout);
231237
reload_timeout = setTimeout(() => {
232-
if (adapter) {
233-
set_iframe_src(adapter.base + path);
234-
}
238+
set_iframe_src(adapter.base + path);
235239
}, 1000);
236240
}
237241
@@ -283,7 +287,7 @@
283287
284288
clearTimeout(timeout);
285289
timeout = setTimeout(() => {
286-
if ((dev && !iframe) || !adapter) return;
290+
if (dev && !iframe) return;
287291
288292
// we lost contact, refresh the page
289293
loading = true;
@@ -330,11 +334,9 @@
330334
331335
/** @param {string} path */
332336
function route_to(path) {
333-
if (adapter) {
334-
const url = new URL(path, adapter.base);
335-
path = url.pathname + url.search + url.hash;
336-
set_iframe_src(adapter.base + path);
337-
}
337+
const url = new URL(path, adapter.base);
338+
path = url.pathname + url.search + url.hash;
339+
set_iframe_src(adapter.base + path);
338340
}
339341
340342
/** @param {string | null} new_path */
@@ -467,9 +469,7 @@
467469
{path}
468470
{loading}
469471
on:refresh={() => {
470-
if (adapter) {
471-
set_iframe_src(adapter.base + path);
472-
}
472+
set_iframe_src(adapter.base + path);
473473
}}
474474
on:change={(e) => nav_to(e.detail.value)}
475475
on:back={go_bwd}

Diff for: src/routes/tutorial/[slug]/adapter.js

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @param {import('$lib/types').Stub[]} initial_stubs
3+
* @param {(progress: number, status: string) => void} callback
4+
* @returns {import('$lib/types').Adapter}
5+
*/
6+
export function create_adapter(initial_stubs, callback) {
7+
/**
8+
* @typedef {{ type: 'reset'; stubs: import('$lib/types').Stub[]; } | { type: 'update'; stubs: import('$lib/types').FileStub[]; }} State
9+
*/
10+
11+
/** @type {State | undefined} */
12+
let state;
13+
/** @type {Promise<import('$lib/types').AdapterInternal>} */
14+
let adapter_promise;
15+
let adapter_base = '';
16+
17+
async function init() {
18+
const module = await import('$lib/client/adapters/webcontainer/index.js');
19+
adapter_promise = module.create(initial_stubs, callback);
20+
adapter_base = (await adapter_promise).base;
21+
}
22+
23+
// Keep track of what's currently running, and what's next
24+
/** @type {Promise<boolean>} */
25+
let current;
26+
let token = {};
27+
async function next() {
28+
const current_token = (token = {});
29+
await current;
30+
if (current_token !== token || !state) return 'cancelled';
31+
32+
const _state = state;
33+
state = undefined;
34+
current = (async () => {
35+
if (_state.type === 'reset') {
36+
const adapter = await adapter_promise;
37+
return await adapter.reset(_state.stubs);
38+
} else {
39+
const adapter = await adapter_promise;
40+
return await adapter.update(_state.stubs);
41+
}
42+
})();
43+
44+
return current;
45+
}
46+
47+
current = init().then(() => true);
48+
49+
return {
50+
init: current.then(() => {}),
51+
get base() {
52+
return adapter_base;
53+
},
54+
update: async (stubs) => {
55+
if (state) {
56+
// add new stubs (which have up-to-date content) to existing stubs
57+
const new_stubs = new Set(stubs.map((stub) => stub.name));
58+
state = {
59+
...state,
60+
// @ts-expect-error TS doesn't understand that the union type will be well-formed
61+
stubs: state.stubs.filter((stub) => !new_stubs.has(stub.name)).concat(stubs)
62+
};
63+
} else {
64+
state = { type: 'update', stubs };
65+
}
66+
return next();
67+
},
68+
reset: async (stubs) => {
69+
state = { type: 'reset', stubs };
70+
return next();
71+
},
72+
destroy: async () => {
73+
const adapter = await adapter_promise;
74+
return adapter.destroy();
75+
}
76+
};
77+
}

0 commit comments

Comments
 (0)