# `AskResponderRegistry` ## Purpose `AskResponderRegistry` is the response-side counterpart to `AskController`. It maps an inbound `handlerKey` to the single responder function registered for that key via `TypedBroadcastChannel.onAsk(handlerKey, handler)` and enforces the package's "at most one responder per key" invariant. The single-responder rule is deliberate: multi-responder fan-out (`askMany()` / `askAll()`) is its own future capability with its own design surface, and forcing a second registration to throw `BroadcastChannelAskHandlerDuplicateError` makes collisions impossible to ignore at integration time. ## Features - Single-handler-per-key invariant enforced synchronously on the second `register()` call. - Returned disposer is identity-safe: it only removes the slot if the current occupant is still the handler the caller registered, so an out-of-order dispose after a re-register does nothing. - `resolve(key)` returns `undefined` for unregistered keys so the channel can drop the `ask` frame silently without any error path on the receiver. - `clear()` drops every responder, invoked once per `channel.close()`. ## Usage: Basic ```ts import { AskResponderRegistry } from "@suaveplan/broadcast-channel/ask/responder"; const registry = new AskResponderRegistry(); const dispose = registry.register<{ n: number }, { n: number }>( "increment", async ({ n }) => ({ n: n + 1 }) ); const handler = registry.resolve("increment"); if (handler !== undefined) { const out = await handler({ n: 41 }, { correlationId: "c1", senderId: "tab-a", handlerKey: "increment", }); // out = { n: 42 } } dispose(); ``` ## Usage: Advanced ```ts // Re-registration after disposal is fine; double-register without disposal // throws synchronously. const r = new AskResponderRegistry(); const d1 = r.register("k", () => 1); expect(() => r.register("k", () => 2)).toThrowError( /ASK_HANDLER_DUPLICATE/ ); d1(); const d2 = r.register("k", () => 3); // ok d2(); ``` ## API Reference ### `class AskResponderRegistry` - `register(key: string, handler: AskHandler): () => void` — registers `handler` for `key`; throws `BroadcastChannelAskHandlerDuplicateError` (code `BROADCAST_CHANNEL.ASK_HANDLER_DUPLICATE`) if `key` is already taken; returns an identity-safe disposer. - `resolve(key: string): AskHandler | undefined` — returns the registered handler or `undefined`. Receiver path drops unregistered `ask` frames. - `clear(): void` — drop every handler; called from `channel.close()`. ### `type AskHandler` ```ts type AskHandler = ( payload: TReq, ctx: AskHandlerContext ) => TResp | Promise; ``` The receiver may return a synchronous value or a Promise. The channel awaits the return value and posts the result as the `ask-response` frame's payload. ## Implementation Notes The registry deliberately exposes only `register` / `resolve` / `clear`; the "call the handler and post the response" logic lives on `TypedBroadcastChannel` because that is where the wire transport, the OTel counters, and the error projection live. Keeping the registry pure makes it trivial to unit-test in isolation: every test in `responder.test.ts` constructs a fresh registry, so state cannot leak between cases. Identity-safe disposal matters when a handler is re-registered after disposal in a long-running session — without the identity check, a delayed disposer from the previous registration would silently drop the new handler and the asker would see `ASK_TIMEOUT` errors with no obvious cause.