@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.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
|
@@ -1,509 +1,385 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
7
|
+
# WebSocket Component Reference
|
|
4
8
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
## Quick reference
|
|
42
20
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
30
|
+
## Import paths
|
|
78
31
|
|
|
79
|
-
|
|
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
|
-
|
|
66
|
+
## Configuration
|
|
106
67
|
|
|
107
|
-
`
|
|
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
|
|
113
|
-
identifier
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
identifier: 'my-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
### `IBunWebSocketConfig`
|
|
139
123
|
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
type TWebSocketValidateRoomFn = (opts: {
|
|
175
|
+
clientId: string;
|
|
176
|
+
userId?: string;
|
|
177
|
+
rooms: string[];
|
|
178
|
+
}) => ValueOrPromise<string[]>;
|
|
152
179
|
|
|
153
|
-
|
|
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
|
-
|
|
184
|
+
type TWebSocketClientDisconnectedFn = (opts: { clientId: string; userId?: string }) => ValueOrPromise<void>;
|
|
156
185
|
|
|
157
|
-
|
|
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:
|
|
162
|
-
}):
|
|
163
|
-
```
|
|
198
|
+
data: DataType;
|
|
199
|
+
}) => ValueOrPromise<TNullable<{ event: string; data: DataType }>>;
|
|
164
200
|
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
async toUser(opts: {
|
|
171
|
-
userId: string;
|
|
172
|
-
event: string;
|
|
173
|
-
data: unknown;
|
|
174
|
-
}): Promise<void>
|
|
175
|
-
```
|
|
211
|
+
## Architecture
|
|
176
212
|
|
|
177
|
-
|
|
213
|
+
### Lifecycle integration
|
|
178
214
|
|
|
179
|
-
|
|
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
|
-
|
|
235
|
+
`createBunFetchHandler()` builds the `fetch` function passed to `server.reload()`. It routes every incoming request:
|
|
191
236
|
|
|
192
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}): Promise<
|
|
247
|
+
function createBunFetchHandler(opts: {
|
|
248
|
+
wsPath: string;
|
|
249
|
+
honoServer: OpenAPIHono;
|
|
250
|
+
}): (req: Request, server: TBunServerInstance) => Promise<Response | undefined>
|
|
199
251
|
```
|
|
200
252
|
|
|
201
|
-
|
|
253
|
+
## `WebSocketEmitter` API
|
|
202
254
|
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
222
|
-
| `VALIDATE_ROOM_HANDLER` | Optional,
|
|
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
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
+
### `deliverToSocket()` backpressure handling
|
|
371
322
|
|
|
372
|
-
|
|
|
373
|
-
|
|
374
|
-
| `> 0`
|
|
375
|
-
| `0` |
|
|
376
|
-
| `-1` | Backpressure (Bun's send buffer
|
|
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
|
|
329
|
+
Any exception thrown by `socket.send()` is caught and logged.
|
|
379
330
|
|
|
380
|
-
### `send()`
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
376
|
+
`emitter.shutdown()` is simpler - it only owns one Redis client and no local state: `redisPub.quit()`.
|
|
501
377
|
|
|
502
|
-
## See
|
|
378
|
+
## See also
|
|
503
379
|
|
|
504
|
-
- [
|
|
505
|
-
- [Usage & Examples](./usage) -
|
|
506
|
-
- [Error Reference](./errors) -
|
|
507
|
-
- [WebSocketServerHelper](/extensions/helpers/websocket/) -
|
|
508
|
-
- [Socket.IO Component](../socket-io/) - Node.js-compatible alternative
|
|
509
|
-
- [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) -
|
|
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
|