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

103 lines
6.0 KiB
Markdown

# 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`.