Skip to content

fix: decouple stream listeners from stream entry lifecycle#183

Merged
op7418 merged 1 commit intoop7418:mainfrom
jasonjaclyn2017:fix/decouple-stream-listeners
Mar 7, 2026
Merged

fix: decouple stream listeners from stream entry lifecycle#183
op7418 merged 1 commit intoop7418:mainfrom
jasonjaclyn2017:fix/decouple-stream-listeners

Conversation

@jasonjaclyn2017
Copy link
Copy Markdown
Contributor

Summary

Stream event listeners were stored inside ActiveStream objects, which get garbage collected 5 minutes after completion. When a new stream started after GC — especially during Fast Refresh/HMR in development — startStream created a fresh ActiveStream entry with an empty listener set. This caused the UI to freeze with no updates delivered, since the component's subscription was attached to the old (now-deleted) entry.

Root cause

stream.listeners lived on the ActiveStream object itself. The GC lifecycle (scheduleGCdelete streams[sessionId]) destroyed the listener set along with the stream. Any component that subscribed before GC but received events after GC would silently lose its subscription.

Fix

Moved listeners to a separate globalThis-persisted registry (__streamSessionListeners__) that survives stream entry GC. The subscribe and emit functions now read/write from this independent map instead of stream.listeners.

This means:

  • Stream entries can be created and destroyed freely without affecting active UI subscriptions
  • Listeners persist as long as the component is mounted, regardless of stream lifecycle
  • No behavioral change for normal conversations — only affects the edge case where GC runs between stream completion and the next message

Files changed

  • src/lib/stream-session-manager.ts — extract listener registry from ActiveStream to globalThis.__streamSessionListeners__

Test plan

  • Start a conversation, let it complete, wait >5 minutes (or temporarily reduce GC timeout), then send a new message — UI should receive streaming updates normally
  • Normal back-and-forth conversation should work identically to before
  • npm run test passes (typecheck + unit tests)

🤖 Generated with Claude Code

Stream event listeners were stored inside ActiveStream objects, which get
garbage collected 5 minutes after completion. When a new stream started
after GC (especially during Fast Refresh/HMR), startStream created a
fresh entry with an empty listener set, causing the UI to freeze with
no updates delivered.

Move listeners to a separate globalThis-persisted registry
(__streamSessionListeners__) that survives stream entry GC. The subscribe
and emit functions now read/write from this independent map instead of
stream.listeners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@op7418 op7418 merged commit 03d2636 into op7418:main Mar 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants