@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,574 +1,139 @@
1
- # WebSocket
2
-
3
- Bun-native WebSocket server and Redis-backed emitter for real-time communication with post-connection authentication, room management, cross-instance messaging, and optional per-client encryption.
4
-
5
- > [!IMPORTANT]
6
- > **Bun only.** The `WebSocketServerHelper` uses Bun's native WebSocket API and will not work on Node.js. For Node.js support, use the [Socket.IO Helper](../socket-io/) instead.
7
-
8
- ## Quick Reference
1
+ ---
2
+ title: WebSocket
3
+ description: Bun-native WebSocket server and Redis-backed emitter for real-time communication
4
+ difficulty: intermediate
5
+ ---
9
6
 
10
- | Class | Extends | Role |
11
- |-------|---------|------|
12
- | `WebSocketServerHelper` | `BaseHelper` | Bun-native WebSocket server with auth, rooms, heartbeat, Redis Pub/Sub scaling |
13
- | `WebSocketEmitter` | `BaseHelper` | Publish messages to WebSocket clients from any process via Redis |
14
-
15
- #### Import Paths
16
-
17
- ```typescript
18
- // Server helper
19
- import { WebSocketServerHelper } from '@venizia/ignis-helpers';
7
+ # WebSocket
20
8
 
21
- // Emitter helper
22
- import { WebSocketEmitter } from '@venizia/ignis-helpers';
9
+ A Bun-native WebSocket server with post-connection authentication, rooms, heartbeat, and Redis Pub/Sub for horizontal scaling. A Redis-only emitter sends to it from other processes that run no server of their own.
23
10
 
24
- // Types and constants
25
- import type {
26
- IWebSocketServerOptions,
27
- IWebSocketEmitterOptions,
28
- IWebSocketClient,
29
- IWebSocketMessage,
30
- TWebSocketAuthenticateFn,
31
- TWebSocketValidateRoomFn,
32
- TWebSocketClientConnectedFn,
33
- TWebSocketClientDisconnectedFn,
34
- TWebSocketMessageHandler,
35
- TWebSocketOutboundTransformer,
36
- TWebSocketHandshakeFn,
37
- } from '@venizia/ignis-helpers';
38
-
39
- import {
40
- WebSocketEvents,
41
- WebSocketChannels,
42
- WebSocketDefaults,
43
- WebSocketMessageTypes,
44
- WebSocketClientStates,
45
- } from '@venizia/ignis-helpers';
46
- ```
11
+ This helper is the raw class: construct it and wire it into your own server yourself. Need it inside an IGNIS app instead? [`WebSocketComponent`](/extensions/components/websocket/) does that wiring for you. It creates and binds the helper through DI once your app's Bun server is listening.
47
12
 
48
- ## Creating an Instance
13
+ > [!IMPORTANT]
14
+ > **Bun only.** `WebSocketServerHelper` uses Bun's native WebSocket API and will not run on Node.js. For Node.js, use the [Socket.IO Helper](../socket-io/) instead.
49
15
 
50
- ### Server
16
+ ## In one example
51
17
 
52
- `WebSocketServerHelper` wraps Bun's native WebSocket server with built-in authentication, client tracking, room management, Redis Pub/Sub for horizontal scaling, and application-level heartbeat.
18
+ The smallest working server: authenticate, then send.
53
19
 
54
20
  ```typescript
55
21
  import { WebSocketServerHelper } from '@venizia/ignis-helpers';
56
22
 
57
23
  const helper = new WebSocketServerHelper({
58
24
  identifier: 'my-ws-server',
59
- path: '/ws',
60
- server: bunServerInstance, // Bun.Server
61
- redisConnection: myRedisHelper, // IRedisHelper
62
- authenticateFn: async (data) => {
25
+ server: bunServer, // Bun.Server
26
+ redisConnection: redis, // IRedisHelper
27
+ authenticateFn: async data => {
63
28
  const { token } = data as { token: string };
64
29
  const user = await verifyJWT(token);
65
- if (!user) return null; // Reject
66
- return { userId: user.id }; // Accept
67
- },
68
- validateRoomFn: ({ clientId, userId, rooms }) => {
69
- return rooms.filter(room => room.startsWith('public-'));
70
- },
71
- clientConnectedFn: ({ clientId, userId }) => {
72
- console.log('Client authenticated:', clientId, userId);
73
- },
74
- clientDisconnectedFn: ({ clientId, userId }) => {
75
- console.log('Client disconnected:', clientId, userId);
76
- },
77
- messageHandler: ({ clientId, userId, message }) => {
78
- console.log('Custom event:', message.event, message.data);
79
- },
80
- });
81
-
82
- await helper.configure();
83
- ```
84
-
85
- #### `IWebSocketServerOptions`
86
-
87
- | Option | Type | Required | Default | Description |
88
- |--------|------|----------|---------|-------------|
89
- | `identifier` | `string` | Yes | -- | Unique name for this WebSocket server instance |
90
- | `path` | `string` | No | `'/ws'` | URL path for WebSocket upgrade requests |
91
- | `server` | `IBunServer` | Yes | -- | Bun server instance (provides `publish()` for native pub/sub) |
92
- | `redisConnection` | `IRedisHelper` | Yes | -- | Redis helper for cross-instance messaging. Creates 2 duplicate connections internally |
93
- | `defaultRooms` | `string[]` | No | `['ws-default', 'ws-notification']` | Rooms clients auto-join after authentication |
94
- | `serverOptions` | `IBunWebSocketConfig` | No | See defaults below | Bun native WebSocket configuration |
95
- | `authTimeout` | `number` | No | `5000` (5s) | Milliseconds before unauthenticated clients are disconnected (close code `4001`) |
96
- | `heartbeatInterval` | `number` | No | `30000` (30s) | Milliseconds between heartbeat sweeps |
97
- | `heartbeatTimeout` | `number` | No | `90000` (90s) | Milliseconds of inactivity before a client is closed (close code `4002`) |
98
- | `encryptedBatchLimit` | `number` | No | `10` | Max concurrent encryption operations for room/broadcast delivery |
99
- | `requireEncryption` | `boolean` | No | `false` | When `true`, clients must complete ECDH handshake during auth or get disconnected (code `4004`) |
100
- | `authenticateFn` | `TWebSocketAuthenticateFn` | Yes | -- | Called when client sends `{ event: 'authenticate' }`. Return `{ userId, metadata }` on success, `null`/`false` to reject |
101
- | `validateRoomFn` | `TWebSocketValidateRoomFn` | No | -- | Called when client requests to join rooms. Return allowed room names. All joins are rejected if not provided |
102
- | `clientConnectedFn` | `TWebSocketClientConnectedFn` | No | -- | Called after successful authentication |
103
- | `clientDisconnectedFn` | `TWebSocketClientDisconnectedFn` | No | -- | Called when a client disconnects |
104
- | `messageHandler` | `TWebSocketMessageHandler` | No | -- | Called for non-system events from authenticated clients |
105
- | `outboundTransformer` | `TWebSocketOutboundTransformer` | No | -- | Intercepts outbound messages before `socket.send()`. Enables per-client encryption |
106
- | `handshakeFn` | `TWebSocketHandshakeFn` | No | -- | ECDH key exchange callback. Required when `requireEncryption` is `true`. Returns `{ serverPublicKey, salt }` on success |
107
-
108
- #### Generic Type Parameters
109
-
110
- `WebSocketServerHelper` supports two generics for type-safe auth payloads and client metadata:
111
-
112
- ```typescript
113
- interface AuthPayload { type: string; token: string; publicKey?: string }
114
- interface UserMetadata { role: string; permissions: string[] }
115
-
116
- const helper = new WebSocketServerHelper<AuthPayload, UserMetadata>({
117
- identifier: 'typed-ws',
118
- server: bunServer,
119
- redisConnection: redis,
120
- authenticateFn: async (data) => {
121
- // data is typed as AuthPayload
122
- const user = await verifyJWT(data.token);
123
- if (!user) return null;
124
- return {
125
- userId: user.id,
126
- metadata: { role: user.role, permissions: user.permissions },
127
- };
128
- },
129
- clientConnectedFn: ({ metadata }) => {
130
- // metadata is typed as UserMetadata | undefined
131
- if (metadata?.role === 'admin') {
132
- console.log('Admin connected with permissions:', metadata.permissions);
133
- }
134
- },
135
- });
136
- ```
137
-
138
- ### Emitter
139
-
140
- `WebSocketEmitter` is a lightweight Redis-only publisher for sending messages to WebSocket clients from non-WebSocket processes (background workers, microservices, cron jobs). It uses `serverId: 'emitter'` so all server instances process its messages (no dedup).
141
-
142
- ```typescript
143
- import { WebSocketEmitter } from '@venizia/ignis-helpers';
144
-
145
- const emitter = new WebSocketEmitter({
146
- identifier: 'my-emitter',
147
- redisConnection: myRedisHelper,
148
- });
149
-
150
- await emitter.configure();
151
- ```
152
-
153
- #### `IWebSocketEmitterOptions`
154
-
155
- | Option | Type | Required | Default | Description |
156
- |--------|------|----------|---------|-------------|
157
- | `identifier` | `string` | No | `'WebSocketEmitter'` | Unique name for logging |
158
- | `redisConnection` | `IRedisHelper` | Yes | -- | Redis helper. Creates 1 duplicate connection internally |
159
-
160
- ## Usage
161
-
162
- ### Server Setup
163
-
164
- After constructing the helper, call `configure()` to initialize Redis connections, set up pub/sub subscriptions, and start the heartbeat timer. Then wire the Bun WebSocket handler into your server:
165
-
166
- ```typescript
167
- const helper = new WebSocketServerHelper({
168
- identifier: 'my-ws',
169
- server: bunServer,
170
- redisConnection: redis,
171
- authenticateFn: async (data) => {
172
- const user = await verifyJWT((data as { token: string }).token);
173
- return user ? { userId: user.id } : null;
30
+ return user ? { userId: user.id } : null; // null rejects
174
31
  },
175
32
  });
176
33
 
177
34
  await helper.configure();
178
35
 
179
- // Get the Bun WebSocket handler
180
- const wsHandler = helper.getBunWebSocketHandler();
181
-
182
- // Wire into the Bun server
183
36
  bunServer.reload({
184
37
  fetch: myFetchHandler,
185
- websocket: wsHandler,
38
+ websocket: helper.getBunWebSocketHandler(),
186
39
  });
187
40
  ```
