@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,509 +1,416 @@
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
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
- ```
9
+ Every binding key, configuration option, callback signature, and internal mechanism of `WebSocketComponent`. For the task-oriented walkthrough, see [Usage & Examples](./usage).
38
10
 
39
- ### Lifecycle Integration
11
+ **Files:**
40
12
 
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.
13
+ - [`packages/core-server/src/components/websocket/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/websocket/component.ts)
14
+ - [`packages/core-server/src/components/websocket/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/websocket/common/types.ts)
15
+ - [`packages/core-server/src/components/websocket/handlers/bun.handler.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/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)
42
18
 
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
- ```
19
+ ## Quick reference
74
20
 
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
-
105
- ### Overview
106
65
 
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.
66
+ ## Configuration
108
67
 
109
- ### `IWebSocketEmitterOptions`
68
+ `WebSocketComponent`'s own `IServerOptions` interface is a **subset** of the helper's `IWebSocketServerOptions`. Before constructing the helper, the component fills in `server` and `redisConnection` from the running application, plus every callback function resolved from the DI container. It does not forward `authTimeout` or `encryptedBatchLimit` - see the note below.
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 (for example 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`, and `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>
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 })
188
231
  ```
189
232
 
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.
233
+ ### Fetch handler
191
234
 
192
- #### `broadcast()`
235
+ `createBunFetchHandler()` builds the `fetch` function passed to `server.reload()`. It routes every incoming request:
236
+
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:**
265
+ - Calls `super({ scope })`.
266
+ - Throws `"Invalid redis connection!"` if `redisConnection` is falsy.
267
+ - Calls `redisConnection.duplicateClient()` to create an isolated pub client.
268
+ - **`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.
269
+
270
+ | Method | Signature | Behavior |
271
+ |--------|-----------|----------|
272
+ | `configure()` | `(): Promise<void>` | Registers a Redis `error` handler, connects if lazy (`status === 'wait'`), waits for `'ready'` (30s timeout). Call before any send method. |
273
+ | `toClient()` | `(opts: { clientId; event; data }): Promise<void>` | Publishes to `ws:client:{clientId}` |
274
+ | `toUser()` | `(opts: { userId; event; data }): Promise<void>` | Publishes to `ws:user:{userId}` |
275
+ | `toRoom()` | `(opts: { room; event; data; exclude? }): Promise<void>` | Publishes to `ws:room:{room}`, forwarding `exclude` |
276
+ | `broadcast()` | `(opts: { event; data }): Promise<void>` | Publishes to `ws:broadcast` |
277
+ | `shutdown()` | `(): Promise<void>` | Calls `redisPub.quit()` - always call when the emitter is no longer needed |
210
278
 
211
279
  ## Internals
212
280
 
213
281
  ### `resolveBindings()`
214
282
 
215
- Reads all binding keys from the DI container and validates required ones:
283
+ Reads all binding keys and validates the required ones, throwing before the post-start hook is even registered:
216
284
 
217
- | Binding | Validation | Error on Failure |
285
+ | Binding | Validation | Error on failure |
218
286
  |---------|-----------|------------------|
219
287
  | `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`) | -- |
288
+ | `REDIS_CONNECTION` | Must pass `isRedisHelper()` | `"Invalid instance of redisConnection ..."` |
289
+ | `AUTHENTICATE_HANDLER` | Must be truthy | `"Invalid authenticateFn to setup WebSocket server!"` |
290
+ | `VALIDATE_ROOM_HANDLER` / `CLIENT_CONNECTED_HANDLER` / `CLIENT_DISCONNECTED_HANDLER` / `MESSAGE_HANDLER` / `OUTBOUND_TRANSFORMER` / `HANDSHAKE_HANDLER` | Optional, `null` coerced to `undefined` | -- |
228
291
 
229
292
  ### `registerBunHook()`
230
293
 
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 })`
241
-
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
- }
255
-
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
- });
275
-
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
+ Registers the `websocket-initialize` post-start hook:
294
295
 
