Files
suaveplan/openspec/archive/2026-05-20-add-broadcast-channel-ask/tasks.md
2026-05-27 23:29:53 +01:00

6.0 KiB

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<TReq, TResp>(handlerKey, payload, opts?): Promise<TResp>. Calls recordAskSent, schedules timeout via TimersLike.setTimer, posts ask frame through the transport, returns the controller's promise.
  • 5.4 Implement public onAsk<TReq, TResp>(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.