5.9 KiB
Add @suaveplan/broadcast-channel ask() request/response
Package: @suaveplan/broadcast-channel
Status: PROPOSED
Type: capability-extension
Spec delta: ./specs/broadcast-channel/spec.md
Why
The initial @suaveplan/broadcast-channel ship (openspec/archive/2026-05-19-add-broadcast-channel/)
explicitly deferred the ask() request/response helper under "Out of scope":
ask()request/response — not in the four mandated gap fixes; consumers can layer correlation-ID on top ofsend/on. Will land in a follow-up change withBROADCAST_CHANNEL.ASK_TIMEOUTcode.
Consumers (cross-tab leader queries, per-tab capability probes, "who-has-feature-X?" fan-outs)
currently have to re-implement correlation-ID + timeout plumbing on top of send / on. That is
exactly the duplication the package exists to eliminate, and it is the one §4 enhancement that is
small, self-contained, and trivially testable with deterministic injectables. This change ships
ONLY ask() + onAsk(). Every other §4 item (leader-election strategies, replay buffer, CRDT,
encryption, compression, namespacing, dev overlay, presence hooks) remains deferred to its own
dedicated change.
What changes
- ADDED:
TypedBroadcastChannel.ask<TReq, TResp>(handlerKey, payload, opts?) → Promise<TResp>with injectable timeout, correlation IDs, and optionalrequireResponderguard. - ADDED:
TypedBroadcastChannel.onAsk(handlerKey, handler)— single-handler-per-key registration. - ADDED: Two new wire-frame variants —
ask(carryingcorrelationId,handlerKey,payload,expiresAt) andask-response(carryingresponseTo,handlerKey,outcome: "ok" | "error",payload). - ADDED: Frame schema extended via discriminated union —
ChannelMessageSchemais widened, not replaced; all existingsend/on/ heartbeat paths remain bit-compatible. - ADDED: Three new error codes —
BROADCAST_CHANNEL.ASK_TIMEOUT,BROADCAST_CHANNEL.ASK_NO_RESPONDER,BROADCAST_CHANNEL.ASK_HANDLER_DUPLICATE— each thrown by ≥1 production path. - ADDED: Three new OTel counters —
channel.ask.sent{key},channel.ask.responded{key,outcome},channel.ask.timeout{key}— under the existing@suaveplan/broadcast-channelmeter. - ADDED: Injectable
TimersLikeonTypedBroadcastChannelOptions(default backed by globalsetTimeout/clearTimeout) so timeout tests run deterministically against a FakeClock-driven timer pair.
Out of scope
- Multi-responder fan-out collection (
askMany()/askAll()). - Streaming / progressive responses (responses are one-shot per
correlationId). - Leader-election strategies (round-robin, active-tab-wins).
- Replay buffer, CRDT adapter, encryption hook, compression, namespacing, dev overlay,
onTabFocus/onTabBlurhooks.
Source mapping
This change derives from ../suaveplan/enterprise/broadcast-channel.md §4.1 and the initial
ship's "Out of scope" deferral list.
| Requirement | Source lines / symbol | Notes |
|---|---|---|
ask() request/response |
enterprise.md §4.1 ("Request/response (ask) primitive with correlation ID, timeout, and typed response.") |
§4 enhancement #1, deferred from initial ship |
ASK_TIMEOUT code |
initial proposal "Out of scope" line referencing BROADCAST_CHANNEL.ASK_TIMEOUT |
explicit follow-up |
Single-handler onAsk registration |
genesis design choice — multi-responder is its own future change | genesis-original |
| Three OTel counters | genesis cross-cutting (rule 6 of CLAUDE.md — every hot path gets a counter) | genesis-original |
Injectable TimersLike |
@suaveplan/types::TimersLike; CLAUDE.md rule 2 (no real timers in tests) |
genesis cross-cutting |
Requirements with no source line
| Requirement | Justification |
|---|---|
requireResponder config flag → ASK_NO_RESPONDER |
Genesis safety knob: callers that must have a peer answer (e.g. leader-only queries) need a distinct error code from a generic timeout to drive retry policy. |
| 100% Istanbul coverage on touched modules | Genesis-wide policy (AGENTS.md §7) |
@suaveplan/testing/runner test imports |
Genesis-wide rule 11 / CLAUDE.md |
Verification approach
src/ask/ask.test.ts—ask()happy path round-trip with twoTypedBroadcastChannels sharing a fake transport pair; correlation-ID isolation under concurrent calls; timeout via FakeClock-drivenTimersLike;requireResponderflag togglingASK_NO_RESPONDERvs.ASK_TIMEOUT; handler-thrown errors propagate asask-responseoutcome: "error"and reject the caller; cleanup onclose()rejects all pending asks.src/ask/responder.test.ts—onAskregistration; duplicate registration throwsASK_HANDLER_DUPLICATE; unregister via returned disposer; non-matchingaskframes are dropped silently.src/schemas/messages.test.ts— widened union accepts both legacy frames and newask/ask-responsevariants; malformedask(missingcorrelationId) rejected.src/otel/metrics.test.ts— three new counter names observed via in-memoryMeterProvider.src/errors/codes.test.ts— registry size + 7-code shape; every new code thrown by a real path.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/broadcast-channel to enumerate. The package is browser-tier
and has no internal dependents on genesis at the time of this change.
- No dependents (verified by
bunx turbo ls --filter=...@suaveplan/broadcast-channel— package is a leaf browser-tier addition that landed 2026-05-19; nothing else has wired against it yet).
Migration impact
None — additive. Existing send / on / leader / heartbeat call sites are byte-compatible: the
frame schema widens via discriminated union, the BROADCAST_CHANNEL_CODES registry grows by three
codes (existing codes unchanged), and the OTel meter gains three counters (existing six unchanged).
No public types are renamed or removed.