188
41
 
189
- ### Handling Connections
42
+ A client connects, then authenticates before it can send or receive anything else:
190
43
 
191
- The server implements a post-connection authentication flow. Clients connect first (the WebSocket upgrade is always accepted), then must send an `authenticate` event with credentials before they can interact.
192
-
193
- ```
194
- Client Server (WebSocketServerHelper)
195
- | |
196
- |-- WebSocket upgrade ---------> | server.upgrade(req, { data: { clientId } })
197
- | |
198
- |-- open event ----------------> | onClientConnect()
199
- | | |-- Create client entry (state: UNAUTHORIZED)
200
- | | |-- Subscribe to clientId topic (Bun pub/sub)
201
- | | +-- Start authTimeout (5s default)
202
- | |
203
- | { event: 'authenticate', |
204
- | data: { token: '...' } } --> | handleAuthenticate()
205
- | | |-- Set state: AUTHENTICATING
206
- | | +-- Call authenticateFn(data)
207
- | |
208
- | | -- Success: { userId, metadata } --
209
- | | |-- Set state: AUTHENTICATED
210
- | | |-- Index by userId
211
- | | |-- Join default rooms
212
- | <-- { event: 'connected', | +-- Send 'connected' event
213
- | data: { id, userId, |
214
- | time } } ------------- |
215
- | |
216
- | | -- Failure: null/false --
217
- | <-- { event: 'error' } ------- | +-- Close with code 4003
44
+ ```javascript
45
+ const ws = new WebSocket('wss://example.com/ws');
46
+ ws.onopen = () => ws.send(JSON.stringify({ event: 'authenticate', data: { token: '...' } }));
218
47
  ```
219
48
 
220
- #### Client States
49
+ ## How it works
221
50
 
222
- | State | Value | Description |
223
- |-------|-------|-------------|
224
- | `WebSocketClientStates.UNAUTHORIZED` | `'unauthorized'` | Initial state after connection |
225
- | `WebSocketClientStates.AUTHENTICATING` | `'authenticating'` | `authenticate` event received, awaiting `authenticateFn` |
226
- | `WebSocketClientStates.AUTHENTICATED` | `'authenticated'` | Successfully authenticated, fully operational |
227
- | `WebSocketClientStates.DISCONNECTED` | `'disconnected'` | Client has disconnected |
51
+ - **Every connection starts unauthenticated.** The WebSocket upgrade always succeeds, and the client starts as `UNAUTHORIZED`. It must send `{ event: 'authenticate' }` within `authTimeout` (5s default) or get closed with code `4001`.
52
+ - **`authenticateFn` decides accept or reject.** On success the client is indexed by `userId`, joins `defaultRooms`, and moves to `AUTHENTICATED`.
53
+ - **Delivery has two tiers** - local-only fan-out on this process, or a Redis Pub/Sub layer that also reaches other server instances:
228
54
 
229
- #### Close Codes
55
+ | Tier | Methods | Reach | Mechanism |
56
+ |------|---------|-------|-----------|
57
+ | Local-only | `sendToClient`, `sendToUser`, `sendToRoom`, `broadcast` | Clients connected to this process | Bun's native `server.publish()` - O(1) fan-out when possible |
58
+ | Cross-instance | `send()`, `WebSocketEmitter` | Clients on any server instance | Redis Pub/Sub, via the server's duplicated `redisPub`/`redisSub` connections |
230
59
 
231
- | Code | Meaning | Trigger |
232
- |------|---------|---------|
233
- | `4001` | Authentication timeout | Client did not authenticate within `authTimeout` (5s default) |
234
- | `4002` | Heartbeat timeout | No activity for `heartbeatTimeout` (90s default) |
235
- | `4003` | Authentication failed | `authenticateFn` returned `null`/`false` or threw |
236
- | `4004` | Encryption required | `requireEncryption` is `true` and `handshakeFn` rejected or was not configured |
237
- | `1001` | Going away | Server shutting down gracefully |
60
+ - **Liveness is passive.** The server never pings clients - they must periodically send `{ event: 'heartbeat' }`. A sweep every `heartbeatInterval` (30s) closes anyone silent for longer than `heartbeatTimeout` (90s) with code `4002`.
61
+ - **Encryption is opt-in per client.** An `outboundTransformer` callback intercepts every outbound message before `socket.send()`.
62
+ - **Once encrypted, delivery changes.** Bun native pub/sub is bypassed for that client. `sendToRoom`/`broadcast` then fall back to iterating encrypted clients individually.
238
63
 
239
- ### Sending Messages
64
+ ## Common tasks
240
65
 
241
- #### Local Delivery
242
-
243
- Send messages directly to clients, users, or rooms on the current server instance:
66
+ ### Send to a client, user, or room (same process)
244
67
 
245
68
  ```typescript
