Files
pubsub/docs/architecture/event-targets/websocket-server.md
glm-5.1 371dabc20d Add per-adapter architecture docs in event-targets/ directory
- Create docs/architecture/event-targets/ with individual specs:
  in-process, redis, websocket-client, websocket-server,
  worker, iroh-spoke, iroh-hub
- Update event-targets.md to serve as index with topology model
  (symmetric vs fan-out) and adapter status table
- Update architecture.md index to reference new directory
2026-05-07 14:49:50 +00:00

86 lines
3.5 KiB
Markdown

---
status: draft
last_updated: 2026-05-07
---
# WebSocket Server Event Target
**Import**: `@alkdev/pubsub/event-target-websocket-server`
**Peer dep**: none (WebSocket is a web standard, but a server framework like Hono may be needed for upgrade handling)
**Status**: Not yet implemented.
Manages multiple WebSocket connections for the server (hub) side. Handles fan-out: `dispatchEvent` sends to all connected spokes; `addEventListener` aggregates subscriptions across all connections.
## `createWebSocketServerEventTarget`
```ts
function createWebSocketServerEventTarget<TEvent extends TypedEvent>(
options: CreateWebSocketServerEventTargetArgs,
): TypedEventTarget<TEvent>;
```
### `CreateWebSocketServerEventTargetArgs`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `onConnection` | callback | No | Called when a new spoke connects. Receives the spoke's event target for per-connection customization. |
| `onDisconnection` | callback | No | Called when a spoke disconnects. Receives the spoke's event target for cleanup. |
## How It Works
Unlike the client adapter, the server adapter manages a `Set<WebSocket>` of active connections:
- `dispatchEvent` → iterates all connected WebSockets, sends JSON envelope to each
- `addEventListener` → registers local listeners. The server doesn't subscribe to individual spokes — it listens for events from any spoke
- `removeEventListener` → removes local listeners
### Incoming Messages
Each connected spoke sends JSON envelopes. The server listens on `ws.onmessage` for each connection:
```ts
for (const ws of this.connections) {
ws.onmessage = (msg) => {
const envelope = JSON.parse(msg.data);
const topic = `${envelope.type}:${envelope.id}`;
const event = new CustomEvent(topic, { detail: envelope });
this.dispatchEvent(event); // dispatches to local listeners
};
}
```
### Outgoing Messages (Fan-out)
```ts
dispatchEvent(event) {
const message = JSON.stringify(event.detail);
for (const ws of this.connections) {
ws.send(message);
}
return true;
}
```
## Per-Connection Spoke Targets
The server adapter creates a `WebSocketClientEventTarget` for each incoming connection. This allows the hub to target specific spokes if needed (e.g., responding to a specific request).
## Key Properties
- **Fan-out** — dispatchEvent sends to all connected spokes
- **Aggregate subscription** — addEventListener listens for events from any spoke
- **Connection lifecycle** — manages add/remove of WebSocket connections
- **No native deps** — works with any WebSocket server (Node ws, Bun, Deno, Hono)
## Open Questions
1. **Server framework coupling** — Should this adapter take a raw `WebSocketServer` or just handle `WebSocket` instances? Raw `WebSocket` instances keeps it framework-agnostic. The caller (hub code) handles the HTTP upgrade and passes connected `WebSocket`s to the adapter.
2. **Backpressure** — What happens when `ws.send()` blocks or buffers? Should there be a max buffer size per connection?
3. **Selective fan-out** — Should `dispatchEvent` always send to all connections, or should there be a way to target a specific spoke?
## Test Plan
1. **Fan-out** — dispatchEvent sends to all connected WebSockets
2. **Incoming aggregation** — messages from any spoke dispatch to local listeners
3. **Connection add/remove** — new connections are tracked, disconnections are cleaned up
4. **Mixed topology** — server adapter and client adapters can communicate bidirectionally