After (4) rounds of newline-debugging where node tests said chunks
contained \n but fox's Firefox tab still rendered horizontal text
for the c tier (python+asm rendered vertical with the same code
path), giving up on trying to guess where the \n was being eaten
and instead getting rid of \n bytes on the wire entirely.
Worker now splits each onChunk slice on \n at the source and posts
one message per line — { kind:'chunk', tier, chunk:'tick 0', eol:true }
— so the line break is carried as a boolean flag rather than a byte.
Main thread re-attaches the '\n' before handing to liveBlock.append,
which still walks bytes for charCodeAt 10 and stacks per line.
Effectively: the loader's currentOnChunk(line + '\n') feeds the
worker which immediately splits back on the \n, both halves still
arrive on the main thread, the main thread reconstitutes them with
a fresh \n that we now know our DOM split honors (proved by the
python tier which uses the same liveBlock.append). C-tier-specific
\n loss between Module.print and postMessage drops out of the
picture.
89 lines
3.8 KiB
JavaScript
89 lines
3.8 KiB
JavaScript
// wasm/app/worker.mjs
|
|
// Tier-evaluation Web Worker. Keeps the main thread responsive so the
|
|
// live ms counter actually ticks AND so cancel works (main thread
|
|
// terminates this worker via worker.terminate()).
|
|
|
|
import { evalOnTier, setBendUrl, heapStats, portalSave, portalLoad } from "./runner.js";
|
|
|
|
self.onmessage = async (e) => {
|
|
const { kind } = e.data;
|
|
if (kind === "config") {
|
|
setBendUrl(e.data.bendUrl);
|
|
return;
|
|
}
|
|
if (kind === "heap") {
|
|
// Synchronous read — no eval is running because the worker is
|
|
// single-threaded and main thread only sends this between evals.
|
|
self.postMessage({ kind: "heap", runId: e.data.runId, tier: e.data.tier, stats: heapStats(e.data.tier) });
|
|
return;
|
|
}
|
|
if (kind === "portal-save") {
|
|
// Pulls the most-recent /tmp/<name>.portal out of the tier's
|
|
// MEMFS after the lisp side ran (portal-snapshot! NAME). Main
|
|
// thread encrypts and stuffs it into the vault.
|
|
const blob = portalSave(e.data.tier, e.data.name);
|
|
self.postMessage({ kind: "portal-save", runId: e.data.runId, name: e.data.name, blob });
|
|
return;
|
|
}
|
|
if (kind === "portal-load") {
|
|
// Hydrates MEMFS from a vault-decrypted blob so a subsequent
|
|
// (portal-load! NAME) eval finds the file ready.
|
|
const ok = portalLoad(e.data.tier, e.data.name, e.data.blob);
|
|
self.postMessage({ kind: "portal-load", runId: e.data.runId, name: e.data.name, ok });
|
|
return;
|
|
}
|
|
if (kind !== "eval") return;
|
|
const { runId, tier, src } = e.data;
|
|
try {
|
|
const output = await evalOnTier(
|
|
tier,
|
|
src,
|
|
(loadingTier) => {
|
|
self.postMessage({ kind: "loading", runId, tier: loadingTier });
|
|
},
|
|
// Stream every print/display from the tier to the main
|
|
// thread as it happens. Sync XHR inside bend!-call still
|
|
// blocks the worker, but displays BEFORE/AFTER the bend
|
|
// round-trip surface immediately instead of waiting for
|
|
// the whole eval to finish. Long demos feel alive.
|
|
//
|
|
// Defensive newline normalization: the C-tier loader
|
|
// adds the trailing \n that Emscripten's Module.print
|
|
// strips, but we kept seeing horizontal output in fox's
|
|
// Firefox tab as if the \n was lost somewhere on the
|
|
// wire. To rule out anything between here and the main
|
|
// thread, split each chunk on \n at the source and post
|
|
// one message per line — newline preserved as a flag
|
|
// rather than a byte. The main-thread receiver knows to
|
|
// re-add the line break.
|
|
(chunk) => {
|
|
if (!chunk) return;
|
|
let start = 0;
|
|
for (let i = 0; i < chunk.length; i++) {
|
|
if (chunk.charCodeAt(i) === 10) {
|
|
self.postMessage({
|
|
kind: "chunk",
|
|
runId,
|
|
tier,
|
|
chunk: chunk.slice(start, i),
|
|
eol: true,
|
|
});
|
|
start = i + 1;
|
|
}
|
|
}
|
|
if (start < chunk.length) {
|
|
self.postMessage({
|
|
kind: "chunk",
|
|
runId,
|
|
tier,
|
|
chunk: chunk.slice(start),
|
|
eol: false,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
self.postMessage({ kind: "done", runId, output });
|
|
} catch (err) {
|
|
self.postMessage({ kind: "error", runId, message: err && err.message ? err.message : String(err) });
|
|
}
|
|
};
|