3.5 KiB
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)returnsundefinedfor unregistered keys so the channel can drop theaskframe silently without any error path on the receiver.clear()drops every responder, invoked once perchannel.close().
Usage: Basic
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
// 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<TReq, TResp>(key: string, handler: AskHandler<TReq, TResp>): () => void— registershandlerforkey; throwsBroadcastChannelAskHandlerDuplicateError(codeBROADCAST_CHANNEL.ASK_HANDLER_DUPLICATE) ifkeyis already taken; returns an identity-safe disposer.resolve(key: string): AskHandler | undefined— returns the registered handler orundefined. Receiver path drops unregisteredaskframes.clear(): void— drop every handler; called fromchannel.close().
type AskHandler<TReq, TResp>
type AskHandler<TReq, TResp> = (
payload: TReq,
ctx: AskHandlerContext
) => TResp | Promise<TResp>;
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.