@venizia/ignis-docs 0.2.0 → 0.2.1-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/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- 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 +27 -3
- 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 +8 -4
- 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 +247 -153
- 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 +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- 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/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- 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 +58 -218
- package/content/references/utilities/retry.md +139 -0
- 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/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +24 -23
|
@@ -1,241 +1,155 @@
|
|
|
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**, in this order:
|
|
93
65
|
|
|
94
|
-
|
|
66
|
+
| Order | Checks |
|
|
67
|
+
|---|---|
|
|
68
|
+
| 1 | Local clients |
|
|
69
|
+
| 2 | Local rooms |
|
|
70
|
+
| 3 | Redis, publishing as a `ROOM` message (fallback) |
|
|
95
71
|
|
|
96
|
-
|
|
72
|
+
There is no `userId` destination in `send()`. To reach every session of a user, use `sendToUser()` (local) or `WebSocketEmitter.toUser()` (cross-instance).
|
|
97
73
|
|
|
98
|
-
|
|
74
|
+
## Send from a process with no WebSocket server
|
|
99
75
|
|
|
100
|
-
|
|
76
|
+
`WebSocketEmitter` is a standalone, Redis-only publisher. Use it from 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
77
|
|
|
102
78
|
| Scenario | Use |
|
|
103
79
|
|----------|-----|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
109
|
-
#### Emitter Setup
|
|
80
|
+
| Controller or service inside the main app | `WebSocketServerHelper` (injected via DI) |
|
|
81
|
+
| Background worker or cron job | `WebSocketEmitter` |
|
|
82
|
+
| Separate microservice | `WebSocketEmitter` |
|
|
83
|
+
| CLI script | `WebSocketEmitter` |
|
|
110
84
|
|
|
111
85
|
```typescript
|
|
112
86
|
import { WebSocketEmitter, RedisSingleHelper } from '@venizia/ignis-helpers';
|
|
113
87
|
|
|
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
|
-
```
|
|
88
|
+
const redisHelper = new RedisSingleHelper({ name: 'emitter-redis', host: 'localhost', port: 6379, autoConnect: false });
|
|
132
89
|
|
|
133
|
-
|
|
90
|
+
const emitter = new WebSocketEmitter({ identifier: 'my-worker-emitter', redisConnection: redisHelper });
|
|
91
|
+
await emitter.configure(); // connects the Redis pub client
|
|
134
92
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await emitter.
|
|
138
|
-
|
|
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
|
-
```
|
|
93
|
+
await emitter.toClient({ clientId: 'uuid-of-client', event: 'job:progress', data: { jobId: '123', progress: 75 } });
|
|
94
|
+
await emitter.toUser({ userId: 'user-456', event: 'notification', data: { message: 'Your report is ready' } });
|
|
95
|
+
await emitter.toRoom({ room: 'dashboard-viewers', event: 'data:update', data: { metric: 'cpu', value: 42.5 }, exclude: ['client-id-to-skip'] });
|
|
96
|
+
await emitter.broadcast({ event: 'system:maintenance', data: { message: 'Scheduled maintenance in 10 minutes' } });
|
|
164
97
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// Always shut down when done to release the Redis connection
|
|
169
|
-
await emitter.shutdown();
|
|
98
|
+
await emitter.shutdown(); // always release the Redis connection when done
|
|
170
99
|
```
|
|
171
100
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
> [!TIP]
|
|
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.
|
|
177
|
-
|
|
178
|
-
## Wire Protocol
|
|
101
|
+
- **Fixed `serverId`.** The emitter always publishes with `serverId: 'emitter'`. That value never matches a server's `crypto.randomUUID()`, so every server instance processes its messages - none self-dedup.
|
|
102
|
+
- **One Redis client, not two.** The emitter only needs a pub client. The server helper needs both pub and sub.
|
|
103
|
+
- **`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.
|
|
179
104
|
|
|
180
|
-
|
|
105
|
+
## Read the wire protocol
|
|
181
106
|
|
|
182
|
-
|
|
107
|
+
Every message between client and server is a JSON-serialized `IWebSocketMessage` envelope:
|
|
183
108
|
|
|
184
109
|
```typescript
|
|
185
110
|
interface IWebSocketMessage<DataType = unknown> {
|
|
186
|
-
event: string; //
|
|
187
|
-
data?: DataType;
|
|
188
|
-
id?: string;
|
|
111
|
+
event: string; // Required - messages without it are logged and dropped
|
|
112
|
+
data?: DataType;
|
|
113
|
+
id?: string; // Optional, application-defined
|
|
189
114
|
}
|
|
190
115
|
```
|
|
191
116
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
### System Events
|
|
117
|
+
**System events**
|
|
195
118
|
|
|
196
119
|
| Event | Direction | Payload | Description |
|
|
197
120
|
|-------|-----------|---------|-------------|
|
|
198
|
-
| `authenticate` | Client
|
|
199
|
-
| `connected` | Server
|
|
121
|
+
| `authenticate` | Client -> Server | <code v-pre>{ type, token, publicKey? }</code> | Sent after connection opens |
|
|
122
|
+
| `connected` | Server -> Client | <code v-pre>{ id, userId, time, serverPublicKey?, salt? }</code> | Sent after successful authentication |
|
|
200
123
|
| `disconnect` | Both | -- | Connection closing |
|
|
201
|
-
| `join` | Client
|
|
202
|
-
| `leave` | Client
|
|
203
|
-
| `error` | Server
|
|
204
|
-
| `heartbeat` | Client
|
|
124
|
+
| `join` | Client -> Server | <code v-pre>{ rooms: string[] }</code> | Request to join rooms |
|
|
125
|
+
| `leave` | Client -> Server | <code v-pre>{ rooms: string[] }</code> | Request to leave rooms |
|
|
126
|
+
| `error` | Server -> Client | <code v-pre>{ message: string }</code> | Error notification |
|
|
127
|
+
| `heartbeat` | Client -> Server | -- | Keep-alive; server updates `lastActivity`, no callback fires |
|
|
205
128
|
| `encrypted` | Both | Varies | Encryption handshake data |
|
|
206
129
|
|
|
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
|
|
130
|
+
**Close codes**
|
|
211
131
|
|
|
212
132
|
| Code | Reason | Trigger |
|
|
213
133
|
|------|--------|---------|
|
|
214
134
|
| `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
|
|
219
|
-
|
|
220
|
-
### Redis Message Envelope
|
|
135
|
+
| `4001` | Authentication timeout | No `authenticate` within `authTimeout`, or `authenticateFn`/`handshakeFn` didn't finish within `authTimeout * 3` |
|
|
136
|
+
| `4002` | Heartbeat timeout | No messages within `heartbeatTimeout` |
|
|
137
|
+
| `4003` | Authentication failed | `authenticateFn` returned `null`/`false` or threw |
|
|
138
|
+
| `4004` | Encryption required | `requireEncryption: true` and no `handshakeFn`, or it returned `null`/`false` |
|
|
221
139
|
|
|
222
|
-
|
|
140
|
+
**Redis Pub/Sub envelope** (cross-instance messages only):
|
|
223
141
|
|
|
224
142
|
```typescript
|
|
225
143
|
interface IRedisSocketMessage<DataType = unknown> {
|
|
226
|
-
serverId: string; // Source server
|
|
227
|
-
type:
|
|
144
|
+
serverId: string; // Source server UUID, or 'emitter'
|
|
145
|
+
type: 'client' | 'user' | 'room' | 'broadcast';
|
|
228
146
|
target?: string; // Target clientId / userId / room name
|
|
229
|
-
event: string;
|
|
230
|
-
data: DataType;
|
|
231
|
-
exclude?: string[]; // Client IDs to
|
|
147
|
+
event: string;
|
|
148
|
+
data: DataType;
|
|
149
|
+
exclude?: string[]; // Client IDs to skip during delivery
|
|
232
150
|
}
|
|
233
151
|
```
|
|
234
152
|
|
|
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
153
|
| Type | Channel Pattern | Description |
|
|
240
154
|
|------|----------------|-------------|
|
|
241
155
|
| `client` | `ws:client:{clientId}` | Direct to specific client |
|
|
@@ -243,33 +157,30 @@ Messages from the same `serverId` are ignored (self-dedup) -- the sending server
|
|
|
243
157
|
| `room` | `ws:room:{roomName}` | To all clients in a room |
|
|
244
158
|
| `broadcast` | `ws:broadcast` | To all connected, authenticated clients |
|
|
245
159
|
|
|
246
|
-
|
|
160
|
+
- **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
161
|
|
|
248
|
-
|
|
162
|
+
## Track connected clients
|
|
249
163
|
|
|
250
|
-
Each
|
|
164
|
+
Each connection is an `IWebSocketClient` entry in an in-memory `Map<string, IWebSocketClient>`:
|
|
251
165
|
|
|
252
166
|
```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)
|
|
167
|
+
interface IWebSocketClient<MetadataType extends Record<string, unknown> = Record<string, unknown>> {
|
|
168
|
+
id: string; // UUID, assigned during upgrade
|
|
169
|
+
userId?: string; // Set after authentication
|
|
170
|
+
socket: IWebSocket; // Bun native WebSocket reference
|
|
171
|
+
state: 'unauthorized' | 'authenticating' | 'authenticated' | 'disconnected';
|
|
172
|
+
rooms: Set<string>; // Joined rooms, including default rooms + own clientId room
|
|
173
|
+
backpressured: boolean; // True when socket.send() returned -1
|
|
174
|
+
encrypted: boolean; // Completed the encryption handshake
|
|
175
|
+
connectedAt: number;
|
|
176
|
+
lastActivity: number; // Last heartbeat/message timestamp
|
|
177
|
+
metadata?: MetadataType; // From authenticateFn's return value
|
|
178
|
+
serverPublicKey?: string;
|
|
179
|
+
salt?: string;
|
|
269
180
|
}
|
|
270
181
|
```
|
|
271
182
|
|
|
272
|
-
|
|
183
|
+
**State transitions**
|
|
273
184
|
|
|
274
185
|
```
|
|
275
186
|
UNAUTHORIZED --(authenticate event)--> AUTHENTICATING
|
|
@@ -282,25 +193,11 @@ UNAUTHORIZED --(authenticate event)--> AUTHENTICATING
|
|
|
282
193
|
DISCONNECTED AUTHENTICATED DISCONNECTED
|
|
283
194
|
|
|
|
284
195
|
(close / heartbeat timeout)
|
|
285
|
-
|
|
|
286
196
|
v
|
|
287
197
|
DISCONNECTED
|
|
288
198
|
```
|
|
289
199
|
|
|
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:
|
|
200
|
+
**Index maps**
|
|
304
201
|
|
|
305
202
|
| Map | Key | Value | Purpose |
|
|
306
203
|
|-----|-----|-------|---------|
|
|
@@ -308,168 +205,53 @@ The server maintains three index maps for efficient lookups:
|
|
|
308
205
|
| `users` | `userId` | `Set<clientId>` | Multi-session user index |
|
|
309
206
|
| `rooms` | `room` | `Set<clientId>` | Room membership index |
|
|
310
207
|
|
|
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.
|
|
313
|
-
|
|
314
|
-
## Redis Channel Architecture
|
|
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
|
-
```
|
|
208
|
+
- **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.
|
|
388
209
|
|
|
389
|
-
##
|
|
210
|
+
## Understand the authentication flow
|
|
390
211
|
|
|
391
212
|
```
|
|
392
213
|
Client Server
|
|
393
|
-
| |
|
|
394
214
|
|-- 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
|
-
| |
|
|
215
|
+
|<-- 101 Switching Protocols ---| (Bun handles the upgrade)
|
|
216
|
+
| |-- state = UNAUTHORIZED, subscribe(clientId), start authTimer (5s)
|
|
401
217
|
|-- { event: 'authenticate', |
|
|
402
|
-
| data: { token: '...' } } >|--
|
|
403
|
-
| | state = AUTHENTICATING
|
|
404
|
-
| | replace timer with authTimeout * 3
|
|
218
|
+
| data: { token: '...' } } >|-- state = AUTHENTICATING, replace timer with authTimeout * 3
|
|
405
219
|
| | 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? } } -------------|
|
|
220
|
+
| | (if requireEncryption) await handshakeFn(data)
|
|
221
|
+
| | state = AUTHENTICATED, index by userId
|
|
222
|
+
| | subscribe(BROADCAST_TOPIC) + joinRoom(clientId + default rooms) <- unless encrypted
|
|
223
|
+
|<-- { event: 'connected', ... }-|
|
|
420
224
|
| |-- clientConnectedFn()
|
|
421
225
|
```
|
|
422
226
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
There are two timeout phases:
|
|
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
|
|
227
|
+
- **Two timeout phases, not one:**
|
|
447
228
|
|
|
448
|
-
|
|
229
|
+
| Phase | Timeout | Starts when | On expiry |
|
|
230
|
+
|---|---|---|---|
|
|
231
|
+
| Initial | `authTimeout` (5s default) | Connection opens | Closes with `4001` if no `authenticate` event arrives |
|
|
232
|
+
| Post-authenticate | `authTimeout * 3` (15s default) | `authenticate` event is received | Gives the async `authenticateFn` (and `handshakeFn`, when encryption is required) room to complete |
|
|
233
|
+
- **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.
|
|
234
|
+
- **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
235
|
|
|
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
|
|
236
|
+
## Understand the delivery strategy
|
|
454
237
|
|
|
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
|
|
238
|
+
The helper picks a delivery path per call, based on encryption and `exclude`:
|
|
460
239
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
240
|
+
| Condition | Path |
|
|
241
|
+
|-----------|------|
|
|
242
|
+
| No encryption, no `exclude` | Bun's native `server.publish(topic, payload)` - O(1) C++ fan-out, zero JS iteration |
|
|
243
|
+
| No encryption, `exclude` provided | Iterates clients in the room/broadcast set, skipping excluded IDs |
|
|
244
|
+
| Encryption active | Unsubscribed from Bun topics; iterates clients individually through `outboundTransformer`, bounded by `executePromiseWithLimit({ limit: encryptedBatchLimit })` |
|
|
245
|
+
| `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
246
|
|
|
465
247
|
> [!IMPORTANT]
|
|
466
|
-
>
|
|
248
|
+
> Only bind `outboundTransformer` when you actually need per-client message transformation (for example per-client encryption). Binding it removes the fast path for every room/broadcast send, encrypted or not.
|
|
467
249
|
|
|
468
|
-
## See
|
|
250
|
+
## See also
|
|
469
251
|
|
|
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) -
|
|
252
|
+
- [Overview](./) - quick start, imports, and common configuration tasks
|
|
253
|
+
- [Full Reference](./api) - lifecycle diagram, binding keys, `WebSocketEmitter` API, internals
|
|
254
|
+
- [Error Reference](./errors) - error conditions and troubleshooting
|
|
255
|
+
- [WebSocketServerHelper](/extensions/helpers/websocket/) - helper API documentation
|
|
256
|
+
- [Socket.IO Component](../socket-io/) - Node.js-compatible alternative
|
|
257
|
+
- [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - official Bun WebSocket API reference
|