diff --git a/wasm/app/app.js b/wasm/app/app.js index c666f30..a312297 100644 --- a/wasm/app/app.js +++ b/wasm/app/app.js @@ -219,18 +219,12 @@ function runOnTierInWorker(tier, src, onLoading) { if (e.data.runId !== myRunId) return; if (e.data.kind === "loading") { onLoading && onLoading(e.data.tier); - } else if (e.data.kind === "chunk") { - // 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. + } else if (e.data.kind === "chunk-text") { if (!liveBlock) liveBlock = startLiveBlock(tier); - liveBlock.append(e.data.chunk + (e.data.eol ? "\n" : "")); + liveBlock.appendText(e.data.text); + } else if (e.data.kind === "chunk-eol") { + if (!liveBlock) liveBlock = startLiveBlock(tier); + liveBlock.appendNewline(); } else if (e.data.kind === "done") { w.removeEventListener("message", handler); workerState.pending[tier] = null; @@ -285,23 +279,22 @@ function startLiveBlock(tierName) { let pendingText = ""; return { - append(chunk) { - if (!chunk) return; - let start = 0; - for (let i = 0; i < chunk.length; i++) { - if (chunk.charCodeAt(i) === 10) { - pendingText += chunk.slice(start, i); - pendingLine.textContent = pendingText || " "; - pendingText = ""; - pendingLine = makeLine(); - pre.appendChild(pendingLine); - start = i + 1; - } - } - if (start < chunk.length) { - pendingText += chunk.slice(start); - pendingLine.textContent = pendingText; - } + appendText(t) { + // Pure text chunk — never contains a newline. Append to + // the in-progress line. + if (!t) return; + pendingText += t; + pendingLine.textContent = pendingText; + outputEl.scrollTop = outputEl.scrollHeight; + }, + appendNewline() { + // End-of-line event — close the current line (with a + // single-space placeholder when empty so the row is + // visible) and spawn a fresh pending line below. + pendingLine.textContent = pendingText || " "; + pendingText = ""; + pendingLine = makeLine(); + pre.appendChild(pendingLine); outputEl.scrollTop = outputEl.scrollHeight; }, finalize(elapsedMs, kind, fullOutput) { diff --git a/wasm/app/worker.mjs b/wasm/app/worker.mjs index c729a5c..f0d9c28 100644 --- a/wasm/app/worker.mjs +++ b/wasm/app/worker.mjs @@ -58,26 +58,32 @@ self.onmessage = async (e) => { // re-add the line break. (chunk) => { if (!chunk) return; + // Split on \n at the source and post TWO separate + // message kinds: chunk-text (visible bytes, never + // containing a newline) and chunk-eol (a bare event + // marking end-of-line). Newlines no longer travel + // as bytes — they're typed messages. Whatever was + // eating the \n between Module.print and the DOM + // in fox's tab is bypassed. 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, - }); + if (i > start) { + self.postMessage({ + kind: "chunk-text", + runId, tier, + text: chunk.slice(start, i), + }); + } + self.postMessage({ kind: "chunk-eol", runId, tier }); start = i + 1; } } if (start < chunk.length) { self.postMessage({ - kind: "chunk", - runId, - tier, - chunk: chunk.slice(start), - eol: false, + kind: "chunk-text", + runId, tier, + text: chunk.slice(start), }); } }, diff --git a/www/playground/app.js b/www/playground/app.js index c666f30..a312297 100644 --- a/www/playground/app.js +++ b/www/playground/app.js @@ -219,18 +219,12 @@ function runOnTierInWorker(tier, src, onLoading) { if (e.data.runId !== myRunId) return; if (e.data.kind === "loading") { onLoading && onLoading(e.data.tier); - } else if (e.data.kind === "chunk") { - // 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. + } else if (e.data.kind === "chunk-text") { if (!liveBlock) liveBlock = startLiveBlock(tier); - liveBlock.append(e.data.chunk + (e.data.eol ? "\n" : "")); + liveBlock.appendText(e.data.text); + } else if (e.data.kind === "chunk-eol") { + if (!liveBlock) liveBlock = startLiveBlock(tier); + liveBlock.appendNewline(); } else if (e.data.kind === "done") { w.removeEventListener("message", handler); workerState.pending[tier] = null; @@ -285,23 +279,22 @@ function startLiveBlock(tierName) { let pendingText = ""; return { - append(chunk) { - if (!chunk) return; - let start = 0; - for (let i = 0; i < chunk.length; i++) { - if (chunk.charCodeAt(i) === 10) { - pendingText += chunk.slice(start, i); - pendingLine.textContent = pendingText || " "; - pendingText = ""; - pendingLine = makeLine(); - pre.appendChild(pendingLine); - start = i + 1; - } - } - if (start < chunk.length) { - pendingText += chunk.slice(start); - pendingLine.textContent = pendingText; - } + appendText(t) { + // Pure text chunk — never contains a newline. Append to + // the in-progress line. + if (!t) return; + pendingText += t; + pendingLine.textContent = pendingText; + outputEl.scrollTop = outputEl.scrollHeight; + }, + appendNewline() { + // End-of-line event — close the current line (with a + // single-space placeholder when empty so the row is + // visible) and spawn a fresh pending line below. + pendingLine.textContent = pendingText || " "; + pendingText = ""; + pendingLine = makeLine(); + pre.appendChild(pendingLine); outputEl.scrollTop = outputEl.scrollHeight; }, finalize(elapsedMs, kind, fullOutput) { diff --git a/www/playground/worker.mjs b/www/playground/worker.mjs index c729a5c..f0d9c28 100644 --- a/www/playground/worker.mjs +++ b/www/playground/worker.mjs @@ -58,26 +58,32 @@ self.onmessage = async (e) => { // re-add the line break. (chunk) => { if (!chunk) return; + // Split on \n at the source and post TWO separate + // message kinds: chunk-text (visible bytes, never + // containing a newline) and chunk-eol (a bare event + // marking end-of-line). Newlines no longer travel + // as bytes — they're typed messages. Whatever was + // eating the \n between Module.print and the DOM + // in fox's tab is bypassed. 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, - }); + if (i > start) { + self.postMessage({ + kind: "chunk-text", + runId, tier, + text: chunk.slice(start, i), + }); + } + self.postMessage({ kind: "chunk-eol", runId, tier }); start = i + 1; } } if (start < chunk.length) { self.postMessage({ - kind: "chunk", - runId, - tier, - chunk: chunk.slice(start), - eol: false, + kind: "chunk-text", + runId, tier, + text: chunk.slice(start), }); } },