# 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 of `send`/`on`. Will land in a follow-up change with `BROADCAST_CHANNEL.ASK_TIMEOUT` code. 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(handlerKey, payload, opts?) → Promise` with injectable timeout, correlation IDs, and optional `requireResponder` guard. - ADDED: `TypedBroadcastChannel.onAsk(handlerKey, handler)` — single-handler-per-key registration. - ADDED: Two new wire-frame variants — `ask` (carrying `correlationId`, `handlerKey`, `payload`, `expiresAt`) and `ask-response` (carrying `responseTo`, `handlerKey`, `outcome: "ok" | "error"`, `payload`). - ADDED: Frame schema extended via discriminated union — `ChannelMessageSchema` is widened, not replaced; all existing `send` / `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-channel` meter. - ADDED: Injectable `TimersLike` on `TypedBroadcastChannelOptions` (default backed by global `setTimeout` / `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` / `onTabBlur` hooks. ## 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 two `TypedBroadcastChannel`s sharing a fake transport pair; correlation-ID isolation under concurrent calls; timeout via FakeClock-driven `TimersLike`; `requireResponder` flag toggling `ASK_NO_RESPONDER` vs. `ASK_TIMEOUT`; handler-thrown errors propagate as `ask-response` `outcome: "error"` and reject the caller; cleanup on `close()` rejects all pending asks. - `src/ask/responder.test.ts` — `onAsk` registration; duplicate registration throws `ASK_HANDLER_DUPLICATE`; unregister via returned disposer; non-matching `ask` frames are dropped silently. - `src/schemas/messages.test.ts` — widened union accepts both legacy frames and new `ask` / `ask-response` variants; malformed `ask` (missing `correlationId`) rejected. - `src/otel/metrics.test.ts` — three new counter names observed via in-memory `MeterProvider`. - `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.