@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.
Files changed (174) 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 +24 -13
  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 +27 -3
  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 +8 -4
  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 +247 -153
  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 +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -1,241 +1,155 @@
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**, in this order:
93
65
 
94
- ## WebSocket Emitter
66
+ | Order | Checks |
67
+ |---|---|
68
+ | 1 | Local clients |
69
+ | 2 | Local rooms |
70
+ | 3 | Redis, publishing as a `ROOM` message (fallback) |
95
71
 
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.
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
- 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.
74
+ ## Send from a process with no WebSocket server
99
75
 
100
- #### When to Use WebSocketEmitter
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
- | 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
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
- // 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
- ```
88
+ const redisHelper = new RedisSingleHelper({ name: 'emitter-redis', host: 'localhost', port: 6379, autoConnect: false });
132
89
 
133
- #### Sending Messages
90
+ const emitter = new WebSocketEmitter({ identifier: 'my-worker-emitter', redisConnection: redisHelper });
91
+ await emitter.configure(); // connects the Redis pub client
134
92
 
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
- ```
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
- #### Shutdown
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
- > [!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.
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
- ### Client-Server Message Format
105
+ ## Read the wire protocol
181
106
 
182
- All messages exchanged between client and server follow the `IWebSocketMessage` envelope:
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; // Event name (system or custom)
187
- data?: DataType; // Payload data
188
- id?: string; // Optional message ID (application-defined)
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
- 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
117
+ **System events**
195
118
 
196
119
  | Event | Direction | Payload | Description |
197
120
  |-------|-----------|---------|-------------|
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 |
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 --> 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`) |
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
- > [!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
130
+ **Close codes**
211
131
 
212
132
  | Code | Reason | Trigger |
213
133
  |------|--------|---------|
214
134
  | `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` |
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
- Cross-instance messages are published via Redis Pub/Sub using the `IRedisSocketMessage` envelope:
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 instance ID (UUID or 'emitter')
227
- type: TWebSocketMessageType; // 'client' | 'user' | 'room' | 'broadcast'
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; // Event to deliver
230
- data: DataType; // Payload
231
- exclude?: string[]; // Client IDs to exclude from delivery
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
- ## Client Tracking
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
- ### `IWebSocketClient` Interface
162
+ ## Track connected clients
249
163
 
250
- Each connected client is tracked in an in-memory `Map<string, IWebSocketClient>`:
164
+ Each connection is an `IWebSocketClient` entry in an in-memory `Map<string, IWebSocketClient>`:
251
165
 
252
166
  ```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)
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
- ### Client State Transitions
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
- 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:
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
- > [!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.
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
- ## Authentication Flow
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
- | |-- onClientConnect()
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: '...' } } >|-- handleAuthenticate()
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
- | | (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? } } -------------|
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
- ### 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
227
+ - **Two timeout phases, not one:**
447
228
 
448
- The helper uses a dual delivery strategy depending on whether encryption is active:
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
- **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
236
+ ## Understand the delivery strategy
454
237
 
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
238
+ The helper picks a delivery path per call, based on encryption and `exclude`:
460
239
 
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
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
- > 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.
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 Also
250
+ ## See also
469
251
 
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
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