Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
# `broker` — multi-host fan-out contract
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
A single-host deployment never needs a broker — the `SubscriptionBus`
inside that process already sees every connection. Production deployments
that run multiple host replicas behind a load balancer DO need one: a
broadcast issued on host A has to reach the connections attached to host
B without migrating connections between processes. The `WsBroker` contract
defined here is the seam those broker adapters implement.
Concrete brokers (`api-ws-broker-in-memory` for single-host tests,
`api-ws-broker-redis` for production) plug behind this interface and stay
out of plugin code paths.
## Features
- `BrokerEnvelope` — wire payload (`topic`, `tenant`, `payload`, optional
`originHost`).
- `BrokerHandler` — subscriber callback signature.
- `BrokerSubscription` — disposal handle returned from `subscribe`.
- `WsBroker<Payload>` interface — `publish`, `subscribe`, `dispose`. Each
method may return synchronously or asynchronously so in-memory brokers
stay cheap and Redis-backed brokers can `await` round trips.
## Usage: Basic
```ts
import type { WsBroker } from "@suaveplan/api-ws/broker";
async function fanOut(broker: WsBroker<{ orderId: string }>): Promise<void> {
await broker.publish("orders.created", {
topic: "orders.created",
tenant: tenantId,
payload: { orderId: "o1" },
});
}
```
## Usage: Advanced
```ts
import type { WsBroker, BrokerSubscription } from "@suaveplan/api-ws/broker";
async function bridge(
broker: WsBroker<unknown>,
local: { broadcast: (...args: unknown[]) => void },
): Promise<BrokerSubscription> {
const subscription = await broker.subscribe("orders.created", (envelope) => {
local.broadcast(envelope.topic, {
tenant: envelope.tenant,
payload: envelope.payload,
});
});
return subscription;
}
```
## API Reference
- `interface BrokerEnvelope<Payload> { topic, tenant, payload, originHost? }`.
- `type BrokerHandler<Payload> = (env: BrokerEnvelope<Payload>) => void | Promise<void>`.
- `interface BrokerSubscription { topic; unsubscribe(): void | Promise<void> }`.
- `interface WsBroker<Payload> { publish, subscribe, dispose }`.
## Implementation Notes
- The contract intentionally does not specify reliability semantics. Each
adapter declares its own delivery guarantees (`in-memory` is at-most-
once with FIFO inside the process; `redis` is at-most-once with Redis
pub/sub semantics). Plugins that need stronger guarantees use the
saga / 2PC capabilities elsewhere in the foundation.
- `originHost` is optional so adapters that don't need loop-back
suppression can omit it. Adapters that DO suppress loop-back compare
against the local host's identity at the receive side.
- `dispose` MUST close every subscription cleanly; the conformance
harness pins this invariant against every concrete adapter.

View File

@@ -0,0 +1,37 @@
import { describe, expectTypeOf, it } from "@suaveplan/testing/runner";
import type {
BrokerEnvelope,
BrokerHandler,
BrokerSubscription,
WsBroker,
} from "./broker.js";
describe("WsBroker conformance", () => {
it("declares publish, subscribe, and dispose", { timeout: 5000 }, () => {
expectTypeOf<WsBroker>().toHaveProperty("publish");
expectTypeOf<WsBroker>().toHaveProperty("subscribe");
expectTypeOf<WsBroker>().toHaveProperty("dispose");
});
it("publish accepts a BrokerEnvelope", { timeout: 5000 }, () => {
expectTypeOf<WsBroker["publish"]>().parameters.toEqualTypeOf<
[topic: string, envelope: BrokerEnvelope<unknown>]
>();
});
it("subscribe returns a BrokerSubscription (sync or async)", {
timeout: 5000,
}, () => {
expectTypeOf<WsBroker["subscribe"]>().returns.toEqualTypeOf<
BrokerSubscription | Promise<BrokerSubscription>
>();
});
it("BrokerHandler is a function of BrokerEnvelope", {
timeout: 5000,
}, () => {
expectTypeOf<BrokerHandler<number>>().toEqualTypeOf<
(envelope: BrokerEnvelope<number>) => void | Promise<void>
>();
});
});

View File

@@ -0,0 +1,47 @@
import type { TenantId } from "@suaveplan/api-types/tenant";
/**
* Wire payload published across host replicas. Carries everything the
* receiving host needs to redeliver the broadcast to its locally-attached
* subscribers — connections are NOT migrated, only the broadcast event is.
*/
export interface BrokerEnvelope<Payload = unknown> {
readonly topic: string;
readonly tenant: TenantId;
readonly payload: Payload;
readonly originHost?: string;
}
/** Subscriber handler invoked once per broker-delivered envelope. */
export type BrokerHandler<Payload = unknown> = (
envelope: BrokerEnvelope<Payload>
) => void | Promise<void>;
/** Disposal handle returned by {@link WsBroker.subscribe}. */
export interface BrokerSubscription {
readonly topic: string;
unsubscribe(): void | Promise<void>;
}
/**
* Multi-host fan-out contract.
*
* Brokers wrap a host's local `SubscriptionBus` so that
* `bus.broadcast(...)` is visible to every other host replica in the
* deployment. The broker MUST NOT migrate connections between hosts; it
* only makes broadcast events visible.
*
* Concrete implementations live in `api-ws-broker-in-memory` (single-host
* default) and `api-ws-broker-redis` (multi-host production).
*/
export interface WsBroker<Payload = unknown> {
publish(
topic: string,
envelope: BrokerEnvelope<Payload>
): void | Promise<void>;
subscribe(
topic: string,
handler: BrokerHandler<Payload>
): BrokerSubscription | Promise<BrokerSubscription>;
dispose(): void | Promise<void>;
}

View File

@@ -0,0 +1,6 @@
export type {
BrokerEnvelope,
BrokerHandler,
BrokerSubscription,
WsBroker,
} from "./broker.js";