246
- // Send to a specific client
247
- helper.sendToClient({
248
- clientId: 'abc-123',
249
- event: 'notification',
250
- data: { message: 'Hello!' },
251
- });
252
-
253
- // Send to all clients belonging to a user
254
- helper.sendToUser({
255
- userId: 'user-123',
256
- event: 'notification',
257
- data: { message: 'You have a new message' },
258
- });
259
-
260
- // Send to all clients in a room
261
- helper.sendToRoom({
262
- room: 'ws-notification',
263
- event: 'alert',
264
- data: { level: 'warning', text: 'CPU high' },
265
- });
266
-
267
- // Send to all clients in a room, excluding specific clients
268
- helper.sendToRoom({
269
- room: 'game-lobby',
270
- event: 'player-moved',
271
- data: { x: 10, y: 20 },
272
- exclude: ['abc-123'],
273
- });
69
+ helper.sendToClient({ clientId: 'abc-123', event: 'notification', data: { message: 'Hello!' } });
70
+ helper.sendToUser({ userId: 'user-123', event: 'notification', data: { message: 'New message' } });
71
+ helper.sendToRoom({ room: 'ws-notification', event: 'alert', data: { level: 'warning' } });
274
72
  ```
275
73
 
276
- #### Cross-Instance Delivery
74
+ ### Send across all server instances
277
75
 
278
- Use `send()` to deliver messages both locally and via Redis for horizontal scaling:
76
+ Use `send()` from within the server process, or `WebSocketEmitter` from a process with no WebSocket server (worker, cron job):
279
77
 
280
78
  ```typescript
