From 89d4877ee9e19da54bf6dc3eebcee058a24b4b80 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 15 Jun 2026 06:24:57 -0400 Subject: [PATCH] playground streaming: split chunks on \n in the worker, send eol flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- wasm/app/app.js | 8 +++++++- wasm/app/worker.mjs | 34 +++++++++++++++++++++++++++++++++- www/playground/app.js | 8 +++++++- www/playground/worker.mjs | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/wasm/app/app.js b/wasm/app/app.js index 67a172e..c666f30 100644 --- a/wasm/app/app.js +++ b/wasm/app/app.js @@ -223,8 +223,14 @@ function runOnTierInWorker(tier, src, onLoading) { // First chunk spawns the in-progress block; subsequent // chunks append to it. User sees displays land // immediately, not just at the end of the run. + // Worker splits chunks on \n at the source and sends + // each line with an eol flag — re-attach the newline + // here so liveBlock.append still walks bytes looking + // for charCodeAt 10 and stacks per line. Avoids any + // unknown-quantity newline loss between Module.print + // and the main thread. if (!liveBlock) liveBlock = startLiveBlock(tier); - liveBlock.append(e.data.chunk); + liveBlock.append(e.data.chunk + (e.data.eol ? "\n" : "")); } else if (e.data.kind === "done") { w.removeEventListener("message", handler); workerState.pending[tier] = null; diff --git a/wasm/app/worker.mjs b/wasm/app/worker.mjs index 795f0eb..c729a5c 100644 --- a/wasm/app/worker.mjs +++ b/wasm/app/worker.mjs @@ -46,8 +46,40 @@ self.onmessage = async (e) => { // 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) => { - self.postMessage({ kind: "chunk", runId, tier, 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 }); diff --git a/www/playground/app.js b/www/playground/app.js index 67a172e..c666f30 100644 --- a/www/playground/app.js +++ b/www/playground/app.js @@ -223,8 +223,14 @@ function runOnTierInWorker(tier, src, onLoading) { // First chunk spawns the in-progress block; subsequent // chunks append to it. User sees displays land // immediately, not just at the end of the run. + // Worker splits chunks on \n at the source and sends + // each line with an eol flag — re-attach the newline + // here so liveBlock.append still walks bytes looking + // for charCodeAt 10 and stacks per line. Avoids any + // unknown-quantity newline loss between Module.print + // and the main thread. if (!liveBlock) liveBlock = startLiveBlock(tier); - liveBlock.append(e.data.chunk); + liveBlock.append(e.data.chunk + (e.data.eol ? "\n" : "")); } else if (e.data.kind === "done") { w.removeEventListener("message", handler); workerState.pending[tier] = null; diff --git a/www/playground/worker.mjs b/www/playground/worker.mjs index 795f0eb..c729a5c 100644 --- a/www/playground/worker.mjs +++ b/www/playground/worker.mjs @@ -46,8 +46,40 @@ self.onmessage = async (e) => { // 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) => { - self.postMessage({ kind: "chunk", runId, tier, 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 });