@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,45 +1,98 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
---
|
|
2
|
+
title: WebSocket - Full Reference
|
|
3
|
+
description: Complete reference for WebSocketServerHelper and WebSocketEmitter - constructors, every method, the client lifecycle, types, and constants
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# WebSocket - Full Reference
|
|
8
|
+
|
|
9
|
+
Exhaustive reference for `WebSocketServerHelper` and `WebSocketEmitter`. For a readable introduction and the most common tasks, start with the [WebSocket overview](/extensions/helpers/websocket/).
|
|
10
|
+
|
|
11
|
+
**Files:**
|
|
12
|
+
|
|
13
|
+
- [`packages/helpers/src/modules/socket/websocket/server/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/server/helper.ts) - `WebSocketServerHelper`
|
|
14
|
+
- [`packages/helpers/src/modules/socket/websocket/emitter/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/emitter/helper.ts) - `WebSocketEmitter`
|
|
15
|
+
- [`packages/helpers/src/modules/socket/websocket/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/common/types.ts) - option, callback, and wire types
|
|
16
|
+
- [`packages/helpers/src/modules/socket/websocket/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/common/constants.ts) - `WebSocketEvents`, `WebSocketChannels`, `WebSocketDefaults`, `WebSocketMessageTypes`, `WebSocketClientStates`
|
|
17
|
+
|
|
18
|
+
## Quick Reference
|
|
19
|
+
|
|
20
|
+
| Class | Extends | Role |
|
|
21
|
+
|-------|---------|------|
|
|
22
|
+
| `WebSocketServerHelper` | `BaseHelper` | Bun-native WebSocket server with auth, rooms, heartbeat, Redis Pub/Sub scaling |
|
|
23
|
+
| `WebSocketEmitter` | `BaseHelper` | Publish messages to WebSocket clients from any process via Redis |
|
|
24
|
+
|
|
25
|
+
### Import Reference
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { WebSocketServerHelper, WebSocketEmitter } from '@venizia/ignis-helpers';
|
|
29
|
+
|
|
30
|
+
import type {
|
|
31
|
+
IWebSocketServerOptions,
|
|
32
|
+
IWebSocketEmitterOptions,
|
|
33
|
+
IWebSocketClient,
|
|
34
|
+
IWebSocketData,
|
|
35
|
+
IWebSocketMessage,
|
|
36
|
+
IRedisSocketMessage,
|
|
37
|
+
IWebSocket,
|
|
38
|
+
IBunServer,
|
|
39
|
+
IBunWebSocketConfig,
|
|
40
|
+
IBunWebSocketHandler,
|
|
41
|
+
TWebSocketAuthenticateFn,
|
|
42
|
+
TWebSocketValidateRoomFn,
|
|
43
|
+
TWebSocketClientConnectedFn,
|
|
44
|
+
TWebSocketClientDisconnectedFn,
|
|
45
|
+
TWebSocketMessageHandler,
|
|
46
|
+
TWebSocketOutboundTransformer,
|
|
47
|
+
TWebSocketHandshakeFn,
|
|
48
|
+
TWebSocketClientState,
|
|
49
|
+
TWebSocketEvent,
|
|
50
|
+
TWebSocketMessageType,
|
|
51
|
+
} from '@venizia/ignis-helpers';
|
|
52
|
+
|
|
53
|
+
import {
|
|
54
|
+
WebSocketEvents,
|
|
55
|
+
WebSocketChannels,
|
|
56
|
+
WebSocketDefaults,
|
|
57
|
+
WebSocketMessageTypes,
|
|
58
|
+
WebSocketClientStates,
|
|
59
|
+
} from '@venizia/ignis-helpers';
|
|
60
|
+
```
|
|
4
61
|
|
|
5
62
|
## Architecture
|
|
6
63
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#### Architecture Diagram
|
|
64
|
+
`Source ->` [`server/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/server/helper.ts), [`emitter/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/emitter/helper.ts)
|
|
10
65
|
|
|
11
66
|
```
|
|
12
67
|
WebSocketServerHelper
|
|
13
68
|
+---------------------------------------------------+
|
|
14
|
-
| |
|
|
15
69
|
| constructor(opts) |
|
|
16
|
-
| |-- identifier, path, serverId (UUID)
|
|
17
|
-
| |-- Store callbacks (auth, rooms, messages)
|
|
18
|
-
| |-- Apply defaults (rooms, timeouts)
|
|
19
|
-
| +-- initRedisClients(redisConnection)
|
|
20
|
-
| +-- redisPub = duplicateClient()
|
|
21
|
-
| +-- redisSub = duplicateClient()
|
|
70
|
+
| |-- identifier, path, serverId (UUID) |
|
|
71
|
+
| |-- Store callbacks (auth, rooms, messages) |
|
|
72
|
+
| |-- Apply defaults (rooms, timeouts) |
|
|
73
|
+
| +-- initRedisClients(redisConnection) |
|
|
74
|
+
| +-- redisPub = duplicateClient() |
|
|
75
|
+
| +-- redisSub = duplicateClient() |
|
|
22
76
|
| |
|
|
23
77
|
| configure() [async] |
|
|
24
|
-
| |-- Connect Redis clients (if lazyConnect)
|
|
25
|
-
| |-- await Redis ready (pub + sub)
|
|
26
|
-
| |-- setupRedisSubscriptions()
|
|
27
|
-
| | |-- subscribe(ws:broadcast)
|
|
28
|
-
| | |-- psubscribe(ws:room:*)
|
|
29
|
-
| | |-- psubscribe(ws:client:*)
|
|
30
|
-
| | +-- psubscribe(ws:user:*)
|
|
31
|
-
| +-- startHeartbeatTimer()
|
|
78
|
+
| |-- Connect Redis clients (if lazyConnect) |
|
|
79
|
+
| |-- await Redis ready (pub + sub) |
|
|
80
|
+
| |-- setupRedisSubscriptions() |
|
|
81
|
+
| | |-- subscribe(ws:broadcast) |
|
|
82
|
+
| | |-- psubscribe(ws:room:*) |
|
|
83
|
+
| | |-- psubscribe(ws:client:*) |
|
|
84
|
+
| | +-- psubscribe(ws:user:*) |
|
|
85
|
+
| +-- startHeartbeatTimer() |
|
|
32
86
|
| |
|
|
33
87
|
| getBunWebSocketHandler() |
|
|
34
|
-
| +-- Returns { open, message, close, drain,
|
|
35
|
-
| ...serverOptions }
|
|
36
|
-
| |
|
|
88
|
+
| +-- Returns { open, message, close, drain, |
|
|
89
|
+
| ...serverOptions } |
|
|
37
90
|
+---------------------------------------------------+
|
|
38
91
|
|
|
39
92
|
WebSocketEmitter
|
|
40
93
|
+---------------------------------------------------+
|
|
41
94
|
| constructor(opts) |
|
|
42
|
-
| +-- redisPub = duplicateClient()
|
|
95
|
+
| +-- redisPub = duplicateClient() |
|
|
43
96
|
| |
|
|
44
97
|
| configure() [async] |
|
|
45
98
|
| +-- await Redis ready |
|
|
@@ -49,7 +102,7 @@ The WebSocket helper provides two classes: `WebSocketServerHelper` for managing
|
|
|
49
102
|
+---------------------------------------------------+
|
|
50
103
|
```
|
|
51
104
|
|
|
52
|
-
|
|
105
|
+
### Client connection lifecycle
|
|
53
106
|
|
|
54
107
|
```
|
|
55
108
|
Client connects via WebSocket upgrade
|
|
@@ -69,18 +122,18 @@ Client connects via WebSocket upgrade
|
|
|
69
122
|
| | +-- [requireEncryption?] -> handshakeFn() -> enableClientEncryption()
|
|
70
123
|
| | +-- Index by userId
|
|
71
124
|
| | +-- Subscribe to broadcast topic (unless encrypted)
|
|
72
|
-
| | +-- Join
|
|
125
|
+
| | +-- Join clientId room + defaultRooms
|
|
73
126
|
| | +-- Send 'connected' event
|
|
74
127
|
| | +-- Call clientConnectedFn()
|
|
75
|
-
| +-- Failure:
|
|
128
|
+
| +-- Failure (null/false or throw):
|
|
76
129
|
| +-- Send 'error' event
|
|
77
130
|
| +-- Close with code 4003
|
|
78
131
|
|
|
|
79
|
-
+-- Auth timeout expires (
|
|
132
|
+
+-- Auth timeout expires (still UNAUTHORIZED or AUTHENTICATING)
|
|
80
133
|
| +-- Close with code 4001
|
|
81
134
|
|
|
|
82
135
|
+-- Heartbeat sweep (every heartbeatInterval)
|
|
83
|
-
| +-- If now - lastActivity > heartbeatTimeout
|
|
136
|
+
| +-- If now - lastActivity > heartbeatTimeout (AUTHENTICATED clients only)
|
|
84
137
|
| +-- Close with code 4002
|
|
85
138
|
|
|
|
86
139
|
+-- Client disconnects
|
|
@@ -92,27 +145,81 @@ Client connects via WebSocket upgrade
|
|
|
92
145
|
+-- Call clientDisconnectedFn()
|
|
93
146
|
```
|
|
94
147
|
|
|
95
|
-
|
|
148
|
+
### Redis two-client architecture
|
|
96
149
|
|
|
97
150
|
```
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
|
102
|
-
|
|
151
|
+
Server A Redis Server B
|
|
152
|
+
+-----------+ +----------+ +-----------+
|
|
153
|
+
| WS Server |--redisPub-->| |<--redisPub----| WS Server |
|
|
154
|
+
| |<--redisSub--| Pub/Sub |---redisSub--->| |
|
|
155
|
+
+-----------+ +----------+ +-----------+
|
|
103
156
|
```
|
|
104
157
|
|
|
105
|
-
|
|
158
|
+
- **Duplication.** `WebSocketServerHelper` duplicates its `redisConnection` twice (`redisPub`, `redisSub`); `WebSocketEmitter` duplicates it once (`redisPub` only).
|
|
159
|
+
- **Connection types.** Both single-instance `Redis` and `Cluster` connections from ioredis are supported - the parent helper connection stays independent and unconsumed.
|
|
160
|
+
- **Dedup on receipt.** Every server instance generates a unique `serverId` (UUID) at construction; messages carrying the same `serverId` on receipt are skipped to prevent double delivery to the originating instance.
|
|
106
161
|
|
|
107
162
|
## Server API
|
|
108
163
|
|
|
109
|
-
|
|
164
|
+
`Source ->` [`server/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/server/helper.ts)
|
|
165
|
+
|
|
166
|
+
### Constructor
|
|
110
167
|
|
|
111
168
|
```typescript
|
|
112
169
|
constructor(opts: IWebSocketServerOptions<AuthDataType, MetadataType>)
|
|
113
170
|
```
|
|
114
171
|
|
|
115
|
-
|
|
172
|
+
Generates a unique `serverId` (UUID), stores all options with defaults applied, and initializes two duplicated Redis clients (`redisPub`, `redisSub`). Throws `getError({ statusCode: 500, message: '[WebSocketServerHelper] Invalid redis connection!' })` if `redisConnection` is falsy.
|
|
173
|
+
|
|
174
|
+
#### Generic type parameters
|
|
175
|
+
|
|
176
|
+
`AuthDataType` types the payload passed to `authenticateFn`/`handshakeFn`; `MetadataType` types the value returned as `metadata` and stored on `IWebSocketClient`. Both default to `Record<string, unknown>`.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
interface AuthPayload { type: string; token: string; publicKey?: string }
|
|
180
|
+
interface UserMetadata { role: string; permissions: string[] }
|
|
181
|
+
|
|
182
|
+
const helper = new WebSocketServerHelper<AuthPayload, UserMetadata>({
|
|
183
|
+
identifier: 'typed-ws',
|
|
184
|
+
server: bunServer,
|
|
185
|
+
redisConnection: redis,
|
|
186
|
+
authenticateFn: async data => {
|
|
187
|
+
// data is typed as AuthPayload
|
|
188
|
+
const user = await verifyJWT(data.token);
|
|
189
|
+
return user ? { userId: user.id, metadata: { role: user.role, permissions: user.permissions } } : null;
|
|
190
|
+
},
|
|
191
|
+
clientConnectedFn: ({ metadata }) => {
|
|
192
|
+
// metadata is typed as UserMetadata | undefined
|
|
193
|
+
if (metadata?.role === 'admin') console.log('admin connected');
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `IWebSocketServerOptions`
|
|
199
|
+
|
|
200
|
+
| Option | Type | Required | Default | Description |
|
|
201
|
+
|--------|------|----------|---------|-------------|
|
|
202
|
+
| `identifier` | `string` | Yes | - | Unique name for this instance; also used as the `BaseHelper` logging scope |
|
|
203
|
+
| `path` | `string` | No | `'/ws'` | URL path for WebSocket upgrade requests |
|
|
204
|
+
| `server` | `IBunServer` | Yes | - | Bun server instance (provides `publish()` for native pub/sub) |
|
|
205
|
+
| `redisConnection` | `IRedisHelper` | Yes | - | Redis helper for cross-instance messaging; duplicated twice internally |
|
|
206
|
+
| `defaultRooms` | `string[]` | No | `['ws-default', 'ws-notification']` | Rooms every client auto-joins after authentication |
|
|
207
|
+
| `serverOptions` | `IBunWebSocketConfig` | No | See [Bun native configuration](#bun-native-configuration) | Bun native WebSocket configuration |
|
|
208
|
+
| `authTimeout` | `number` | No | `5000` (5s) | Milliseconds before an unauthenticated client is disconnected (close code `4001`); extended to `authTimeout * 3` while `authenticateFn` is in flight |
|
|
209
|
+
| `heartbeatInterval` | `number` | No | `30000` (30s) | Milliseconds between heartbeat sweeps |
|
|
210
|
+
| `heartbeatTimeout` | `number` | No | `90000` (90s) | Milliseconds of inactivity before an authenticated client is closed (close code `4002`) |
|
|
211
|
+
| `encryptedBatchLimit` | `number` | No | `10` | Max concurrent `outboundTransformer` invocations during room/broadcast delivery |
|
|
212
|
+
| `requireEncryption` | `boolean` | No | `false` | When `true`, clients must complete the handshake during auth or get disconnected (code `4004`) |
|
|
213
|
+
| `authenticateFn` | `TWebSocketAuthenticateFn` | Yes | - | Called on `{ event: 'authenticate' }`. Return `{ userId, metadata }` to accept, `null`/`false` (or throw) to reject |
|
|
214
|
+
| `validateRoomFn` | `TWebSocketValidateRoomFn` | No | - | Called on `{ event: 'join' }`. Return the allowed subset of requested rooms. All joins are rejected when this is not provided |
|
|
215
|
+
| `clientConnectedFn` | `TWebSocketClientConnectedFn` | No | - | Called after successful authentication |
|
|
216
|
+
| `clientDisconnectedFn` | `TWebSocketClientDisconnectedFn` | No | - | Called during disconnect cleanup |
|
|
217
|
+
| `messageHandler` | `TWebSocketMessageHandler` | No | - | Called for events other than `authenticate`, `heartbeat`, `join`, `leave` from authenticated clients |
|
|
218
|
+
| `outboundTransformer` | `TWebSocketOutboundTransformer` | No | - | Intercepts outbound `{ event, data }` before `socket.send()`; enables per-client encryption |
|
|
219
|
+
| `handshakeFn` | `TWebSocketHandshakeFn` | No | - | Required when `requireEncryption` is `true`. Returns `{ serverPublicKey, salt }` to accept, `null`/`false` to reject |
|
|
220
|
+
|
|
221
|
+
- **All callbacks run through `invokeHook()`.** Applies to `authenticateFn`, `validateRoomFn`, `clientConnectedFn`, `clientDisconnectedFn`, `messageHandler`, and `handshakeFn`.
|
|
222
|
+
- **Failures do not crash the process.** A synchronous throw inside a Bun socket handler is caught and logged; rejected promises are logged the same way via `voidExecution`.
|
|
116
223
|
|
|
117
224
|
### `configure()`
|
|
118
225
|
|
|
@@ -120,15 +227,13 @@ Creates the server helper, generates a unique `serverId` (UUID), stores all opti
|
|
|
120
227
|
configure(): Promise<void>
|
|
121
228
|
```
|
|
122
229
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
#### Internal Flow
|
|
230
|
+
Must be called after construction and before accepting connections.
|
|
126
231
|
|
|
127
|
-
1.
|
|
128
|
-
2.
|
|
129
|
-
3. `await Promise.all([waitForRedisReady(
|
|
130
|
-
4.
|
|
131
|
-
5.
|
|
232
|
+
1. Registers `error` listeners on `redisPub` and `redisSub`.
|
|
233
|
+
2. Connects both duplicated clients if their status is `'wait'` (`lazyConnect` mode).
|
|
234
|
+
3. `await Promise.all([waitForRedisReady(redisPub), waitForRedisReady(redisSub)])`.
|
|
235
|
+
4. Subscribes: `subscribe('ws:broadcast')`, `psubscribe('ws:room:*')`, `psubscribe('ws:client:*')`, `psubscribe('ws:user:*')` - awaited together before proceeding.
|
|
236
|
+
5. Starts the heartbeat timer: `setInterval(heartbeatAll, heartbeatInterval)`.
|
|
132
237
|
|
|
133
238
|
### `getBunWebSocketHandler()`
|
|
134
239
|
|
|
@@ -136,30 +241,29 @@ Initializes Redis connections, sets up pub/sub subscriptions, and starts the hea
|
|
|
136
241
|
getBunWebSocketHandler(): IBunWebSocketHandler
|
|
137
242
|
```
|
|
138
243
|
|
|
139
|
-
Returns the Bun WebSocket handler object
|
|
140
|
-
|
|
141
|
-
#### Lifecycle Callbacks
|
|
244
|
+
Returns the Bun WebSocket handler object. Pass it to `server.reload({ websocket })`.
|
|
142
245
|
|
|
143
246
|
| Callback | When | Behavior |
|
|
144
247
|
|----------|------|----------|
|
|
145
|
-
| `open` |
|
|
146
|
-
| `message` | Message received | Updates `lastActivity`, calls `onClientMessage()` for routing |
|
|
248
|
+
| `open` | Connection established | Reads `clientId` from `socket.data`, calls `onClientConnect()` |
|
|
249
|
+
| `message` | Message received | Updates `client.lastActivity`, calls `onClientMessage()` for routing |
|
|
147
250
|
| `close` | Connection closed | Calls `onClientDisconnect()` for cleanup |
|
|
148
251
|
| `drain` | Backpressure cleared | Sets `client.backpressured = false` |
|
|
149
252
|
|
|
150
|
-
#### Bun
|
|
253
|
+
#### Bun native configuration
|
|
151
254
|
|
|
152
|
-
|
|
255
|
+
- **`serverOptions` is spread into the returned handler.** `WebSocketServerHelper` only applies its own default for `sendPings`, `idleTimeout`, and `maxPayloadLength`.
|
|
256
|
+
- **Everything else falls through to Bun.** The remaining fields are `undefined` unless you set them, and Bun applies its own runtime default in that case.
|
|
153
257
|
|
|
154
|
-
| Option | Type |
|
|
155
|
-
|
|
156
|
-
| `
|
|
258
|
+
| Option | Type | Helper default | Description |
|
|
259
|
+
|--------|------|-----------------|--------------|
|
|
260
|
+
| `sendPings` | `boolean` | `true` | Bun transport-level pings |
|
|
261
|
+
| `idleTimeout` | `number` | `60` (seconds) | Bun-level idle timeout (transport layer, separate from `heartbeatTimeout`) |
|
|
157
262
|
| `maxPayloadLength` | `number` | `131072` (128KB) | Maximum incoming message size in bytes |
|
|
158
|
-
| `
|
|
159
|
-
| `backpressureLimit` | `number` |
|
|
160
|
-
| `closeOnBackpressureLimit` | `boolean` |
|
|
161
|
-
| `
|
|
162
|
-
| `publishToSelf` | `boolean` | `false` | Whether `server.publish()` delivers to the publishing socket |
|
|
263
|
+
| `perMessageDeflate` | `boolean` | not set by the helper | Enable per-message compression |
|
|
264
|
+
| `backpressureLimit` | `number` | not set by the helper | Backpressure threshold in bytes |
|
|
265
|
+
| `closeOnBackpressureLimit` | `boolean` | not set by the helper | Close socket when the backpressure limit is exceeded |
|
|
266
|
+
| `publishToSelf` | `boolean` | not set by the helper | Whether `server.publish()` delivers to the publishing socket |
|
|
163
267
|
|
|
164
268
|
### `getPath()`
|
|
165
269
|
|
|
@@ -178,7 +282,7 @@ getClients(opts?: { id?: string }):
|
|
|
178
282
|
| undefined
|
|
179
283
|
```
|
|
180
284
|
|
|
181
|
-
|
|
285
|
+
Without `id` (or an empty `opts`), returns the full `Map<string, IWebSocketClient>`. With `{ id }`, returns that client entry or `undefined`.
|
|
182
286
|
|
|
183
287
|
### `getClientsByUser()`
|
|
184
288
|
|
|
@@ -186,7 +290,7 @@ When called without arguments or with an empty opts, returns the full `Map<strin
|
|
|
186
290
|
getClientsByUser(opts: { userId: string }): IWebSocketClient<MetadataType>[]
|
|
187
291
|
```
|
|
188
292
|
|
|
189
|
-
Returns
|
|
293
|
+
Returns every client belonging to `userId`; `[]` if the user has no active connections.
|
|
190
294
|
|
|
191
295
|
### `getClientsByRoom()`
|
|
192
296
|
|
|
@@ -194,7 +298,7 @@ Returns all clients belonging to the given user ID. Returns an empty array if th
|
|
|
194
298
|
getClientsByRoom(opts: { room: string }): IWebSocketClient<MetadataType>[]
|
|
195
299
|
```
|
|
196
300
|
|
|
197
|
-
Returns
|
|
301
|
+
Returns every client in `room`; `[]` if the room does not exist or is empty.
|
|
198
302
|
|
|
199
303
|
### `onClientConnect()`
|
|
200
304
|
|
|
@@ -202,7 +306,7 @@ Returns all clients in the given room. Returns an empty array if the room does n
|
|
|
202
306
|
onClientConnect(opts: { clientId: string; socket: IWebSocket }): void
|
|
203
307
|
```
|
|
204
308
|
|
|
205
|
-
Handles a new WebSocket connection
|
|
309
|
+
Handles a new WebSocket connection: creates an `IWebSocketClient` entry with state `UNAUTHORIZED`, subscribes the socket to its own `clientId` topic (Bun native pub/sub), and starts the authentication timeout. Returns early - and logs `'Client already existed'` - if the client ID already exists.
|
|
206
310
|
|
|
207
311
|
### `onClientMessage()`
|
|
208
312
|
|
|
@@ -210,15 +314,14 @@ Handles a new WebSocket connection. Creates an `IWebSocketClient` entry with sta
|
|
|
210
314
|
onClientMessage(opts: { clientId: string; raw: string }): void
|
|
211
315
|
```
|
|
212
316
|
|
|
213
|
-
Routes incoming
|
|
214
|
-
|
|
215
|
-
- `heartbeat` events: silently consumed (updates `lastActivity` via the `message` callback)
|
|
216
|
-
- `authenticate` events: delegates to `handleAuthenticate()`
|
|
217
|
-
- Unauthenticated clients sending non-auth events: receives an `error` event (`'Not authenticated'`)
|
|
218
|
-
- `join` / `leave` events: delegates to room handlers
|
|
219
|
-
- All other events: delegates to `messageHandler` (if configured)
|
|
317
|
+
Routes an incoming message:
|
|
220
318
|
|
|
221
|
-
|
|
319
|
+
- Parses `raw` as JSON. On failure, sends `{ event: 'error', data: { message: 'Invalid message format' } }` and returns. A message with no `event` field is logged and dropped.
|
|
320
|
+
- `heartbeat`: consumed silently (`lastActivity` is already updated by the `message` callback before this runs).
|
|
321
|
+
- `authenticate`: delegates to `handleAuthenticate()`.
|
|
322
|
+
- Any other event, when the client is not `AUTHENTICATED`: sends `{ event: 'error', data: { message: 'Not authenticated' } }`.
|
|
323
|
+
- `join` / `leave`: delegate to the internal room handlers.
|
|
324
|
+
- Everything else: delegates to `messageHandler` via `invokeHook()`, if configured; otherwise logged at `debug` and dropped.
|
|
222
325
|
|
|
223
326
|
### `onClientDisconnect()`
|
|
224
327
|
|
|
@@ -226,13 +329,7 @@ Sends an `error` event (`'Invalid message format'`) if JSON parsing fails.
|
|
|
226
329
|
onClientDisconnect(opts: { clientId: string }): void
|
|
227
330
|
```
|
|
228
331
|
|
|
229
|
-
Cleans up a disconnected client:
|
|
230
|
-
|
|
231
|
-
1. Clears auth timeout if pending
|
|
232
|
-
2. Removes from user index
|
|
233
|
-
3. Removes from all rooms
|
|
234
|
-
4. Removes from clients map
|
|
235
|
-
5. Invokes `clientDisconnectedFn` callback
|
|
332
|
+
Cleans up a disconnected client: clears the pending auth timer, removes the client from its user's index, removes it from every joined room, removes it from the clients map, then invokes `clientDisconnectedFn`.
|
|
236
333
|
|
|
237
334
|
### `joinRoom()`
|
|
238
335
|
|
|
@@ -240,7 +337,7 @@ Cleans up a disconnected client:
|
|
|
240
337
|
joinRoom(opts: { clientId: string; room: string }): void
|
|
241
338
|
```
|
|
242
339
|
|
|
243
|
-
|
|
340
|
+
Adds `clientId` to the room index and to `client.rooms`, and subscribes the socket to the room's Bun native pub/sub topic - unless the client is encrypted, in which case the subscribe step is skipped (delivery goes through the transformer instead). No-op if the client does not exist.
|
|
244
341
|
|
|
245
342
|
### `leaveRoom()`
|
|
246
343
|
|
|
@@ -248,7 +345,19 @@ Programmatically joins a client to a room. Adds to the room index, adds to the c
|
|
|
248
345
|
leaveRoom(opts: { clientId: string; room: string }): void
|
|
249
346
|
```
|
|
250
347
|
|
|
251
|
-
Removes
|
|
348
|
+
Removes `clientId` from the room index and from `client.rooms`, and unsubscribes the socket from the room's topic. No-op if the client does not exist.
|
|
349
|
+
|
|
350
|
+
#### Room name validation (client-initiated `join`/`leave` only)
|
|
351
|
+
|
|
352
|
+
`handleJoin()` filters requested room names before calling `validateRoomFn`:
|
|
353
|
+
|
|
354
|
+
- Must be a non-empty string.
|
|
355
|
+
- Maximum 256 characters.
|
|
356
|
+
- Cannot start with `ws:` (reserved for the internal Redis channel prefix).
|
|
357
|
+
|
|
358
|
+
- **Rejection is silent.** If every requested room is filtered out, `validateRoomFn` is not configured, or it resolves to an empty array, the join is rejected and logged - no error is sent back to the client.
|
|
359
|
+
- **`leave` does not re-filter.** `{ event: 'leave' }` only processes rooms the client is actually a member of (`client.rooms.has(room)`).
|
|
360
|
+
- **Programmatic calls bypass validation.** `joinRoom()`/`leaveRoom()` called directly skip all of the above.
|
|
252
361
|
|
|
253
362
|
### `enableClientEncryption()`
|
|
254
363
|
|
|
@@ -256,74 +365,51 @@ Removes a client from a room. Removes from the room index, removes from the clie
|
|
|
256
365
|
enableClientEncryption(opts: { clientId: string }): void
|
|
257
366
|
```
|
|
258
367
|
|
|
259
|
-
|
|
368
|
+
Unsubscribes the client from every Bun native pub/sub topic (the broadcast topic plus all joined rooms) so `server.publish()` no longer reaches it; messages are instead delivered individually through `outboundTransformer`. No-op if the client is already encrypted or does not exist.
|
|
260
369
|
|
|
261
370
|
> [!WARNING]
|
|
262
|
-
>
|
|
371
|
+
> Irreversible for the lifetime of the connection. Once a client is encrypted it cannot be switched back to Bun native pub/sub delivery.
|
|
263
372
|
|
|
264
373
|
### `sendToClient()`
|
|
265
374
|
|
|
266
375
|
```typescript
|
|
267
|
-
sendToClient(opts: {
|
|
268
|
-
clientId: string;
|
|
269
|
-
event: string;
|
|
270
|
-
data: unknown;
|
|
271
|
-
doLog?: boolean;
|
|
272
|
-
}): void
|
|
376
|
+
sendToClient(opts: { clientId: string; event: string; data: unknown; doLog?: boolean }): void
|
|
273
377
|
```
|
|
274
378
|
|
|
275
|
-
|
|
379
|
+
- **Local delivery only.**
|
|
380
|
+
- **Encrypted client, `outboundTransformer` configured.** The transformer runs (async) before `socket.send()`; a transformer error is logged and the message is dropped.
|
|
381
|
+
- **Otherwise.** Sends `JSON.stringify({ event, data })` directly.
|
|
382
|
+
- **`doLog: true`** emits an info log after delivery.
|
|
276
383
|
|
|
277
384
|
### `sendToUser()`
|
|
278
385
|
|
|
279
386
|
```typescript
|
|
280
|
-
sendToUser(opts: {
|
|
281
|
-
userId: string;
|
|
282
|
-
event: string;
|
|
283
|
-
data: unknown;
|
|
284
|
-
}): void
|
|
387
|
+
sendToUser(opts: { userId: string; event: string; data: unknown }): void
|
|
285
388
|
```
|
|
286
389
|
|
|
287
|
-
|
|
390
|
+
Local delivery only. Iterates the user's client set and calls `sendToClient()` for each.
|
|
288
391
|
|
|
289
392
|
### `sendToRoom()`
|
|
290
393
|
|
|
291
394
|
```typescript
|
|
292
|
-
sendToRoom(opts: {
|
|
293
|
-
room: string;
|
|
294
|
-
event: string;
|
|
295
|
-
data: unknown;
|
|
296
|
-
exclude?: string[];
|
|
297
|
-
}): void
|
|
395
|
+
sendToRoom(opts: { room: string; event: string; data: unknown; exclude?: string[] }): void
|
|
298
396
|
```
|
|
299
397
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
#### Delivery Strategy
|
|
398
|
+
Local delivery only.
|
|
303
399
|
|
|
304
400
|
| Condition | Strategy |
|
|
305
401
|
|-----------|----------|
|
|
306
|
-
| No `outboundTransformer`, no `exclude` | Bun native `server.publish()`
|
|
307
|
-
| `outboundTransformer` set, no `exclude` | Iterates
|
|
308
|
-
| `exclude` provided | Always iterates clients individually
|
|
402
|
+
| No `outboundTransformer`, no `exclude` | Bun native `server.publish(room, payload)` - O(1) C++ fan-out |
|
|
403
|
+
| `outboundTransformer` set, no `exclude` | Iterates room clients via `executePromiseWithLimit` (max `encryptedBatchLimit` concurrent) |
|
|
404
|
+
| `exclude` provided | Always iterates clients individually - Bun pub/sub cannot exclude |
|
|
309
405
|
|
|
310
406
|
### `broadcast()`
|
|
311
407
|
|
|
312
408
|
```typescript
|
|
313
|
-
broadcast(opts: {
|
|
314
|
-
event: string;
|
|
315
|
-
data: unknown;
|
|
316
|
-
exclude?: string[];
|
|
317
|
-
}): void
|
|
409
|
+
broadcast(opts: { event: string; data: unknown; exclude?: string[] }): void
|
|
318
410
|
```
|
|
319
411
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
| Condition | Strategy |
|
|
323
|
-
|-----------|----------|
|
|
324
|
-
| No `outboundTransformer`, no `exclude` | Bun native `server.publish()` via broadcast topic |
|
|
325
|
-
| `outboundTransformer` set, no `exclude` | Iterates all authenticated clients with concurrency limit |
|
|
326
|
-
| `exclude` provided | Always iterates clients individually |
|
|
412
|
+
Local delivery only, to `AUTHENTICATED` clients. Same delivery strategy as `sendToRoom()`, publishing to `WebSocketDefaults.BROADCAST_TOPIC` (`'ws:internal:broadcast'`) instead of a room topic when no transformer/exclude applies.
|
|
327
413
|
|
|
328
414
|
### `send()`
|
|
329
415
|
|
|
@@ -332,24 +418,20 @@ send<T = unknown>(opts: {
|
|
|
332
418
|
destination?: string;
|
|
333
419
|
payload: { topic: string; data: T };
|
|
334
420
|
doLog?: boolean;
|
|
335
|
-
|
|
421
|
+
callback?: () => void;
|
|
336
422
|
}): void
|
|
337
423
|
```
|
|
338
424
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
Routing logic:
|
|
425
|
+
Cross-instance messaging: delivers locally **and** publishes to Redis so other server instances receive it.
|
|
342
426
|
|
|
343
427
|
| `destination` | Local delivery | Redis channel |
|
|
344
|
-
|
|
428
|
+
|---------------|----------------|----------------|
|
|
345
429
|
| Omitted | `broadcast()` | `ws:broadcast` |
|
|
346
|
-
| Matches a
|
|
347
|
-
| Matches a
|
|
348
|
-
|
|
|
430
|
+
| Matches a locally-tracked client ID | `sendToClient()` | `ws:client:{clientId}` |
|
|
431
|
+
| Matches a locally-tracked room name | `sendToRoom()` | `ws:room:{room}` |
|
|
432
|
+
| Matches neither (remote target) | None | `ws:room:{destination}` |
|
|
349
433
|
|
|
350
|
-
Silent no-op when `payload` is falsy, `payload.topic` is falsy, or `payload.data` is `undefined`.
|
|
351
|
-
|
|
352
|
-
If `cb` is provided, it is executed asynchronously via `setTimeout(cb, 0)`.
|
|
434
|
+
Silent no-op when `payload` is falsy, `payload.topic` is falsy, or `payload.data` is `undefined`. When `callback` is provided it runs via `setTimeout(callback, 0)` regardless of delivery outcome.
|
|
353
435
|
|
|
354
436
|
### `shutdown()`
|
|
355
437
|
|
|
@@ -357,23 +439,33 @@ If `cb` is provided, it is executed asynchronously via `setTimeout(cb, 0)`.
|
|
|
357
439
|
shutdown(): Promise<void>
|
|
358
440
|
```
|
|
359
441
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
4. Clear `clients`, `users`, and `rooms` maps
|
|
366
|
-
5. `await Promise.all([redisPub.quit(), redisSub.quit()])`
|
|
442
|
+
1. Clears the heartbeat timer.
|
|
443
|
+
2. Closes every client socket with code `1001` (`'Server shutting down'`).
|
|
444
|
+
3. Calls `onClientDisconnect()` for every tracked client (clears auth timers, removes from indexes, invokes `clientDisconnectedFn`).
|
|
445
|
+
4. Clears the `clients`, `users`, and `rooms` maps.
|
|
446
|
+
5. `await Promise.all([redisPub.quit(), redisSub.quit()])`.
|
|
367
447
|
|
|
368
448
|
## Emitter API
|
|
369
449
|
|
|
370
|
-
|
|
450
|
+
`Source ->` [`emitter/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/emitter/helper.ts)
|
|
451
|
+
|
|
452
|
+
- **Role.** A Redis-only publisher for processes with no WebSocket server of their own - background workers, other microservices, cron jobs.
|
|
453
|
+
- **No self-skip.** It always publishes with `serverId: 'emitter'`, so every server instance processes the message; there is no dedup skip on the sending side.
|
|
454
|
+
|
|
455
|
+
### Constructor
|
|
371
456
|
|
|
372
457
|
```typescript
|
|
373
458
|
constructor(opts: IWebSocketEmitterOptions)
|
|
374
459
|
```
|
|
375
460
|
|
|
376
|
-
|
|
461
|
+
Duplicates one Redis client from `redisConnection`. Throws `getError({ statusCode: 500, message: '[WebSocketEmitter] Invalid redis connection!' })` if `redisConnection` is falsy.
|
|
462
|
+
|
|
463
|
+
### `IWebSocketEmitterOptions`
|
|
464
|
+
|
|
465
|
+
| Field | Type | Required | Default | Description |
|
|
466
|
+
|-------|------|----------|---------|-------------|
|
|
467
|
+
| `identifier` | `string` | No | `'WebSocketEmitter'` | Instance name; also the `BaseHelper` logging scope |
|
|
468
|
+
| `redisConnection` | `IRedisHelper` | Yes | - | Redis helper; duplicated once internally |
|
|
377
469
|
|
|
378
470
|
### `configure()`
|
|
379
471
|
|
|
@@ -381,55 +473,39 @@ Creates the emitter, duplicates one Redis client from `redisConnection`. Throws
|
|
|
381
473
|
configure(): Promise<void>
|
|
382
474
|
```
|
|
383
475
|
|
|
384
|
-
Connects the Redis client (if
|
|
476
|
+
Connects the Redis client (if status is `'wait'`) and `await`s it reaching `'ready'`. Must be called before emitting.
|
|
385
477
|
|
|
386
478
|
### `toClient()`
|
|
387
479
|
|
|
388
480
|
```typescript
|
|
389
|
-
toClient(opts: {
|
|
390
|
-
clientId: string;
|
|
391
|
-
event: string;
|
|
392
|
-
data: unknown;
|
|
393
|
-
}): Promise<void>
|
|
481
|
+
toClient(opts: { clientId: string; event: string; data: unknown }): Promise<void>
|
|
394
482
|
```
|
|
395
483
|
|
|
396
|
-
Publishes
|
|
484
|
+
Publishes to `ws:client:{clientId}`. Every server instance subscribed via `psubscribe('ws:client:*')` delivers it to that client if connected locally.
|
|
397
485
|
|
|
398
486
|
### `toUser()`
|
|
399
487
|
|
|
400
488
|
```typescript
|
|
401
|
-
toUser(opts: {
|
|
402
|
-
userId: string;
|
|
403
|
-
event: string;
|
|
404
|
-
data: unknown;
|
|
405
|
-
}): Promise<void>
|
|
489
|
+
toUser(opts: { userId: string; event: string; data: unknown }): Promise<void>
|
|
406
490
|
```
|
|
407
491
|
|
|
408
|
-
Publishes
|
|
492
|
+
Publishes to `ws:user:{userId}`. Every server instance delivers it to every session belonging to that user.
|
|
409
493
|
|
|
410
494
|
### `toRoom()`
|
|
411
495
|
|
|
412
496
|
```typescript
|
|
413
|
-
toRoom(opts: {
|
|
414
|
-
room: string;
|
|
415
|
-
event: string;
|
|
416
|
-
data: unknown;
|
|
417
|
-
exclude?: string[];
|
|
418
|
-
}): Promise<void>
|
|
497
|
+
toRoom(opts: { room: string; event: string; data: unknown; exclude?: string[] }): Promise<void>
|
|
419
498
|
```
|
|
420
499
|
|
|
421
|
-
Publishes
|
|
500
|
+
Publishes to `ws:room:{room}`. Every server instance delivers it to every client in that room.
|
|
422
501
|
|
|
423
502
|
### `broadcast()`
|
|
424
503
|
|
|
425
504
|
```typescript
|
|
426
|
-
broadcast(opts: {
|
|
427
|
-
event: string;
|
|
428
|
-
data: unknown;
|
|
429
|
-
}): Promise<void>
|
|
505
|
+
broadcast(opts: { event: string; data: unknown }): Promise<void>
|
|
430
506
|
```
|
|
431
507
|
|
|
432
|
-
Publishes
|
|
508
|
+
Publishes to `ws:broadcast`. Every server instance delivers it to every authenticated client.
|
|
433
509
|
|
|
434
510
|
### `shutdown()`
|
|
435
511
|
|
|
@@ -437,11 +513,13 @@ Publishes a message to the `ws:broadcast` Redis channel. All server instances de
|
|
|
437
513
|
shutdown(): Promise<void>
|
|
438
514
|
```
|
|
439
515
|
|
|
440
|
-
Quits the Redis connection.
|
|
516
|
+
Quits the duplicated Redis connection.
|
|
441
517
|
|
|
442
518
|
## Types Reference
|
|
443
519
|
|
|
444
|
-
|
|
520
|
+
`Source ->` [`common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/common/types.ts)
|
|
521
|
+
|
|
522
|
+
### Wire protocol
|
|
445
523
|
|
|
446
524
|
```typescript
|
|
447
525
|
/** Client <-> Server message envelope */
|
|
@@ -454,7 +532,7 @@ interface IWebSocketMessage<DataType = unknown> {
|
|
|
454
532
|
/** Internal Redis Pub/Sub message envelope */
|
|
455
533
|
interface IRedisSocketMessage<DataType = unknown> {
|
|
456
534
|
serverId: string;
|
|
457
|
-
type: TWebSocketMessageType;
|
|
535
|
+
type: TWebSocketMessageType; // 'client' | 'user' | 'room' | 'broadcast'
|
|
458
536
|
target?: string;
|
|
459
537
|
event: string;
|
|
460
538
|
data: DataType;
|
|
@@ -462,7 +540,7 @@ interface IRedisSocketMessage<DataType = unknown> {
|
|
|
462
540
|
}
|
|
463
541
|
```
|
|
464
542
|
|
|
465
|
-
### Client
|
|
543
|
+
### Client tracking
|
|
466
544
|
|
|
467
545
|
```typescript
|
|
468
546
|
interface IWebSocketClient<
|
|
@@ -493,10 +571,10 @@ interface IWebSocketData<
|
|
|
493
571
|
}
|
|
494
572
|
```
|
|
495
573
|
|
|
496
|
-
### Bun
|
|
574
|
+
### Bun interfaces
|
|
497
575
|
|
|
498
576
|
```typescript
|
|
499
|
-
/** Bun WebSocket handle
|
|
577
|
+
/** Bun WebSocket handle - defined locally to avoid an @types/bun dependency */
|
|
500
578
|
interface IWebSocket<T = unknown> {
|
|
501
579
|
readonly data: T;
|
|
502
580
|
readonly remoteAddress: string;
|
|
@@ -507,7 +585,7 @@ interface IWebSocket<T = unknown> {
|
|
|
507
585
|
unsubscribe(topic: string): void;
|
|
508
586
|
isSubscribed(topic: string): boolean;
|
|
509
587
|
close(code?: number, reason?: string): void;
|
|
510
|
-
cork(
|
|
588
|
+
cork(callback: (ws: IWebSocket<T>) => void): void;
|
|
511
589
|
}
|
|
512
590
|
|
|
513
591
|
/** Bun server interface for native pub/sub */
|
|
@@ -540,7 +618,7 @@ interface IBunWebSocketHandler extends IBunWebSocketConfig {
|
|
|
540
618
|
}
|
|
541
619
|
```
|
|
542
620
|
|
|
543
|
-
### Server
|
|
621
|
+
### Server and emitter options
|
|
544
622
|
|
|
545
623
|
```typescript
|
|
546
624
|
interface IWebSocketServerOptions<
|
|
@@ -548,16 +626,16 @@ interface IWebSocketServerOptions<
|
|
|
548
626
|
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
549
627
|
> {
|
|
550
628
|
identifier: string;
|
|
551
|
-
path?: string;
|
|
629
|
+
path?: string; // Default: '/ws'
|
|
552
630
|
redisConnection: IRedisHelper;
|
|
553
631
|
server: IBunServer;
|
|
554
|
-
defaultRooms?: string[];
|
|
632
|
+
defaultRooms?: string[]; // Default: ['ws-default', 'ws-notification']
|
|
555
633
|
serverOptions?: IBunWebSocketConfig;
|
|
556
|
-
authTimeout?: number;
|
|
557
|
-
heartbeatInterval?: number;
|
|
558
|
-
heartbeatTimeout?: number;
|
|
559
|
-
encryptedBatchLimit?: number;
|
|
560
|
-
requireEncryption?: boolean;
|
|
634
|
+
authTimeout?: number; // Default: 5_000
|
|
635
|
+
heartbeatInterval?: number; // Default: 30_000
|
|
636
|
+
heartbeatTimeout?: number; // Default: 90_000
|
|
637
|
+
encryptedBatchLimit?: number; // Default: 10
|
|
638
|
+
requireEncryption?: boolean; // Default: false
|
|
561
639
|
|
|
562
640
|
authenticateFn: TWebSocketAuthenticateFn<AuthDataType, MetadataType>;
|
|
563
641
|
validateRoomFn?: TWebSocketValidateRoomFn;
|
|
@@ -565,19 +643,19 @@ interface IWebSocketServerOptions<
|
|
|
565
643
|
clientDisconnectedFn?: TWebSocketClientDisconnectedFn;
|
|
566
644
|
messageHandler?: TWebSocketMessageHandler;
|
|
567
645
|
outboundTransformer?: TWebSocketOutboundTransformer<unknown, MetadataType>;
|
|
568
|
-
handshakeFn?: TWebSocketHandshakeFn<AuthDataType>;
|
|
646
|
+
handshakeFn?: TWebSocketHandshakeFn<AuthDataType>; // Required when requireEncryption is true
|
|
569
647
|
}
|
|
570
648
|
|
|
571
649
|
interface IWebSocketEmitterOptions {
|
|
572
|
-
identifier?: string;
|
|
650
|
+
identifier?: string; // Default: 'WebSocketEmitter'
|
|
573
651
|
redisConnection: IRedisHelper;
|
|
574
652
|
}
|
|
575
653
|
```
|
|
576
654
|
|
|
577
|
-
### Callback
|
|
655
|
+
### Callback types
|
|
578
656
|
|
|
579
657
|
```typescript
|
|
580
|
-
/** Authentication
|
|
658
|
+
/** Authentication - return { userId, metadata } to accept, null/false to reject */
|
|
581
659
|
type TWebSocketAuthenticateFn<
|
|
582
660
|
AuthDataType extends Record<string, unknown> = Record<string, unknown>,
|
|
583
661
|
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
@@ -585,7 +663,7 @@ type TWebSocketAuthenticateFn<
|
|
|
585
663
|
opts: AuthDataType,
|
|
586
664
|
) => ValueOrPromise<{ userId?: string; metadata?: MetadataType } | null | false>;
|
|
587
665
|
|
|
588
|
-
/**
|
|
666
|
+
/** Handshake during auth - return { serverPublicKey, salt } to accept, null/false to reject */
|
|
589
667
|
type TWebSocketHandshakeFn<
|
|
590
668
|
AuthDataType extends Record<string, unknown> = Record<string, unknown>,
|
|
591
669
|
> = (opts: {
|
|
@@ -594,7 +672,7 @@ type TWebSocketHandshakeFn<
|
|
|
594
672
|
data: AuthDataType;
|
|
595
673
|
}) => ValueOrPromise<{ serverPublicKey: string; salt: string } | null | false>;
|
|
596
674
|
|
|
597
|
-
/** Room validation
|
|
675
|
+
/** Room validation - return the allowed subset of requested rooms */
|
|
598
676
|
type TWebSocketValidateRoomFn = (opts: {
|
|
599
677
|
clientId: string;
|
|
600
678
|
userId?: string;
|
|
@@ -604,11 +682,7 @@ type TWebSocketValidateRoomFn = (opts: {
|
|
|
604
682
|
/** Post-authentication callback */
|
|
605
683
|
type TWebSocketClientConnectedFn<
|
|
606
684
|
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
607
|
-
> = (opts: {
|
|
608
|
-
clientId: string;
|
|
609
|
-
userId?: string;
|
|
610
|
-
metadata?: MetadataType;
|
|
611
|
-
}) => ValueOrPromise<void>;
|
|
685
|
+
> = (opts: { clientId: string; userId?: string; metadata?: MetadataType }) => ValueOrPromise<void>;
|
|
612
686
|
|
|
613
687
|
/** Disconnect callback */
|
|
614
688
|
type TWebSocketClientDisconnectedFn = (opts: {
|
|
@@ -616,14 +690,14 @@ type TWebSocketClientDisconnectedFn = (opts: {
|
|
|
616
690
|
userId?: string;
|
|
617
691
|
}) => ValueOrPromise<void>;
|
|
618
692
|
|
|
619
|
-
/** Custom event handler for
|
|
693
|
+
/** Custom event handler for events other than authenticate/heartbeat/join/leave */
|
|
620
694
|
type TWebSocketMessageHandler = (opts: {
|
|
621
695
|
clientId: string;
|
|
622
696
|
userId?: string;
|
|
623
697
|
message: IWebSocketMessage;
|
|
624
698
|
}) => ValueOrPromise<void>;
|
|
625
699
|
|
|
626
|
-
/** Outbound transformer
|
|
700
|
+
/** Outbound transformer - intercepts messages before socket.send() */
|
|
627
701
|
type TWebSocketOutboundTransformer<
|
|
628
702
|
DataType = unknown,
|
|
629
703
|
MetadataType extends Record<string, unknown> = Record<string, unknown>,
|
|
@@ -634,7 +708,7 @@ type TWebSocketOutboundTransformer<
|
|
|
634
708
|
}) => ValueOrPromise<TNullable<{ event: string; data: DataType }>>;
|
|
635
709
|
```
|
|
636
710
|
|
|
637
|
-
### State
|
|
711
|
+
### State types
|
|
638
712
|
|
|
639
713
|
```typescript
|
|
640
714
|
type TWebSocketClientState = 'unauthorized' | 'authenticating' | 'authenticated' | 'disconnected';
|
|
@@ -644,6 +718,8 @@ type TWebSocketMessageType = 'client' | 'user' | 'room' | 'broadcast';
|
|
|
644
718
|
|
|
645
719
|
## Constants
|
|
646
720
|
|
|
721
|
+
`Source ->` [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/common/constants.ts)
|
|
722
|
+
|
|
647
723
|
### `WebSocketEvents`
|
|
648
724
|
|
|
649
725
|
| Constant | Value | Description |
|
|
@@ -657,12 +733,10 @@ type TWebSocketMessageType = 'client' | 'user' | 'room' | 'broadcast';
|
|
|
657
733
|
| `HEARTBEAT` | `'heartbeat'` | Client -> Server keep-alive |
|
|
658
734
|
| `ENCRYPTED` | `'encrypted'` | Encrypted message wrapper |
|
|
659
735
|
|
|
660
|
-
Utility methods:
|
|
661
|
-
|
|
662
736
|
```typescript
|
|
663
737
|
WebSocketEvents.isValid('authenticate'); // true
|
|
664
738
|
WebSocketEvents.isValid('invalid'); // false
|
|
665
|
-
WebSocketEvents.SCHEME_SET;
|
|
739
|
+
WebSocketEvents.SCHEME_SET; // Set of all valid event strings
|
|
666
740
|
```
|
|
667
741
|
|
|
668
742
|
### `WebSocketChannels`
|
|
@@ -673,9 +747,9 @@ WebSocketEvents.SCHEME_SET; // Set of all valid event strings
|
|
|
673
747
|
| `ROOM_PREFIX` | `'ws:room:'` | Room channel prefix |
|
|
674
748
|
| `CLIENT_PREFIX` | `'ws:client:'` | Client channel prefix |
|
|
675
749
|
| `USER_PREFIX` | `'ws:user:'` | User channel prefix |
|
|
676
|
-
| `forRoom({ room })` | `'ws:room:{room}'` | Build room channel |
|
|
677
|
-
| `forClient({ clientId })` | `'ws:client:{clientId}'` | Build client channel |
|
|
678
|
-
| `forUser({ userId })` | `'ws:user:{userId}'` | Build user channel |
|
|
750
|
+
| `forRoom({ room })` | `'ws:room:{room}'` | Build a room channel name |
|
|
751
|
+
| `forClient({ clientId })` | `'ws:client:{clientId}'` | Build a client channel name |
|
|
752
|
+
| `forUser({ userId })` | `'ws:user:{userId}'` | Build a user channel name |
|
|
679
753
|
| `forRoomPattern()` | `'ws:room:*'` | Room pattern for `psubscribe` |
|
|
680
754
|
| `forClientPattern()` | `'ws:client:*'` | Client pattern for `psubscribe` |
|
|
681
755
|
| `forUserPattern()` | `'ws:user:*'` | User pattern for `psubscribe` |
|
|
@@ -690,13 +764,13 @@ WebSocketEvents.SCHEME_SET; // Set of all valid event strings
|
|
|
690
764
|
| `BROADCAST_TOPIC` | `'ws:internal:broadcast'` | Bun pub/sub broadcast topic |
|
|
691
765
|
| `MAX_PAYLOAD_LENGTH` | `131072` (128KB) | Maximum incoming payload size |
|
|
692
766
|
| `IDLE_TIMEOUT` | `60` (seconds) | Bun transport idle timeout |
|
|
693
|
-
| `BACKPRESSURE_LIMIT` | `1048576` (1MB) | Bun backpressure threshold |
|
|
767
|
+
| `BACKPRESSURE_LIMIT` | `1048576` (1MB) | Bun backpressure threshold (Bun's own default, not applied by the helper) |
|
|
694
768
|
| `SEND_PINGS` | `true` | Bun transport pings enabled |
|
|
695
|
-
| `PUBLISH_TO_SELF` | `false` | Bun pub/sub self-delivery disabled |
|
|
769
|
+
| `PUBLISH_TO_SELF` | `false` | Bun pub/sub self-delivery disabled (Bun's own default, not applied by the helper) |
|
|
696
770
|
| `AUTH_TIMEOUT` | `5000` (5s) | Authentication timeout |
|
|
697
771
|
| `HEARTBEAT_INTERVAL` | `30000` (30s) | Heartbeat sweep interval |
|
|
698
|
-
| `HEARTBEAT_TIMEOUT` | `90000` (90s) | Heartbeat inactivity threshold |
|
|
699
|
-
| `ENCRYPTED_BATCH_LIMIT` | `10` | Max concurrent
|
|
772
|
+
| `HEARTBEAT_TIMEOUT` | `90000` (90s, 3x interval) | Heartbeat inactivity threshold |
|
|
773
|
+
| `ENCRYPTED_BATCH_LIMIT` | `10` | Max concurrent `outboundTransformer` invocations |
|
|
700
774
|
|
|
701
775
|
### `WebSocketMessageTypes`
|
|
702
776
|
|
|
@@ -707,11 +781,9 @@ WebSocketEvents.SCHEME_SET; // Set of all valid event strings
|
|
|
707
781
|
| `ROOM` | `'room'` | Message targeted at a room |
|
|
708
782
|
| `BROADCAST` | `'broadcast'` | Message targeted at all clients |
|
|
709
783
|
|
|
710
|
-
Utility methods:
|
|
711
|
-
|
|
712
784
|
```typescript
|
|
713
785
|
WebSocketMessageTypes.isValid('room'); // true
|
|
714
|
-
WebSocketMessageTypes.SCHEME_SET;
|
|
786
|
+
WebSocketMessageTypes.SCHEME_SET; // Set { 'client', 'user', 'room', 'broadcast' }
|
|
715
787
|
```
|
|
716
788
|
|
|
717
789
|
### `WebSocketClientStates`
|
|
@@ -719,18 +791,59 @@ WebSocketMessageTypes.SCHEME_SET; // Set { 'client', 'user', 'room', 'broadc
|
|
|
719
791
|
| Constant | Value | Description |
|
|
720
792
|
|----------|-------|-------------|
|
|
721
793
|
| `UNAUTHORIZED` | `'unauthorized'` | Initial state after connection |
|
|
722
|
-
| `AUTHENTICATING` | `'authenticating'` |
|
|
723
|
-
| `AUTHENTICATED` | `'authenticated'` | Successfully authenticated |
|
|
794
|
+
| `AUTHENTICATING` | `'authenticating'` | `authenticate` event received, awaiting `authenticateFn` |
|
|
795
|
+
| `AUTHENTICATED` | `'authenticated'` | Successfully authenticated, fully operational |
|
|
724
796
|
| `DISCONNECTED` | `'disconnected'` | Client has disconnected |
|
|
725
797
|
|
|
726
|
-
Utility methods:
|
|
727
|
-
|
|
728
798
|
```typescript
|
|
729
799
|
WebSocketClientStates.isValid('authenticated'); // true
|
|
730
800
|
WebSocketClientStates.SCHEME_SET; // Set of all valid state strings
|
|
731
801
|
```
|
|
732
802
|
|
|
733
|
-
|
|
803
|
+
### Close codes
|
|
804
|
+
|
|
805
|
+
| Code | Meaning | Trigger |
|
|
806
|
+
|------|---------|---------|
|
|
807
|
+
| `4001` | Authentication timeout | Client did not authenticate within `authTimeout` (or `authTimeout * 3` once `authenticateFn` started) |
|
|
808
|
+
| `4002` | Heartbeat timeout | No activity for `heartbeatTimeout` |
|
|
809
|
+
| `4003` | Authentication failed | `authenticateFn` returned `null`/`false` or threw |
|
|
810
|
+
| `4004` | Encryption required | `requireEncryption` is `true` and `handshakeFn` is missing or rejected |
|
|
811
|
+
| `1001` | Going away | Server shutting down gracefully (`shutdown()`) |
|
|
812
|
+
|
|
813
|
+
## Troubleshooting
|
|
814
|
+
|
|
815
|
+
### Client disconnects immediately with close code 4001
|
|
816
|
+
|
|
817
|
+
- **Symptom.** The client connects but is closed before it can interact - it did not send `{ event: 'authenticate', data: { ... } }` within `authTimeout` (default 5s).
|
|
818
|
+
- **Wrong message shape.** e.g. sending `{ type: 'auth' }` instead of `{ event: 'authenticate' }`.
|
|
819
|
+
- **Client waits for the server first.** The server sends nothing after upgrade - the client must initiate.
|
|
820
|
+
- **Slow token retrieval.** Pushes the auth message past the timeout window.
|
|
821
|
+
|
|
822
|
+
> [!TIP]
|
|
823
|
+
> Send `{ event: 'authenticate', data: { token: '...' } }` immediately in the client's `onopen` handler. If token retrieval is slow, raise `authTimeout`.
|
|
824
|
+
|
|
825
|
+
### `helper.send()` delivers locally but other instances never receive it
|
|
826
|
+
|
|
827
|
+
- **Topology mismatch.** A single-instance Redis client against a Redis Cluster deployment will not route correctly.
|
|
828
|
+
- **Configure ordering.** `await helper.configure()` must run to completion before you start accepting connections - subscriptions are set up asynchronously.
|
|
829
|
+
- **Network/ACL.** No firewall or ACL should block `SUBSCRIBE`/`PSUBSCRIBE` on the duplicated clients.
|
|
830
|
+
|
|
831
|
+
### `requireEncryption` is true but clients get disconnected with code 4004
|
|
832
|
+
|
|
833
|
+
- **`handshakeFn` missing.** The server logs `"requireEncryption is true but no handshakeFn configured"` and closes the client.
|
|
834
|
+
- **`handshakeFn` rejected.** It returned `null`/`false` - typically because required key-exchange data (e.g. `publicKey`) was missing from the authenticate payload.
|
|
835
|
+
|
|
836
|
+
### `[WebSocketServerHelper] Invalid redis connection!` / `[WebSocketEmitter] Invalid redis connection!`
|
|
837
|
+
|
|
838
|
+
Thrown synchronously during construction when `redisConnection` is `null`/`undefined`. Pass a valid `IRedisHelper` instance (e.g. `RedisSingleHelper`).
|
|
839
|
+
|
|
840
|
+
### `Redis client did not become ready within 30000ms`
|
|
841
|
+
|
|
842
|
+
Thrown during `configure()` when a duplicated Redis client fails to reach `'ready'` status. Check that the Redis server is reachable and the underlying `IRedisHelper` instance is configured correctly.
|
|
843
|
+
|
|
844
|
+
## See also
|
|
734
845
|
|
|
735
|
-
- [
|
|
736
|
-
- [Socket.IO Helper](
|
|
846
|
+
- [WebSocket overview](/extensions/helpers/websocket/) - getting started and the most common tasks
|
|
847
|
+
- [Socket.IO Helper](/extensions/helpers/socket-io/) - Socket.IO-based alternative with Node.js support
|
|
848
|
+
- [Redis Helper](/extensions/helpers/redis/) - `RedisSingleHelper` / `RedisClusterHelper` used for cross-instance messaging
|
|
849
|
+
- [WebSocket Component](/extensions/components/websocket/) - component-level lifecycle integration
|