feat(chat): stream AI replies over SSE (#159) - #160
Conversation
…e-watchdog retry - shared: chatStreamEventSchema, incremental SSE parser + stream consumer, CHAT_STREAM_IDLE_TIMEOUT_MS (replaces CHAT_SEND_TIMEOUT_MS), chat-store updateMessage/appendToMessageContent - web: streaming BFF route (the catch-all proxy buffers), composer streams into a draft bubble with a 60s idle watchdog; sendChatMessage server action removed - mobile: expo/fetch transport with SecureStore auth + single 401 refresh; composer mirrors web Closes #159 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Solid end-to-end streaming implementation. The shared SSE parser and consumer are well-factored: CRLF normalization happens at concatenation time (so cross-chunk splits are handled correctly), unknown event types are silently dropped for forward compat, and the incomplete outcome cleanly covers the half-open case. Both platform hooks are structurally identical — same idle watchdog, same 401 refresh path, same final-always-wins semantics — which satisfies the cross-platform parity rule. The streaming BFF route correctly pipes the upstream body without buffering, mirrors the catch-all's auth pattern, and the x-accel-buffering: no header is the right touch for Nginx deployments. CHAT_SEND_TIMEOUT_MS and sendChatMessage are fully removed with zero remaining callers. Test coverage is thorough: fragmented frames, CRLF splits, comments/unknown events, watchdog fire, limit-no-retry, retry-no-duplicate-user-bubble, and pre-stream HTTP classification are all exercised.
|



Closes #159. Paired backend PR: thomasluizon/orbit-api#199 (merge that one first — the SSE endpoint must be live before these clients consume it).
Also carries the fix for the reported "liste meus hábitos de hoje is missing habits" bug — that fix is backend prompt rules in the paired PR (no client change needed); details there.
What streams now
Send a message → tokens render into the AI bubble as they arrive, on web and mobile identically. Typing dots show until the first token, then the bubble grows; tool-calling turns keep the dots during tool work (the backend emits
roundkeepalives) and stream only the final answer.chatStreamEventSchema(mirrors the backend record — contract-aligner verified 0 drift), an incremental SSE parser (createChatSseParser) + stream consumer (consumeChatSseStream), new chat-store actionsappendToMessageContent/updateMessage, andCHAT_STREAM_IDLE_TIMEOUT_MS = 60sreplacing the whole-responseCHAT_SEND_TIMEOUT_MS.app/api/chat/stream/route.tsthat pipes the upstream body straight through (the catch-all proxy buffers, which would defeat streaming) with the same cookie-auth + single 401-refresh as the catch-all. The composer fetches it directly; thesendChatMessageServer Action is deleted (Server Actions can't stream).lib/chat-stream.tsonexpo/fetch(RN's built-in fetch has no readable response body) with SecureStore bearer + single refresh rotation.TextDecodercomes from Expo's winter runtime — no polyfill needed.Failure semantics (the #156 affordances survive)
roundevents covering long tool work. Firing aborts → 408 → the existing timeout copy + Retry button.errorevents and pre-stream HTTP failures feed the sameclassifySendFailureflow: timeout/generic arm Retry, limit/upgrade don't, premium denials still route to /upgrade.finalevent ALWAYS replaces the streamed draft content (covers the backend'sStripJsonWrapperedge) and attaches actions/correlationId/relatedSurfaces; aresetclears the draft when a round turns out to be a tool round.aiMessagesUsedbump, query invalidations: unchanged.Tests
fetch+ realReadableStreamSSE bytes: incremental render, reset-no-duplicate, idle-watchdog timeout + retry, limit no-retry, retry-no-duplicate-user-bubble, pre-stream classification. Suite: 1,460.openChatStreamseam + offline gate + quota + pending-ops. Suite: 415.Manual verification checklist (auth-walled, needs your eyes)
First content token after Nmswith N ≤ ~2000 after the final round.AI API respondedround in Render logs.[trace: …]in the email.🤖 Generated with Claude Code