Merge remote-tracking branch 'gh/main' into wire2/2wu3

This commit is contained in:
Edward Langley
2026-09-22 22:44:47 -07:00
19 changed files with 673 additions and 105 deletions
+3 -1
View File
@@ -181,7 +181,8 @@ function workerTransport() {
shell.fail(`worker failed to start: ${event.message ?? "see console"}`);
console.error("improvise: worker error", event);
});
shell.setShell(`in-page worker · ${DOC}`);
// Not yet a claim that it works: the first frame makes that (improvise-i0v9).
shell.setShell(`starting the worker · ${DOC}…`);
// Frames posted before the worker has finished `start()` are held by
// worker.js in arrival order, so the handshake can go out immediately.
return { send: (text) => worker.postMessage(text), ready: Promise.resolve() };
@@ -801,6 +802,7 @@ shell.arm(
WS_URL
? `${WS_URL} never answered — is improvise-ws-server running, and did it send a document?`
: "the in-page worker did not answer (see console)",
{ answer: WS_URL ? null : `in-page worker · ${DOC}` },
);
await transport.ready;
sendViewport(); // the handshake: the snapshot is built at this extent
+13 -7
View File
@@ -339,7 +339,12 @@ export function createShell({
clearTimer = clearTimeout,
}) {
let watchdog = null;
let answered = null;
let arms = 0;
const disarm = () => {
clearTimer(watchdog);
watchdog = null;
};
return {
setShell(text) {
@@ -350,24 +355,25 @@ export function createShell({
},
/** A peer that never answers the handshake. Deliberately about the *first
* frame*, not either peer's own failure modes — clients.md. */
arm(message) {
arm(message, { answer = null } = {}) {
arms += 1;
clearTimer(watchdog);
answered = answer;
disarm();
watchdog = setTimer(() => {
watchdog = null;
this.setShell(message);
}, timeoutMs);
},
/** A frame arrived, so the peer answered: the watchdog has no question
* left to ask. */
/** A frame arrived, so the peer answered. Only the FIRST, and only if no
* failure spoke first, may say so (improvise-i0v9, clients.md). */
sawFrame() {
clearTimer(watchdog);
watchdog = null;
if (watchdog !== null && answered !== null) this.setShell(answered);
disarm();
},
/** A peer that failed *loudly* has already said something more precise, so
* the watchdog must be DISARMED and not merely pre-empted (PR #13). */
fail(text) {
this.sawFrame();
disarm();
this.setShell(text);
},
armed: () => watchdog !== null,
+37
View File
@@ -351,6 +351,43 @@ test("a notice DISARMS the watchdog rather than being replaced by it", () => {
);
});
// improvise-i0v9: "in-page worker · <doc>" was written before the worker had
// answered anything, and read as success over a blank page for eight seconds.
test("only the first frame claims the peer answered, and not before it", () => {
const clock = fakeClock();
const seen = [];
const shell = createShell({
onShell: (t) => seen.push(t),
timeoutMs: 8000,
setTimer: clock.setTimer,
clearTimer: clock.clearTimer,
});
shell.arm("did not answer", { answer: "in-page worker · a.improv" });
assert.deepEqual(seen, [], "the claim was made before any frame arrived");
shell.sawFrame();
shell.sawFrame();
assert.deepEqual(seen, ["in-page worker · a.improv"]);
});
// The order worker.js really produces on a locked document: the ReadOnly
// notice is posted after `start()` and BEFORE the Snapshot answers the handshake.
test("a reason given before the first frame is not replaced by the frame's claim", () => {
const clock = fakeClock();
const seen = [];
const shell = createShell({
onShell: (t) => seen.push(t),
timeoutMs: 8000,
setTimer: clock.setTimer,
clearTimer: clock.clearTimer,
});
shell.arm("did not answer", { answer: "in-page worker · a.improv" });
shell.fail(noticeText({ kind: "ReadOnly", document: "a.improv", detail: "" }));
shell.sawFrame();
clock.fire();
assert.equal(seen.length, 1, `#status said: ${JSON.stringify(seen)}`);
assert.match(seen[0], /read-only/);
});
// The notice's remembered state. The value crosses `localStorage`, which is
// allowed to forget and allowed to throw, so the interesting inputs are the
// ones that are NOT either spelling.