@venizia/ignis-docs 0.2.0 → 0.2.1-0
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/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
|
@@ -1,241 +1,147 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: WebSocket Component - Usage & Examples
|
|
3
|
+
description: Injecting the WebSocket helper, the standalone emitter, wire protocol, client tracking, and delivery strategy
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# Usage & Examples
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Task-oriented patterns for working with the WebSocket component once it is registered: sending messages, reading the wire protocol, and understanding delivery.
|
|
6
10
|
|
|
7
|
-
Inject
|
|
11
|
+
## Inject the helper in a service or controller
|
|
12
|
+
|
|
13
|
+
`WebSocketServerHelper` is bound to `WEBSOCKET_INSTANCE` inside a post-start hook, so it does not exist at DI-construction time. Use a lazy getter that resolves from the application container on first access.
|
|
8
14
|
|
|
9
15
|
```typescript
|
|
10
|
-
import {
|
|
11
|
-
BaseService,
|
|
12
|
-
inject,
|
|
13
|
-
CoreBindings,
|
|
14
|
-
BaseApplication,
|
|
15
|
-
} from '@venizia/ignis';
|
|
16
|
+
import { BaseService, inject, CoreBindings, BaseApplication } from '@venizia/ignis';
|
|
16
17
|
import { WebSocketBindingKeys } from '@venizia/ignis/websocket';
|
|
17
18
|
import { WebSocketServerHelper } from '@venizia/ignis-helpers';
|
|
18
19
|
|
|
19
20
|
export class NotificationService extends BaseService {
|
|
20
|
-
// Lazy getter pattern -- helper is bound AFTER server starts
|
|
21
21
|
private _ws: WebSocketServerHelper | null = null;
|
|
22
22
|
|
|
23
23
|
constructor(
|
|
24
|
-
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
|
|
25
|
-
private application: BaseApplication,
|
|
24
|
+
@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
|
|
26
25
|
) {
|
|
27
26
|
super({ scope: NotificationService.name });
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
private get ws(): WebSocketServerHelper {
|
|
31
30
|
if (!this._ws) {
|
|
32
|
-
this._ws =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
this._ws =
|
|
32
|
+
this.application.get<WebSocketServerHelper>({
|
|
33
|
+
key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
|
|
34
|
+
isOptional: true,
|
|
35
|
+
}) ?? null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (!this._ws) {
|
|
39
39
|
throw new Error('WebSocket not initialized');
|
|
40
40
|
}
|
|
41
|
-
|
|
42
41
|
return this._ws;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
// Send to a specific client
|
|
46
44
|
notifyClient(opts: { clientId: string; message: string }) {
|
|
47
|
-
this.ws.
|
|
48
|
-
destination: opts.clientId,
|
|
49
|
-
payload: {
|
|
50
|
-
topic: 'notification',
|
|
51
|
-
data: { message: opts.message, time: new Date().toISOString() },
|
|
52
|
-
},
|
|
53
|
-
});
|
|
45
|
+
this.ws.sendToClient({ clientId: opts.clientId, event: 'notification', data: { message: opts.message } });
|
|
54
46
|
}
|
|
55
47
|
|
|
56
|
-
// Send to all sessions of a user (local instance only)
|
|
57
48
|
notifyUser(opts: { userId: string; message: string }) {
|
|
58
|
-
this.ws.sendToUser({
|
|
59
|
-
userId: opts.userId,
|
|
60
|
-
event: 'notification',
|
|
61
|
-
data: { message: opts.message },
|
|
62
|
-
});
|
|
49
|
+
this.ws.sendToUser({ userId: opts.userId, event: 'notification', data: { message: opts.message } });
|
|
63
50
|
}
|
|
64
51
|
|
|
65
|
-
// Send to a room
|
|
66
52
|
notifyRoom(opts: { room: string; message: string }) {
|
|
67
|
-
this.ws.
|
|
68
|
-
destination: opts.room,
|
|
69
|
-
payload: {
|
|
70
|
-
topic: 'room:update',
|
|
71
|
-
data: { message: opts.message },
|
|
72
|
-
},
|
|
73
|
-
});
|
|
53
|
+
this.ws.sendToRoom({ room: opts.room, event: 'room:update', data: { message: opts.message } });
|
|
74
54
|
}
|
|
75
55
|
|
|
76
|
-
// Broadcast to all clients
|
|
77
56
|
broadcastAnnouncement(opts: { message: string }) {
|
|
78
|
-
this.ws.
|
|
79
|
-
payload: {
|
|
80
|
-
topic: 'system:announcement',
|
|
81
|
-
data: { message: opts.message },
|
|
82
|
-
},
|
|
83
|
-
});
|
|
57
|
+
this.ws.broadcast({ event: 'system:announcement', data: { message: opts.message } });
|
|
84
58
|
}
|
|
85
59
|
}
|
|
86
60
|
```
|
|
87
61
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
> [!WARNING]
|
|
92
|
-
> **`send()` does not support cross-instance user targeting.** The `send()` method resolves `destination` by checking local `clients` map then local `rooms` map. There is no `USER` type in `send()`. To reach all sessions of a user across instances, use `sendToUser()` for local delivery or `WebSocketEmitter.toUser()` for Redis-based cross-instance delivery.
|
|
62
|
+
- **Never `@inject` `WEBSOCKET_INSTANCE` in a constructor.** It is not bound yet at that point - the lazy getter is the only correct pattern.
|
|
63
|
+
- **`sendToClient`/`sendToUser`/`sendToRoom`/`broadcast` are local-only.** They fan out to clients connected to this process. Cross-instance delivery goes through `send()` (Redis-backed) or `WebSocketEmitter` - see below.
|
|
64
|
+
- **`send({ destination, payload })` resolves `destination` dynamically** against local clients, then local rooms, then falls back to publishing as a `ROOM` message on Redis. There is no `userId` destination in `send()` - use `sendToUser()` (local) or `WebSocketEmitter.toUser()` (cross-instance) to reach every session of a user.
|
|
93
65
|
|
|
94
|
-
## WebSocket
|
|
66
|
+
## Send from a process with no WebSocket server
|
|
95
67
|
|
|
96
|
-
`WebSocketEmitter` is a
|
|
97
|
-
|
|
98
|
-
It connects to Redis and publishes messages using the same `IRedisSocketMessage` envelope that `WebSocketServerHelper` listens for, so all connected server instances will receive and deliver the messages to their local clients.
|
|
99
|
-
|
|
100
|
-
#### When to Use WebSocketEmitter
|
|
68
|
+
`WebSocketEmitter` is a standalone, Redis-only publisher for background workers, cron jobs, other microservices, or CLI scripts - anything that needs to push a WebSocket message without running a server. It publishes the same `IRedisSocketMessage` envelope the server helper listens for, so every connected server instance delivers it to its local clients.
|
|
101
69
|
|
|
102
70
|
| Scenario | Use |
|
|
103
71
|
|----------|-----|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
109
|
-
#### Emitter Setup
|
|
72
|
+
| Controller or service inside the main app | `WebSocketServerHelper` (injected via DI) |
|
|
73
|
+
| Background worker or cron job | `WebSocketEmitter` |
|
|
74
|
+
| Separate microservice | `WebSocketEmitter` |
|
|
75
|
+
| CLI script | `WebSocketEmitter` |
|
|
110
76
|
|
|
111
77
|
```typescript
|
|
112
78
|
import { WebSocketEmitter, RedisSingleHelper } from '@venizia/ignis-helpers';
|
|
113
79
|
|
|
114
|
-
|
|
115
|
-
const redisHelper = new RedisSingleHelper({
|
|
116
|
-
name: 'emitter-redis',
|
|
117
|
-
host: process.env.REDIS_HOST ?? 'localhost',
|
|
118
|
-
port: +(process.env.REDIS_PORT ?? 6379),
|
|
119
|
-
password: process.env.REDIS_PASSWORD,
|
|
120
|
-
autoConnect: false,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// 2. Create the emitter
|
|
124
|
-
const emitter = new WebSocketEmitter({
|
|
125
|
-
identifier: 'my-worker-emitter', // Optional, defaults to 'WebSocketEmitter'
|
|
126
|
-
redisConnection: redisHelper,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// 3. Configure (connects Redis pub client)
|
|
130
|
-
await emitter.configure();
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
#### Sending Messages
|
|
80
|
+
const redisHelper = new RedisSingleHelper({ name: 'emitter-redis', host: 'localhost', port: 6379, autoConnect: false });
|
|
134
81
|
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
await emitter.toClient({
|
|
138
|
-
clientId: 'uuid-of-client',
|
|
139
|
-
event: 'job:progress',
|
|
140
|
-
data: { jobId: '123', progress: 75 },
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Send to all sessions of a user (cross-instance)
|
|
144
|
-
await emitter.toUser({
|
|
145
|
-
userId: 'user-456',
|
|
146
|
-
event: 'notification',
|
|
147
|
-
data: { message: 'Your report is ready' },
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Send to a room
|
|
151
|
-
await emitter.toRoom({
|
|
152
|
-
room: 'dashboard-viewers',
|
|
153
|
-
event: 'data:update',
|
|
154
|
-
data: { metric: 'cpu', value: 42.5 },
|
|
155
|
-
exclude: ['client-id-to-skip'], // Optional: exclude specific clients
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Broadcast to all connected, authenticated clients
|
|
159
|
-
await emitter.broadcast({
|
|
160
|
-
event: 'system:maintenance',
|
|
161
|
-
data: { message: 'Scheduled maintenance in 10 minutes' },
|
|
162
|
-
});
|
|
163
|
-
```
|
|
82
|
+
const emitter = new WebSocketEmitter({ identifier: 'my-worker-emitter', redisConnection: redisHelper });
|
|
83
|
+
await emitter.configure(); // connects the Redis pub client
|
|
164
84
|
|
|
165
|
-
|
|
85
|
+
await emitter.toClient({ clientId: 'uuid-of-client', event: 'job:progress', data: { jobId: '123', progress: 75 } });
|
|
86
|
+
await emitter.toUser({ userId: 'user-456', event: 'notification', data: { message: 'Your report is ready' } });
|
|
87
|
+
await emitter.toRoom({ room: 'dashboard-viewers', event: 'data:update', data: { metric: 'cpu', value: 42.5 }, exclude: ['client-id-to-skip'] });
|
|
88
|
+
await emitter.broadcast({ event: 'system:maintenance', data: { message: 'Scheduled maintenance in 10 minutes' } });
|
|
166
89
|
|
|
167
|
-
|
|
168
|
-
// Always shut down when done to release the Redis connection
|
|
169
|
-
await emitter.shutdown();
|
|
90
|
+
await emitter.shutdown(); // always release the Redis connection when done
|
|
170
91
|
```
|
|
171
92
|
|
|
172
|
-
|
|
173
|
-
|
|
93
|
+
- **Fixed `serverId`.** The emitter always publishes with `serverId: 'emitter'`, which never matches a server's `crypto.randomUUID()` - so every server instance processes its messages, none self-dedup.
|
|
94
|
+
- **One Redis client, not two.** The emitter only needs a pub client; the server helper needs pub + sub.
|
|
95
|
+
- **`toUser()` is the recommended cross-instance path.** It publishes to `ws:user:{userId}`; every server subscribed via `psubscribe('ws:user:*')` receives it and calls `sendToUser()` locally, reaching every session of that user across all instances.
|
|
174
96
|
|
|
175
|
-
|
|
176
|
-
> `WebSocketEmitter.toUser()` publishes to the `ws:user:{userId}` Redis channel. All server instances subscribed via `psubscribe('ws:user:*')` will receive it and call `sendToUser()` locally, reaching every session of that user across all instances. This is the **recommended way** to send to a user from outside the main application process.
|
|
97
|
+
## Read the wire protocol
|
|
177
98
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
### Client-Server Message Format
|
|
181
|
-
|
|
182
|
-
All messages exchanged between client and server follow the `IWebSocketMessage` envelope:
|
|
99
|
+
Every message between client and server is a JSON-serialized `IWebSocketMessage` envelope:
|
|
183
100
|
|
|
184
101
|
```typescript
|
|
185
102
|
interface IWebSocketMessage<DataType = unknown> {
|
|
186
|
-
event: string; //
|
|
187
|
-
data?: DataType;
|
|
188
|
-
id?: string;
|
|
103
|
+
event: string; // Required - messages without it are logged and dropped
|
|
104
|
+
data?: DataType;
|
|
105
|
+
id?: string; // Optional, application-defined
|
|
189
106
|
}
|
|
190
107
|
```
|
|
191
108
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
### System Events
|
|
109
|
+
**System events**
|
|
195
110
|
|
|
196
111
|
| Event | Direction | Payload | Description |
|
|
197
112
|
|-------|-----------|---------|-------------|
|
|
198
|
-
| `authenticate` | Client
|
|
199
|
-
| `connected` | Server
|
|
113
|
+
| `authenticate` | Client -> Server | <code v-pre>{ type, token, publicKey? }</code> | Sent after connection opens |
|
|
114
|
+
| `connected` | Server -> Client | <code v-pre>{ id, userId, time, serverPublicKey?, salt? }</code> | Sent after successful authentication |
|
|
200
115
|
| `disconnect` | Both | -- | Connection closing |
|
|
201
|
-
| `join` | Client
|
|
202
|
-
| `leave` | Client
|
|
203
|
-
| `error` | Server
|
|
204
|
-
| `heartbeat` | Client
|
|
116
|
+
| `join` | Client -> Server | <code v-pre>{ rooms: string[] }</code> | Request to join rooms |
|
|
117
|
+
| `leave` | Client -> Server | <code v-pre>{ rooms: string[] }</code> | Request to leave rooms |
|
|
118
|
+
| `error` | Server -> Client | <code v-pre>{ message: string }</code> | Error notification |
|
|
119
|
+
| `heartbeat` | Client -> Server | -- | Keep-alive; server updates `lastActivity`, no callback fires |
|
|
205
120
|
| `encrypted` | Both | Varies | Encryption handshake data |
|
|
206
121
|
|
|
207
|
-
|
|
208
|
-
> The `heartbeat` event is handled specially -- it updates the client's `lastActivity` timestamp and returns immediately without triggering any callbacks. Clients must send heartbeats within the `heartbeatTimeout` interval to avoid being disconnected with code `4002`.
|
|
209
|
-
|
|
210
|
-
### Close Codes
|
|
122
|
+
**Close codes**
|
|
211
123
|
|
|
212
124
|
| Code | Reason | Trigger |
|
|
213
125
|
|------|--------|---------|
|
|
214
126
|
| `1001` | Server shutting down | `wsHelper.shutdown()` |
|
|
215
|
-
| `4001` | Authentication timeout |
|
|
216
|
-
| `4002` | Heartbeat timeout | No messages
|
|
217
|
-
| `4003` | Authentication failed | `authenticateFn` returned `null`/`false` or threw
|
|
218
|
-
| `4004` | Encryption required | `requireEncryption: true` and
|
|
127
|
+
| `4001` | Authentication timeout | No `authenticate` within `authTimeout`, or `authenticateFn`/`handshakeFn` didn't finish within `authTimeout * 3` |
|
|
128
|
+
| `4002` | Heartbeat timeout | No messages within `heartbeatTimeout` |
|
|
129
|
+
| `4003` | Authentication failed | `authenticateFn` returned `null`/`false` or threw |
|
|
130
|
+
| `4004` | Encryption required | `requireEncryption: true` and no `handshakeFn`, or it returned `null`/`false` |
|
|
219
131
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Cross-instance messages are published via Redis Pub/Sub using the `IRedisSocketMessage` envelope:
|
|
132
|
+
**Redis Pub/Sub envelope** (cross-instance messages only):
|
|
223
133
|
|
|
224
134
|
```typescript
|
|
225
135
|
interface IRedisSocketMessage<DataType = unknown> {
|
|
226
|
-
serverId: string; // Source server
|
|
227
|
-
type:
|
|
136
|
+
serverId: string; // Source server UUID, or 'emitter'
|
|
137
|
+
type: 'client' | 'user' | 'room' | 'broadcast';
|
|
228
138
|
target?: string; // Target clientId / userId / room name
|
|
229
|
-
event: string;
|
|
230
|
-
data: DataType;
|
|
231
|
-
exclude?: string[]; // Client IDs to
|
|
139
|
+
event: string;
|
|
140
|
+
data: DataType;
|
|
141
|
+
exclude?: string[]; // Client IDs to skip during delivery
|
|
232
142
|
}
|
|
233
143
|
```
|
|
234
144
|
|
|
235
|
-
Messages from the same `serverId` are ignored (self-dedup) -- the sending server already delivered locally before publishing to Redis. Messages from the `WebSocketEmitter` use `serverId = 'emitter'`, which never matches any server's UUID, so all servers process them.
|
|
236
|
-
|
|
237
|
-
### Message Types
|
|
238
|
-
|
|
239
145
|
| Type | Channel Pattern | Description |
|
|
240
146
|
|------|----------------|-------------|
|
|
241
147
|
| `client` | `ws:client:{clientId}` | Direct to specific client |
|
|
@@ -243,33 +149,30 @@ Messages from the same `serverId` are ignored (self-dedup) -- the sending server
|
|
|
243
149
|
| `room` | `ws:room:{roomName}` | To all clients in a room |
|
|
244
150
|
| `broadcast` | `ws:broadcast` | To all connected, authenticated clients |
|
|
245
151
|
|
|
246
|
-
|
|
152
|
+
- **Self-dedup by `serverId`.** A server ignores Redis messages carrying its own `serverId` - it already delivered locally before publishing. `WebSocketEmitter` messages use `serverId: 'emitter'`, which never matches, so all servers process them.
|
|
247
153
|
|
|
248
|
-
|
|
154
|
+
## Track connected clients
|
|
249
155
|
|
|
250
|
-
Each
|
|
156
|
+
Each connection is an `IWebSocketClient` entry in an in-memory `Map<string, IWebSocketClient>`:
|
|
251
157
|
|
|
252
158
|
```typescript
|
|
253
|
-
interface IWebSocketClient<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
serverPublicKey?: string; // ECDH public key (set if encrypted)
|
|
267
|
-
salt?: string; // Encryption salt (set if encrypted)
|
|
268
|
-
authTimer?: ReturnType<typeof setTimeout>; // Auth timeout timer (cleared after auth)
|
|
159
|
+
interface IWebSocketClient<MetadataType extends Record<string, unknown> = Record<string, unknown>> {
|
|
160
|
+
id: string; // UUID, assigned during upgrade
|
|
161
|
+
userId?: string; // Set after authentication
|
|
162
|
+
socket: IWebSocket; // Bun native WebSocket reference
|
|
163
|
+
state: 'unauthorized' | 'authenticating' | 'authenticated' | 'disconnected';
|
|
164
|
+
rooms: Set<string>; // Joined rooms, including default rooms + own clientId room
|
|
165
|
+
backpressured: boolean; // True when socket.send() returned -1
|
|
166
|
+
encrypted: boolean; // Completed the encryption handshake
|
|
167
|
+
connectedAt: number;
|
|
168
|
+
lastActivity: number; // Last heartbeat/message timestamp
|
|
169
|
+
metadata?: MetadataType; // From authenticateFn's return value
|
|
170
|
+
serverPublicKey?: string;
|
|
171
|
+
salt?: string;
|
|
269
172
|
}
|
|
270
173
|
```
|
|
271
174
|
|
|
272
|
-
|
|
175
|
+
**State transitions**
|
|
273
176
|
|
|
274
177
|
```
|
|
275
178
|
UNAUTHORIZED --(authenticate event)--> AUTHENTICATING
|
|
@@ -282,25 +185,11 @@ UNAUTHORIZED --(authenticate event)--> AUTHENTICATING
|
|
|
282
185
|
DISCONNECTED AUTHENTICATED DISCONNECTED
|
|
283
186
|
|
|
|
284
187
|
(close / heartbeat timeout)
|
|
285
|
-
|
|
|
286
188
|
v
|
|
287
189
|
DISCONNECTED
|
|
288
190
|
```
|
|
289
191
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
```typescript
|
|
293
|
-
class WebSocketClientStates {
|
|
294
|
-
static readonly UNAUTHORIZED = 'unauthorized';
|
|
295
|
-
static readonly AUTHENTICATING = 'authenticating';
|
|
296
|
-
static readonly AUTHENTICATED = 'authenticated';
|
|
297
|
-
static readonly DISCONNECTED = 'disconnected';
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Tracking Maps
|
|
302
|
-
|
|
303
|
-
The server maintains three index maps for efficient lookups:
|
|
192
|
+
**Index maps**
|
|
304
193
|
|
|
305
194
|
| Map | Key | Value | Purpose |
|
|
306
195
|
|-----|-----|-------|---------|
|
|
@@ -308,168 +197,48 @@ The server maintains three index maps for efficient lookups:
|
|
|
308
197
|
| `users` | `userId` | `Set<clientId>` | Multi-session user index |
|
|
309
198
|
| `rooms` | `room` | `Set<clientId>` | Room membership index |
|
|
310
199
|
|
|
311
|
-
|
|
312
|
-
> A single user can have multiple client connections (e.g., browser tab + mobile). Use `getClientsByUser({ userId })` to reach all sessions. The `users` map entry is automatically cleaned up when the last client for a user disconnects.
|
|
200
|
+
- **One user, many sessions.** `getClientsByUser({ userId })` returns every session for a user (browser tab, mobile app, ...). The `users` map entry is removed automatically once the last session disconnects.
|
|
313
201
|
|
|
314
|
-
##
|
|
315
|
-
|
|
316
|
-
### `WebSocketChannels` Class
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
class WebSocketChannels {
|
|
320
|
-
// --- Static channel names ---
|
|
321
|
-
static readonly BROADCAST = 'ws:broadcast';
|
|
322
|
-
static readonly ROOM_PREFIX = 'ws:room:';
|
|
323
|
-
static readonly CLIENT_PREFIX = 'ws:client:';
|
|
324
|
-
static readonly USER_PREFIX = 'ws:user:';
|
|
325
|
-
|
|
326
|
-
// --- Channel builders ---
|
|
327
|
-
static forRoom(opts: { room: string }): string; // 'ws:room:{room}'
|
|
328
|
-
static forClient(opts: { clientId: string }): string; // 'ws:client:{clientId}'
|
|
329
|
-
static forUser(opts: { userId: string }): string; // 'ws:user:{userId}'
|
|
330
|
-
|
|
331
|
-
// --- Pattern builders (for Redis PSUBSCRIBE) ---
|
|
332
|
-
static forRoomPattern(): string; // 'ws:room:*'
|
|
333
|
-
static forClientPattern(): string; // 'ws:client:*'
|
|
334
|
-
static forUserPattern(): string; // 'ws:user:*'
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### Redis Client Type
|
|
339
|
-
|
|
340
|
-
Both `WebSocketServerHelper` and `WebSocketEmitter` support Redis single instance and Redis Cluster:
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
type TRedisClient = Redis | Cluster;
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
The Redis client is obtained via `redisConnection.duplicateClient()`. This creates a fresh connection that inherits the parent's configuration (including cluster mode). This ensures WebSocket pub/sub traffic does not interfere with application Redis usage.
|
|
347
|
-
|
|
348
|
-
### Subscription Setup
|
|
349
|
-
|
|
350
|
-
During `configure()`, the server subscribes to all channels:
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
// Direct subscribe (exact match)
|
|
354
|
-
redisSub.subscribe(WebSocketChannels.BROADCAST); // 'ws:broadcast'
|
|
355
|
-
|
|
356
|
-
// Pattern subscribe (wildcard match)
|
|
357
|
-
redisSub.psubscribe(WebSocketChannels.forRoomPattern()); // 'ws:room:*'
|
|
358
|
-
redisSub.psubscribe(WebSocketChannels.forClientPattern()); // 'ws:client:*'
|
|
359
|
-
redisSub.psubscribe(WebSocketChannels.forUserPattern()); // 'ws:user:*'
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
> [!NOTE]
|
|
363
|
-
> Redis PSUBSCRIBE uses pattern matching -- a message published to `ws:room:chat-general` is received by all servers subscribed to `ws:room:*`. This allows the server to receive messages for any room without knowing room names in advance.
|
|
364
|
-
|
|
365
|
-
### Message Flow (Cross-Instance)
|
|
366
|
-
|
|
367
|
-
```
|
|
368
|
-
Server A Redis Server B
|
|
369
|
-
| | |
|
|
370
|
-
|-- send({ destination: room }) -| |
|
|
371
|
-
| 1. sendToRoom() locally | |
|
|
372
|
-
| 2. publishToRedis() -------->|-- ws:room:chat ------> |
|
|
373
|
-
| | onRedisMessage()
|
|
374
|
-
| | |-- skip if serverId === own
|
|
375
|
-
| | +-- sendToRoom() locally
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### Message Flow (Emitter to Servers)
|
|
379
|
-
|
|
380
|
-
```
|
|
381
|
-
WebSocketEmitter Redis Server A + Server B
|
|
382
|
-
| | |
|
|
383
|
-
|-- toUser({ userId }) -------->|-- ws:user:u1 --------> |
|
|
384
|
-
| serverId = 'emitter' | onRedisMessage()
|
|
385
|
-
| | |-- serverId !== own -> process
|
|
386
|
-
| | +-- sendToUser() locally
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
## Authentication Flow
|
|
202
|
+
## Understand the authentication flow
|
|
390
203
|
|
|
391
204
|
```
|
|
392
205
|
Client Server
|
|
393
|
-
| |
|
|
394
206
|
|-- WS upgrade request -------->|
|
|
395
|
-
|<-- 101 Switching Protocols ---| (Bun handles upgrade)
|
|
396
|
-
| |--
|
|
397
|
-
| | state = UNAUTHORIZED
|
|
398
|
-
| | subscribe(clientId) <-- Bun topic for direct messaging
|
|
399
|
-
| | start authTimer (5s default)
|
|
400
|
-
| |
|
|
207
|
+
|<-- 101 Switching Protocols ---| (Bun handles the upgrade)
|
|
208
|
+
| |-- state = UNAUTHORIZED, subscribe(clientId), start authTimer (5s)
|
|
401
209
|
|-- { event: 'authenticate', |
|
|
402
|
-
| data: { token: '...' } } >|--
|
|
403
|
-
| | state = AUTHENTICATING
|
|
404
|
-
| | replace timer with authTimeout * 3
|
|
210
|
+
| data: { token: '...' } } >|-- state = AUTHENTICATING, replace timer with authTimeout * 3
|
|
405
211
|
| | await authenticateFn(data)
|
|
406
|
-
| |
|
|
407
|
-
| |
|
|
408
|
-
| |
|
|
409
|
-
|
|
410
|
-
| | state = AUTHENTICATED
|
|
411
|
-
| | index by userId
|
|
412
|
-
| | subscribe(BROADCAST_TOPIC) <-- unless encrypted
|
|
413
|
-
| | joinRoom(clientId) <-- auto-join own ID as room
|
|
414
|
-
| | joinRoom(default rooms)
|
|
415
|
-
| |
|
|
416
|
-
|<-- { event: 'connected', |
|
|
417
|
-
| data: { id, userId, |
|
|
418
|
-
| time, serverPublicKey?, |
|
|
419
|
-
| salt? } } -------------|
|
|
212
|
+
| | (if requireEncryption) await handshakeFn(data)
|
|
213
|
+
| | state = AUTHENTICATED, index by userId
|
|
214
|
+
| | subscribe(BROADCAST_TOPIC) + joinRoom(clientId + default rooms) <- unless encrypted
|
|
215
|
+
|<-- { event: 'connected', ... }-|
|
|
420
216
|
| |-- clientConnectedFn()
|
|
421
217
|
```
|
|
422
218
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
1. **Initial timeout** (`authTimeout`, default 5 s): Starts when the client connects. If the client does not send an `authenticate` event within this window, the socket is closed with code `4001`.
|
|
428
|
-
|
|
429
|
-
2. **In-progress timeout** (`authTimeout * 3`, default 15 s): Replaces the initial timer when the `authenticate` event is received. This provides a longer window for the async `authenticateFn` (and optionally `handshakeFn`) to complete. If authentication does not finish within this window, the socket is closed with code `4001`.
|
|
430
|
-
|
|
431
|
-
### Client ID Auto-Join
|
|
432
|
-
|
|
433
|
-
After successful authentication, the server calls `joinRoom({ clientId, room: clientId })`. This means the client's own ID is registered as both a Bun native topic subscription (set during `onClientConnect`) and an application-level room. This enables targeting a specific client via `send({ destination: clientId })` or `sendToRoom({ room: clientId })`.
|
|
434
|
-
|
|
435
|
-
### Bun Topic Subscription Timing
|
|
436
|
-
|
|
437
|
-
| Topic | Subscribed At | Condition |
|
|
438
|
-
|-------|--------------|-----------|
|
|
439
|
-
| Client's own `clientId` | `onClientConnect()` (before auth) | Always |
|
|
440
|
-
| `BROADCAST_TOPIC` | `handleAuthenticate()` (after auth) | Only if `!client.encrypted` |
|
|
441
|
-
| Default rooms | `handleAuthenticate()` (after auth, via `joinRoom()`) | Only if `!client.encrypted` |
|
|
442
|
-
| Custom rooms | `handleJoin()` (on client request) | Only if `!client.encrypted` |
|
|
443
|
-
|
|
444
|
-
Encrypted clients are **never** subscribed to Bun native topics (except `clientId` which is set before encryption status is known). All delivery to encrypted clients goes through the per-client `outboundTransformer` path.
|
|
445
|
-
|
|
446
|
-
## Delivery Strategy
|
|
447
|
-
|
|
448
|
-
The helper uses a dual delivery strategy depending on whether encryption is active:
|
|
219
|
+
- **Two timeout phases, not one.** The initial `authTimeout` (5s default) starts on connect and closes with `4001` if no `authenticate` event arrives. Once `authenticate` is received, that timer is replaced with `authTimeout * 3` (15s default) to give the async `authenticateFn` (and `handshakeFn`) room to complete.
|
|
220
|
+
- **A client's own ID becomes a room.** After authentication, `joinRoom({ clientId, room: clientId })` runs automatically - this is what lets `send({ destination: clientId })` or `sendToRoom({ room: clientId })` target one specific client.
|
|
221
|
+
- **Encrypted clients skip Bun's native topics.** A client's own `clientId` topic is subscribed before auth (always). `BROADCAST_TOPIC` and rooms are subscribed after auth, but only when `!client.encrypted`. Encrypted clients rely entirely on the per-client `outboundTransformer` path.
|
|
449
222
|
|
|
450
|
-
|
|
451
|
-
- Room/broadcast messages use Bun's native `server.publish(topic, payload)` -- O(1) C++ fan-out
|
|
452
|
-
- Client-direct messages use `socket.send()` directly
|
|
453
|
-
- Zero JavaScript iteration for room fan-out
|
|
223
|
+
## Understand the delivery strategy
|
|
454
224
|
|
|
455
|
-
|
|
456
|
-
- Encrypted clients are unsubscribed from all Bun native topics (`enableClientEncryption()`)
|
|
457
|
-
- Room/broadcast messages iterate clients individually, running each through `outboundTransformer`
|
|
458
|
-
- Uses `executePromiseWithLimit({ tasks, limit: encryptedBatchLimit })` for concurrency control
|
|
459
|
-
- Non-encrypted clients in the same room still use the Bun fast path
|
|
225
|
+
The helper picks a delivery path per call, based on encryption and `exclude`:
|
|
460
226
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
227
|
+
| Condition | Path |
|
|
228
|
+
|-----------|------|
|
|
229
|
+
| No encryption, no `exclude` | Bun's native `server.publish(topic, payload)` - O(1) C++ fan-out, zero JS iteration |
|
|
230
|
+
| No encryption, `exclude` provided | Iterates clients in the room/broadcast set, skipping excluded IDs |
|
|
231
|
+
| Encryption active | Unsubscribed from Bun topics; iterates clients individually through `outboundTransformer`, bounded by `executePromiseWithLimit({ limit: encryptedBatchLimit })` |
|
|
232
|
+
| `outboundTransformer` bound at all | **All** room/broadcast sends fall back to per-client iteration, even for non-encrypted clients in the same room - Bun's native pub/sub cannot selectively transform |
|
|
464
233
|
|
|
465
234
|
> [!IMPORTANT]
|
|
466
|
-
>
|
|
235
|
+
> Only bind `outboundTransformer` when you actually need per-client message transformation (e.g. per-client encryption). Binding it removes the fast path for every room/broadcast send, encrypted or not.
|
|
467
236
|
|
|
468
|
-
## See
|
|
237
|
+
## See also
|
|
469
238
|
|
|
470
|
-
- [
|
|
471
|
-
- [
|
|
472
|
-
- [Error Reference](./errors) -
|
|
473
|
-
- [WebSocketServerHelper](/extensions/helpers/websocket/) -
|
|
474
|
-
- [Socket.IO Component](../socket-io/) - Node.js-compatible alternative
|
|
475
|
-
- [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) -
|
|
239
|
+
- [Overview](./) - quick start, imports, and common configuration tasks
|
|
240
|
+
- [Full Reference](./api) - lifecycle diagram, binding keys, `WebSocketEmitter` API, internals
|
|
241
|
+
- [Error Reference](./errors) - error conditions and troubleshooting
|
|
242
|
+
- [WebSocketServerHelper](/extensions/helpers/websocket/) - helper API documentation
|
|
243
|
+
- [Socket.IO Component](../socket-io/) - Node.js-compatible alternative
|
|
244
|
+
- [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - official Bun WebSocket API reference
|