281
- // Send to specific client (local + Redis)
282
- helper.send({
283
- destination: clientId,
284
- payload: { topic: 'notification', data: { message: 'Hello!' } },
285
- });
286
-
287
- // Send to room (local + Redis)
288
79
  helper.send({
289
80
  destination: 'ws-notification',
290
81
  payload: { topic: 'alert', data: { level: 'warning', text: 'CPU high' } },
291
82
  });
292
-
293
- // Broadcast to all (local + Redis)
294
- helper.send({
295
- payload: { topic: 'system:announcement', data: { text: 'Maintenance in 5 min' } },
296
- });
297
83
  ```
298
84
 
299
- ### Broadcasting
300
-
301
- Broadcast to all authenticated clients on the current instance:
302
-
303
85
  ```typescript
304
- helper.broadcast({
305
- event: 'system:announcement',
306
- data: { text: 'Maintenance in 5 min' },
307
- });
308
-
309
- // With exclusions
310
- helper.broadcast({
311
- event: 'system:announcement',
312
- data: { text: 'Maintenance in 5 min' },
313
- exclude: ['abc-123'],
314
- });
315
- ```
316
-
317
- ### Emitter Pattern
318
-
319
- Use `WebSocketEmitter` from processes that do not run a WebSocket server (background workers, microservices, cron jobs):
86
+ import { WebSocketEmitter } from '@venizia/ignis-helpers';
320
87
 
321
- ```typescript
322
- const emitter = new WebSocketEmitter({
323
- identifier: 'cron-emitter',
324
- redisConnection: redis,
325
- });
88
+ const emitter = new WebSocketEmitter({ identifier: 'cron-emitter', redisConnection: redis });
326
89
  await emitter.configure();
327
90
 
328
- // Send to a specific client
329
- await emitter.toClient({
330
- clientId: 'abc-123',
331
- event: 'notification',
332
- data: { message: 'Your report is ready' },
333
- });
334
-
335
- // Send to all sessions of a user
336
- await emitter.toUser({
337
- userId: 'user-123',
338
- event: 'notification',
339
- data: { message: 'New message from admin' },
340
- });
341
-
342
- // Send to a room
343
- await emitter.toRoom({
344
- room: 'ws-notification',
345
- event: 'alert',
346
- data: { level: 'critical', text: 'Database failover' },
347
- });
348
-
349
- // Broadcast to all clients
350
- await emitter.broadcast({
351
- event: 'system:announcement',
352
- data: { text: 'Scheduled maintenance in 5 minutes' },
353
- });
354
-
355
- // Graceful shutdown
356
- await emitter.shutdown();
91
+ await emitter.toRoom({ room: 'ws-notification', event: 'alert', data: { level: 'critical' } });
357
92
  ```
358
93
 
359
- ### Rooms
360
-
361
- After authentication, clients auto-join default rooms (configurable via `defaultRooms`, defaults to `['ws-default', 'ws-notification']`) and their own `clientId` room.
94
+ ### Broadcast to everyone
362
95
 
363
- #### Client-Initiated Room Management
364
-
365
- Clients can request to join or leave rooms by sending events:
366
-
367
- ```javascript
368
- // Client-side (browser)
369
- ws.send(JSON.stringify({ event: 'join', data: { rooms: ['game-lobby', 'chat-room'] } }));
370
- ws.send(JSON.stringify({ event: 'leave', data: { rooms: ['game-lobby'] } }));
96
+ ```typescript
97
+ helper.broadcast({ event: 'system:announcement', data: { text: 'Maintenance in 5 min' } });
371
98
  ```
