6.0 KiB
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 existingChannelMessageSchemashape viaz.discriminatedUnion("type", ...)-equivalent (orz.union) — adding two new variantsAskFrameSchema(withcorrelationId,handlerKey,payload,expiresAt) andAskResponseFrameSchema(withresponseTo,handlerKey,outcome,payload). Legacy frames (typeoftab-join/tab-leave/heartbeat/ user-defined string + arbitrarypayload) remain accepted. - 1.2 Add
AskFrame/AskResponseFrametype exports tosrc/types/broadcast-channel.types.ts. ExtendInternalMessageTypeunion by"ask"and"ask-response". - 1.3 Update
src/schemas/messages.test.ts— add scenarios: accepts well-formedask, accepts well-formedask-response, rejectsaskmissingcorrelationId, rejectsask-responsemissingresponseTo.
2. Error codes
- 2.1 Extend
BROADCAST_CHANNEL_CODESinsrc/errors/codes.tsbyASK_TIMEOUT,ASK_NO_RESPONDER,ASK_HANDLER_DUPLICATE. - 2.2 Add three error classes:
BroadcastChannelAskTimeoutError,BroadcastChannelAskNoResponderError,BroadcastChannelAskHandlerDuplicateError. Each defaults its message and acceptsBaseOpts. - 2.3 Update
src/errors/codes.test.ts—every code is thrown by a production pathto include the three new errors; registry-shape assertion grows to 7 codes.
3. OTel counters
- 3.1 In
src/otel/metrics.tsdeclare module-level countersaskSent,askResponded,askTimeout; expose three recorder functionsrecordAskSent(key, channelName),recordAskResponded(key, outcome, channelName),recordAskTimeout(key, channelName). UpdaterefreshBroadcastChannelMetersto re-resolve them. - 3.2 Update
src/otel/metrics.test.ts— add three scenarios:asksent incrementschannel.ask.sent, response incrementschannel.ask.respondedwithoutcomeattribute, timeout incrementschannel.ask.timeout.
4. ask feature module
- 4.1
src/ask/ask.ts—AskControllerclass owning the pending-ask registry: keyed bycorrelationId, each entry stores{ key, resolve, reject, timerHandle, requireResponder, respondersClaimed }. Exposesissue(handlerKey, payload, opts)andcancelAll(reason). Uses injectedClock,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—AskResponderRegistrymappinghandlerKey → handler. ThrowsBroadcastChannelAskHandlerDuplicateErroron second registration. Returns a disposer. - 4.4
src/ask/responder.md— co-located doc. - 4.5
src/ask/index.ts— explicit named re-exports (noexport *).
5. Wire into TypedBroadcastChannel
- 5.1 Add
timers?: TimersLiketoTypedBroadcastChannelOptions; default to{ setTimer: setTimeout, clearTimer: clearTimeout }(wrapped to satisfy the signature). - 5.2 Construct
AskControllerandAskResponderRegistryin the constructor. - 5.3 Implement
public ask<TReq, TResp>(handlerKey, payload, opts?): Promise<TResp>. CallsrecordAskSent, schedules timeout viaTimersLike.setTimer, postsaskframe through the transport, returns the controller's promise. - 5.4 Implement
public onAsk<TReq, TResp>(handlerKey, handler): () => void. ThrowsASK_HANDLER_DUPLICATEon double-registration viaAskResponderRegistry. - 5.5 Extend
handleIncoming— onaskframe: look up responder, if absent andrequireResponderwould matter, incrementrespondersClaimed(we let the asker's timeout decide). Onask-responseframe: resolve the matching pending ask, callrecordAskResponded(key, outcome, channelName). - 5.6 On
close(): callAskController.cancelAll("channel-closed")rejecting every pending promise withBroadcastChannelClosedError.
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 + fakeTimersLikedrives timeout →ASK_TIMEOUT;requireResponder: truewith no peer answers →ASK_NO_RESPONDER; handler-thrown error propagates asoutcome: "error"and rejects the ask; close() rejects pending asks withCHANNEL_CLOSED; defaultopts.timeoutMs = 5000. - 6.2
src/ask/responder.test.ts— register / dispose / re-register; duplicate throwsASK_HANDLER_DUPLICATE; non-matchinghandlerKeyis 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.tswith named exports:BroadcastChannelAskTimeoutError,BroadcastChannelAskNoResponderError,BroadcastChannelAskHandlerDuplicateError,recordAskSent,recordAskResponded,recordAskTimeout,AskFrameSchema,AskResponseFrameSchema, plus the two frame types. - 7.2 Add
./ask/ask,./ask/respondersubpath exports inpackage.jsonfor 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 Requirementsblock) intoopenspec/specs/broadcast-channel/spec.md. - 9.2 Move
openspec/changes/add-broadcast-channel-ask→openspec/archive/2026-05-20-add-broadcast-channel-ask.