@venizia/ignis-docs 0.2.0 → 0.2.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,509 +1,385 @@
1
- # WebSocket -- API Reference
1
+ ---
2
+ title: WebSocket Component - Full Reference
3
+ description: Binding keys, configuration options, WebSocketEmitter API, lifecycle diagrams, and internals
4
+ difficulty: intermediate
5
+ ---
2
6
 
3
- > Architecture deep dive, WebSocketEmitter API, and component internals.
7
+ # WebSocket Component Reference
4
8
 
5
- ## Architecture
9
+ Every binding key, configuration option, callback signature, and internal mechanism of `WebSocketComponent`. For the task-oriented walkthrough, see [Usage & Examples](./usage).
6
10
 
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
- ```
11
+ **Files:**
38
12
 
39
- ### Lifecycle Integration
13
+ - [`packages/core/src/components/websocket/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/component.ts)
14
+ - [`packages/core/src/components/websocket/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/common/types.ts)
15
+ - [`packages/core/src/components/websocket/handlers/bun.handler.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/handlers/bun.handler.ts)
16
+ - [`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)
17
+ - [`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)
40
18
 
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.
19
+ ## Quick reference
42
20
 
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
21
+ | Item | Value |
22
+ |------|-------|
23
+ | Package | `@venizia/ignis` (core component) + `@venizia/ignis-helpers` (helper classes) |
24
+ | Component class | `WebSocketComponent` |
25
+ | Server helper | [`WebSocketServerHelper`](/extensions/helpers/websocket/) |
26
+ | Emitter helper | `WebSocketEmitter` (standalone Redis publisher) |
27
+ | Runtimes | Bun only - throws on Node.js |
28
+ | Scaling | Redis Pub/Sub (`ioredis` - single or Cluster) |
76
29
 
