@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.
Files changed (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,241 +1,147 @@
1
- # WebSocket -- Usage & Examples
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
- > Server-side usage patterns, WebSocket Emitter, wire protocol, client tracking, Redis channel architecture, authentication flow, and delivery strategy.
7
+ # Usage & Examples
4
8
 
5
- ## Using in Services/Controllers
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 `WebSocketServerHelper` to interact with WebSocket:
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 = this.application.get<WebSocketServerHelper>({
33
- key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
34
- isOptional: true,
35
- }) ?? null;
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.send({
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.send({
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.send({
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
- > [!IMPORTANT]
89
- > **Lazy getter pattern**: Since `WebSocketServerHelper` is bound via a post-start hook, it is not available during DI construction. Use a lazy getter that resolves from the application container on first access.
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 Emitter
66
+ ## Send from a process with no WebSocket server
95
67
 
96
- `WebSocketEmitter` is a **standalone, lightweight Redis-only publisher** for sending WebSocket messages from processes that do not run a WebSocket server -- such as background workers, cron jobs, microservices, or CLI scripts.
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
- | Send from a controller or service in the main app | `WebSocketServerHelper` (injected via DI) |
105
- | Send from a background worker or cron job | `WebSocketEmitter` |
106
- | Send from a separate microservice | `WebSocketEmitter` |
107
- | Broadcast from a CLI script | `WebSocketEmitter` |
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
- // 1. Create a Redis connection (same Redis instance as the WebSocket server)
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
- ```typescript
136
- // Send to a specific client by ID
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
- #### Shutdown
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
- ```typescript
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
- > [!NOTE]
173
- > The emitter uses a fixed `serverId` of `'emitter'` instead of a random UUID. This means all server instances will process emitter messages (none will self-dedup). The emitter only needs a single Redis client (pub), not two (pub + sub) like the server helper.
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
- > [!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.
97
+ ## Read the wire protocol
177
98
 
178
- ## Wire Protocol
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; // Event name (system or custom)
187
- data?: DataType; // Payload data
188
- id?: string; // Optional message ID (application-defined)
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
- Messages are serialized as JSON strings over the WebSocket connection. The `event` field is required -- messages without it are logged and dropped.
193
-
194
- ### System Events
109
+ **System events**
195
110
 
196
111
  | Event | Direction | Payload | Description |
197
112
  |-------|-----------|---------|-------------|
198
- | `authenticate` | Client --> Server | Auth credentials (<code v-pre>{ type, token, publicKey? }</code>) | Client sends credentials after connection opens |
199
- | `connected` | Server --> Client | <code v-pre>{ id, userId, time, serverPublicKey?, salt? }</code> | Sent after successful authentication |
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 --> Server | <code v-pre>{ rooms: string[] }</code> | Request to join rooms |
202
- | `leave` | Client --> Server | <code v-pre>{ rooms: string[] }</code> | Request to leave rooms |
203
- | `error` | Server --> Client | <code v-pre>{ message: string }</code> | Error notification |
204
- | `heartbeat` | Client --> Server | -- | Keep-alive ping (client sends, server updates `lastActivity`) |
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
- > [!NOTE]
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 | Client did not send `authenticate` within `authTimeout`, or `authenticateFn` did not complete within `authTimeout * 3` |
216
- | `4002` | Heartbeat timeout | No messages received within `heartbeatTimeout` |
217
- | `4003` | Authentication failed | `authenticateFn` returned `null`/`false` or threw an exception |
218
- | `4004` | Encryption required | `requireEncryption: true` and either no `handshakeFn` configured or `handshakeFn` returned `null`/`false` |
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
- ### Redis Message Envelope
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 instance ID (UUID or 'emitter')
227
- type: TWebSocketMessageType; // 'client' | 'user' | 'room' | 'broadcast'
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; // Event to deliver
230
- data: DataType; // Payload
231
- exclude?: string[]; // Client IDs to exclude from delivery
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
- ## Client Tracking
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
- ### `IWebSocketClient` Interface
154
+ ## Track connected clients
249
155
 
250
- Each connected client is tracked in an in-memory `Map<string, IWebSocketClient>`:
156
+ Each connection is an `IWebSocketClient` entry in an in-memory `Map<string, IWebSocketClient>`:
251
157
 
252
158
  ```typescript
253
- interface IWebSocketClient<
254
- MetadataType extends Record<string, unknown> = Record<string, unknown>,
255
- > {
256
- id: string; // Unique client ID (UUID, assigned during upgrade)
257
- userId?: string; // Set after authentication
258
- socket: IWebSocket; // Bun native WebSocket reference
259
- state: TWebSocketClientState; // 'unauthorized' | 'authenticating' | 'authenticated' | 'disconnected'
260
- rooms: Set<string>; // Joined rooms (including default rooms and own clientId room)
261
- backpressured: boolean; // True when socket.send() returns -1 (Bun backpressure)
262
- encrypted: boolean; // Whether client has completed encryption handshake
263
- connectedAt: number; // Connection timestamp (Date.now())
264
- lastActivity: number; // Last heartbeat/message timestamp (Date.now())
265
- metadata?: MetadataType; // Custom metadata from authenticateFn return value
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
- ### Client State Transitions
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
- States are defined in the `WebSocketClientStates` constant class:
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
- > [!TIP]
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
- ## 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
- ```
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
- | |-- onClientConnect()
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: '...' } } >|-- handleAuthenticate()
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
- | | (if requireEncryption)
408
- | | await handshakeFn(data)
409
- | | enableClientEncryption()
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
- ### Authentication Timeout Details
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
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
- **Without encryption (fast path):**
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
- **With encryption (per-client path):**
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
- **With `exclude` parameter:**
462
- - When `exclude` is provided in `sendToRoom()` or `broadcast()`, the fast path is bypassed even without encryption
463
- - The server iterates all clients, skipping those in the `exclude` set
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
- > When an `outboundTransformer` is bound, **all** room/broadcast messages fall back to the per-client iteration path (even for non-encrypted clients in the same room). This is because Bun native pub/sub cannot selectively apply transformations. Only bind `outboundTransformer` when you actually need per-client message transformation.
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 Also
237
+ ## See also
469
238
 
470
- - [Setup & Configuration](./) - Quick reference, imports, setup steps, configuration, and binding keys
471
- - [API Reference](./api) - Architecture, WebSocketEmitter API, and internals
472
- - [Error Reference](./errors) - Error conditions table and troubleshooting
473
- - [WebSocketServerHelper](/extensions/helpers/websocket/) - Helper API documentation
474
- - [Socket.IO Component](../socket-io/) - Node.js-compatible alternative with Socket.IO
475
- - [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - Official Bun WebSocket API reference
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