Initial Commit
This commit is contained in:
102
openspec/archive/2026-05-20-add-broadcast-channel-ask/tasks.md
Normal file
102
openspec/archive/2026-05-20-add-broadcast-channel-ask/tasks.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user