first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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.