diff --git a/wasm/app/worker.mjs b/wasm/app/worker.mjs index a5f39b7..cae8b53 100644 --- a/wasm/app/worker.mjs +++ b/wasm/app/worker.mjs @@ -55,13 +55,22 @@ self.onmessage = async (e) => { // calls. const CHUNK_FLUSH_BYTES = 65536; const CHUNK_FLUSH_NEWLINES = 4096; + // Wall-clock flush — guarantees the first emission lands on the UI + // immediately and caps low-rate streams at ~10 flushes/sec. Without + // this, a (let loop ...) that emits every 10K iterations would + // accumulate 30 bytes per emit, take 2000+ emits to hit the 64KB + // threshold, and the user sees nothing for seconds. lastFlushTime + // starts at 0 so the first chunk to land always triggers a flush. + const FLUSH_INTERVAL_MS = 100; let chunkBuffer = ""; let chunkNewlines = 0; + let lastFlushTime = 0; function flushChunkBuffer() { if (!chunkBuffer) return; self.postMessage({ kind: "chunk-batch", runId, tier, text: chunkBuffer }); chunkBuffer = ""; chunkNewlines = 0; + lastFlushTime = Date.now(); } try { const output = await evalOnTier( @@ -77,7 +86,8 @@ self.onmessage = async (e) => { if (chunk.charCodeAt(i) === 10) chunkNewlines++; } if (chunkBuffer.length >= CHUNK_FLUSH_BYTES - || chunkNewlines >= CHUNK_FLUSH_NEWLINES) { + || chunkNewlines >= CHUNK_FLUSH_NEWLINES + || (Date.now() - lastFlushTime) >= FLUSH_INTERVAL_MS) { flushChunkBuffer(); } }, diff --git a/wasm/dist-repl/worker.mjs b/wasm/dist-repl/worker.mjs index a5f39b7..cae8b53 100644 --- a/wasm/dist-repl/worker.mjs +++ b/wasm/dist-repl/worker.mjs @@ -55,13 +55,22 @@ self.onmessage = async (e) => { // calls. const CHUNK_FLUSH_BYTES = 65536; const CHUNK_FLUSH_NEWLINES = 4096; + // Wall-clock flush — guarantees the first emission lands on the UI + // immediately and caps low-rate streams at ~10 flushes/sec. Without + // this, a (let loop ...) that emits every 10K iterations would + // accumulate 30 bytes per emit, take 2000+ emits to hit the 64KB + // threshold, and the user sees nothing for seconds. lastFlushTime + // starts at 0 so the first chunk to land always triggers a flush. + const FLUSH_INTERVAL_MS = 100; let chunkBuffer = ""; let chunkNewlines = 0; + let lastFlushTime = 0; function flushChunkBuffer() { if (!chunkBuffer) return; self.postMessage({ kind: "chunk-batch", runId, tier, text: chunkBuffer }); chunkBuffer = ""; chunkNewlines = 0; + lastFlushTime = Date.now(); } try { const output = await evalOnTier( @@ -77,7 +86,8 @@ self.onmessage = async (e) => { if (chunk.charCodeAt(i) === 10) chunkNewlines++; } if (chunkBuffer.length >= CHUNK_FLUSH_BYTES - || chunkNewlines >= CHUNK_FLUSH_NEWLINES) { + || chunkNewlines >= CHUNK_FLUSH_NEWLINES + || (Date.now() - lastFlushTime) >= FLUSH_INTERVAL_MS) { flushChunkBuffer(); } }, diff --git a/www/playground/worker.mjs b/www/playground/worker.mjs index a5f39b7..cae8b53 100644 --- a/www/playground/worker.mjs +++ b/www/playground/worker.mjs @@ -55,13 +55,22 @@ self.onmessage = async (e) => { // calls. const CHUNK_FLUSH_BYTES = 65536; const CHUNK_FLUSH_NEWLINES = 4096; + // Wall-clock flush — guarantees the first emission lands on the UI + // immediately and caps low-rate streams at ~10 flushes/sec. Without + // this, a (let loop ...) that emits every 10K iterations would + // accumulate 30 bytes per emit, take 2000+ emits to hit the 64KB + // threshold, and the user sees nothing for seconds. lastFlushTime + // starts at 0 so the first chunk to land always triggers a flush. + const FLUSH_INTERVAL_MS = 100; let chunkBuffer = ""; let chunkNewlines = 0; + let lastFlushTime = 0; function flushChunkBuffer() { if (!chunkBuffer) return; self.postMessage({ kind: "chunk-batch", runId, tier, text: chunkBuffer }); chunkBuffer = ""; chunkNewlines = 0; + lastFlushTime = Date.now(); } try { const output = await evalOnTier( @@ -77,7 +86,8 @@ self.onmessage = async (e) => { if (chunk.charCodeAt(i) === 10) chunkNewlines++; } if (chunkBuffer.length >= CHUNK_FLUSH_BYTES - || chunkNewlines >= CHUNK_FLUSH_NEWLINES) { + || chunkNewlines >= CHUNK_FLUSH_NEWLINES + || (Date.now() - lastFlushTime) >= FLUSH_INTERVAL_MS) { flushChunkBuffer(); } }, diff --git a/www/repl/worker.mjs b/www/repl/worker.mjs index a5f39b7..cae8b53 100644 --- a/www/repl/worker.mjs +++ b/www/repl/worker.mjs @@ -55,13 +55,22 @@ self.onmessage = async (e) => { // calls. const CHUNK_FLUSH_BYTES = 65536; const CHUNK_FLUSH_NEWLINES = 4096; + // Wall-clock flush — guarantees the first emission lands on the UI + // immediately and caps low-rate streams at ~10 flushes/sec. Without + // this, a (let loop ...) that emits every 10K iterations would + // accumulate 30 bytes per emit, take 2000+ emits to hit the 64KB + // threshold, and the user sees nothing for seconds. lastFlushTime + // starts at 0 so the first chunk to land always triggers a flush. + const FLUSH_INTERVAL_MS = 100; let chunkBuffer = ""; let chunkNewlines = 0; + let lastFlushTime = 0; function flushChunkBuffer() { if (!chunkBuffer) return; self.postMessage({ kind: "chunk-batch", runId, tier, text: chunkBuffer }); chunkBuffer = ""; chunkNewlines = 0; + lastFlushTime = Date.now(); } try { const output = await evalOnTier( @@ -77,7 +86,8 @@ self.onmessage = async (e) => { if (chunk.charCodeAt(i) === 10) chunkNewlines++; } if (chunkBuffer.length >= CHUNK_FLUSH_BYTES - || chunkNewlines >= CHUNK_FLUSH_NEWLINES) { + || chunkNewlines >= CHUNK_FLUSH_NEWLINES + || (Date.now() - lastFlushTime) >= FLUSH_INTERVAL_MS) { flushChunkBuffer(); } },