@venizia/ignis-docs 0.0.5 → 0.0.6-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 (123) hide show
  1. package/package.json +1 -1
  2. package/wiki/best-practices/architectural-patterns.md +0 -2
  3. package/wiki/best-practices/architecture-decisions.md +0 -8
  4. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  5. package/wiki/best-practices/code-style-standards/index.md +0 -1
  6. package/wiki/best-practices/code-style-standards/tooling.md +0 -3
  7. package/wiki/best-practices/contribution-workflow.md +12 -12
  8. package/wiki/best-practices/index.md +4 -14
  9. package/wiki/best-practices/performance-optimization.md +3 -3
  10. package/wiki/best-practices/security-guidelines.md +2 -2
  11. package/wiki/best-practices/troubleshooting-tips.md +1 -1
  12. package/wiki/guides/core-concepts/application/bootstrapping.md +6 -7
  13. package/wiki/guides/core-concepts/components-guide.md +1 -1
  14. package/wiki/guides/core-concepts/components.md +2 -2
  15. package/wiki/guides/core-concepts/dependency-injection.md +4 -5
  16. package/wiki/guides/core-concepts/persistent/datasources.md +4 -5
  17. package/wiki/guides/core-concepts/services.md +1 -1
  18. package/wiki/guides/get-started/5-minute-quickstart.md +4 -5
  19. package/wiki/guides/get-started/philosophy.md +12 -24
  20. package/wiki/guides/index.md +2 -9
  21. package/wiki/guides/reference/mcp-docs-server.md +13 -13
  22. package/wiki/guides/tutorials/building-a-crud-api.md +10 -10
  23. package/wiki/guides/tutorials/complete-installation.md +11 -12
  24. package/wiki/guides/tutorials/ecommerce-api.md +3 -3
  25. package/wiki/guides/tutorials/realtime-chat.md +6 -6
  26. package/wiki/guides/tutorials/testing.md +4 -5
  27. package/wiki/index.md +8 -14
  28. package/wiki/references/base/bootstrapping.md +0 -3
  29. package/wiki/references/base/components.md +2 -2
  30. package/wiki/references/base/controllers.md +0 -1
  31. package/wiki/references/base/datasources.md +1 -1
  32. package/wiki/references/base/dependency-injection.md +2 -2
  33. package/wiki/references/base/filter-system/default-filter.md +2 -3
  34. package/wiki/references/base/filter-system/index.md +1 -1
  35. package/wiki/references/base/filter-system/quick-reference.md +0 -14
  36. package/wiki/references/base/middlewares.md +0 -8
  37. package/wiki/references/base/providers.md +0 -9
  38. package/wiki/references/base/repositories/advanced.md +1 -1
  39. package/wiki/references/base/repositories/mixins.md +2 -3
  40. package/wiki/references/base/services.md +0 -1
  41. package/wiki/references/components/authentication/api.md +444 -0
  42. package/wiki/references/components/authentication/errors.md +177 -0
  43. package/wiki/references/components/authentication/index.md +571 -0
  44. package/wiki/references/components/authentication/usage.md +781 -0
  45. package/wiki/references/components/health-check.md +292 -103
  46. package/wiki/references/components/index.md +14 -12
  47. package/wiki/references/components/mail/api.md +505 -0
  48. package/wiki/references/components/mail/errors.md +176 -0
  49. package/wiki/references/components/mail/index.md +535 -0
  50. package/wiki/references/components/mail/usage.md +404 -0
  51. package/wiki/references/components/request-tracker.md +229 -25
  52. package/wiki/references/components/socket-io/api.md +1051 -0
  53. package/wiki/references/components/socket-io/errors.md +119 -0
  54. package/wiki/references/components/socket-io/index.md +410 -0
  55. package/wiki/references/components/socket-io/usage.md +322 -0
  56. package/wiki/references/components/static-asset/api.md +261 -0
  57. package/wiki/references/components/static-asset/errors.md +89 -0
  58. package/wiki/references/components/static-asset/index.md +617 -0
  59. package/wiki/references/components/static-asset/usage.md +364 -0
  60. package/wiki/references/components/swagger.md +390 -110
  61. package/wiki/references/components/template/api-page.md +125 -0
  62. package/wiki/references/components/template/errors-page.md +100 -0
  63. package/wiki/references/components/template/index.md +104 -0
  64. package/wiki/references/components/template/setup-page.md +134 -0
  65. package/wiki/references/components/template/single-page.md +132 -0
  66. package/wiki/references/components/template/usage-page.md +127 -0
  67. package/wiki/references/components/websocket/api.md +508 -0
  68. package/wiki/references/components/websocket/errors.md +123 -0
  69. package/wiki/references/components/websocket/index.md +453 -0
  70. package/wiki/references/components/websocket/usage.md +475 -0
  71. package/wiki/references/helpers/cron/index.md +224 -0
  72. package/wiki/references/helpers/crypto/index.md +537 -0
  73. package/wiki/references/helpers/env/index.md +214 -0
  74. package/wiki/references/helpers/error/index.md +232 -0
  75. package/wiki/references/helpers/index.md +16 -15
  76. package/wiki/references/helpers/inversion/index.md +608 -0
  77. package/wiki/references/helpers/logger/index.md +600 -0
  78. package/wiki/references/helpers/network/api.md +986 -0
  79. package/wiki/references/helpers/network/index.md +620 -0
  80. package/wiki/references/helpers/queue/index.md +589 -0
  81. package/wiki/references/helpers/redis/index.md +495 -0
  82. package/wiki/references/helpers/socket-io/api.md +497 -0
  83. package/wiki/references/helpers/socket-io/index.md +513 -0
  84. package/wiki/references/helpers/storage/api.md +705 -0
  85. package/wiki/references/helpers/storage/index.md +583 -0
  86. package/wiki/references/helpers/template/index.md +66 -0
  87. package/wiki/references/helpers/template/single-page.md +126 -0
  88. package/wiki/references/helpers/testing/index.md +510 -0
  89. package/wiki/references/helpers/types/index.md +512 -0
  90. package/wiki/references/helpers/uid/index.md +272 -0
  91. package/wiki/references/helpers/websocket/api.md +736 -0
  92. package/wiki/references/helpers/websocket/index.md +574 -0
  93. package/wiki/references/helpers/worker-thread/index.md +470 -0
  94. package/wiki/references/index.md +2 -9
  95. package/wiki/references/quick-reference.md +3 -18
  96. package/wiki/references/utilities/jsx.md +1 -8
  97. package/wiki/references/utilities/statuses.md +0 -7
  98. package/wiki/references/components/authentication.md +0 -476
  99. package/wiki/references/components/mail.md +0 -687
  100. package/wiki/references/components/socket-io.md +0 -562
  101. package/wiki/references/components/static-asset.md +0 -1277
  102. package/wiki/references/helpers/cron.md +0 -108
  103. package/wiki/references/helpers/crypto.md +0 -132
  104. package/wiki/references/helpers/env.md +0 -83
  105. package/wiki/references/helpers/error.md +0 -97
  106. package/wiki/references/helpers/inversion.md +0 -176
  107. package/wiki/references/helpers/logger.md +0 -296
  108. package/wiki/references/helpers/network.md +0 -396
  109. package/wiki/references/helpers/queue.md +0 -150
  110. package/wiki/references/helpers/redis.md +0 -142
  111. package/wiki/references/helpers/socket-io.md +0 -932
  112. package/wiki/references/helpers/storage.md +0 -665
  113. package/wiki/references/helpers/testing.md +0 -133
  114. package/wiki/references/helpers/types.md +0 -167
  115. package/wiki/references/helpers/uid.md +0 -167
  116. package/wiki/references/helpers/worker-thread.md +0 -178
  117. package/wiki/references/src-details/boot.md +0 -379
  118. package/wiki/references/src-details/core.md +0 -263
  119. package/wiki/references/src-details/dev-configs.md +0 -298
  120. package/wiki/references/src-details/docs.md +0 -71
  121. package/wiki/references/src-details/helpers.md +0 -211
  122. package/wiki/references/src-details/index.md +0 -86
  123. package/wiki/references/src-details/inversion.md +0 -340
