first commit
This commit is contained in:
102
docs/networking/events/broadcast-channel/src/ask/responder.md
Normal file
102
docs/networking/events/broadcast-channel/src/ask/responder.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# `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<TReq, TResp>(key: string, handler: AskHandler<TReq, TResp>): () => 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<TReq, TResp>`
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user