12714c7527
improvise-1wft and the five beads it blocked were one failure: nothing
carried a boot failure's REASON from the worker to the page. The channel
is one rule -- a protocol frame crosses the port as a STRING, a shell
notice as an OBJECT -- so it needs no protocol change and the
discriminator is total.
A boot failure is a STATE, not a rejected promise. A document another
context holds the exclusive handle to now OPENS read-only: measured first
(harness probe, Chromium + Firefox), getFile() reads the bytes while the
handle is held, with the second-handle refusal asserted so the probe
cannot pass vacuously.
- improvise-1wft the channel: Notice/NoticeKind, boot_notice, take_notice
- improvise-i0v9 read-only boot over vfs::ReadOnly + OpfsFs::read_unattached
- improvise-86nw start() rejecting for any reason now posts BootFailed
- improvise-9cma boot::document_name -- and a/b/c.improv was ACCEPTED as
c.improv, not rejected, which is worse than the report
- improvise-iobc vfs::Origin: a seed error is a notice only when the
document really had to be created without it
- improvise-vep8 the watchdog arms BEFORE transport.ready; its message
stopped claiming the connection was accepted
- improvise-7gip DedicatedWorkerGlobalScope (needs a web-sys feature)
- improvise-zre1 a persist that fails raises a notice; the trigger is the
dirty flag surviving, observed rather than predicted
- improvise-7gf8 the render breaker trips once instead of retrying forever
- improvise-ociy non-OPEN sends are loud; a viewport announcement is not
- improvise-y01k the three ws-server drops are logged
- improvise-r4nf serde failures throw instead of reading as "nothing to send"
Every notice routes through shell.fail, so gotcha 48's disarm half is not
re-broken. Two comments in shell.spec.js stated the opposite of the new
arming order and were corrected rather than left.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
87 lines
3.2 KiB
JavaScript
87 lines
3.2 KiB
JavaScript
// The worker half of the standalone deployment (improvise-9x6): strings only,
|
|
// every decision in Rust. The two rules it must not break: context/code/clients.md.
|
|
|
|
import init, { start } from "./worker-pkg/improvise_worker_server.js";
|
|
import { serialize } from "./serial.js";
|
|
|
|
const params = new URLSearchParams(self.location.search);
|
|
const DOC = params.get("doc") || "demo.improv";
|
|
// Where the first copy of the document comes from on a fresh origin: fetched
|
|
// and WRITTEN through the VFS, never injected past storage.
|
|
const SEED = params.get("seed") || "demo.improv";
|
|
|
|
// Fetch the seed. A failure is NOT decided here: the bytes and the reason both
|
|
// go to Rust, which knows whether the document needed a first copy at all.
|
|
async function seedBytes() {
|
|
try {
|
|
const res = await fetch(SEED);
|
|
if (!res.ok) return { error: `${SEED} answered ${res.status} ${res.statusText}` };
|
|
return { bytes: new Uint8Array(await res.arrayBuffer()) };
|
|
} catch (err) {
|
|
return { error: `${SEED} could not be fetched: ${err?.message ?? err}` };
|
|
}
|
|
}
|
|
|
|
let server = null;
|
|
|
|
function post(replies) {
|
|
for (const reply of replies) self.postMessage(reply);
|
|
}
|
|
|
|
/** A shell notice, posted as an OBJECT so the page can tell it from a protocol
|
|
* frame without parsing one. See `shell.js::classifyWorkerMessage`. */
|
|
function notify(notice) {
|
|
self.postMessage(notice);
|
|
}
|
|
|
|
async function handle(data) {
|
|
const replies = server.on_message(data);
|
|
post(replies);
|
|
await server.flush();
|
|
// A persist nobody asked for that did not happen (improvise-zre1). Drained
|
|
// per message, so one failure is reported once.
|
|
const raised = server.take_notice();
|
|
if (raised) notify(JSON.parse(raised));
|
|
}
|
|
|
|
// Startup, and the head of the chain: frames arrive before this settles, and
|
|
// chaining onto it holds them in arrival order with no queue to drain.
|
|
const started = (async () => {
|
|
await init();
|
|
const seed = await seedBytes();
|
|
server = await start(DOC, seed.bytes, seed.error);
|
|
// Read once, straight after start: read-only, or an empty document whose
|
|
// seed never arrived. Both are open sessions, so this is not a failure.
|
|
const boot = server.boot_notice();
|
|
if (boot) notify(JSON.parse(boot));
|
|
})();
|
|
started.catch((err) => {
|
|
console.error("improvise worker: failed to start", err);
|
|
// A worker that rejects in `start()` fires NO error on the main thread, so
|
|
// without this the page has only a watchdog and no reason (improvise-1wft).
|
|
notify({
|
|
kind: "BootFailed",
|
|
document: DOC,
|
|
detail: String(err?.message ?? err),
|
|
});
|
|
});
|
|
|
|
// One frame at a time, in arrival order, and the listener is deliberately NOT
|
|
// `async`: the chaining is `serial.js`'s and is tested there.
|
|
const onFrame = serialize(
|
|
started,
|
|
async (data) => {
|
|
// `start()` failed, and the page has already been told why by the
|
|
// `BootFailed` notice above. There is nothing to answer with.
|
|
if (!server) return;
|
|
await handle(data);
|
|
},
|
|
// An unhandled rejection here reaches the devtools console and nothing
|
|
// else. Caught, so one bad frame does not poison every later one.
|
|
(err, data) => console.error("improvise worker: frame rejected", data, err),
|
|
);
|
|
|
|
self.onmessage = (event) => {
|
|
onFrame(event.data);
|
|
};
|