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,60 @@
# `Channel`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Pub/sub channel primitive with topic-based pattern matching. The gateway uses one `Channel` per registered logical channel id and routes `channel.publish` frames through it. Channels are independent of rooms — they let a single TCP connection carry multiple logical streams without interleaving (the daemon's "channel multiplexing" requirement).
## Features
- Per-client subscription set, keyed by exact topic OR pattern.
- Two wildcard verbs when `allowPatterns: true`: `*` matches one segment, `**` matches zero or more segments (separator `.`).
- `publish(topic, message, excludeSender?)` returns deterministic recipient ids so the daemon dispatches outside the channel.
- Invalid topics (`""`, or wildcard tokens under `allowPatterns: false`) throw `WS_GATEWAY.CHANNEL_INVALID` so the daemon can surface the typed denial back to the offending client.
- Empty / negative subscriber sets clean up automatically so the channel can be observed for membership-based cleanup.
## Usage: Basic Example
```ts
import { Channel } from "@suaveplan/websocket-gateway";
const ch = new Channel({ name: "events", allowPatterns: true });
ch.subscribe("alice", "user.*");
ch.publish("user.created", message);
```
## Usage: Advanced Example
```ts
import { Channel, WS_GATEWAY_CODES, WsGatewayError } from "@suaveplan/websocket-gateway";
const ch = new Channel({ name: "events", allowPatterns: false });
try {
ch.subscribe("alice", "user.*");
} catch (err) {
if (err instanceof WsGatewayError && err.code === WS_GATEWAY_CODES.CHANNEL_INVALID) {
// wildcard rejected under allowPatterns:false
}
}
const patterns = new Channel({ name: "logs", allowPatterns: true });
patterns.subscribe("bob", "logs.**"); // matches logs.app, logs.app.error, etc.
patterns.unsubscribe("bob", "logs.**");
patterns.unsubscribeAll("bob");
```
## API Reference
- `new Channel(config: ChannelConfig)` — throws `CHANNEL_INVALID` on empty `name`.
- `name` / `allowPatterns` / `subscriberCount` getters.
- `subscribe(clientId, topic): boolean` — false if already subscribed, throws on invalid topic.
- `unsubscribe(clientId, topic): boolean` — false if not present.
- `unsubscribeAll(clientId): boolean` — drops every subscription for one client.
- `publish(topic, message, excludeSender?): string[]` — deterministic recipient set.
- `getSubscriptions(clientId)` / `getSubscribers()` / `hasSubscriber(clientId)`.
- `topicMatches(pattern, topic): boolean` — exposed for callers that want to check match without subscribing.
## Implementation Notes
The wildcard matcher is a hand-rolled recursive descent over `.`-delimited segments. `**` recurses with the same pattern index across every possible tail of the topic, enabling zero-segment matches at the trailing position (`logs.**` matches `logs`). The flat per-client `Set<string>` storage keeps subscribe/unsubscribe O(topics-per-client) and lets the daemon's disconnect path call `unsubscribeAll` in O(1). Publish is O(subscribers × topics-per-subscriber) — the design assumes channels stay under a few thousand subscribers in practice; sharded channels are the future job of a `websocket-gateway-store-redis` adapter.