295
- ```typescript
296
- function createBunFetchHandler(opts: {
297
- wsPath: string;
298
- honoServer: OpenAPIHono;
299
- }): (req: Request, server: TBunServerInstance) => Promise<Response | undefined>
300
- ```
296
+ 1. Gets the Bun server instance via `getServerInstance()` and the Hono server via `getServer()`. Throws `"[WebSocketComponent] Bun server instance not available!"` if the Bun instance is missing.
297
+ 2. Constructs `WebSocketServerHelper` with all resolved bindings plus the running server instance.
298
+ 3. Awaits `wsHelper.configure()` - connects Redis clients, sets up subscriptions, starts the heartbeat timer.
299
+ 4. Binds the helper to `WEBSOCKET_INSTANCE`.
300
+ 5. Calls `serverInstance.reload({ fetch: createBunFetchHandler(...), websocket: wsHelper.getBunWebSocketHandler() })`.
301
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 })`
302
+ ### Runtime check
309
303
 
310
- ### Runtime Check
311
-
312
- The component checks the runtime during `binding()`:
304
+ Runs at the top of `binding()`, before anything else:
313
305
 
314
306
  ```typescript
315
307
  const runtime = RuntimeModules.detect();
316
308
  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
- });
309
+ throw getError({ message: '[WebSocketComponent] Node.js runtime is not supported yet. Please use Bun runtime.' });
321
310
  }
322
311
  ```
323
312
 
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
313
+ ### Bun WebSocket handler
327
314
 
328
- The helper's `getBunWebSocketHandler()` returns an `IBunWebSocketHandler` -- a Bun-native WebSocket handler object with four lifecycle callbacks plus config spread:
315
+ `WebSocketServerHelper.getBunWebSocketHandler()` returns an `IBunWebSocketHandler` - four lifecycle callbacks plus the config spread from `serverOptions`:
329
316
 
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
336
- }
337
- ```
317
+ | Callback | Responsibility |
318
+ |----------|---------------|
319
+ | `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) |
320
+ | `message` | Updates `lastActivity`, then routes the parsed event - see the table below |
321
+ | `close` | Clears the auth timer, removes the client from `users`/`rooms`/`clients`, invokes `clientDisconnectedFn` (errors caught and logged) |
322
+ | `drain` | Resets `client.backpressured = false` |
338
323
 
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)
324
+ The `message` callback routes each event like this:
344
325
 
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
326
+ | Event | Handling |
327
+ |-------|----------|
328
+ | Not valid JSON | Sends an `error` event back to the client |
329
+ | `heartbeat` | No-op - `lastActivity` was already updated |
330
+ | `authenticate` | Runs the authentication flow |
331
+ | Any other event, client not yet authenticated | Sends `error: "Not authenticated"` |
332
+ | `join` / `leave` | Runs the room join/leave flow |
333
+ | Any other event, client authenticated | Forwarded to `messageHandler` |
356
334
 
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)
335
+ ### `deliverToSocket()` backpressure handling
363
336
 
364
- The `drain` handler:
365
- 1. Sets `client.backpressured = false`
366
- 2. Logs a debug message
337
+ | `socket.send()` return | Meaning | Action |
338
+ |------------------------|---------|--------|
339
+ | `> 0` | Sent successfully (byte count) | None |
340
+ | `0` | Dropped (socket already closed) | Logs `"Message dropped (socket closed)"` |
341
+ | `-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 |
367
342
 
368
- ### `deliverToSocket()` Backpressure Handling
343
+ Any exception thrown by `socket.send()` is caught and logged.
369
344
 
370
- The `deliverToSocket()` method handles three return values from Bun's `socket.send()`:
371
-
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` |
377
-
378
- Any exception thrown by `socket.send()` is caught and logged as an error.
379
-
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:
345
+ ### `send()` destination resolution
383
346
 
384
347
  ```
385
348
  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)
