Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

queue all updates #272

Merged
merged 3 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"port-authority": "^2.0.1",
"prism-svelte": "^0.5.0",
"prismjs": "^1.29.0",
"ws": "^8.12.1"
"ws": "^8.12.1",
"yootils": "^0.3.1"
},
"packageManager": "[email protected]"
}
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

183 changes: 87 additions & 96 deletions src/lib/client/adapters/webcontainer/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { WebContainer } from '@webcontainer/api';
import base64 from 'base64-js';
import AnsiToHtml from 'ansi-to-html';
import * as yootils from 'yootils';
import { escape_html, get_depth } from '../../../utils.js';
import { ready } from '../common/index.js';

Expand All @@ -25,12 +26,7 @@ export async function create(base, error, progress, logs) {

progress.set({ value: 0, text: 'loading files' });

/**
* Keeps track of the latest create/reset to ensure things are not processed in parallel.
* (if this turns out to be insufficient, we can use a queue)
* @type {Promise<any> | undefined}
*/
let running;
const q = yootils.queue(1);

/** Paths and contents of the currently loaded file stubs */
let current_stubs = stubs_to_map([]);
Expand Down Expand Up @@ -118,116 +114,111 @@ export async function create(base, error, progress, logs) {
}

return {
reset: async (stubs) => {
await running;
/** @type {Function} */
let resolve = () => {};
running = new Promise((fulfil) => (resolve = fulfil));

/** @type {import('$lib/types').Stub[]} */
const to_write = [];

for (const stub of stubs) {
if (stub.type === 'file') {
const current = /** @type {import('$lib/types').FileStub} */ (
current_stubs.get(stub.name)
);

if (current?.contents !== stub.contents) {
reset: (stubs) => {
return q.add(async () => {
/** @type {import('$lib/types').Stub[]} */
const to_write = [];

for (const stub of stubs) {
if (stub.type === 'file') {
const current = /** @type {import('$lib/types').FileStub} */ (
current_stubs.get(stub.name)
);

if (current?.contents !== stub.contents) {
to_write.push(stub);
}
} else {
// always add directories, otherwise convert_stubs_to_tree will fail
to_write.push(stub);
}
} else {
// always add directories, otherwise convert_stubs_to_tree will fail
to_write.push(stub);
}

current_stubs.delete(stub.name);
}

// Don't delete the node_modules folder when switching from one exercise to another
// where, as this crashes the dev server.
['/node_modules', '/node_modules/.bin'].forEach((name) => current_stubs.delete(name));

const to_delete = Array.from(current_stubs.keys());
current_stubs = stubs_to_map(stubs);

// For some reason, server-ready is fired again when the vite dev server is restarted.
// We need to wait for it to finish before we can continue, else we might
// request files from Vite before it's ready, leading to a timeout.
const will_restart = launched && to_write.some(will_restart_vite_dev_server);
const promise = will_restart
? new Promise((fulfil, reject) => {
const error_unsub = vm.on('error', (error) => {
error_unsub();
resolve();
reject(new Error(error.message));
});

const ready_unsub = vm.on('server-ready', (port, base) => {
ready_unsub();
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
resolve();
fulfil(undefined);
});

setTimeout(() => {
resolve();
reject(new Error('Timed out resetting WebContainer'));
}, 10000);
})
: Promise.resolve();

for (const file of to_delete) {
await vm.fs.rm(file, { force: true, recursive: true });
}
current_stubs.delete(stub.name);
}

await vm.mount(convert_stubs_to_tree(to_write));
await promise;
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
// Don't delete the node_modules folder when switching from one exercise to another
// where, as this crashes the dev server.
['/node_modules', '/node_modules/.bin'].forEach((name) => current_stubs.delete(name));

const to_delete = Array.from(current_stubs.keys());
current_stubs = stubs_to_map(stubs);

// For some reason, server-ready is fired again when the vite dev server is restarted.
// We need to wait for it to finish before we can continue, else we might
// request files from Vite before it's ready, leading to a timeout.
const will_restart = launched && to_write.some(will_restart_vite_dev_server);
const promise = will_restart
? new Promise((fulfil, reject) => {
const error_unsub = vm.on('error', (error) => {
error_unsub();
reject(new Error(error.message));
});

const ready_unsub = vm.on('server-ready', (port, base) => {
ready_unsub();
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
fulfil(undefined);
});

setTimeout(() => {
reject(new Error('Timed out resetting WebContainer'));
}, 10000);
})
: Promise.resolve();

for (const file of to_delete) {
await vm.fs.rm(file, { force: true, recursive: true });
}

resolve();
await vm.mount(convert_stubs_to_tree(to_write));
await promise;
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar

// Also trigger a reload of the iframe in case new files were added / old ones deleted,
// because that can result in a broken UI state
const should_reload = !launched || will_restart || to_delete.length > 0;
// Also trigger a reload of the iframe in case new files were added / old ones deleted,
// because that can result in a broken UI state
const should_reload = !launched || will_restart || to_delete.length > 0;

await launch();
await launch();

return should_reload;
return should_reload;
});
},
update: async (file) => {
await running;
update: (file) => {
return q.add(async () => {
/** @type {import('@webcontainer/api').FileSystemTree} */
const root = {};

/** @type {import('@webcontainer/api').FileSystemTree} */
const root = {};
let tree = root;

let tree = root;
const path = file.name.split('/').slice(1);
const basename = /** @type {string} */ (path.pop());

const path = file.name.split('/').slice(1);
const basename = /** @type {string} */ (path.pop());
for (const part of path) {
if (!tree[part]) {
/** @type {import('@webcontainer/api').FileSystemTree} */
const directory = {};

for (const part of path) {
if (!tree[part]) {
/** @type {import('@webcontainer/api').FileSystemTree} */
const directory = {};
tree[part] = {
directory
};
}

tree[part] = {
directory
};
tree = /** @type {import('@webcontainer/api').DirectoryNode} */ (tree[part]).directory;
}

tree = /** @type {import('@webcontainer/api').DirectoryNode} */ (tree[part]).directory;
}

tree[basename] = to_file(file);
tree[basename] = to_file(file);

await vm.mount(root);
await vm.mount(root);

current_stubs.set(file.name, file);
current_stubs.set(file.name, file);

await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
// we need to stagger sequential updates, just enough that the HMR
// wires don't get crossed. 50ms seems to be enough of a delay
// to avoid glitches without noticeably affecting update speed
await new Promise((f) => setTimeout(f, 50));

return will_restart_vite_dev_server(file);
return will_restart_vite_dev_server(file);
});
}
};
}
Expand Down