372
99
 
373
- > [!WARNING]
374
- > Without a `validateRoomFn` bound, clients **cannot** join any custom rooms. All join requests are silently rejected. This is a security-by-default design.
100
+ ### Let clients join rooms
375
101
 
376
- Room names are validated before joining:
377
- - Must be a non-empty string
378
- - Maximum 256 characters
379
- - Cannot start with the internal prefix `ws:` (reserved for Redis channels)
380
-
381
- #### Programmatic Room Management
382
-
383
- From your service code, manage rooms directly via the helper:
102
+ Clients can only join custom rooms when you supply `validateRoomFn`. Without it, every join request is rejected by default:
384
103
 
385
104
  ```typescript
386
- // Join a room
387
- helper.joinRoom({ clientId: 'abc-123', room: 'game-lobby' });
388
-
389
- // Leave a room
390
- helper.leaveRoom({ clientId: 'abc-123', room: 'game-lobby' });
391
-
392
- // Get clients in a room
393
- const clients = helper.getClientsByRoom({ room: 'game-lobby' });
394
- console.log('Room has', clients.length, 'clients');
105
+ const helper = new WebSocketServerHelper({
106
+ // ...
107
+ validateRoomFn: ({ userId, rooms }) => rooms.filter(room => room.startsWith('public-')),
108
+ });
395
109
  ```
396
110
 
397
- ### Heartbeat
398
-
399
- The WebSocket helper uses a **passive heartbeat** model. The server does not send pings to clients. Instead, clients must periodically send `{ event: 'heartbeat' }` messages to keep their connection alive.
400
-
401
- 1. The server runs a periodic sweep at `heartbeatInterval` (default: 30s).
402
- 2. On each sweep, it checks every authenticated client's `lastActivity` timestamp.
403
- 3. If `now - lastActivity > heartbeatTimeout` (default: 90s), the client is closed with code `4002`.
404
- 4. Any message from the client (including `{ event: 'heartbeat' }`) updates `lastActivity`.
405
-
406
111
  ```javascript
407
- // Client-side (browser)
408
- const ws = new WebSocket('wss://example.com/ws');
409
-
410
- // After authentication, start heartbeat
411
- const heartbeatInterval = setInterval(() => {
412
- if (ws.readyState === WebSocket.OPEN) {
413
- ws.send(JSON.stringify({ event: 'heartbeat' }));
414
- }
415
- }, 30000); // Every 30 seconds
416
-
417
- ws.onclose = () => clearInterval(heartbeatInterval);
112
+ // Client-side
113
+ ws.send(JSON.stringify({ event: 'join', data: { rooms: ['game-lobby'] } }));
418
114
  ```
419
115
 
420
- > [!NOTE]
421
- > `sendPings` and `idleTimeout` in the Bun server options are **transport-level** mechanisms. They are separate from the application-level heartbeat system which tracks `lastActivity` via actual message content.
422
-
423
- ### Encryption
424
-
425
- The WebSocket helper supports **per-client encryption** via an outbound transformer -- a callback that intercepts every outbound message before `socket.send()`.
426
-
427
- #### Enforced Encryption
428
-
429
- When `requireEncryption` is `true`, clients must complete an ECDH key exchange during authentication:
116
+ ### React to connect and disconnect
430
117
 