349
+ destination undefined/null? -> broadcast locally + publishToRedis(BROADCAST)
350
+ destination is a local clientId? -> sendToClient locally + publishToRedis(CLIENT)
351
+ destination is a local room? -> sendToRoom locally + publishToRedis(ROOM)
352
+ destination unknown locally? -> publishToRedis(ROOM, target: destination) // may be a room on another instance
399
353
  ```
400
354
 
401
355
  > [!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.
356
+ > `send()` has no `USER` type. To reach every session of a user, use `sendToUser()` (local) or `WebSocketEmitter.toUser()` (cross-instance).
406
357
 
407
- ### Room Join Validation
358
+ ### Room join / leave validation
408
359
 
409
- Room names go through two validation stages:
360
+ - **Server-side sanitization always applies:**
410
361
 
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)
362
+ | Rule | Requirement |
363
+ |------|-------------|
364
+ | Type | Non-empty string |
365
+ | Length | At most 256 characters |
366
+ | Prefix | Must not start with the reserved `ws:` prefix |
415
367
 
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:
425
-
426
- ```typescript
427
- const validRooms = rooms.filter(r => client.rooms.has(r));
428
- ```
368
+ - **`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.
369
+ - **Leave is filtered against joined rooms.** `handleLeave()` computes `rooms.filter(r => client.rooms.has(r))` before leaving. A client can never unsubscribe from a room it never joined - or an internal topic it was auto-subscribed to.
370
+ - If nothing remains after filtering, the leave is silently ignored.
429
371
 
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.
372
+ ### Graceful shutdown
431
373
 
432
- ### Graceful Shutdown
374
+ `WebSocketServerHelper` has a real `shutdown()` method, but `WebSocketComponent` never calls it automatically. Components have no `stop()` lifecycle, and the component does not register a post-stop hook for you.
433
375
 
434
- Always shut down the WebSocket server before stopping the application:
376
+ To disconnect clients cleanly when your application stops, register your own hook with `registerPostStopHook()` - the same mechanism IGNIS's secrets provider uses internally:
435
377
 
436
378
  ```typescript
437
- override async stop(): Promise<void> {
438
- // 1. Shut down WebSocket (disconnects all clients, quits Redis)
439
- const wsHelper = this.get<WebSocketServerHelper>({
440
- key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
441
- isOptional: true,
442
- });
443
-
444
- if (wsHelper) {
445
- await wsHelper.shutdown();
446
- }
447
-
448
- // 2. Disconnect Redis helper
449
- if (this.redisHelper) {
450
- await this.redisHelper.disconnect();
379
+ export class Application extends BaseApplication {
380
+ preConfigure(): ValueOrPromise<void> {
381
+ // ... bind REDIS_CONNECTION, AUTHENTICATE_HANDLER, register WebSocketComponent ...
382
+
383
+ this.registerPostStopHook({
384
+ identifier: 'websocket-shutdown',
385
+ hook: async () => {
386
+ const wsHelper = this.get<WebSocketServerHelper>({
387
+ key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
388
+ isOptional: true,
389
+ });
390
+ if (wsHelper) {
391
+ await wsHelper.shutdown();
392
+ }
393
+ },
394
+ });
451
395
  }
452
-
453
- // 3. Stop the Bun server
454
- await super.stop();
455
396
  }
456
397
  ```
457
398
 
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
492
-
493
- ### WebSocketEmitter Shutdown
399
+ `wsHelper.shutdown()`:
494
400
 
495
- ```
496
- emitter.shutdown()
497
- +-- redisPub.quit()
498
- ```
401
+ 1. Clears the heartbeat timer.
402
+ 2. Closes every socket with `close(1001, 'Server shutting down')` (errors caught per-client - already-disconnected clients are logged, not thrown).
403
+ 3. Runs `onClientDisconnect()` for every client, so `clientDisconnectedFn` still fires for each.
404
+ 4. Clears the `clients`, `users`, and `rooms` maps.
405
+ 5. Quits both Redis clients (`redisPub.quit()` + `redisSub.quit()`) in parallel.
499
406
 
500
- The emitter shutdown is simpler since it only has one Redis client and no local state to clean up.
407
+ `emitter.shutdown()` is simpler - it only owns one Redis client and no local state: `redisPub.quit()`.
501
408
 
502
- ## See Also
409
+ ## See also
503
410
 
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
411
+ - [Overview](./) - quick start, imports, common configuration tasks
412
+ - [Usage & Examples](./usage) - injecting the helper, `WebSocketEmitter`, wire protocol, client tracking, delivery strategy
413
+ - [Error Reference](./errors) - error conditions and troubleshooting
414
+ - [WebSocketServerHelper](/extensions/helpers/websocket/) - helper API documentation
415
+ - [Socket.IO Component](../socket-io/) - Node.js-compatible alternative
416
+ - [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - official Bun WebSocket API reference