Skip to content

Commit

Permalink
queue all updates (#272)
Browse files Browse the repository at this point in the history
* queue all updates

* remove some unused stuff

---------

Co-authored-by: Rich Harris <[email protected]>
  • Loading branch information
Rich-Harris and Rich Harris authored Mar 17, 2023
1 parent 49c9e66 commit bd20c7a
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 97 deletions.
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

1 comment on commit bd20c7a

@vercel
Copy link

@vercel vercel bot commented on bd20c7a Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.