103 lines
5.9 KiB
Markdown
103 lines
5.9 KiB
Markdown
# 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<TReq, TResp>(handlerKey, payload, opts?) → Promise<TResp>` 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.
|