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