77
- The component creates a custom `fetch` handler via `createBunFetchHandler()` that routes requests:
30
+ ## Import paths
78
31
 
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.
32
+ `WebSocketComponent` and `WebSocketBindingKeys` are exported only from the `@venizia/ignis/websocket` subpath - never from the `@venizia/ignis` root barrel. `IServerOptions` (the core component's options subset) is not exported from either entry point.
82
33
 
34
+ ```typescript
35
+ // Core - subpath import only
36
+ import { WebSocketComponent, WebSocketBindingKeys } from '@venizia/ignis/websocket';
37
+
38
+ // Helpers - types, classes, constants from the main entry
39
+ import {
40
+ WebSocketServerHelper,
41
+ WebSocketEmitter,
42
+ WebSocketDefaults,
43
+ WebSocketEvents,
44
+ WebSocketChannels,
45
+ WebSocketClientStates,
46
+ WebSocketMessageTypes,
47
+ } from '@venizia/ignis-helpers';
48
+
49
+ import type {
50
+ IWebSocketServerOptions,
51
+ IWebSocketEmitterOptions,
52
+ IWebSocketClient,
53
+ IWebSocketMessage,
54
+ IRedisSocketMessage,
55
+ IBunWebSocketConfig,
56
+ TWebSocketAuthenticateFn,
57
+ TWebSocketValidateRoomFn,
58
+ TWebSocketClientConnectedFn,
59
+ TWebSocketClientDisconnectedFn,
60
+ TWebSocketMessageHandler,
61
+ TWebSocketOutboundTransformer,
62
+ TWebSocketHandshakeFn,
63
+ } from '@venizia/ignis-helpers';
83
64
  ```
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
65
 
105
- ### Overview
66
+ ## Configuration
106
67
 
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`
68
+ `WebSocketComponent`'s own `IServerOptions` interface is a **subset** of the helper's `IWebSocketServerOptions` - the component fills in `server`, `redisConnection`, callback functions, `authTimeout`, and `encryptedBatchLimit` from the DI container before constructing the helper.
110
69
 
111
70
  ```typescript
112
- interface IWebSocketEmitterOptions {
113
- identifier?: string; // Default: 'WebSocketEmitter' (used as logger scope)
114
- redisConnection: IRedisHelper; // Required -- same Redis as the server(s)
71
+ interface IServerOptions {
72
+ identifier: string; // Default: 'WEBSOCKET_SERVER'
73
+ path?: string; // Default: '/ws'
74
+ defaultRooms?: string[]; // Default: ['ws-default', 'ws-notification']
75
+ serverOptions?: IBunWebSocketConfig; // Bun native WebSocket config
76
+ heartbeatInterval?: number; // Default: 30000 (30s)
77
+ heartbeatTimeout?: number; // Default: 90000 (90s)
78
+ requireEncryption?: boolean; // Default: false
115
79
  }
116
80
  ```
117
81
 
118
- ### Constructor
82
+ > [!NOTE]
83
+ > `DEFAULT_SERVER_OPTIONS` in the core component only sets `identifier` and `path`. `defaultRooms`, `heartbeatInterval`, `heartbeatTimeout`, and `serverOptions` fall back to `WebSocketDefaults` inside the helper constructor, not the component.
84
+
85
+ > [!NOTE]
86
+ > `authTimeout` and `encryptedBatchLimit` belong to the helper's `IWebSocketServerOptions`, not the component's `IServerOptions`. There is no binding key for them - the component always passes the helper defaults (`5000` ms, `10`). Customize them only by constructing `WebSocketServerHelper` yourself outside the component.
87
+
88
+ Bind a partial object to `SERVER_OPTIONS` before registering the component to override any field:
119
89
 
120
90
  ```typescript
121
- const emitter = new WebSocketEmitter({
122
- identifier: 'my-worker', // Optional
123
- redisConnection: redisHelper, // Required
91
+ this.bind({ key: WebSocketBindingKeys.SERVER_OPTIONS }).toValue({
92
+ identifier: 'my-app-websocket',
93
+ path: '/realtime',
94
+ defaultRooms: ['general', 'announcements'],
95
+ heartbeatInterval: 20000,
96
+ heartbeatTimeout: 60000,
97
+ requireEncryption: true,
98
+ serverOptions: { maxPayloadLength: 2097152, backpressureLimit: 2097152 },
124
99
  });
125
100
  ```
126
101
 
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.duplicateClient()` to create an isolated pub client
102
+ ### `WebSocketDefaults` constants
131
103
 
132
- ### `EMITTER_SERVER_ID`
104
+ | Constant | Value | Description |
105
+ |----------|-------|-------------|
106
+ | `PATH` | `'/ws'` | Default WebSocket endpoint path |
107
+ | `ROOM` | `'ws-default'` | Default room name |
108
+ | `NOTIFICATION_ROOM` | `'ws-notification'` | Default notification room name |
109
+ | `BROADCAST_TOPIC` | `'ws:internal:broadcast'` | Internal Bun pub/sub broadcast topic |
110
+ | `MAX_PAYLOAD_LENGTH` | `131072` (128 KB) | Maximum message payload size |
111
+ | `IDLE_TIMEOUT` | `60` | Bun idle timeout, seconds |
112
+ | `BACKPRESSURE_LIMIT` | `1048576` (1 MB) | Bun backpressure limit |
113
+ | `SEND_PINGS` | `true` | Enable WebSocket pings |
114
+ | `PUBLISH_TO_SELF` | `false` | Whether the server receives its own publishes |
115
+ | `AUTH_TIMEOUT` | `5000` (5 s) | Time to authenticate before disconnect |
116
+ | `HEARTBEAT_INTERVAL` | `30000` (30 s) | Interval between heartbeat sweeps |
117
+ | `HEARTBEAT_TIMEOUT` | `90000` (90 s) | Disconnect after 3 missed heartbeats |
118
+ | `ENCRYPTED_BATCH_LIMIT` | `10` | Max concurrent encryption operations |
133
119
 
134
- ```typescript
135
- const EMITTER_SERVER_ID = 'emitter';
136
- ```
120
+ `MAX_PAYLOAD_LENGTH`, `IDLE_TIMEOUT`, `BACKPRESSURE_LIMIT`, `SEND_PINGS`, and `PUBLISH_TO_SELF` are Bun-native settings passed via `serverOptions`. The rest are application-level settings read directly off `IWebSocketServerOptions`.
137
121
 
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.
122
+ ### `IBunWebSocketConfig`
139
123
 
140
- ### Methods
124
+ ```typescript
125
+ interface IBunWebSocketConfig {
126
+ perMessageDeflate?: boolean;
127
+ maxPayloadLength?: number; // Default: 128 KB (131072)
128
+ idleTimeout?: number; // Default: 60s
129
+ backpressureLimit?: number; // Default: 1 MB (1048576)
130
+ closeOnBackpressureLimit?: boolean;
131
+ sendPings?: boolean; // Default: true
132
+ publishToSelf?: boolean; // Default: false
133
+ }
134
+ ```
141
135
 
142
- #### `configure()`
136
+ Passed straight through to Bun's native WebSocket handler via `serverOptions` inside `SERVER_OPTIONS`.
137
+
138
+ ## Binding keys
139
+
140
+ | Binding Key | Constant | Type | Required | Default |
141
+ |------------|----------|------|----------|---------|
142
+ | `@app/websocket/server-options` | `WebSocketBindingKeys.SERVER_OPTIONS` | `Partial<IServerOptions>` | No | See [Configuration](#configuration) |
143
+ | `@app/websocket/redis-connection` | `WebSocketBindingKeys.REDIS_CONNECTION` | `AbstractRedisHelper` | **Yes** | `null` |
144
+ | `@app/websocket/authenticate-handler` | `WebSocketBindingKeys.AUTHENTICATE_HANDLER` | `TWebSocketAuthenticateFn` | **Yes** | `null` |
145
+ | `@app/websocket/validate-room-handler` | `WebSocketBindingKeys.VALIDATE_ROOM_HANDLER` | `TWebSocketValidateRoomFn` | No | `null` |
146
+ | `@app/websocket/client-connected-handler` | `WebSocketBindingKeys.CLIENT_CONNECTED_HANDLER` | `TWebSocketClientConnectedFn` | No | `null` |
147
+ | `@app/websocket/client-disconnected-handler` | `WebSocketBindingKeys.CLIENT_DISCONNECTED_HANDLER` | `TWebSocketClientDisconnectedFn` | No | `null` |
148
+ | `@app/websocket/message-handler` | `WebSocketBindingKeys.MESSAGE_HANDLER` | `TWebSocketMessageHandler` | No | `null` |
149
+ | `@app/websocket/outbound-transformer` | `WebSocketBindingKeys.OUTBOUND_TRANSFORMER` | `TWebSocketOutboundTransformer` | No | `null` |
150
+ | `@app/websocket/handshake-handler` | `WebSocketBindingKeys.HANDSHAKE_HANDLER` | `TWebSocketHandshakeFn` | No* | `null` |
151
+ | `@app/websocket/instance` | `WebSocketBindingKeys.WEBSOCKET_INSTANCE` | `WebSocketServerHelper` | -- | Set by the component |
152
+
153
+ - `HANDSHAKE_HANDLER` becomes required when `IServerOptions.requireEncryption` is `true` - it performs the ECDH key exchange during authentication.
154
+ - `WEBSOCKET_INSTANCE` is never bound by application code - the component binds it automatically inside the post-start hook, after the server starts. Inject it lazily; see [Usage & Examples](./usage).
155
+
156
+ ### Callback signatures
157
+
158
+ | Binding Key | Callback Type | Required | Description |
159
+ |-------------|--------------|----------|--------------|
160
+ | `AUTHENTICATE_HANDLER` | `TWebSocketAuthenticateFn` | **Yes** | Returns <code v-pre>{ userId, metadata }</code> or `null`/`false` to reject |
161
+ | `VALIDATE_ROOM_HANDLER` | `TWebSocketValidateRoomFn` | No | Filters requested rooms, returns allowed rooms |
162
+ | `CLIENT_CONNECTED_HANDLER` | `TWebSocketClientConnectedFn` | No | Called after successful authentication |
163
+ | `CLIENT_DISCONNECTED_HANDLER` | `TWebSocketClientDisconnectedFn` | No | Called on disconnect, after cleanup |
164
+ | `MESSAGE_HANDLER` | `TWebSocketMessageHandler` | No | Handles non-system messages from authenticated clients |
165
+ | `OUTBOUND_TRANSFORMER` | `TWebSocketOutboundTransformer` | No | Transforms outbound messages (e.g. per-client encryption) |
166
+ | `HANDSHAKE_HANDLER` | `TWebSocketHandshakeFn` | When `requireEncryption: true` | Returns <code v-pre>{ serverPublicKey, salt }</code> or `null`/`false` to reject |
143
167
 
144
168
  ```typescript
145
- async configure(): Promise<void>
146
- ```
169
+ type TWebSocketAuthenticateFn<
170
+ AuthDataType extends Record<string, unknown> = Record<string, unknown>,
171
+ MetadataType extends Record<string, unknown> = Record<string, unknown>,
172
+ > = (opts: AuthDataType) => ValueOrPromise<{ userId?: string; metadata?: MetadataType } | null | false>;
147
173
 
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)
174
+ type TWebSocketValidateRoomFn = (opts: {
175
+ clientId: string;
176
+ userId?: string;
177
+ rooms: string[];
178
+ }) => ValueOrPromise<string[]>;
152
179
 
153
- Must be called before any `toClient()`, `toUser()`, `toRoom()`, or `broadcast()` calls.
180
+ type TWebSocketClientConnectedFn<MetadataType extends Record<string, unknown> = Record<string, unknown>> = (
181
+ opts: { clientId: string; userId?: string; metadata?: MetadataType },
182
+ ) => ValueOrPromise<void>;
154
183
 
155
- #### `toClient()`
184
+ type TWebSocketClientDisconnectedFn = (opts: { clientId: string; userId?: string }) => ValueOrPromise<void>;
156
185
 
157
- ```typescript
158
- async toClient(opts: {
186
+ type TWebSocketMessageHandler = (opts: {
159
187
  clientId: string;
188
+ userId?: string;
189
+ message: IWebSocketMessage;
190
+ }) => ValueOrPromise<void>;
191
+
192
+ type TWebSocketOutboundTransformer<
193
+ DataType = unknown,
194
+ MetadataType extends Record<string, unknown> = Record<string, unknown>,
195
+ > = (opts: {
196
+ client: IWebSocketClient<MetadataType>;
160
197
  event: string;
161
- data: unknown;
162
- }): Promise<void>
163
- ```
198
+ data: DataType;
199
+ }) => ValueOrPromise<TNullable<{ event: string; data: DataType }>>;
164
200
 
165
- Publishes to `ws:client:{clientId}`. The target server that holds this client will deliver the message via `sendToClient()`.
201
+ type TWebSocketHandshakeFn<AuthDataType extends Record<string, unknown> = Record<string, unknown>> = (
202
+ opts: { clientId: string; userId?: string; data: AuthDataType },
203
+ ) => ValueOrPromise<{ serverPublicKey: string; salt: string } | null | false>;
204
+ ```
166
205
 
167
- #### `toUser()`
206
+ - **`VALIDATE_ROOM_HANDLER` receives sanitized rooms.** Internal `ws:`-prefixed rooms are already filtered out before this callback runs. Without it bound, **all** join requests are rejected.
207
+ - **`CLIENT_CONNECTED_HANDLER` / `CLIENT_DISCONNECTED_HANDLER` errors are caught and logged**, never thrown - a broken hook cannot disconnect a client or crash the server.
208
+ - **`MESSAGE_HANDLER` only sees non-system events** (`authenticate`, `connected`, `disconnect`, `join`, `leave`, `error`, `heartbeat`, `encrypted` are all handled internally). Unbound, non-system messages are silently dropped.
209
+ - **`OUTBOUND_TRANSFORMER` only runs for encrypted clients** (`client.encrypted === true`). Non-encrypted clients bypass it entirely - zero overhead until encryption is enabled.
168
210
 
169
- ```typescript
170
- async toUser(opts: {
171
- userId: string;
172
- event: string;
173
- data: unknown;
174
- }): Promise<void>
175
- ```
211
+ ## Architecture
176
212
 
177
- Publishes to `ws:user:{userId}`. All servers with sessions for this user will call `sendToUser()` locally, reaching every session across all instances.
213
+ ### Lifecycle integration
178
214
 
179
- #### `toRoom()`
215
+ The component uses the application's **post-start hook** system to solve a timing problem: WebSocket needs a running Bun server instance, but components initialize before the server starts.
180
216
 
181
- ```typescript
182
- async toRoom(opts: {
183
- room: string;
184
- event: string;
185
- data: unknown;
186
- exclude?: string[];
187
- }): Promise<void>
188
217
  ```
218
+ preConfigure() <- register WebSocketComponent here
219
+ |
220
+ initialize() <- component.binding() runs: runtime check, resolve bindings, register post-start hook
221
+ |
222
+ setupMiddlewares()
223
+ |
224
+ startBunModule() <- Bun server starts, instance created
225
+ |
226
+ executePostStartHooks() <- websocket-initialize hook runs:
227
+ | new WebSocketServerHelper(...)
228
+ | await wsHelper.configure()
229
+ | bind WEBSOCKET_INSTANCE
230
+ | server.reload({ fetch, websocket })
231
+ ```
232
+
233
+ ### Fetch handler
189
234
 
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.
235
+ `createBunFetchHandler()` builds the `fetch` function passed to `server.reload()`. It routes every incoming request:
191
236
 
192
- #### `broadcast()`
237
+ ```
238
+ Incoming Request
239
+ Is a WebSocket upgrade? (pathname === wsPath && headers.upgrade === 'websocket')
240
+ Yes -> server.upgrade(req, { data: { clientId: crypto.randomUUID() } })
241
+ success -> return undefined (Bun handles the connection)
242
+ failure -> return Response('WebSocket upgrade failed', { status: 500 })
243
+ No -> honoServer.fetch(req, server) // note: second arg is the raw server, not wrapped
244
+ ```
193
245
 
194
246
  ```typescript
195
- async broadcast(opts: {
196
- event: string;
197
- data: unknown;
198
- }): Promise<void>
247
+ function createBunFetchHandler(opts: {
248
+ wsPath: string;
249
+ honoServer: OpenAPIHono;
250
+ }): (req: Request, server: TBunServerInstance) => Promise<Response | undefined>
199
251
  ```
200
252
 
201
- Publishes to `ws:broadcast`. All servers will call `broadcast()` locally, reaching every authenticated client.
253
+ ## `WebSocketEmitter` API
202
254
 
203
- #### `shutdown()`
255
+ Standalone, lightweight Redis-only publisher for processes that do not run a `WebSocketServerHelper`. Extends `BaseHelper`; uses a single Redis pub client.
204
256
 
205
257
  ```typescript
206
- async shutdown(): Promise<void>
258
+ interface IWebSocketEmitterOptions {
259
+ identifier?: string; // Default: 'WebSocketEmitter' (logger scope)
260
+ redisConnection: IRedisHelper; // Required - same Redis as the server(s)
261
+ }
207
262
  ```
208
263
 
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.
264
+ - **Constructor** calls `super({ scope })`, throws `"Invalid redis connection!"` if `redisConnection` is falsy, and calls `redisConnection.duplicateClient()` to create an isolated pub client.
265
+ - **`EMITTER_SERVER_ID = 'emitter'`.** Every message the emitter publishes carries this fixed `serverId`. No `WebSocketServerHelper` ever has this ID (they use `crypto.randomUUID()`), so no server self-dedups an emitter message.
266
+
267
+ | Method | Signature | Behavior |
268
+ |--------|-----------|----------|
269
+ | `configure()` | `(): Promise<void>` | Registers a Redis `error` handler, connects if lazy (`status === 'wait'`), waits for `'ready'` (30s timeout). Call before any send method. |
270
+ | `toClient()` | `(opts: { clientId; event; data }): Promise<void>` | Publishes to `ws:client:{clientId}` |
271
+ | `toUser()` | `(opts: { userId; event; data }): Promise<void>` | Publishes to `ws:user:{userId}` |
272
+ | `toRoom()` | `(opts: { room; event; data; exclude? }): Promise<void>` | Publishes to `ws:room:{room}`, forwarding `exclude` |
273
+ | `broadcast()` | `(opts: { event; data }): Promise<void>` | Publishes to `ws:broadcast` |
274
+ | `shutdown()` | `(): Promise<void>` | Calls `redisPub.quit()` - always call when the emitter is no longer needed |
210
275
 
211
276
  ## Internals
212
277
 
213
278
  ### `resolveBindings()`
214
279
 
215
- Reads all binding keys from the DI container and validates required ones:
280
+ Reads all binding keys and validates the required ones, throwing before the post-start hook is even registered:
216
281
 
217
- | Binding | Validation | Error on Failure |
282
+ | Binding | Validation | Error on failure |
218
283
  |---------|-----------|------------------|
219
284
  | `SERVER_OPTIONS` | Optional, merged with `DEFAULT_SERVER_OPTIONS` via `Object.assign()` | -- |
220
- | `REDIS_CONNECTION` | Must be `instanceof AbstractRedisHelper` | `"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`) | -- |
285
+ | `REDIS_CONNECTION` | Must be `instanceof AbstractRedisHelper` | `"Invalid instance of redisConnection ..."` |
286
+ | `AUTHENTICATE_HANDLER` | Must be truthy | `"Invalid authenticateFn to setup WebSocket server!"` |
287
+ | `VALIDATE_ROOM_HANDLER` / `CLIENT_CONNECTED_HANDLER` / `CLIENT_DISCONNECTED_HANDLER` / `MESSAGE_HANDLER` / `OUTBOUND_TRANSFORMER` / `HANDSHAKE_HANDLER` | Optional, `null` coerced to `undefined` | -- |
228
288
 
229
289
  ### `registerBunHook()`
230
290
 
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. **Validate server instance** -- throws `"[WebSocketComponent] Bun server instance not available!"` if not found
236
- 4. **Create WebSocketServerHelper** with all resolved bindings and server options
237
- 5. **Await `wsHelper.configure()`** which connects Redis clients and sets up subscriptions
238
- 6. **Bind the helper** to `WEBSOCKET_INSTANCE` in the DI container
239
- 7. **Create custom `fetch` handler** via `createBunFetchHandler({ wsPath, honoServer })`
240
- 8. **Wire WebSocket into running server** via `serverInstance.reload({ fetch, websocket })`
291
+ Registers the `websocket-initialize` post-start hook:
241
292
 
242
- #### Post-Start Hook Code Flow
243
- ```typescript
244
- // Simplified post-start hook logic
245
- async () => {
246
- // Step 1 & 2: Get server instances
247
- const serverInstance = this.application.getServerInstance<TBunServerInstance>();
248
- const honoServer = this.application.getServer();
249
-
250
- if (!serverInstance) {
251
- throw getError({
252
- message: '[WebSocketComponent] Bun server instance not available!',
253
- });
254
- }
293
+ 1. Gets the Bun server instance (`getServerInstance()`) and Hono server (`getServer()`) - throws `"[WebSocketComponent] Bun server instance not available!"` if the Bun instance is missing.
294
+ 2. Constructs `WebSocketServerHelper` with all resolved bindings plus the running server instance.
295
+ 3. Awaits `wsHelper.configure()` - connects Redis clients, sets up subscriptions, starts the heartbeat timer.
296
+ 4. Binds the helper to `WEBSOCKET_INSTANCE`.
297
+ 5. Calls `serverInstance.reload({ fetch: createBunFetchHandler(...), websocket: wsHelper.getBunWebSocketHandler() })`.
255
298
 
256
- // Step 3: Create helper
257
- const wsHelper = new WebSocketServerHelper({
258
- identifier: serverOptions.identifier,
259
- path: serverOptions.path,
260
- defaultRooms: serverOptions.defaultRooms,
261
- serverOptions: serverOptions.serverOptions,
262
- heartbeatInterval: serverOptions.heartbeatInterval,
263
- heartbeatTimeout: serverOptions.heartbeatTimeout,
264
- server: serverInstance,
265
- redisConnection: resolved.redisConnection,
266
- authenticateFn: resolved.authenticateFn,
267
- validateRoomFn: resolved.validateRoomFn,
268
- clientConnectedFn: resolved.clientConnectedFn,
269
- clientDisconnectedFn: resolved.clientDisconnectedFn,
270
- messageHandler: resolved.messageHandler,
271
- outboundTransformer: resolved.outboundTransformer,
272
- handshakeFn: resolved.handshakeFn,
273
- requireEncryption: serverOptions.requireEncryption,
274
- });
299
+ ### Runtime check
275
300
 
276
- // Step 4: Configure (Redis + subscriptions + heartbeat timer)
277
- await wsHelper.configure();
278
-
279
- // Step 5: Bind to container
280
- this.application.bind({ key: WebSocketBindingKeys.WEBSOCKET_INSTANCE })
281
- .toValue(wsHelper);
282
-
283
- // Step 6 & 7: Create fetch handler and reload server
284
- serverInstance.reload({
285
- fetch: createBunFetchHandler({ wsPath, honoServer }),
286
- websocket: wsHelper.getBunWebSocketHandler(),
287
- });
288
- }
289
- ```
290
-
291
- ### `createBunFetchHandler()`
292
-
293
- The fetch handler is a standalone function (not a method on the component) that returns an async function:
294
-
295
- ```typescript
296
- function createBunFetchHandler(opts: {
297
- wsPath: string;
298
- honoServer: OpenAPIHono;
299
- }): (req: Request, server: TBunServerInstance) => Promise<Response | undefined>
300
- ```
301
-
302
- The handler logic:
303
- 1. Parse `new URL(req.url)` to get the pathname
304
- 2. Check if `pathname === wsPath && headers.upgrade === 'websocket'`
305
- 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
306
- 4. If a WebSocket upgrade, call `server.upgrade(req, { data: { clientId: crypto.randomUUID() } })`
307
- 5. If upgrade succeeds, return `undefined` (Bun handles the connection)
308
- 6. If upgrade fails, return `new Response('WebSocket upgrade failed', { status: 500 })`
309
-
310
- ### Runtime Check
311
-
312
- The component checks the runtime during `binding()`:
301
+ Runs at the top of `binding()`, before anything else:
313
302
 
314
303
  ```typescript
315
304
  const runtime = RuntimeModules.detect();
316
305
  if (runtime === RuntimeModules.NODE) {
317
- throw getError({
318
- statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
319
- message: '[WebSocketComponent] Node.js runtime is not supported yet. Please use Bun runtime.',
320
- });
321
- }
322
- ```
323
-
324
- This check runs at component initialization time (before any hooks are registered), failing fast if the runtime is incompatible.
325
-
326
- ### Bun WebSocket Handler
327
-
328
- The helper's `getBunWebSocketHandler()` returns an `IBunWebSocketHandler` -- a Bun-native WebSocket handler object with four lifecycle callbacks plus config spread:
329
-
330
- ```typescript
331
- interface IBunWebSocketHandler extends IBunWebSocketConfig {
332
- open: (socket: IWebSocket) => void; // New connection -- creates client entry, starts auth timer
333
- message: (socket: IWebSocket, message: string | Buffer) => void; // Incoming message -- routes to handler
334
- close: (socket: IWebSocket, code: number, reason: string) => void; // Disconnect -- cleanup
335
- drain: (socket: IWebSocket) => void; // Backpressure cleared -- resets backpressured flag
306
+ throw getError({ message: '[WebSocketComponent] Node.js runtime is not supported yet. Please use Bun runtime.' });
336
307
  }
337
308
  ```
338
309
 
339
- The `open` handler (`onClientConnect`):
340
- 1. Checks if clientId already exists (returns early if duplicate)
341
- 2. Creates an `IWebSocketClient` entry in state `UNAUTHORIZED`
342
- 3. Subscribes the socket to its own `clientId` topic (Bun native pub/sub -- enables direct messaging before auth)
343
- 4. Starts an auth timeout timer (`authTimeout`, default 5 s)
344
-
345
- The `message` handler (`onClientMessage`):
346
- 1. Updates `lastActivity` on the client
347
- 2. Parses JSON -- sends `error` event `"Invalid message format"` if parse fails
348
- 3. Validates `event` field exists -- silently drops if missing (with error log)
349
- 4. Routes by event:
350
- - `heartbeat`: returns immediately (no-op, `lastActivity` already updated)
351
- - `authenticate`: delegates to `handleAuthenticate()`
352
- - Any other event from unauthenticated client: sends `error` event `"Not authenticated"`
353
- - `join`: delegates to `handleJoin()`
354
- - `leave`: delegates to `handleLeave()`
355
- - Custom events: delegates to `messageHandler` callback (if bound), otherwise silently dropped
356
-
357
- The `close` handler (`onClientDisconnect`):
358
- 1. Clears auth timer if pending
359
- 2. Removes client from `users` index (deletes user entry if last session)
360
- 3. Removes client from all `rooms` entries (deletes room entry if empty)
361
- 4. Deletes from `clients` map
362
- 5. Invokes `clientDisconnectedFn` callback (errors caught and logged)
310
+ ### Bun WebSocket handler
363
311
 
364
- The `drain` handler:
365
- 1. Sets `client.backpressured = false`
366
- 2. Logs a debug message
312
+ `WebSocketServerHelper.getBunWebSocketHandler()` returns an `IBunWebSocketHandler` - four lifecycle callbacks plus the config spread from `serverOptions`:
367
313
 
368
- ### `deliverToSocket()` Backpressure Handling
314
+ | Callback | Responsibility |
315
+ |----------|---------------|
316
+ | `open` | Creates the `IWebSocketClient` entry in state `UNAUTHORIZED`, subscribes the socket to its own `clientId` topic, starts the auth timer (skips if `clientId` already exists) |
317
+ | `message` | Updates `lastActivity`; parses JSON (sends `error` on failure); routes `heartbeat` (no-op), `authenticate`, other events on unauthenticated clients (`error`: `"Not authenticated"`), `join`, `leave`, or custom events to `messageHandler` |
318
+ | `close` | Clears the auth timer, removes the client from `users`/`rooms`/`clients`, invokes `clientDisconnectedFn` (errors caught and logged) |
319
+ | `drain` | Resets `client.backpressured = false` |
369
320
 
370
- The `deliverToSocket()` method handles three return values from Bun's `socket.send()`:
321
+ ### `deliverToSocket()` backpressure handling
371
322
 
372
- | Return Value | Meaning | Action |
373
- |-------------|---------|--------|
374
- | `> 0` (positive) | Message sent successfully (byte count) | No action |
375
- | `0` | Message dropped (socket already closed) | Logs warning: `"Message dropped (socket closed)"` |
376
- | `-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` |
323
+ | `socket.send()` return | Meaning | Action |
324
+ |------------------------|---------|--------|
325
+ | `> 0` | Sent successfully (byte count) | None |
326
+ | `0` | Dropped (socket already closed) | Logs `"Message dropped (socket closed)"` |
327
+ | `-1` | Backpressure (Bun's send buffer full) | Sets `client.backpressured = true`, logs a warning. Bun still queues the message; `drain` fires and resets the flag once the buffer clears |
377
328
 
378
- Any exception thrown by `socket.send()` is caught and logged as an error.
329
+ Any exception thrown by `socket.send()` is caught and logged.
379
330
 
380
- ### `send()` Destination Resolution
381
-
382
- The `send()` method is the primary public API for sending messages. It resolves the `destination` parameter using the following logic:
331
+ ### `send()` destination resolution
383
332
 
384
333
  ```
385
334
  send({ destination, payload: { topic, data } })
386
- |
387
- +-- destination is undefined/null?
388
- | Yes -> broadcast locally + publishToRedis(BROADCAST)
389
- |
390
- +-- destination matches a local clientId?
391
- | Yes -> sendToClient locally + publishToRedis(CLIENT)
392
- |
393
- +-- destination matches a local room name?
394
- | Yes -> sendToRoom locally + publishToRedis(ROOM)
395
- |
396
- +-- destination is unknown locally?
397
- Yes -> publishToRedis(ROOM, target: destination)
398
- (assumes it might be a room on another instance)
335
+ destination undefined/null? -> broadcast locally + publishToRedis(BROADCAST)
336
+ destination is a local clientId? -> sendToClient locally + publishToRedis(CLIENT)
337
+ destination is a local room? -> sendToRoom locally + publishToRedis(ROOM)
338
+ destination unknown locally? -> publishToRedis(ROOM, target: destination) // may be a room on another instance
399
339
  ```
400
340
 
401
341
  > [!IMPORTANT]
402
- > **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.
403
-
404
- > [!NOTE]
405
- > 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.
406
-
407
- ### Room Join Validation
408
-
409
- Room names go through two validation stages:
410
-
411
- 1. **Server-side sanitization** (always applied):
412
- - Must be a non-empty string (truthy, `typeof r === 'string'`)
413
- - Must be <= 256 characters
414
- - Must not start with `ws:` prefix (reserved for internal channels)
415
-
416
- 2. **Application-level validation** (via `validateRoomFn`):
417
- - Only called if the function is bound
418
- - Receives the sanitized room list
419
- - Returns the subset of rooms the client is allowed to join
420
- - If no `validateRoomFn` is bound, **all join requests are rejected** with a warning log
421
-
422
- ### Room Leave Validation
423
-
424
- The `handleLeave()` method validates that the client has actually joined the requested rooms before leaving:
342
+ > `send()` has no `USER` type. To reach every session of a user, use `sendToUser()` (local) or `WebSocketEmitter.toUser()` (cross-instance).
425
343
 
426
- ```typescript
427
- const validRooms = rooms.filter(r => client.rooms.has(r));
428
- ```
429
-
430
- 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.
344
+ ### Room join / leave validation
431
345
 
432
- ### Graceful Shutdown
346
+ - **Server-side sanitization** always applies: room must be a non-empty string, at most 256 characters, and must not start with the reserved `ws:` prefix.
347
+ - **`validateRoomFn` gates joins.** Only sanitized rooms reach it; it returns the subset the client may actually join. If unbound, every join is rejected with a warning log.
348
+ - **Leave is filtered against joined rooms.** `handleLeave()` computes `rooms.filter(r => client.rooms.has(r))` before leaving, so a client can never unsubscribe from a room it never joined (or an internal topic). If nothing remains after filtering, the leave is silently ignored.
433
349
 
434
- Always shut down the WebSocket server before stopping the application:
350
+ ### Graceful shutdown
435
351
 
436
352
  ```typescript
437
353
  override async stop(): Promise<void> {
438
- // 1. Shut down WebSocket (disconnects all clients, quits Redis)
439
354
  const wsHelper = this.get<WebSocketServerHelper>({
440
355
  key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
441
356
  isOptional: true,
442
357
  });
443
-
444
358
  if (wsHelper) {
445
359
  await wsHelper.shutdown();
446
360
  }
447
-
448
- // 2. Disconnect Redis helper
449
361
  if (this.redisHelper) {
450
362
  await this.redisHelper.disconnect();
451
363
  }
452
-
453
- // 3. Stop the Bun server
454
364
  await super.stop();
455
365
  }
456
366
  ```
457
367
 
458
- #### Shutdown Sequence Diagram
459
- ```
460
- wsHelper.shutdown()
461
- |-- Clear heartbeat timer
462
- | +-- clearInterval(heartbeatTimer)
463
- |
464
- |-- Close all sockets
465
- | +-- For each client: socket.close(1001, 'Server shutting down')
466
- | (errors caught per-client -- already-disconnected clients are logged)
467
- |
468
- |-- Trigger disconnect callbacks
469
- | +-- For each client: onClientDisconnect({ clientId })
470
- | |-- Clear auth timer
471
- | |-- Remove from users map
472
- | |-- Remove from rooms map
473
- | |-- Remove from clients map
474
- | +-- Invoke clientDisconnectedFn callback
475
- |
476
- |-- Clear tracking maps
477
- | |-- clients.clear()
478
- | |-- users.clear()
479
- | +-- rooms.clear()
480
- |
481
- +-- Redis cleanup (parallel)
482
- |-- redisPub.quit()
483
- +-- redisSub.quit()
484
- ```
485
-
486
- The shutdown sequence ensures:
487
- - Active connections are gracefully closed with code `1001` ("Going Away")
488
- - All disconnect callbacks are invoked (so application-level cleanup runs)
489
- - All internal state is cleared (client/user/room maps)
490
- - Redis pub/sub clients are properly disconnected
491
- - No memory leaks from lingering timers or connections
368
+ `wsHelper.shutdown()`:
492
369
 
493
- ### WebSocketEmitter Shutdown
494
-
495
- ```
496
- emitter.shutdown()
497
- +-- redisPub.quit()
498
- ```
370
+ 1. Clears the heartbeat timer.
371
+ 2. Closes every socket with `close(1001, 'Server shutting down')` (errors caught per-client - already-disconnected clients are logged, not thrown).
372
+ 3. Runs `onClientDisconnect()` for every client, so `clientDisconnectedFn` still fires for each.
373
+ 4. Clears the `clients`, `users`, and `rooms` maps.
374
+ 5. Quits both Redis clients (`redisPub.quit()` + `redisSub.quit()`) in parallel.
499
375
 
500
- The emitter shutdown is simpler since it only has one Redis client and no local state to clean up.
376
+ `emitter.shutdown()` is simpler - it only owns one Redis client and no local state: `redisPub.quit()`.
501
377
 
502
- ## See Also
378
+ ## See also
503
379
 
504
- - [Setup & Configuration](./) - Quick reference, imports, setup steps, configuration, and binding keys
505
- - [Usage & Examples](./usage) - Server-side usage, emitter, wire protocol, client tracking, and delivery strategy
506
- - [Error Reference](./errors) - Error conditions table and troubleshooting
507
- - [WebSocketServerHelper](/extensions/helpers/websocket/) - Helper API documentation
508
- - [Socket.IO Component](../socket-io/) - Node.js-compatible alternative with Socket.IO
509
- - [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - Official Bun WebSocket API reference
380
+ - [Overview](./) - quick start, imports, common configuration tasks
381
+ - [Usage & Examples](./usage) - injecting the helper, `WebSocketEmitter`, wire protocol, client tracking, delivery strategy
382
+ - [Error Reference](./errors) - error conditions and troubleshooting
383
+ - [WebSocketServerHelper](/extensions/helpers/websocket/) - helper API documentation
384
+ - [Socket.IO Component](../socket-io/) - Node.js-compatible alternative
385
+ - [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - official Bun WebSocket API reference