431
118
  ```typescript
432
119
  const helper = new WebSocketServerHelper({
433
- identifier: 'encrypted-ws',
434
- server: bunServer,
435
- redisConnection: redis,
436
- requireEncryption: true,
437
- authenticateFn: async (data) => {
438
- const user = await verifyJWT(data.token as string);
439
- return user ? { userId: user.id } : null;
440
- },
441
- handshakeFn: async ({ clientId, userId, data }) => {
442
- const clientPubKeyB64 = data.publicKey as string;
443
- if (!clientPubKeyB64) return null; // Reject
444
-
445
- const peerKey = await ecdh.importPublicKey({ rawKeyB64: clientPubKeyB64 });
446
- const salt = crypto.getRandomValues(new Uint8Array(32));
447
- const saltB64 = Buffer.from(salt).toString('base64');
448
- const aesKey = await ecdh.deriveAESKey({
449
- privateKey: serverKeyPair.keyPair.privateKey,
450
- peerPublicKey: peerKey,
451
- salt,
452
- });
453
-
454
- clientKeys.set(clientId, aesKey);
455
- return { serverPublicKey: serverKeyPair.publicKeyB64, salt: saltB64 };
456
- },
457
- outboundTransformer: async ({ client, event, data }) => {
458
- if (!client.encrypted) return null;
459
- const aesKey = clientKeys.get(client.id);
460
- if (!aesKey) return null;
461
- const encrypted = await ecdh.encrypt({
462
- message: JSON.stringify({ event, data }),
463
- secret: aesKey,
464
- });
465
- return { event: 'encrypted', data: encrypted };
466
- },
120
+ // ...
121
+ clientConnectedFn: ({ clientId, userId }) => console.log('connected', clientId, userId),
122
+ clientDisconnectedFn: ({ clientId, userId }) => console.log('disconnected', clientId, userId),
467
123
  });
468
124
  ```
469
125
 
470
- > [!IMPORTANT]
471
- > When `requireEncryption` is `true`, `handshakeFn` **must** be provided. If it is missing, the server logs an error and closes the client with code `4004`.
472
-
473
- #### Optional Encryption
474
-
475
- If encryption is optional, call `enableClientEncryption()` manually after a key exchange in your `messageHandler`:
476
-
477
- ```typescript
478
- messageHandler: async ({ clientId, message }) => {
479
- if (message.event === 'handshake') {
480
- const peerKey = await ecdh.importPublicKey({ rawKeyB64: message.data.publicKey });
481
- const aesKey = await ecdh.deriveAESKey({
482
- privateKey: serverKeyPair.privateKey,
483
- peerPublicKey: peerKey,
484
- });
485
-
486
- clientKeys.set(clientId, aesKey);
487
- helper.enableClientEncryption({ clientId });
488
-
489
- helper.sendToClient({
490
- clientId,
491
- event: 'handshake-complete',
492
- data: { publicKey: serverKeyPair.publicKeyB64 },
493
- });
494
- }
495
- };
496
- ```
497
-
498
- > [!IMPORTANT]
499
- > When an `outboundTransformer` is configured, **Bun native pub/sub is bypassed** for `sendToRoom()` and `broadcast()`. All clients are iterated individually so the transformer runs per-client. This trades O(1) fan-out for per-client encryption capability.
500
-
501
- ### Redis Integration
502
-
503
- The server creates **two** dedicated Redis connections (duplicated from your `redisConnection`):
504
-
505
- | Connection | Purpose |
506
- |------------|---------|
507
- | `redisPub` | Publish messages to other server instances |
508
- | `redisSub` | Subscribe to messages from other server instances |
509
-
510
- ```
511
- Server A Redis Server B
512
- +-----------+ +----------+ +-----------+
513
- | WS Server |--redisPub-->| |<--redisPub----| WS Server |
514
- | |<--redisSub--| Pub/Sub |---redisSub--->| |
515
- +-----------+ +----------+ +-----------+
516
- ```
517
-
518
- Every server instance generates a unique `serverId` (UUID) at construction. Messages from the same server are skipped on receipt to prevent double delivery.
519
-
520
- ## Troubleshooting
521
-
522
- ### "Client disconnects immediately with close code 4001"
523
-
524
- The client connects but is disconnected before it can interact.
525
-
526
- This happens when the client does not send `{ event: 'authenticate', data: { ... } }` within `authTimeout` (default: 5 seconds). Common causes:
527
-
528
- - The client is sending the auth payload in the wrong format (e.g., `{ type: 'auth' }` instead of `{ event: 'authenticate' }`).
529
- - The client is waiting for a server-initiated message before authenticating. The server sends nothing after the WebSocket upgrade -- the client must initiate.
530
- - Network latency or slow token retrieval causes the auth message to arrive after the timeout window.
531
-
532
- > [!TIP]
533
- > Send `{ event: 'authenticate', data: { token: '...' } }` immediately in the `onopen` handler. If your auth token retrieval is slow, increase `authTimeout` in the server options.
534
-
535
- ### "Redis subscription messages are not received across instances"
536
-
537
- `helper.send()` delivers locally but other server instances do not receive the message.
538
-
539
- - The Redis connection passed to `WebSocketServerHelper` is a single-instance `Redis` client, but your deployment uses Redis Cluster. The duplicated pub/sub clients must be compatible with your Redis topology.
540
- - `configure()` was not awaited. The Redis subscriptions are set up asynchronously during `configure()`. If you accept WebSocket connections before it resolves, subscriptions may not be active.
541
- - A firewall or Redis ACL is blocking `SUBSCRIBE`/`PSUBSCRIBE` commands on the duplicated clients.
542
-
543
- > [!IMPORTANT]
544
- > Always `await helper.configure()` before accepting connections. Verify your Redis connection supports pub/sub (check ACLs, ensure cluster mode is consistent).
545
-
546
- ### "`requireEncryption` is true but clients get disconnected with code 4004"
547
-
548
- Authentication succeeds but the client is immediately closed with code `4004`.
549
-
550
- - `handshakeFn` is not provided in the options. The server logs `"requireEncryption is true but no handshakeFn configured"` and closes the client.
551
- - `handshakeFn` returns `null` or `false`, indicating the handshake was rejected (e.g., missing `publicKey` in the auth payload).
552
-
553
- > [!TIP]
554
- > Ensure `handshakeFn` is configured when `requireEncryption` is `true`, and that the client includes the required key exchange data (e.g., `publicKey`) in the authenticate payload.
555
-
556
- ### "[WebSocketServerHelper] Invalid redis connection!"
557
-
558
- Thrown during construction when `redisConnection` is `null`/`undefined`. Ensure you pass a valid `IRedisHelper` instance (e.g., `RedisSingleHelper`).
559
-
560
- ### "[WebSocketEmitter] Invalid redis connection!"
561
-
562
- Thrown during `WebSocketEmitter` construction when `redisConnection` is `null`/`undefined`. Ensure you pass a valid `IRedisHelper` instance (e.g., `RedisSingleHelper`).
563
-
564
- ### "Redis client did not become ready within 30000ms"
126
+ ## See also
565
127
 
