# Tasks — add-broadcast-channel-ask Sequential. No ticks may be authored retroactively. ## 1. Schema and types - [ ] 1.1 Extend `src/schemas/messages.ts` — keep the existing `ChannelMessageSchema` shape via `z.discriminatedUnion("type", ...)`-equivalent (or `z.union`) — adding two new variants `AskFrameSchema` (with `correlationId`, `handlerKey`, `payload`, `expiresAt`) and `AskResponseFrameSchema` (with `responseTo`, `handlerKey`, `outcome`, `payload`). Legacy frames (`type` of `tab-join` / `tab-leave` / `heartbeat` / user-defined string + arbitrary `payload`) remain accepted. - [ ] 1.2 Add `AskFrame` / `AskResponseFrame` type exports to `src/types/broadcast-channel.types.ts`. Extend `InternalMessageType` union by `"ask"` and `"ask-response"`. - [ ] 1.3 Update `src/schemas/messages.test.ts` — add scenarios: accepts well-formed `ask`, accepts well-formed `ask-response`, rejects `ask` missing `correlationId`, rejects `ask-response` missing `responseTo`. ## 2. Error codes - [ ] 2.1 Extend `BROADCAST_CHANNEL_CODES` in `src/errors/codes.ts` by `ASK_TIMEOUT`, `ASK_NO_RESPONDER`, `ASK_HANDLER_DUPLICATE`. - [ ] 2.2 Add three error classes: `BroadcastChannelAskTimeoutError`, `BroadcastChannelAskNoResponderError`, `BroadcastChannelAskHandlerDuplicateError`. Each defaults its message and accepts `BaseOpts`. - [ ] 2.3 Update `src/errors/codes.test.ts` — `every code is thrown by a production path` to include the three new errors; registry-shape assertion grows to 7 codes. ## 3. OTel counters - [ ] 3.1 In `src/otel/metrics.ts` declare module-level counters `askSent`, `askResponded`, `askTimeout`; expose three recorder functions `recordAskSent(key, channelName)`, `recordAskResponded(key, outcome, channelName)`, `recordAskTimeout(key, channelName)`. Update `refreshBroadcastChannelMeters` to re-resolve them. - [ ] 3.2 Update `src/otel/metrics.test.ts` — add three scenarios: `ask` sent increments `channel.ask.sent`, response increments `channel.ask.responded` with `outcome` attribute, timeout increments `channel.ask.timeout`. ## 4. `ask` feature module - [ ] 4.1 `src/ask/ask.ts` — `AskController` class owning the pending-ask registry: keyed by `correlationId`, each entry stores `{ key, resolve, reject, timerHandle, requireResponder, respondersClaimed }`. Exposes `issue(handlerKey, payload, opts)` and `cancelAll(reason)`. Uses injected `Clock`, `Rng`, `TimersLike`. - [ ] 4.2 `src/ask/ask.md` — co-located doc ≥200 words covering Purpose / Features / Usage:Basic / Usage:Advanced / API Reference / Implementation Notes. - [ ] 4.3 `src/ask/responder.ts` — `AskResponderRegistry` mapping `handlerKey → handler`. Throws `BroadcastChannelAskHandlerDuplicateError` on second registration. Returns a disposer. - [ ] 4.4 `src/ask/responder.md` — co-located doc. - [ ] 4.5 `src/ask/index.ts` — explicit named re-exports (no `export *`). ## 5. Wire into `TypedBroadcastChannel` - [ ] 5.1 Add `timers?: TimersLike` to `TypedBroadcastChannelOptions`; default to `{ setTimer: setTimeout, clearTimer: clearTimeout }` (wrapped to satisfy the signature). - [ ] 5.2 Construct `AskController` and `AskResponderRegistry` in the constructor. - [ ] 5.3 Implement `public ask(handlerKey, payload, opts?): Promise`. Calls `recordAskSent`, schedules timeout via `TimersLike.setTimer`, posts `ask` frame through the transport, returns the controller's promise. - [ ] 5.4 Implement `public onAsk(handlerKey, handler): () => void`. Throws `ASK_HANDLER_DUPLICATE` on double-registration via `AskResponderRegistry`. - [ ] 5.5 Extend `handleIncoming` — on `ask` frame: look up responder, if absent and `requireResponder` would matter, increment `respondersClaimed` (we let the asker's timeout decide). On `ask-response` frame: resolve the matching pending ask, call `recordAskResponded(key, outcome, channelName)`. - [ ] 5.6 On `close()`: call `AskController.cancelAll("channel-closed")` rejecting every pending promise with `BroadcastChannelClosedError`. ## 6. Tests - [ ] 6.1 `src/ask/ask.test.ts` — happy path with two channels sharing a fanout-fake transport; correlation-ID isolation under interleaved asks; FakeClock + fake `TimersLike` drives timeout → `ASK_TIMEOUT`; `requireResponder: true` with no peer answers → `ASK_NO_RESPONDER`; handler-thrown error propagates as `outcome: "error"` and rejects the ask; close() rejects pending asks with `CHANNEL_CLOSED`; default `opts.timeoutMs = 5000`. - [ ] 6.2 `src/ask/responder.test.ts` — register / dispose / re-register; duplicate throws `ASK_HANDLER_DUPLICATE`; non-matching `handlerKey` is dropped silently (no asker error, no outbound frame); registered handler receives `(payload, ctx)` and may return a Promise. ## 7. Barrel and exports - [ ] 7.1 Extend `src/index.ts` with named exports: `BroadcastChannelAskTimeoutError`, `BroadcastChannelAskNoResponderError`, `BroadcastChannelAskHandlerDuplicateError`, `recordAskSent`, `recordAskResponded`, `recordAskTimeout`, `AskFrameSchema`, `AskResponseFrameSchema`, plus the two frame types. - [ ] 7.2 Add `./ask/ask`, `./ask/responder` subpath exports in `package.json` for tree-shaking. ## 8. Pre-ship gates - [ ] 8.1 `bunx biome check src/` — clean (zero diagnostics). - [ ] 8.2 `bunx turbo lint typecheck test --filter=@suaveplan/broadcast-channel` — green. - [ ] 8.3 `bunx turbo test:coverage --filter=@suaveplan/broadcast-channel` — 100/100/100/100. - [ ] 8.4 `bun scripts/openspec-conformance.ts @suaveplan/broadcast-channel` — zero errors. - [ ] 8.5 `bunx turbo build --filter=@suaveplan/broadcast-channel` — green. ## 9. Archive - [ ] 9.1 Fold `specs/broadcast-channel/spec.md` (`## ADDED Requirements` block) into `openspec/specs/broadcast-channel/spec.md`. - [ ] 9.2 Move `openspec/changes/add-broadcast-channel-ask` → `openspec/archive/2026-05-20-add-broadcast-channel-ask`.