---
phase: 20-real-time-subscriptions
plan: 01
subsystem: api
tags: [trpc, sse, subscriptions, async-generators, streaming, event-bus]

requires:
  - phase: 01.1-hexagonal-events
    provides: EventBus port interface and EventEmitterBus adapter
  - phase: 16-frontend-scaffold
    provides: tRPC HTTP adapter and Vite proxy configuration

provides:
  - SSE streaming support in tRPC HTTP adapter
  - Three subscription procedures (onEvent, onAgentUpdate, onTaskUpdate)
  - EventBus-to-async-generator bridge (eventBusIterable helper)
  - SubscriptionEvent type in shared package for frontend consumption

affects: [20-02-frontend-subscriptions, 21-frontend-polish]

tech-stack:
  added: []
  patterns: [queue-based async generator for event bridge, tracked SSE for reconnection]

key-files:
  created:
    - src/trpc/subscriptions.ts
  modified:
    - src/server/trpc-adapter.ts
    - src/trpc/router.ts
    - packages/shared/src/types.ts
    - packages/shared/src/index.ts

key-decisions:
  - "Queue + deferred promise pattern for bridging push (EventBus) to pull (async generator)"
  - "tracked() wrapper on each event for client-side reconnection via lastEventId"
  - "Fallback AbortSignal when opts.signal is undefined (AbortController().signal)"
  - "Vite proxy works for SSE without changes (http-proxy streams by default)"

patterns-established:
  - "eventBusIterable: Reusable async generator factory for any subset of EventBus events"
  - "Subscription procedure pattern: input with optional lastEventId, yield* from eventBusIterable"

duration: 4min
completed: 2026-02-04
---

# Plan 20-01: SSE Streaming & Subscription Procedures Summary

**tRPC SSE streaming via async generators bridging EventBus domain events to three subscription endpoints**

## Performance

- **Duration:** 4 min
- **Started:** 2026-02-04
- **Completed:** 2026-02-04
- **Tasks:** 2
- **Files modified:** 5

## Accomplishments
- Fixed tRPC HTTP adapter to stream ReadableStream responses instead of buffering (required for SSE)
- Created subscriptions module with queue-based async generator bridging EventBus to tRPC subscriptions
- Added onEvent, onAgentUpdate, and onTaskUpdate subscription procedures to the router
- Exported SubscriptionEvent type from shared package for frontend consumption

## Task Commits

Each task was committed atomically:

1. **Task 1: Fix HTTP adapter for SSE streaming and add subscription procedures** - `e5d8dbb` (feat)
2. **Task 2: Update shared types and verify SSE integration** - `42154d6` (feat)

## Files Created/Modified
- `src/trpc/subscriptions.ts` - EventBus-to-async-generator bridge with tracked SSE events
- `src/server/trpc-adapter.ts` - Stream ReadableStream responses instead of buffering
- `src/trpc/router.ts` - Three subscription procedures (onEvent, onAgentUpdate, onTaskUpdate)
- `packages/shared/src/types.ts` - SubscriptionEvent interface for frontend
- `packages/shared/src/index.ts` - Re-export SubscriptionEvent

## Decisions Made
- 20-01: Queue + deferred promise pattern for bridging push (EventBus) to pull (async generator)
- 20-01: tracked() wrapper on each event for client-side reconnection via lastEventId
- 20-01: Fallback AbortSignal (new AbortController().signal) when opts.signal is undefined
- 20-01: Vite proxy works for SSE without changes (http-proxy streams chunked responses by default)

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] TrackedEnvelope type annotation**
- **Found during:** Task 1 (subscriptions module)
- **Issue:** `ReturnType<typeof tracked<string, SubscriptionEventData>>` is invalid TS — generic type inference on `typeof` of a generic function doesn't work
- **Fix:** Import `TrackedEnvelope` type directly from `@trpc/server` and use `TrackedEnvelope<SubscriptionEventData>` as return type
- **Files modified:** src/trpc/subscriptions.ts
- **Verification:** `npm run build` succeeds
- **Committed in:** e5d8dbb (Task 1 commit)

**2. [Rule 3 - Blocking] opts.signal potentially undefined**
- **Found during:** Task 1 (subscription procedures)
- **Issue:** `opts.signal` has type `AbortSignal | undefined` in tRPC subscription context, but eventBusIterable requires `AbortSignal`
- **Fix:** Fallback to `new AbortController().signal` when opts.signal is undefined
- **Files modified:** src/trpc/router.ts
- **Verification:** `npm run build` succeeds
- **Committed in:** e5d8dbb (Task 1 commit)

---

**Total deviations:** 2 auto-fixed (2 blocking)
**Impact on plan:** Both auto-fixes necessary for type safety. No scope creep.

## Issues Encountered
None

## User Setup Required
None - no external service configuration required.

## Next Phase Readiness
- Backend SSE streaming ready for frontend consumption
- Plan 20-02 can add httpSubscriptionLink to the tRPC client and wire subscription hooks
- All existing queries/mutations verified working (452 tests pass)

---
*Phase: 20-real-time-subscriptions*
*Completed: 2026-02-04*
