first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user