ruvocal: streamed answers freeze mid-render in hidden tabs (throttled smoothing timers + rAF-only DOM flush)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 72.8k
- Forks
- 8.6k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 82
Description
Summary
In the ruvocal web UI, a long streamed answer freezes mid-render whenever the browser tab is hidden (user switches tab/app while the answer generates). The server finishes and logs [mcp] completion stream closed + final answer emitted, but the page keeps showing "Stop generating" with the text cut off mid-table, and crawls at ~1 word/second — effectively forever for a long answer. Reloading shows the full message (it persisted fine), so users read this as a hang when it's a client render bug.
Environment
- ruflo 3.38.2 (npm), ruvocal from
ruflo/src/ruvocal, production build (vite build, adapter-node) - Chrome (macOS), any model/endpoint — reproduced with both an OpenAI-compatible shim and Ollama passthrough
Repro
- Start ruvocal, open a conversation, ask for a long answer (~800+ words, include a markdown table so the cutoff is obvious).
- While it streams, switch to another tab/app (i.e.
document.visibilityState === "hidden"). - Wait for the server log to print
[mcp] completion stream closed. - Page still shows "Stop generating"; rendered text grows at ~5.5 chars/sec (measured:
document.body.innerText.lengthgrew 33 chars over 6 s, minutes after stream close).
Root cause — two compounding hidden-tab defects
1. src/lib/utils/messageUpdates.ts → smoothStreamUpdates paces output with setTimeout-based sleeps of 5–80 ms per word chunk. Chrome clamps timers in hidden tabs to ≥1 s, so every chunk costs ≥1 s → a 20k-char answer takes ~1 hour instead of seconds. (The backlog-pressure fast path doesn't rescue it: while generating, the backlog is consumed one ≥1 s sleep at a time, so pressure never builds enough to zero the delays.)
2. src/routes/conversation/[id]/+page.svelte → scheduleFrameFlush flushes buffered tokens to the DOM only via requestAnimationFrame — and rAF never fires in hidden tabs, so even drained tokens don't reach the page.
Fix (verified locally)
Both changes are hidden-tab-scoped; visible-tab smoothing behavior is unchanged and all 14 existing messageUpdates.spec.ts cases still pass.
// messageUpdates.ts — consumer loop, before sleeping:
const pageHidden =
typeof document !== "undefined" && document.visibilityState === "hidden";
if (delayMs > 0 && !pageHidden) {
await sleep(delayMs);
}
// +page.svelte — scheduleFrameFlush: always arm a timeout fallback beside rAF
const flush = () => {
if (!frameFlushScheduled) return; // idempotent: rAF + timeout may both fire
frameFlushScheduled = false;
flushBuffer(new Date());
};
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(flush);
}
setTimeout(flush, 300);
After the fix, the same repro (tab hidden the entire run, 6.6k-token answer) renders completely within seconds of stream close, with the composer state correctly reset.
Happy to open a PR with these two changes if useful.
Related
- Filed the same day as #2990 (HTTP transport residuals); same local install.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/lib/utils/messageUpdates.ts and the smoothStreamUpdates consumer loop, then inspect scheduleFrameFlush in src/routes/conversation/[id]/+page.svelte. Run the existing messageUpdates.spec.ts cases and reproduce with document.visibilityState set to hidden. Done means long hidden-tab streams render completely and the composer leaves the “Stop generating” state after completion, without changing visible-tab smoothing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100