@@ -0,0 +1,508 @@
1
+ # WebSocket -- API Reference
2
+
3
+ > Architecture deep dive, WebSocketEmitter API, and component internals.
4
+
5
+ ## Architecture
6
+
7
+ #### Component Lifecycle Diagram
8
+ ```
9
+ WebSocketComponent
10
+ +----------------------------------------------+
11
+ | |
12
+ | binding() |
13
+ | |-- RuntimeModules.detect() |
14
+ | | +-- NODE -> throw error |
15
+ | | +-- BUN -> continue |
16
+ | | |
17
+ | |-- resolveBindings() |
18
+ | | |-- SERVER_OPTIONS |
19
+ | | |-- REDIS_CONNECTION |
20
+ | | |-- AUTHENTICATE_HANDLER |
21
+ | | |-- VALIDATE_ROOM_HANDLER |
22
+ | | |-- CLIENT_CONNECTED_HANDLER |
23
+ | | |-- CLIENT_DISCONNECTED_HANDLER |
24
+ | | |-- MESSAGE_HANDLER |
25
+ | | |-- OUTBOUND_TRANSFORMER |
26
+ | | +-- HANDSHAKE_HANDLER |
27
+ | | |
28
+ | +-- registerBunHook(resolved) |
29
+ | |
30
+ | (Post-start hook executes after server) |
31
+ | |-- Creates WebSocketServerHelper |
32
+ | |-- await wsHelper.configure() |
33
+ | |-- Binds to WEBSOCKET_INSTANCE |
34
+ | |-- Creates fetch handler (WS + Hono) |
35
+ | +-- server.reload({ fetch, websocket }) |
36
+ +----------------------------------------------+
37
+ ```
38
+
39
+ ### Lifecycle Integration
40
+
41
+ The component uses the **post-start hook** system to solve a fundamental timing problem: WebSocket needs a running Bun server instance, but components are initialized *before* the server starts.
42
+
43
+ #### Application Lifecycle Flow
44
+ ```
45
+ Application Lifecycle
46
+ =====================
47
+
48
+ +------------------+
49
+ | preConfigure() | <-- Register WebSocketComponent here
50
+ +--------+---------+
51
+ |
52
+ +--------v---------+
53
+ | initialize() | <-- Component.binding() runs here
54
+ | | Runtime check, resolve bindings, register post-start hook
55
+ +--------+---------+
56
+ |
57
+ +--------v---------+
58
+ | setupMiddlewares |
59
+ +--------+---------+
60
+ |
61
+ +--------v-----------------------+
62
+ | startBunModule() | <-- Bun server starts, instance created
63
+ +--------+-----------------------+
64
+ |
65
+ +--------v--------------------------+
66
+ | executePostStartHooks() | <-- WebSocketServerHelper created HERE
67
+ | +-- websocket-initialize | Server instance is now available
68
+ | |-- new WebSocketServerHelper
69
+ | |-- wsHelper.configure()
70
+ | |-- bind WEBSOCKET_INSTANCE
71
+ | +-- server.reload({ fetch, websocket })
72
+ +-----------------------------------+
73
+ ```
74
+
75
+ ### Fetch Handler
76
+
77
+ The component creates a custom `fetch` handler via `createBunFetchHandler()` that routes requests:
78
+
79
+ 1. **WebSocket upgrade requests** (`GET /ws` with `Upgrade: websocket` header) are handled by `server.upgrade()` which assigns a `clientId` (via `crypto.randomUUID()`) and passes to Bun's WebSocket handler.
80
+ 2. **All other requests** are delegated to the Hono server for normal HTTP routing.
81
+ 3. **Failed upgrades** return a `500 WebSocket upgrade failed` response.
82
+
83
+ ```
84
+ Incoming Request
85
+ |
86
+ v
87
+ Is WebSocket upgrade?
88
+ (pathname === wsPath &&
89
+ headers.upgrade === 'websocket')
90
+ |
91
+ +----+----+
92
+ | |
93
+ Yes No
94
+ | |
95
+ v v
96
+ server. honoServer.
97
+ upgrade() fetch(req, server)
98
+ |
99
+ +---> success: return undefined (Bun handles it)
100
+ +---> failure: return Response(500)
101
+ ```
102
+
103
+ ## WebSocket Emitter API
104
+
105
+ ### Overview
106
+
107
+ `WebSocketEmitter` is a standalone, lightweight Redis-only publisher designed for processes that do not run a `WebSocketServerHelper`. It extends `BaseHelper` and uses a single Redis pub client to publish `IRedisSocketMessage` envelopes.
108
+
109
+ ### `IWebSocketEmitterOptions`
110
+
111
+ ```typescript
112
+ interface IWebSocketEmitterOptions {
113
+ identifier?: string; // Default: 'WebSocketEmitter' (used as logger scope)
114
+ redisConnection: DefaultRedisHelper; // Required -- same Redis as the server(s)
115
+ }
116
+ ```
117
+
118
+ ### Constructor
119
+
120
+ ```typescript
121
+ const emitter = new WebSocketEmitter({
122
+ identifier: 'my-worker', // Optional
123
+ redisConnection: redisHelper, // Required
124
+ });
125
+ ```
126
+
127
+ The constructor:
128
+ 1. Calls `super({ scope })` with `identifier` (or `'WebSocketEmitter'` if not provided)
129
+ 2. Validates `redisConnection` is truthy (throws `"Invalid redis connection!"` if not)
130
+ 3. Calls `redisConnection.getClient().duplicate()` to create an isolated pub client
131
+
132
+ ### `EMITTER_SERVER_ID`
133
+
134
+ ```typescript
135
+ const EMITTER_SERVER_ID = 'emitter';
136
+ ```
137
+
138
+ All messages published by `WebSocketEmitter` use this fixed `serverId`. Since no `WebSocketServerHelper` instance will have a `serverId` of `'emitter'` (they use `crypto.randomUUID()`), all server instances will process emitter messages -- none will self-dedup.
139
+
140
+ ### Methods
141
+
142
+ #### `configure()`
143
+
144
+ ```typescript
145
+ async configure(): Promise<void>
146
+ ```
147
+
148
+ Prepares the emitter for use:
149
+ 1. Registers a Redis `error` event handler (logs errors)
150
+ 2. Calls `redisPub.connect()` if the client status is `'wait'` (i.e., lazy-connect mode)
151
+ 3. Waits for the Redis client to reach `'ready'` status (30-second timeout)
152
+
153
+ Must be called before any `toClient()`, `toUser()`, `toRoom()`, or `broadcast()` calls.
154
+
155
+ #### `toClient()`
156
+
157
+ ```typescript
158
+ async toClient(opts: {
159
+ clientId: string;
160
+ event: string;
161
+ data: unknown;
162
+ }): Promise<void>
163
+ ```
164
+
165
+ Publishes to `ws:client:{clientId}`. The target server that holds this client will deliver the message via `sendToClient()`.
166
+
167
+ #### `toUser()`
168
+
169
+ ```typescript
170
+ async toUser(opts: {
171
+ userId: string;
172
+ event: string;
173
+ data: unknown;
174
+ }): Promise<void>
175
+ ```
176
+
177
+ Publishes to `ws:user:{userId}`. All servers with sessions for this user will call `sendToUser()` locally, reaching every session across all instances.
178
+
179
+ #### `toRoom()`
180
+
181
+ ```typescript
182
+ async toRoom(opts: {
183
+ room: string;
184
+ event: string;
185
+ data: unknown;
186
+ exclude?: string[];
187
+ }): Promise<void>
188
+ ```
189
+
190
+ Publishes to `ws:room:{room}`. All servers with members in this room will call `sendToRoom()` locally. The optional `exclude` array is forwarded -- servers will skip those client IDs during delivery.
191
+
192
+ #### `broadcast()`
193
+
194
+ ```typescript
195
+ async broadcast(opts: {
196
+ event: string;
197
+ data: unknown;
198
+ }): Promise<void>
199
+ ```
200
+
201
+ Publishes to `ws:broadcast`. All servers will call `broadcast()` locally, reaching every authenticated client.
202
+
203
+ #### `shutdown()`
204
+
205
+ ```typescript
206
+ async shutdown(): Promise<void>
207
+ ```
208
+
209
+ Gracefully shuts down the emitter by calling `redisPub.quit()`. Always call this when the emitter is no longer needed to release the Redis connection.
210
+
211
+ ## Internals
212
+
213
+ ### `resolveBindings()`
214
+
215
+ Reads all binding keys from the DI container and validates required ones:
216
+
217
+ | Binding | Validation | Error on Failure |
218
+ |---------|-----------|------------------|
219
+ | `SERVER_OPTIONS` | Optional, merged with `DEFAULT_SERVER_OPTIONS` via `Object.assign()` | -- |
220
+ | `REDIS_CONNECTION` | Must be `instanceof DefaultRedisHelper` | `"Invalid instance of redisConnection"` |
221
+ | `AUTHENTICATE_HANDLER` | Must be truthy (non-null) | `"Invalid authenticateFn to setup WebSocket server!"` |
222
+ | `VALIDATE_ROOM_HANDLER` | Optional, coerced `null` to `undefined` | -- |
223
+ | `CLIENT_CONNECTED_HANDLER` | Optional, coerced `null` to `undefined` | -- |
224
+ | `CLIENT_DISCONNECTED_HANDLER` | Optional, coerced `null` to `undefined` | -- |
225
+ | `MESSAGE_HANDLER` | Optional, coerced `null` to `undefined` | -- |
226
+ | `OUTBOUND_TRANSFORMER` | Optional, coerced `null` to `undefined` | -- |
227
+ | `HANDSHAKE_HANDLER` | Optional, coerced `null` to `undefined` (required if `requireEncryption`) | -- |
228
+
229
+ ### `registerBunHook()`
230
+
231
+ Registers a post-start hook that executes the following steps:
232
+
233
+ 1. **Get Bun server instance** via `getServerInstance<TBunServerInstance>()`
234
+ 2. **Get Hono server** via `getServer()`
235
+ 3. **Create WebSocketServerHelper** with all resolved bindings and server options
236
+ 4. **Await `wsHelper.configure()`** which connects Redis clients and sets up subscriptions
237
+ 5. **Bind the helper** to `WEBSOCKET_INSTANCE` in the DI container
238
+ 6. **Create custom `fetch` handler** via `createBunFetchHandler({ wsPath, honoServer })`
239
+ 7. **Wire WebSocket into running server** via `serverInstance.reload({ fetch, websocket })`
240
+
241
+ #### Post-Start Hook Code Flow
242
+ ```typescript
243
+ // Simplified post-start hook logic
244
+ async () => {
245
+ // Step 1 & 2: Get server instances
246
+ const serverInstance = this.application.getServerInstance<TBunServerInstance>();
247
+ const honoServer = this.application.getServer();
248
+
249
+ if (!serverInstance) {
250
+ throw getError({
251
+ message: '[WebSocketComponent] Bun server instance not available!',
252
+ });
253
+ }
254
+
255
+ // Step 3: Create helper
256
+ const wsHelper = new WebSocketServerHelper({
257
+ identifier: serverOptions.identifier,
258
+ path: serverOptions.path,
259
+ defaultRooms: serverOptions.defaultRooms,
260
+ serverOptions: serverOptions.serverOptions,
261
+ heartbeatInterval: serverOptions.heartbeatInterval,
262
+ heartbeatTimeout: serverOptions.heartbeatTimeout,
263
+ server: serverInstance,
264
+ redisConnection: resolved.redisConnection,
265
+ authenticateFn: resolved.authenticateFn,
266
+ validateRoomFn: resolved.validateRoomFn,
267
+ clientConnectedFn: resolved.clientConnectedFn,
268
+ clientDisconnectedFn: resolved.clientDisconnectedFn,
269
+ messageHandler: resolved.messageHandler,
270
+ outboundTransformer: resolved.outboundTransformer,
271
+ handshakeFn: resolved.handshakeFn,
272
+ requireEncryption: serverOptions.requireEncryption,
273
+ });
274
+
275
+ // Step 4: Configure (Redis + subscriptions + heartbeat timer)
276
+ await wsHelper.configure();
277
+
278
+ // Step 5: Bind to container
279
+ this.application.bind({ key: WebSocketBindingKeys.WEBSOCKET_INSTANCE })
280
+ .toValue(wsHelper);
281
+
282
+ // Step 6 & 7: Create fetch handler and reload server
283
+ serverInstance.reload({
284
+ fetch: createBunFetchHandler({ wsPath, honoServer }),
285
+ websocket: wsHelper.getBunWebSocketHandler(),
286
+ });
287
+ }
288
+ ```
289
+
290
+ ### `createBunFetchHandler()`
291
+
292
+ The fetch handler is a standalone function (not a method on the component) that returns an async function:
293
+
294
+ ```typescript
295
+ function createBunFetchHandler(opts: {
296
+ wsPath: string;
297
+ honoServer: OpenAPIHono;
298
+ }): (req: Request, server: TBunServerInstance) => Promise<Response | undefined>
299
+ ```
300
+
301
+ The handler logic:
302
+ 1. Parse `new URL(req.url)` to get the pathname
303
+ 2. Check if `pathname === wsPath && headers.upgrade === 'websocket'`
304
+ 3. If **not** a WebSocket upgrade, delegate to `honoServer.fetch(req, server)` -- note the second argument is the raw `server` instance, not wrapped in an object
305
+ 4. If a WebSocket upgrade, call `server.upgrade(req, { data: { clientId: crypto.randomUUID() } })`
306
+ 5. If upgrade succeeds, return `undefined` (Bun handles the connection)
307
+ 6. If upgrade fails, return `new Response('WebSocket upgrade failed', { status: 500 })`
308
+
309
+ ### Runtime Check
310
+
311
+ The component checks the runtime during `binding()`:
312
+
313
+ ```typescript
314
+ const runtime = RuntimeModules.detect();
315
+ if (runtime === RuntimeModules.NODE) {
316
+ throw getError({
317
+ statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
318
+ message: '[WebSocketComponent] Node.js runtime is not supported yet. Please use Bun runtime.',
319
+ });
320
+ }
321
+ ```
322
+
323
+ This check runs at component initialization time (before any hooks are registered), failing fast if the runtime is incompatible.
324
+
325
+ ### Bun WebSocket Handler
326
+
327
+ The helper's `getBunWebSocketHandler()` returns an `IBunWebSocketHandler` -- a Bun-native WebSocket handler object with four lifecycle callbacks plus config spread:
328
+
329
+ ```typescript
330
+ interface IBunWebSocketHandler extends IBunWebSocketConfig {
331
+ open: (socket: IWebSocket) => void; // New connection -- creates client entry, starts auth timer
332
+ message: (socket: IWebSocket, message: string | Buffer) => void; // Incoming message -- routes to handler
333
+ close: (socket: IWebSocket, code: number, reason: string) => void; // Disconnect -- cleanup
334
+ drain: (socket: IWebSocket) => void; // Backpressure cleared -- resets backpressured flag
335
+ }
336
+ ```
337
+
338
+ The `open` handler (`onClientConnect`):
339
+ 1. Checks if clientId already exists (returns early if duplicate)
340
+ 2. Creates an `IWebSocketClient` entry in state `UNAUTHORIZED`
341
+ 3. Subscribes the socket to its own `clientId` topic (Bun native pub/sub -- enables direct messaging before auth)
342
+ 4. Starts an auth timeout timer (`authTimeout`, default 5 s)
343
+
344
+ The `message` handler (`onClientMessage`):
345
+ 1. Updates `lastActivity` on the client
346
+ 2. Parses JSON -- sends `error` event `"Invalid message format"` if parse fails
347
+ 3. Validates `event` field exists -- silently drops if missing (with error log)
348
+ 4. Routes by event:
349
+ - `heartbeat`: returns immediately (no-op, `lastActivity` already updated)
350
+ - `authenticate`: delegates to `handleAuthenticate()`
351
+ - Any other event from unauthenticated client: sends `error` event `"Not authenticated"`
352
+ - `join`: delegates to `handleJoin()`
353
+ - `leave`: delegates to `handleLeave()`
354
+ - Custom events: delegates to `messageHandler` callback (if bound), otherwise silently dropped
355
+
356
+ The `close` handler (`onClientDisconnect`):
357
+ 1. Clears auth timer if pending
358
+ 2. Removes client from `users` index (deletes user entry if last session)
359
+ 3. Removes client from all `rooms` entries (deletes room entry if empty)
360
+ 4. Deletes from `clients` map
361
+ 5. Invokes `clientDisconnectedFn` callback (errors caught and logged)
362
+
363
+ The `drain` handler:
364
+ 1. Sets `client.backpressured = false`
365
+ 2. Logs a debug message
366
+
367
+ ### `deliverToSocket()` Backpressure Handling
368
+
369
+ The `deliverToSocket()` method handles three return values from Bun's `socket.send()`:
370
+
371
+ | Return Value | Meaning | Action |
372
+ |-------------|---------|--------|
373
+ | `> 0` (positive) | Message sent successfully (byte count) | No action |
374
+ | `0` | Message dropped (socket already closed) | Logs warning: `"Message dropped (socket closed)"` |
375
+ | `-1` | Backpressure (Bun's send buffer is full) | Sets `client.backpressured = true`, logs warning. The message is still queued by Bun. When the buffer drains, the `drain` handler fires and resets `backpressured` to `false` |
376
+
377
+ Any exception thrown by `socket.send()` is caught and logged as an error.
378
+
379
+ ### `send()` Destination Resolution
380
+
381
+ The `send()` method is the primary public API for sending messages. It resolves the `destination` parameter using the following logic:
382
+
383
+ ```
384
+ send({ destination, payload: { topic, data } })
385
+ |
386
+ +-- destination is undefined/null?
387
+ | Yes -> broadcast locally + publishToRedis(BROADCAST)
388
+ |
389
+ +-- destination matches a local clientId?
390
+ | Yes -> sendToClient locally + publishToRedis(CLIENT)
391
+ |
392
+ +-- destination matches a local room name?
393
+ | Yes -> sendToRoom locally + publishToRedis(ROOM)
394
+ |
395
+ +-- destination is unknown locally?
396
+ Yes -> publishToRedis(ROOM, target: destination)
397
+ (assumes it might be a room on another instance)
398
+ ```
399
+
400
+ > [!IMPORTANT]
401
+ > **No USER type in `send()`.** The `send()` method does not support `userId` as a destination. To send to all sessions of a user, use `sendToUser()` for local-only delivery or `WebSocketEmitter.toUser()` for cross-instance delivery via Redis.
402
+
403
+ > [!NOTE]
404
+ > When the destination is unknown locally, `send()` publishes it as a `ROOM` type to Redis. This is intentional -- if it is a client ID on another server, that server will not find it in its rooms map either, but the `onRedisMessage` handler routes `CLIENT` and `ROOM` messages differently. For reliable cross-instance client targeting, prefer using `WebSocketEmitter.toClient()` which explicitly uses the `CLIENT` message type.
405
+
406
+ ### Room Join Validation
407
+
408
+ Room names go through two validation stages:
409
+
410
+ 1. **Server-side sanitization** (always applied):
411
+ - Must be a non-empty string (truthy, `typeof r === 'string'`)
412
+ - Must be <= 256 characters
413
+ - Must not start with `ws:` prefix (reserved for internal channels)
414
+
415
+ 2. **Application-level validation** (via `validateRoomFn`):
416
+ - Only called if the function is bound
417
+ - Receives the sanitized room list
418
+ - Returns the subset of rooms the client is allowed to join
419
+ - If no `validateRoomFn` is bound, **all join requests are rejected** with a warning log
420
+
421
+ ### Room Leave Validation
422
+
423
+ The `handleLeave()` method validates that the client has actually joined the requested rooms before leaving:
424
+
425
+ ```typescript
426
+ const validRooms = rooms.filter(r => client.rooms.has(r));
427
+ ```
428
+
429
+ This prevents clients from unsubscribing from internal topics or rooms they never joined. If no valid rooms remain after filtering, the leave is silently ignored.
430
+
431
+ ### Graceful Shutdown
432
+
433
+ Always shut down the WebSocket server before stopping the application:
434
+
435
+ ```typescript
436
+ override async stop(): Promise<void> {
437
+ // 1. Shut down WebSocket (disconnects all clients, quits Redis)
438
+ const wsHelper = this.get<WebSocketServerHelper>({
439
+ key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
440
+ isOptional: true,
441
+ });
442
+
443
+ if (wsHelper) {
444
+ await wsHelper.shutdown();
445
+ }
446
+
447
+ // 2. Disconnect Redis helper
448
+ if (this.redisHelper) {
449
+ await this.redisHelper.disconnect();
450
+ }
451
+
452
+ // 3. Stop the Bun server
453
+ await super.stop();
454
+ }
455
+ ```
456
+
457
+ #### Shutdown Sequence Diagram
458
+ ```
459
+ wsHelper.shutdown()
460
+ |-- Clear heartbeat timer
461
+ | +-- clearInterval(heartbeatTimer)
462
+ |
463
+ |-- Close all sockets
464
+ | +-- For each client: socket.close(1001, 'Server shutting down')
465
+ | (errors caught per-client -- already-disconnected clients are logged)
466
+ |
467
+ |-- Trigger disconnect callbacks
468
+ | +-- For each client: onClientDisconnect({ clientId })
469
+ | |-- Clear auth timer
470
+ | |-- Remove from users map
471
+ | |-- Remove from rooms map
472
+ | |-- Remove from clients map
473
+ | +-- Invoke clientDisconnectedFn callback
474
+ |
475
+ |-- Clear tracking maps
476
+ | |-- clients.clear()
477
+ | |-- users.clear()
478
+ | +-- rooms.clear()
479
+ |
480
+ +-- Redis cleanup (parallel)
481
+ |-- redisPub.quit()
482
+ +-- redisSub.quit()
483
+ ```
484
+
485
+ The shutdown sequence ensures:
486
+ - Active connections are gracefully closed with code `1001` ("Going Away")
487
+ - All disconnect callbacks are invoked (so application-level cleanup runs)
488
+ - All internal state is cleared (client/user/room maps)
489
+ - Redis pub/sub clients are properly disconnected
490
+ - No memory leaks from lingering timers or connections
491
+
492
+ ### WebSocketEmitter Shutdown
493
+
494
+ ```
495
+ emitter.shutdown()
496
+ +-- redisPub.quit()
497
+ ```
498
+
499
+ The emitter shutdown is simpler since it only has one Redis client and no local state to clean up.
500
+
501
+ ## See Also
502
+
503
+ - [Setup & Configuration](./) - Quick reference, imports, setup steps, configuration, and binding keys
504
+ - [Usage & Examples](./usage) - Server-side usage, emitter, wire protocol, client tracking, and delivery strategy
505
+ - [Error Reference](./errors) - Error conditions table and troubleshooting
506
+ - [WebSocketServerHelper](/references/helpers/websocket/) - Helper API documentation
507
+ - [Socket.IO Component](../socket-io/) - Node.js-compatible alternative with Socket.IO
508
+ - [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - Official Bun WebSocket API reference
@@ -0,0 +1,123 @@
1
+ # WebSocket -- Error Reference
2
+
3
+ > Error conditions table and troubleshooting guide for the WebSocket component.
4
+
5
+ ## Error Conditions
6
+
7
+ The server can send `error` events or close the connection under the following conditions:
8
+
9
+ | Error Message / Close Code | Trigger | Event Type |
10
+ |---------------------------|---------|------------|
11
+ | `"Invalid message format"` | Client sent non-JSON data | `error` event |
12
+ | `"Already authenticated"` | Client sent `authenticate` when state is not `UNAUTHORIZED` | `error` event |
13
+ | `"Not authenticated"` | Client sent a non-`authenticate`, non-`heartbeat` event while unauthenticated | `error` event |
14
+ | `"Authentication failed"` | `authenticateFn` returned `null`/`false` | `error` event + close `4003` |
15
+ | `"Authentication error"` | `authenticateFn` threw an exception | `error` event + close `4003` |
16
+ | `"Encryption handshake failed"` | `handshakeFn` returned `null`/`false` | `error` event + close `4004` |
17
+ | Close `4004` (no error event) | `requireEncryption: true` but no `handshakeFn` configured | close `4004` only |
18
+ | Close `4001` | Auth timeout (initial: no `authenticate` sent) | close `4001` only |
19
+ | Close `4001` | Auth in-progress timeout (`authenticateFn`/`handshakeFn` too slow) | close `4001` only |
20
+ | Close `4002` | Heartbeat timeout (no messages within `heartbeatTimeout`) | close `4002` only |
21
+ | Close `1001` | Server shutdown (`wsHelper.shutdown()`) | close `1001` only |
22
+ | `"Invalid redis connection!"` | Constructor: `redisConnection` is falsy | thrown `Error` (startup) |
23
+ | `"WebSocket upgrade failed"` | `server.upgrade()` returned `false` | HTTP `500` response |
24
+
25
+ ## Troubleshooting
26
+
27
+ ### "WebSocket not initialized"
28
+
29
+ **Cause**: You are trying to use `WebSocketServerHelper` before the server has started (e.g., during DI construction).
30
+
31
+ **Fix**: Use the lazy getter pattern shown in [Usage & Examples](./usage). Never `@inject` `WEBSOCKET_INSTANCE` directly in a constructor -- it does not exist yet at construction time.
32
+
33
+ ### "Invalid instance of redisConnection"
34
+
35
+ **Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `DefaultRedisHelper` (or its subclass `RedisHelper`).
36
+
37
+ **Fix**: Use `RedisHelper` (recommended) or `DefaultRedisHelper`:
38
+
39
+ ```typescript
40
+ // Correct
41
+ this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION })
42
+ .toValue(new RedisHelper({ name: 'websocket', host, port, password }));
43
+
44
+ // Wrong -- raw ioredis client
45
+ this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION })
46
+ .toValue(new Redis(6379)); // This is NOT a DefaultRedisHelper!
47
+ ```
48
+
49
+ ### "Invalid authenticateFn to setup WebSocket server!"
50
+
51
+ **Cause**: No authentication function was bound to `AUTHENTICATE_HANDLER`, or it was bound as `null`.
52
+
53
+ **Fix**: Bind a valid authentication function before registering the component:
54
+
55
+ ```typescript
56
+ this.bind<TWebSocketAuthenticateFn>({
57
+ key: WebSocketBindingKeys.AUTHENTICATE_HANDLER,
58
+ }).toValue(async (data) => {
59
+ const token = data.token as string;
60
+ const user = await verifyJWT(token);
61
+ return user ? { userId: user.id } : null;
62
+ });
63
+ ```
64
+
65
+ ### "Node.js runtime is not supported yet"
66
+
67
+ **Cause**: Running the application on Node.js. The WebSocket component only supports Bun.
68
+
69
+ **Fix**: Either switch to Bun runtime, or use the [Socket.IO Component](../socket-io/) which supports both Node.js and Bun.
70
+
71
+ ### "Bun server instance not available!"
72
+
73
+ **Cause**: The post-start hook executed but could not obtain the Bun server instance. This typically means the server failed to start.
74
+
75
+ **Fix**: Check server startup logs for errors. Ensure `start()` completes successfully before post-start hooks run.
76
+
77
+ ### WebSocket connects but messages are not received
78
+
79
+ **Cause**: Clients must send <code v-pre>{ event: 'authenticate', data: { type: '...', token: '...', publicKey?: '...' } }</code> after connecting. Unauthenticated clients are disconnected after the timeout (default: 5 seconds) and cannot receive messages other than `error` events.
80
+
81
+ **Fix**: Ensure your client authenticates immediately after connection:
82
+
83
+ ```javascript
84
+ const ws = new WebSocket('wss://example.com/ws');
85
+
86
+ ws.onopen = () => {
87
+ ws.send(JSON.stringify({
88
+ event: 'authenticate',
89
+ data: { type: 'Bearer', token: 'your-jwt-token' },
90
+ }));
91
+ };
92
+
93
+ ws.onmessage = (event) => {
94
+ const msg = JSON.parse(event.data);
95
+ if (msg.event === 'connected') {
96
+ console.log('Authenticated! Client ID:', msg.data.id);
97
+ // Now ready to send/receive events
98
+ }
99
+ };
100
+ ```
101
+
102
+ ### Client disconnected with code 4002
103
+
104
+ **Cause**: The client did not send any messages (including heartbeat) within the `heartbeatTimeout` period (default: 90 seconds).
105
+
106
+ **Fix**: Implement a heartbeat on the client side:
107
+
108
+ ```javascript
109
+ setInterval(() => {
110
+ if (ws.readyState === WebSocket.OPEN) {
111
+ ws.send(JSON.stringify({ event: 'heartbeat' }));
112
+ }
113
+ }, 30000);
114
+ ```
115
+
116
+ ## See Also
117
+
118
+ - [Setup & Configuration](./) - Quick reference, imports, setup steps, configuration, and binding keys
119
+ - [Usage & Examples](./usage) - Server-side usage, emitter, wire protocol, client tracking, and delivery strategy
120
+ - [API Reference](./api) - Architecture, WebSocketEmitter API, and internals
121
+ - [WebSocketServerHelper](/references/helpers/websocket/) - Helper API documentation
122
+ - [Socket.IO Component](../socket-io/) - Node.js-compatible alternative with Socket.IO
123
+ - [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - Official Bun WebSocket API reference