566
- Thrown during `configure()` when a Redis client fails to reach `ready` status. Check that the Redis server is reachable and the `IRedisHelper` instance is properly configured.
128
+ - [Full reference](./api) - every option, method signature, type, and constant
129
+ - [Socket.IO Helper](../socket-io/) - Socket.IO-based alternative with Node.js support
130
+ - [Redis Helper](../redis/) - `RedisSingleHelper` / `RedisClusterHelper` used for cross-instance messaging
131
+ - [Crypto Helper](../crypto/) - ECDH key exchange for WebSocket encryption
132
+ - [WebSocket Component](/extensions/components/websocket/) - component-level lifecycle integration
567
133
 
568
- ## See Also
134
+ **Files:**
569
135
 
570
- - [API Reference](./api) -- Full method signatures, types, and constants
571
- - [Socket.IO Helper](../socket-io/) -- Socket.IO-based alternative with Node.js support
572
- - [Redis Helper](../redis/) -- `RedisSingleHelper` or `RedisClusterHelper` used for cross-instance messaging
573
- - [Crypto Helper](../crypto/) -- ECDH key exchange for WebSocket encryption
574
- - [WebSocket Component](/extensions/components/websocket/) -- Component-level lifecycle integration
136
+ - [`packages/helpers/src/modules/socket/websocket/server/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/server/helper.ts) - `WebSocketServerHelper`
137
+ - [`packages/helpers/src/modules/socket/websocket/emitter/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/emitter/helper.ts) - `WebSocketEmitter`
138
+ - [`packages/helpers/src/modules/socket/websocket/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/common/types.ts) - option and callback types
139
+ - [`packages/helpers/src/modules/socket/websocket/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/common/constants.ts) - `WebSocketEvents`, `WebSocketChannels`, `WebSocketDefaults`, `WebSocketMessageTypes`, `WebSocketClientStates`