playground streaming: typed messages, no \n bytes on the wire
Going nuclear on the c-tier-horizontal-output saga. Six attempts at
preserving the \n byte from Module.print through Web Worker
postMessage to the DOM all reported clean chunks in node tests but
still rendered horizontal in fox's Firefox tab.
Removing \n bytes from the chunk-transmission path entirely. Worker
walks each onChunk slice byte-by-byte and posts two separate kinds
of message:
{ kind: 'chunk-text', tier, text: 'tick 0' } <- visible chars only
{ kind: 'chunk-eol', tier } <- bare line break
Newlines no longer travel as bytes — they're typed events. Whatever
was eating them between Emscripten's TTY out() and the main thread
in fox's browser drops out of the path entirely.
liveBlock.append split into appendText (extend the in-progress
line) and appendNewline (close pending div, spawn fresh sibling).
Python tier paths through the same worker code so it gets the same
typed-event treatment — should still render vertical because that's
what it was doing already.
This commit is contained in:
parent
89d4877ee9
commit
25d2765ed9
4 changed files with 78 additions and 80 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue