first commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# `SseConnectionRegistry` + `createConnectionRegistry`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`SseConnectionRegistry` is the multi-index registry for active SSE connections. Engines call `add` on handshake, `remove` on disconnect, and the host fans out broadcasts via `lookupByTenant` / `lookupByTopic`. Lookups are O(1) by design — the registry keeps a parallel `tenantId → Set<conn>` and `(topic, tenantId) → Set<conn>` index so broadcast paths avoid scanning the full connection set.
|
||||
|
||||
`remove` is idempotent: engines can call it from both the graceful-close path and a finaliser without risk of throwing or double-decrementing tenant counts. This matches the shape of `WsConnectionRegistry` in `@suaveplan/api-types/ws` so engines that bridge both transports can share lifecycle code.
|
||||
|
||||
## Features
|
||||
|
||||
- O(1) lookup by `tenantId` via `lookupByTenant`.
|
||||
- O(1) lookup by `(topic, tenantId)` via `lookupByTopic` — cross-tenant fan-out is impossible by construction.
|
||||
- `countByTenant` powers the per-principal cap check at handshake.
|
||||
- `add(conn, topics)` is mutation-only; topics MUST be supplied at `add` time (no `subscribe(conn, topic)` here — topic subscriptions live with the SSE manifest).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createConnectionRegistry } from "@suaveplan/api-sse/connection-registry/connection-registry";
|
||||
|
||||
const reg = createConnectionRegistry();
|
||||
declare const connection: import("@suaveplan/api-sse/connection/connection").SseConnection;
|
||||
|
||||
reg.add(connection, ["orders", "notifications"]);
|
||||
const audience = reg.lookupByTopic("orders", connection.tenantId);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createConnectionRegistry } from "@suaveplan/api-sse/connection-registry/connection-registry";
|
||||
|
||||
const reg = createConnectionRegistry();
|
||||
|
||||
function broadcast(
|
||||
tenantId: import("@suaveplan/api-types/tenant").TenantId,
|
||||
topic: string,
|
||||
event: import("@suaveplan/api-sse/event/event").SseEvent
|
||||
) {
|
||||
return Promise.all(
|
||||
reg.lookupByTopic(topic, tenantId).map((conn) => conn.send(event))
|
||||
);
|
||||
}
|
||||
|
||||
// idempotent close
|
||||
reg.remove(conn);
|
||||
reg.remove(conn); // does not throw
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createConnectionRegistry(): SseConnectionRegistry`
|
||||
- `interface SseConnectionRegistry`
|
||||
- `add(conn: SseConnection, topics?: readonly string[]): void`
|
||||
- `remove(conn: SseConnection): void` (idempotent)
|
||||
- `lookupByTenant(tenantId: TenantId): readonly SseConnection[]`
|
||||
- `lookupByTopic(topic: string, tenantId: TenantId): readonly SseConnection[]`
|
||||
- `countByTenant(tenantId: TenantId): number`
|
||||
- `size: number`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The implementation uses a `Set` per tenant and per `(topic, tenantId)` key for O(1) `add` / `delete`. Topics-per-connection are stashed in a `WeakMap` so the registry never holds a strong reference past `remove` — the connection becomes eligible for GC as soon as the engine drops its own handle. The `(topic, tenantId)` composite key uses a space separator (`${tenantId} ${topic}`) because branded `TenantId` cannot contain spaces by construction (resolved at the trust boundary).
|
||||
Reference in New Issue
Block a user