@venizia/ignis-docs 0.0.5 → 0.0.6-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/wiki/best-practices/architectural-patterns.md +0 -2
- package/wiki/best-practices/architecture-decisions.md +0 -8
- package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
- package/wiki/best-practices/code-style-standards/index.md +0 -1
- package/wiki/best-practices/code-style-standards/tooling.md +0 -3
- package/wiki/best-practices/contribution-workflow.md +12 -12
- package/wiki/best-practices/index.md +4 -14
- package/wiki/best-practices/performance-optimization.md +3 -3
- package/wiki/best-practices/security-guidelines.md +2 -2
- package/wiki/best-practices/troubleshooting-tips.md +1 -1
- package/wiki/guides/core-concepts/application/bootstrapping.md +6 -7
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/components.md +2 -2
- package/wiki/guides/core-concepts/dependency-injection.md +4 -5
- package/wiki/guides/core-concepts/persistent/datasources.md +4 -5
- package/wiki/guides/core-concepts/services.md +1 -1
- package/wiki/guides/get-started/5-minute-quickstart.md +4 -5
- package/wiki/guides/get-started/philosophy.md +12 -24
- package/wiki/guides/index.md +2 -9
- package/wiki/guides/reference/mcp-docs-server.md +13 -13
- package/wiki/guides/tutorials/building-a-crud-api.md +10 -10
- package/wiki/guides/tutorials/complete-installation.md +11 -12
- package/wiki/guides/tutorials/ecommerce-api.md +3 -3
- package/wiki/guides/tutorials/realtime-chat.md +6 -6
- package/wiki/guides/tutorials/testing.md +4 -5
- package/wiki/index.md +8 -14
- package/wiki/references/base/bootstrapping.md +0 -3
- package/wiki/references/base/components.md +2 -2
- package/wiki/references/base/controllers.md +0 -1
- package/wiki/references/base/datasources.md +1 -1
- package/wiki/references/base/dependency-injection.md +2 -2
- package/wiki/references/base/filter-system/default-filter.md +2 -3
- package/wiki/references/base/filter-system/index.md +1 -1
- package/wiki/references/base/filter-system/quick-reference.md +0 -14
- package/wiki/references/base/middlewares.md +0 -8
- package/wiki/references/base/providers.md +0 -9
- package/wiki/references/base/repositories/advanced.md +1 -1
- package/wiki/references/base/repositories/mixins.md +2 -3
- package/wiki/references/base/services.md +0 -1
- package/wiki/references/components/authentication/api.md +444 -0
- package/wiki/references/components/authentication/errors.md +177 -0
- package/wiki/references/components/authentication/index.md +571 -0
- package/wiki/references/components/authentication/usage.md +781 -0
- package/wiki/references/components/health-check.md +292 -103
- package/wiki/references/components/index.md +14 -12
- package/wiki/references/components/mail/api.md +505 -0
- package/wiki/references/components/mail/errors.md +176 -0
- package/wiki/references/components/mail/index.md +535 -0
- package/wiki/references/components/mail/usage.md +404 -0
- package/wiki/references/components/request-tracker.md +229 -25
- package/wiki/references/components/socket-io/api.md +1051 -0
- package/wiki/references/components/socket-io/errors.md +119 -0
- package/wiki/references/components/socket-io/index.md +410 -0
- package/wiki/references/components/socket-io/usage.md +322 -0
- package/wiki/references/components/static-asset/api.md +261 -0
- package/wiki/references/components/static-asset/errors.md +89 -0
- package/wiki/references/components/static-asset/index.md +617 -0
- package/wiki/references/components/static-asset/usage.md +364 -0
- package/wiki/references/components/swagger.md +390 -110
- package/wiki/references/components/template/api-page.md +125 -0
- package/wiki/references/components/template/errors-page.md +100 -0
- package/wiki/references/components/template/index.md +104 -0
- package/wiki/references/components/template/setup-page.md +134 -0
- package/wiki/references/components/template/single-page.md +132 -0
- package/wiki/references/components/template/usage-page.md +127 -0
- package/wiki/references/components/websocket/api.md +508 -0
- package/wiki/references/components/websocket/errors.md +123 -0
- package/wiki/references/components/websocket/index.md +453 -0
- package/wiki/references/components/websocket/usage.md +475 -0
- package/wiki/references/helpers/cron/index.md +224 -0
- package/wiki/references/helpers/crypto/index.md +537 -0
- package/wiki/references/helpers/env/index.md +214 -0
- package/wiki/references/helpers/error/index.md +232 -0
- package/wiki/references/helpers/index.md +16 -15
- package/wiki/references/helpers/inversion/index.md +608 -0
- package/wiki/references/helpers/logger/index.md +600 -0
- package/wiki/references/helpers/network/api.md +986 -0
- package/wiki/references/helpers/network/index.md +620 -0
- package/wiki/references/helpers/queue/index.md +589 -0
- package/wiki/references/helpers/redis/index.md +495 -0
- package/wiki/references/helpers/socket-io/api.md +497 -0
- package/wiki/references/helpers/socket-io/index.md +513 -0
- package/wiki/references/helpers/storage/api.md +705 -0
- package/wiki/references/helpers/storage/index.md +583 -0
- package/wiki/references/helpers/template/index.md +66 -0
- package/wiki/references/helpers/template/single-page.md +126 -0
- package/wiki/references/helpers/testing/index.md +510 -0
- package/wiki/references/helpers/types/index.md +512 -0
- package/wiki/references/helpers/uid/index.md +272 -0
- package/wiki/references/helpers/websocket/api.md +736 -0
- package/wiki/references/helpers/websocket/index.md +574 -0
- package/wiki/references/helpers/worker-thread/index.md +470 -0
- package/wiki/references/index.md +2 -9
- package/wiki/references/quick-reference.md +3 -18
- package/wiki/references/utilities/jsx.md +1 -8
- package/wiki/references/utilities/statuses.md +0 -7
- package/wiki/references/components/authentication.md +0 -476
- package/wiki/references/components/mail.md +0 -687
- package/wiki/references/components/socket-io.md +0 -562
- package/wiki/references/components/static-asset.md +0 -1277
- package/wiki/references/helpers/cron.md +0 -108
- package/wiki/references/helpers/crypto.md +0 -132
- package/wiki/references/helpers/env.md +0 -83
- package/wiki/references/helpers/error.md +0 -97
- package/wiki/references/helpers/inversion.md +0 -176
- package/wiki/references/helpers/logger.md +0 -296
- package/wiki/references/helpers/network.md +0 -396
- package/wiki/references/helpers/queue.md +0 -150
- package/wiki/references/helpers/redis.md +0 -142
- package/wiki/references/helpers/socket-io.md +0 -932
- package/wiki/references/helpers/storage.md +0 -665
- package/wiki/references/helpers/testing.md +0 -133
- package/wiki/references/helpers/types.md +0 -167
- package/wiki/references/helpers/uid.md +0 -167
- package/wiki/references/helpers/worker-thread.md +0 -178
- package/wiki/references/src-details/boot.md +0 -379
- package/wiki/references/src-details/core.md +0 -263
- package/wiki/references/src-details/dev-configs.md +0 -298
- package/wiki/references/src-details/docs.md +0 -71
- package/wiki/references/src-details/helpers.md +0 -211
- package/wiki/references/src-details/index.md +0 -86
- package/wiki/references/src-details/inversion.md +0 -340
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
# WebSocket -- API Reference
|
|
2
|
+
|
|
3
|
+
> Architecture, method signatures, internals, and type definitions.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
The WebSocket helper provides two classes: `WebSocketServerHelper` for managing a Bun-native WebSocket server with Redis Pub/Sub, and `WebSocketEmitter` for publishing messages from external processes.
|
|
8
|
+
|
|
9
|
+
#### Architecture Diagram
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
WebSocketServerHelper
|
|
13
|
+
+---------------------------------------------------+
|
|
14
|
+
| |
|
|
15
|
+
| constructor(opts) |
|
|
16
|
+
| |-- identifier, path, serverId (UUID) |
|
|
17
|
+
| |-- Store callbacks (auth, rooms, messages) |
|
|
18
|
+
| |-- Apply defaults (rooms, timeouts) |
|
|
19
|
+
| +-- initRedisClients(redisConnection) |
|
|
20
|
+
| +-- redisPub = client.duplicate() |
|
|
21
|
+
| +-- redisSub = client.duplicate() |
|
|
22
|
+
| |
|
|
23
|
+
| configure() [async] |
|
|
24
|
+
| |-- Connect Redis clients (if lazyConnect) |
|
|
25
|
+
| |-- await Redis ready (pub + sub) |
|
|
26
|
+
| |-- setupRedisSubscriptions() |
|
|
27
|
+
| | |-- subscribe(ws:broadcast) |
|
|
28
|
+
| | |-- psubscribe(ws:room:*) |
|
|
29
|
+
| | |-- psubscribe(ws:client:*) |
|
|
30
|
+
| | +-- psubscribe(ws:user:*) |
|
|
31
|
+
| +-- startHeartbeatTimer() |
|
|
32
|
+
| |
|
|
33
|
+
| getBunWebSocketHandler() |
|
|
34
|
+
| +-- Returns { open, message, close, drain, |
|
|
35
|
+
| ...serverOptions } |
|
|
36
|
+
| |
|
|
37
|
+
+---------------------------------------------------+
|
|
38
|
+
|
|
39
|
+
WebSocketEmitter
|
|
40
|
+
+---------------------------------------------------+
|
|
41
|
+
| constructor(opts) |
|
|
42
|
+
| +-- redisPub = client.duplicate() |
|
|
43
|
+
| |
|
|
44
|
+
| configure() [async] |
|
|
45
|
+
| +-- await Redis ready |
|
|
46
|
+
| |
|
|
47
|
+
| toClient / toUser / toRoom / broadcast |
|
|
48
|
+
| +-- publish(channel, IRedisSocketMessage) |
|
|
49
|
+
+---------------------------------------------------+
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Client Connection Lifecycle
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Client connects via WebSocket upgrade
|
|
56
|
+
|
|
|
57
|
+
+-- onClientConnect({ clientId, socket })
|
|
58
|
+
| +-- Create IWebSocketClient entry (state: UNAUTHORIZED)
|
|
59
|
+
| +-- Subscribe to clientId topic (Bun native pub/sub)
|
|
60
|
+
| +-- Start auth timeout (authTimeout ms)
|
|
61
|
+
|
|
|
62
|
+
+-- Client sends { event: 'authenticate', data: { ... } }
|
|
63
|
+
| +-- handleAuthenticate()
|
|
64
|
+
| +-- Set state: AUTHENTICATING
|
|
65
|
+
| +-- Extended timeout (authTimeout * 3) for async auth
|
|
66
|
+
| +-- Call authenticateFn(data)
|
|
67
|
+
| +-- Success:
|
|
68
|
+
| | +-- Set state: AUTHENTICATED
|
|
69
|
+
| | +-- [requireEncryption?] -> handshakeFn() -> enableClientEncryption()
|
|
70
|
+
| | +-- Index by userId
|
|
71
|
+
| | +-- Subscribe to broadcast topic (unless encrypted)
|
|
72
|
+
| | +-- Join default rooms + clientId room
|
|
73
|
+
| | +-- Send 'connected' event
|
|
74
|
+
| | +-- Call clientConnectedFn()
|
|
75
|
+
| +-- Failure:
|
|
76
|
+
| +-- Send 'error' event
|
|
77
|
+
| +-- Close with code 4003
|
|
78
|
+
|
|
|
79
|
+
+-- Auth timeout expires (if still UNAUTHORIZED)
|
|
80
|
+
| +-- Close with code 4001
|
|
81
|
+
|
|
|
82
|
+
+-- Heartbeat sweep (every heartbeatInterval)
|
|
83
|
+
| +-- If now - lastActivity > heartbeatTimeout
|
|
84
|
+
| +-- Close with code 4002
|
|
85
|
+
|
|
|
86
|
+
+-- Client disconnects
|
|
87
|
+
+-- onClientDisconnect()
|
|
88
|
+
+-- Clear auth timer
|
|
89
|
+
+-- Remove from user index
|
|
90
|
+
+-- Remove from all rooms
|
|
91
|
+
+-- Remove from clients map
|
|
92
|
+
+-- Call clientDisconnectedFn()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Redis 2-Client Architecture
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
RedisHelper (parent -- NOT consumed)
|
|
99
|
+
|
|
|
100
|
+
+-- client.duplicate() --> redisPub (publishes cross-instance messages)
|
|
101
|
+
|
|
|
102
|
+
+-- client.duplicate() --> redisSub (subscribes to cross-instance messages)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Both single-instance `Redis` and `Cluster` connections from ioredis are supported. The parent `RedisHelper` connection remains independent.
|
|
106
|
+
|
|
107
|
+
## Server API
|
|
108
|
+
|
|
109
|
+
### `WebSocketServerHelper` Constructor
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
constructor(opts: IWebSocketServerOptions<AuthDataType, MetadataType>)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Creates the server helper, generates a unique `serverId` (UUID), stores all options with defaults, and initializes two Redis client duplicates. Throws if `redisConnection` is falsy.
|
|
116
|
+
|
|
117
|
+
### `configure()`
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
configure(): Promise<void>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Initializes Redis connections, sets up pub/sub subscriptions, and starts the heartbeat timer. Must be called after construction and before accepting connections.
|
|
124
|
+
|
|
125
|
+
#### Internal Flow
|
|
126
|
+
|
|
127
|
+
1. Register error handlers on `redisPub` and `redisSub`
|
|
128
|
+
2. Connect duplicated clients if status is `'wait'` (lazyConnect mode)
|
|
129
|
+
3. `await Promise.all([waitForRedisReady(pub), waitForRedisReady(sub)])`
|
|
130
|
+
4. Set up Redis subscriptions (direct + pattern subscribe)
|
|
131
|
+
5. Start heartbeat timer via `setInterval(heartbeatAll, heartbeatInterval)`
|
|
132
|
+
|
|
133
|
+
### `getBunWebSocketHandler()`
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
getBunWebSocketHandler(): IBunWebSocketHandler
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Returns the Bun WebSocket handler object containing lifecycle callbacks and native configuration. Pass this to `server.reload({ websocket })`.
|
|
140
|
+
|
|
141
|
+
#### Lifecycle Callbacks
|
|
142
|
+
|
|
143
|
+
| Callback | When | Behavior |
|
|
144
|
+
|----------|------|----------|
|
|
145
|
+
| `open` | WebSocket connection established | Extracts `clientId` from `socket.data`, calls `onClientConnect()` |
|
|
146
|
+
| `message` | Message received | Updates `lastActivity`, calls `onClientMessage()` for routing |
|
|
147
|
+
| `close` | Connection closed | Calls `onClientDisconnect()` for cleanup |
|
|
148
|
+
| `drain` | Backpressure cleared | Sets `client.backpressured = false` |
|
|
149
|
+
|
|
150
|
+
#### Bun Native Configuration
|
|
151
|
+
|
|
152
|
+
These values are spread from `serverOptions` into the returned handler:
|
|
153
|
+
|
|
154
|
+
| Option | Type | Default | Description |
|
|
155
|
+
|--------|------|---------|-------------|
|
|
156
|
+
| `perMessageDeflate` | `boolean` | `undefined` | Enable per-message compression |
|
|
157
|
+
| `maxPayloadLength` | `number` | `131072` (128KB) | Maximum incoming message size in bytes |
|
|
158
|
+
| `idleTimeout` | `number` | `60` (seconds) | Bun-level idle timeout (transport layer) |
|
|
159
|
+
| `backpressureLimit` | `number` | `1048576` (1MB) | Backpressure threshold in bytes |
|
|
160
|
+
| `closeOnBackpressureLimit` | `boolean` | `undefined` | Close socket when backpressure limit is exceeded |
|
|
161
|
+
| `sendPings` | `boolean` | `true` | Enable Bun transport-level pings |
|
|
162
|
+
| `publishToSelf` | `boolean` | `false` | Whether `server.publish()` delivers to the publishing socket |
|
|
163
|
+
|
|
164
|
+
### `getPath()`
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
getPath(): string
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Returns the configured WebSocket path (default: `'/ws'`).
|
|
171
|
+
|
|
172
|
+
### `getClients()`
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
getClients(opts?: { id?: string }):
|
|
176
|
+
| IWebSocketClient<MetadataType>
|
|
177
|
+
| Map<string, IWebSocketClient<MetadataType>>
|
|
178
|
+
| undefined
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
When called without arguments or with an empty opts, returns the full `Map<string, IWebSocketClient>`. When called with `{ id }`, returns the specific client entry or `undefined`.
|
|
182
|
+
|
|
183
|
+
### `getClientsByUser()`
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
getClientsByUser(opts: { userId: string }): IWebSocketClient<MetadataType>[]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Returns all clients belonging to the given user ID. Returns an empty array if the user has no active connections.
|
|
190
|
+
|
|
191
|
+
### `getClientsByRoom()`
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
getClientsByRoom(opts: { room: string }): IWebSocketClient<MetadataType>[]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Returns all clients in the given room. Returns an empty array if the room does not exist or is empty.
|
|
198
|
+
|
|
199
|
+
### `onClientConnect()`
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
onClientConnect(opts: { clientId: string; socket: IWebSocket }): void
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Handles a new WebSocket connection. Creates an `IWebSocketClient` entry with state `UNAUTHORIZED`, subscribes the socket to its own `clientId` topic (Bun native pub/sub), and starts the authentication timeout. Returns early if the client ID already exists.
|
|
206
|
+
|
|
207
|
+
### `onClientMessage()`
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
onClientMessage(opts: { clientId: string; raw: string }): void
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Routes incoming messages. Parses JSON, then:
|
|
214
|
+
|
|
215
|
+
- `heartbeat` events: silently consumed (updates `lastActivity` via the `message` callback)
|
|
216
|
+
- `authenticate` events: delegates to `handleAuthenticate()`
|
|
217
|
+
- Unauthenticated clients sending non-auth events: receives an `error` event (`'Not authenticated'`)
|
|
218
|
+
- `join` / `leave` events: delegates to room handlers
|
|
219
|
+
- All other events: delegates to `messageHandler` (if configured)
|
|
220
|
+
|
|
221
|
+
Sends an `error` event (`'Invalid message format'`) if JSON parsing fails.
|
|
222
|
+
|
|
223
|
+
### `onClientDisconnect()`
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
onClientDisconnect(opts: { clientId: string }): void
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Cleans up a disconnected client:
|
|
230
|
+
|
|
231
|
+
1. Clears auth timeout if pending
|
|
232
|
+
2. Removes from user index
|
|
233
|
+
3. Removes from all rooms
|
|
234
|
+
4. Removes from clients map
|
|
235
|
+
5. Invokes `clientDisconnectedFn` callback
|
|
236
|
+
|
|
237
|
+
### `joinRoom()`
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
joinRoom(opts: { clientId: string; room: string }): void
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Programmatically joins a client to a room. Adds to the room index, adds to the client's room set, and subscribes the socket to the room's Bun native pub/sub topic (unless the client has encryption enabled).
|
|
244
|
+
|
|
245
|
+
### `leaveRoom()`
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
leaveRoom(opts: { clientId: string; room: string }): void
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Removes a client from a room. Removes from the room index, removes from the client's room set, and unsubscribes the socket from the Bun native pub/sub topic.
|
|
252
|
+
|
|
253
|
+
### `enableClientEncryption()`
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
enableClientEncryption(opts: { clientId: string }): void
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Enables encryption for a client. Unsubscribes the client from all Bun native pub/sub topics (broadcast topic + all rooms) so `server.publish()` will not reach them. Messages are instead delivered individually through the `outboundTransformer`. No-op if the client is already encrypted or does not exist.
|
|
260
|
+
|
|
261
|
+
> [!WARNING]
|
|
262
|
+
> This is **irreversible** for the lifetime of the connection. Once encrypted, the client cannot be switched back to Bun native pub/sub delivery.
|
|
263
|
+
|
|
264
|
+
### `sendToClient()`
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
sendToClient(opts: {
|
|
268
|
+
clientId: string;
|
|
269
|
+
event: string;
|
|
270
|
+
data: unknown;
|
|
271
|
+
doLog?: boolean;
|
|
272
|
+
}): void
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Sends a message to a specific client (local delivery only). If the client has encryption enabled and an `outboundTransformer` is configured, the transformer runs before delivery. Otherwise, sends the raw `{ event, data }` JSON.
|
|
276
|
+
|
|
277
|
+
### `sendToUser()`
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
sendToUser(opts: {
|
|
281
|
+
userId: string;
|
|
282
|
+
event: string;
|
|
283
|
+
data: unknown;
|
|
284
|
+
}): void
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Sends a message to all local clients belonging to a user. Iterates the user's client set and calls `sendToClient()` for each.
|
|
288
|
+
|
|
289
|
+
### `sendToRoom()`
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
sendToRoom(opts: {
|
|
293
|
+
room: string;
|
|
294
|
+
event: string;
|
|
295
|
+
data: unknown;
|
|
296
|
+
exclude?: string[];
|
|
297
|
+
}): void
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Sends a message to all clients in a room (local delivery only).
|
|
301
|
+
|
|
302
|
+
#### Delivery Strategy
|
|
303
|
+
|
|
304
|
+
| Condition | Strategy |
|
|
305
|
+
|-----------|----------|
|
|
306
|
+
| No `outboundTransformer`, no `exclude` | Bun native `server.publish()` -- O(1) C++ fan-out |
|
|
307
|
+
| `outboundTransformer` set, no `exclude` | Iterates all room clients via `executePromiseWithLimit` (max `encryptedBatchLimit` concurrent) |
|
|
308
|
+
| `exclude` provided | Always iterates clients individually (cannot exclude from Bun pub/sub) |
|
|
309
|
+
|
|
310
|
+
### `broadcast()`
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
broadcast(opts: {
|
|
314
|
+
event: string;
|
|
315
|
+
data: unknown;
|
|
316
|
+
exclude?: string[];
|
|
317
|
+
}): void
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Sends a message to all authenticated clients on this instance (local delivery only). Delivery strategy follows the same pattern as `sendToRoom()`:
|
|
321
|
+
|
|
322
|
+
| Condition | Strategy |
|
|
323
|
+
|-----------|----------|
|
|
324
|
+
| No `outboundTransformer`, no `exclude` | Bun native `server.publish()` via broadcast topic |
|
|
325
|
+
| `outboundTransformer` set, no `exclude` | Iterates all authenticated clients with concurrency limit |
|
|
326
|
+
| `exclude` provided | Always iterates clients individually |
|
|
327
|
+
|
|
328
|
+
### `send()`
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
send<T = unknown>(opts: {
|
|
332
|
+
destination?: string;
|
|
333
|
+
payload: { topic: string; data: T };
|
|
334
|
+
doLog?: boolean;
|
|
335
|
+
cb?: () => void;
|
|
336
|
+
}): void
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Public API for cross-instance messaging. Delivers locally **and** publishes to Redis so other server instances receive the message.
|
|
340
|
+
|
|
341
|
+
Routing logic:
|
|
342
|
+
|
|
343
|
+
| `destination` | Local delivery | Redis channel |
|
|
344
|
+
|---------------|----------------|---------------|
|
|
345
|
+
| Omitted | `broadcast()` | `ws:broadcast` |
|
|
346
|
+
| Matches a local client ID | `sendToClient()` | `ws:client:{clientId}` |
|
|
347
|
+
| Matches a local room name | `sendToRoom()` | `ws:room:{room}` |
|
|
348
|
+
| Neither (remote target) | None | `ws:room:{destination}` |
|
|
349
|
+
|
|
350
|
+
Silent no-op when `payload` is falsy, `payload.topic` is falsy, or `payload.data` is `undefined`.
|
|
351
|
+
|
|
352
|
+
If `cb` is provided, it is executed asynchronously via `setTimeout(cb, 0)`.
|
|
353
|
+
|
|
354
|
+
### `shutdown()`
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
shutdown(): Promise<void>
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Graceful shutdown:
|
|
361
|
+
|
|
362
|
+
1. Clear heartbeat timer
|
|
363
|
+
2. Close all client sockets with code `1001` (`'Server shutting down'`)
|
|
364
|
+
3. Trigger disconnect callbacks for all tracked clients
|
|
365
|
+
4. Clear `clients`, `users`, and `rooms` maps
|
|
366
|
+
5. `await Promise.all([redisPub.quit(), redisSub.quit()])`
|
|
367
|
+
|
|
368
|
+
## Emitter API
|
|
369
|
+
|
|
370
|
+
### `WebSocketEmitter` Constructor
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
constructor(opts: IWebSocketEmitterOptions)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Creates the emitter, duplicates one Redis client from `redisConnection`. Throws if `redisConnection` is falsy.
|
|
377
|
+
|
|
378
|
+
### `configure()`
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
configure(): Promise<void>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Connects the Redis client (if in `'wait'` status) and waits for it to reach `ready` status. Must be called before emitting.
|
|
385
|
+
|
|
386
|
+
### `toClient()`
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
toClient(opts: {
|
|
390
|
+
clientId: string;
|
|
391
|
+
event: string;
|
|
392
|
+
data: unknown;
|
|
393
|
+
}): Promise<void>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Publishes a message to the `ws:client:{clientId}` Redis channel. All server instances subscribed via `psubscribe('ws:client:*')` will deliver it to the target client if connected locally.
|
|
397
|
+
|
|
398
|
+
### `toUser()`
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
toUser(opts: {
|
|
402
|
+
userId: string;
|
|
403
|
+
event: string;
|
|
404
|
+
data: unknown;
|
|
405
|
+
}): Promise<void>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Publishes a message to the `ws:user:{userId}` Redis channel. All server instances deliver it to every session belonging to that user.
|
|
409
|
+
|
|
410
|
+
### `toRoom()`
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
toRoom(opts: {
|
|
414
|
+
room: string;
|
|
415
|
+
event: string;
|
|
416
|
+
data: unknown;
|
|
417
|
+
exclude?: string[];
|
|
418
|
+
}): Promise<void>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Publishes a message to the `ws:room:{room}` Redis channel. All server instances deliver it to every client in that room.
|
|
422
|
+
|
|
423
|
+
### `broadcast()`
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
broadcast(opts: {
|
|
427
|
+
event: string;
|
|
428
|
+
data: unknown;
|
|
429
|
+
}): Promise<void>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Publishes a message to the `ws:broadcast` Redis channel. All server instances deliver it to every authenticated client.
|
|
433
|
+
|
|
434
|
+
### `shutdown()`
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
shutdown(): Promise<void>
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Quits the Redis connection.
|
|
441
|
+
|
|
442
|
+
## Types Reference
|
|
443
|
+
|
|
444
|
+
### Wire Protocol
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
/** Client <-> Server message envelope */
|
|
448
|
+
interface IWebSocketMessage<DataType = unknown> {
|
|
449
|
+
event: string;
|
|
450
|
+
data?: DataType;
|
|
451
|
+
id?: string;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Internal Redis Pub/Sub message envelope */
|
|
455
|
+
interface IRedisSocketMessage<DataType = unknown> {
|
|
456
|
+
serverId: string;
|
|
457
|
+
type: TWebSocketMessageType; // 'client' | 'user' | 'room' | 'broadcast'
|
|
458
|
+
target?: string;
|
|
459
|
+
event: string;
|
|
460
|
+
data: DataType;
|
|
461
|
+
exclude?: string[];
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Client Tracking
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
interface IWebSocketClient<
|
|
469
|
+
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
470
|
+
> {
|
|
471
|
+
id: string;
|
|
472
|
+
userId?: string;
|
|
473
|
+
socket: IWebSocket;
|
|
474
|
+
state: TWebSocketClientState;
|
|
475
|
+
rooms: Set<string>;
|
|
476
|
+
backpressured: boolean;
|
|
477
|
+
encrypted: boolean;
|
|
478
|
+
connectedAt: number;
|
|
479
|
+
lastActivity: number;
|
|
480
|
+
metadata?: MetadataType;
|
|
481
|
+
serverPublicKey?: string;
|
|
482
|
+
salt?: string;
|
|
483
|
+
authTimer?: ReturnType<typeof setTimeout>;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Data attached during server.upgrade() */
|
|
487
|
+
interface IWebSocketData<
|
|
488
|
+
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
489
|
+
> {
|
|
490
|
+
clientId: string;
|
|
491
|
+
userId?: string;
|
|
492
|
+
metadata?: MetadataType;
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Bun Interfaces
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
/** Bun WebSocket handle (defined locally to avoid @types/bun dependency) */
|
|
500
|
+
interface IWebSocket<T = unknown> {
|
|
501
|
+
readonly data: T;
|
|
502
|
+
readonly remoteAddress: string;
|
|
503
|
+
readonly readyState: number;
|
|
504
|
+
|
|
505
|
+
send(data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer, compress?: boolean): number;
|
|
506
|
+
subscribe(topic: string): void;
|
|
507
|
+
unsubscribe(topic: string): void;
|
|
508
|
+
isSubscribed(topic: string): boolean;
|
|
509
|
+
close(code?: number, reason?: string): void;
|
|
510
|
+
cork(cb: (ws: IWebSocket<T>) => void): void;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Bun server interface for native pub/sub */
|
|
514
|
+
interface IBunServer {
|
|
515
|
+
readonly pendingWebSockets: number;
|
|
516
|
+
publish(
|
|
517
|
+
topic: string,
|
|
518
|
+
data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer,
|
|
519
|
+
compress?: boolean,
|
|
520
|
+
): number;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Bun native WebSocket configuration */
|
|
524
|
+
interface IBunWebSocketConfig {
|
|
525
|
+
perMessageDeflate?: boolean;
|
|
526
|
+
maxPayloadLength?: number;
|
|
527
|
+
idleTimeout?: number;
|
|
528
|
+
backpressureLimit?: number;
|
|
529
|
+
closeOnBackpressureLimit?: boolean;
|
|
530
|
+
sendPings?: boolean;
|
|
531
|
+
publishToSelf?: boolean;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Return type of getBunWebSocketHandler() */
|
|
535
|
+
interface IBunWebSocketHandler extends IBunWebSocketConfig {
|
|
536
|
+
open: (socket: IWebSocket) => void;
|
|
537
|
+
message: (socket: IWebSocket, message: string | Buffer) => void;
|
|
538
|
+
close: (socket: IWebSocket, code: number, reason: string) => void;
|
|
539
|
+
drain: (socket: IWebSocket) => void;
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Server Options
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
interface IWebSocketServerOptions<
|
|
547
|
+
AuthDataType extends Record<string, unknown> = Record<string, unknown>,
|
|
548
|
+
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
549
|
+
> {
|
|
550
|
+
identifier: string;
|
|
551
|
+
path?: string; // Default: '/ws'
|
|
552
|
+
redisConnection: DefaultRedisHelper;
|
|
553
|
+
server: IBunServer;
|
|
554
|
+
defaultRooms?: string[]; // Default: ['ws-default', 'ws-notification']
|
|
555
|
+
serverOptions?: IBunWebSocketConfig;
|
|
556
|
+
authTimeout?: number; // Default: 5000
|
|
557
|
+
heartbeatInterval?: number; // Default: 30000
|
|
558
|
+
heartbeatTimeout?: number; // Default: 90000
|
|
559
|
+
encryptedBatchLimit?: number; // Default: 10
|
|
560
|
+
requireEncryption?: boolean; // Default: false
|
|
561
|
+
|
|
562
|
+
authenticateFn: TWebSocketAuthenticateFn<AuthDataType, MetadataType>;
|
|
563
|
+
validateRoomFn?: TWebSocketValidateRoomFn;
|
|
564
|
+
clientConnectedFn?: TWebSocketClientConnectedFn<MetadataType>;
|
|
565
|
+
clientDisconnectedFn?: TWebSocketClientDisconnectedFn;
|
|
566
|
+
messageHandler?: TWebSocketMessageHandler;
|
|
567
|
+
outboundTransformer?: TWebSocketOutboundTransformer<unknown, MetadataType>;
|
|
568
|
+
handshakeFn?: TWebSocketHandshakeFn<AuthDataType>;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
interface IWebSocketEmitterOptions {
|
|
572
|
+
identifier?: string; // Default: 'WebSocketEmitter'
|
|
573
|
+
redisConnection: DefaultRedisHelper;
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Callback Types
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
/** Authentication -- return { userId, metadata } on success, null/false to reject */
|
|
581
|
+
type TWebSocketAuthenticateFn<
|
|
582
|
+
AuthDataType extends Record<string, unknown> = Record<string, unknown>,
|
|
583
|
+
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
584
|
+
> = (
|
|
585
|
+
opts: AuthDataType,
|
|
586
|
+
) => ValueOrPromise<{ userId?: string; metadata?: MetadataType } | null | false>;
|
|
587
|
+
|
|
588
|
+
/** ECDH key exchange during auth -- return { serverPublicKey, salt } or null/false */
|
|
589
|
+
type TWebSocketHandshakeFn<
|
|
590
|
+
AuthDataType extends Record<string, unknown> = Record<string, unknown>,
|
|
591
|
+
> = (opts: {
|
|
592
|
+
clientId: string;
|
|
593
|
+
userId?: string;
|
|
594
|
+
data: AuthDataType;
|
|
595
|
+
}) => ValueOrPromise<{ serverPublicKey: string; salt: string } | null | false>;
|
|
596
|
+
|
|
597
|
+
/** Room validation -- return the allowed subset of requested rooms */
|
|
598
|
+
type TWebSocketValidateRoomFn = (opts: {
|
|
599
|
+
clientId: string;
|
|
600
|
+
userId?: string;
|
|
601
|
+
rooms: string[];
|
|
602
|
+
}) => ValueOrPromise<string[]>;
|
|
603
|
+
|
|
604
|
+
/** Post-authentication callback */
|
|
605
|
+
type TWebSocketClientConnectedFn<
|
|
606
|
+
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
607
|
+
> = (opts: {
|
|
608
|
+
clientId: string;
|
|
609
|
+
userId?: string;
|
|
610
|
+
metadata?: MetadataType;
|
|
611
|
+
}) => ValueOrPromise<void>;
|
|
612
|
+
|
|
613
|
+
/** Disconnect callback */
|
|
614
|
+
type TWebSocketClientDisconnectedFn = (opts: {
|
|
615
|
+
clientId: string;
|
|
616
|
+
userId?: string;
|
|
617
|
+
}) => ValueOrPromise<void>;
|
|
618
|
+
|
|
619
|
+
/** Custom event handler for non-system events from authenticated clients */
|
|
620
|
+
type TWebSocketMessageHandler = (opts: {
|
|
621
|
+
clientId: string;
|
|
622
|
+
userId?: string;
|
|
623
|
+
message: IWebSocketMessage;
|
|
624
|
+
}) => ValueOrPromise<void>;
|
|
625
|
+
|
|
626
|
+
/** Outbound transformer -- intercepts messages before socket.send() */
|
|
627
|
+
type TWebSocketOutboundTransformer<
|
|
628
|
+
DataType = unknown,
|
|
629
|
+
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
630
|
+
> = (opts: {
|
|
631
|
+
client: IWebSocketClient<MetadataType>;
|
|
632
|
+
event: string;
|
|
633
|
+
data: DataType;
|
|
634
|
+
}) => ValueOrPromise<TNullable<{ event: string; data: DataType }>>;
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### State Types
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
type TWebSocketClientState = 'unauthorized' | 'authenticating' | 'authenticated' | 'disconnected';
|
|
641
|
+
type TWebSocketEvent = 'authenticate' | 'connected' | 'disconnect' | 'join' | 'leave' | 'error' | 'heartbeat' | 'encrypted';
|
|
642
|
+
type TWebSocketMessageType = 'client' | 'user' | 'room' | 'broadcast';
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
## Constants
|
|
646
|
+
|
|
647
|
+
### `WebSocketEvents`
|
|
648
|
+
|
|
649
|
+
| Constant | Value | Description |
|
|
650
|
+
|----------|-------|-------------|
|
|
651
|
+
| `AUTHENTICATE` | `'authenticate'` | Client -> Server auth request |
|
|
652
|
+
| `CONNECTED` | `'connected'` | Server -> Client auth success |
|
|
653
|
+
| `DISCONNECT` | `'disconnect'` | Disconnection event |
|
|
654
|
+
| `JOIN` | `'join'` | Room join request |
|
|
655
|
+
| `LEAVE` | `'leave'` | Room leave request |
|
|
656
|
+
| `ERROR` | `'error'` | Server -> Client error message |
|
|
657
|
+
| `HEARTBEAT` | `'heartbeat'` | Client -> Server keep-alive |
|
|
658
|
+
| `ENCRYPTED` | `'encrypted'` | Encrypted message wrapper |
|
|
659
|
+
|
|
660
|
+
Utility methods:
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
WebSocketEvents.isValid('authenticate'); // true
|
|
664
|
+
WebSocketEvents.isValid('invalid'); // false
|
|
665
|
+
WebSocketEvents.SCHEME_SET; // Set of all valid event strings
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### `WebSocketChannels`
|
|
669
|
+
|
|
670
|
+
| Constant / Method | Value | Description |
|
|
671
|
+
|-------------------|-------|-------------|
|
|
672
|
+
| `BROADCAST` | `'ws:broadcast'` | Broadcast channel |
|
|
673
|
+
| `ROOM_PREFIX` | `'ws:room:'` | Room channel prefix |
|
|
674
|
+
| `CLIENT_PREFIX` | `'ws:client:'` | Client channel prefix |
|
|
675
|
+
| `USER_PREFIX` | `'ws:user:'` | User channel prefix |
|
|
676
|
+
| `forRoom({ room })` | `'ws:room:{room}'` | Build room channel |
|
|
677
|
+
| `forClient({ clientId })` | `'ws:client:{clientId}'` | Build client channel |
|
|
678
|
+
| `forUser({ userId })` | `'ws:user:{userId}'` | Build user channel |
|
|
679
|
+
| `forRoomPattern()` | `'ws:room:*'` | Room pattern for `psubscribe` |
|
|
680
|
+
| `forClientPattern()` | `'ws:client:*'` | Client pattern for `psubscribe` |
|
|
681
|
+
| `forUserPattern()` | `'ws:user:*'` | User pattern for `psubscribe` |
|
|
682
|
+
|
|
683
|
+
### `WebSocketDefaults`
|
|
684
|
+
|
|
685
|
+
| Constant | Value | Description |
|
|
686
|
+
|----------|-------|-------------|
|
|
687
|
+
| `PATH` | `'/ws'` | Default WebSocket path |
|
|
688
|
+
| `ROOM` | `'ws-default'` | Default room |
|
|
689
|
+
| `NOTIFICATION_ROOM` | `'ws-notification'` | Default notification room |
|
|
690
|
+
| `BROADCAST_TOPIC` | `'ws:internal:broadcast'` | Bun pub/sub broadcast topic |
|
|
691
|
+
| `MAX_PAYLOAD_LENGTH` | `131072` (128KB) | Maximum incoming payload size |
|
|
692
|
+
| `IDLE_TIMEOUT` | `60` (seconds) | Bun transport idle timeout |
|
|
693
|
+
| `BACKPRESSURE_LIMIT` | `1048576` (1MB) | Bun backpressure threshold |
|
|
694
|
+
| `SEND_PINGS` | `true` | Bun transport pings enabled |
|
|
695
|
+
| `PUBLISH_TO_SELF` | `false` | Bun pub/sub self-delivery disabled |
|
|
696
|
+
| `AUTH_TIMEOUT` | `5000` (5s) | Authentication timeout |
|
|
697
|
+
| `HEARTBEAT_INTERVAL` | `30000` (30s) | Heartbeat sweep interval |
|
|
698
|
+
| `HEARTBEAT_TIMEOUT` | `90000` (90s) | Heartbeat inactivity threshold |
|
|
699
|
+
| `ENCRYPTED_BATCH_LIMIT` | `10` | Max concurrent encryption operations |
|
|
700
|
+
|
|
701
|
+
### `WebSocketMessageTypes`
|
|
702
|
+
|
|
703
|
+
| Constant | Value | Description |
|
|
704
|
+
|----------|-------|-------------|
|
|
705
|
+
| `CLIENT` | `'client'` | Message targeted at a specific client |
|
|
706
|
+
| `USER` | `'user'` | Message targeted at all sessions of a user |
|
|
707
|
+
| `ROOM` | `'room'` | Message targeted at a room |
|
|
708
|
+
| `BROADCAST` | `'broadcast'` | Message targeted at all clients |
|
|
709
|
+
|
|
710
|
+
Utility methods:
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
WebSocketMessageTypes.isValid('room'); // true
|
|
714
|
+
WebSocketMessageTypes.SCHEME_SET; // Set { 'client', 'user', 'room', 'broadcast' }
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### `WebSocketClientStates`
|
|
718
|
+
|
|
719
|
+
| Constant | Value | Description |
|
|
720
|
+
|----------|-------|-------------|
|
|
721
|
+
| `UNAUTHORIZED` | `'unauthorized'` | Initial state after connection |
|
|
722
|
+
| `AUTHENTICATING` | `'authenticating'` | Auth in progress |
|
|
723
|
+
| `AUTHENTICATED` | `'authenticated'` | Successfully authenticated |
|
|
724
|
+
| `DISCONNECTED` | `'disconnected'` | Client has disconnected |
|
|
725
|
+
|
|
726
|
+
Utility methods:
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
WebSocketClientStates.isValid('authenticated'); // true
|
|
730
|
+
WebSocketClientStates.SCHEME_SET; // Set of all valid state strings
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
## See Also
|
|
734
|
+
|
|
735
|
+
- [Setup & Usage](./) -- Getting started, examples, and troubleshooting
|
|
736
|
+
- [Socket.IO Helper](../socket-io/) -- Socket.IO-based alternative with Node.js support
|