@venizia/ignis-docs 0.0.5 → 0.0.6-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/package.json +1 -1
- package/wiki/best-practices/architectural-patterns.md +0 -2
- package/wiki/best-practices/architecture-decisions.md +0 -8
- package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
- package/wiki/best-practices/code-style-standards/index.md +0 -1
- package/wiki/best-practices/code-style-standards/tooling.md +0 -3
- package/wiki/best-practices/contribution-workflow.md +12 -12
- package/wiki/best-practices/index.md +4 -14
- package/wiki/best-practices/performance-optimization.md +3 -3
- package/wiki/best-practices/security-guidelines.md +2 -2
- package/wiki/best-practices/troubleshooting-tips.md +1 -1
- package/wiki/guides/core-concepts/application/bootstrapping.md +6 -7
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/components.md +2 -2
- package/wiki/guides/core-concepts/dependency-injection.md +4 -5
- package/wiki/guides/core-concepts/persistent/datasources.md +4 -5
- package/wiki/guides/core-concepts/services.md +1 -1
- package/wiki/guides/get-started/5-minute-quickstart.md +4 -5
- package/wiki/guides/get-started/philosophy.md +12 -24
- package/wiki/guides/index.md +2 -9
- package/wiki/guides/reference/mcp-docs-server.md +13 -13
- package/wiki/guides/tutorials/building-a-crud-api.md +10 -10
- package/wiki/guides/tutorials/complete-installation.md +11 -12
- package/wiki/guides/tutorials/ecommerce-api.md +3 -3
- package/wiki/guides/tutorials/realtime-chat.md +6 -6
- package/wiki/guides/tutorials/testing.md +4 -5
- package/wiki/index.md +8 -14
- package/wiki/references/base/bootstrapping.md +0 -3
- package/wiki/references/base/components.md +2 -2
- package/wiki/references/base/controllers.md +0 -1
- package/wiki/references/base/datasources.md +1 -1
- package/wiki/references/base/dependency-injection.md +2 -2
- package/wiki/references/base/filter-system/default-filter.md +2 -3
- package/wiki/references/base/filter-system/index.md +1 -1
- package/wiki/references/base/filter-system/quick-reference.md +0 -14
- package/wiki/references/base/middlewares.md +0 -8
- package/wiki/references/base/providers.md +0 -9
- package/wiki/references/base/repositories/advanced.md +1 -1
- package/wiki/references/base/repositories/mixins.md +2 -3
- package/wiki/references/base/services.md +0 -1
- package/wiki/references/components/authentication/api.md +444 -0
- package/wiki/references/components/authentication/errors.md +177 -0
- package/wiki/references/components/authentication/index.md +571 -0
- package/wiki/references/components/authentication/usage.md +781 -0
- package/wiki/references/components/health-check.md +292 -103
- package/wiki/references/components/index.md +14 -12
- package/wiki/references/components/mail/api.md +505 -0
- package/wiki/references/components/mail/errors.md +176 -0
- package/wiki/references/components/mail/index.md +535 -0
- package/wiki/references/components/mail/usage.md +404 -0
- package/wiki/references/components/request-tracker.md +229 -25
- package/wiki/references/components/socket-io/api.md +1051 -0
- package/wiki/references/components/socket-io/errors.md +119 -0
- package/wiki/references/components/socket-io/index.md +410 -0
- package/wiki/references/components/socket-io/usage.md +322 -0
- package/wiki/references/components/static-asset/api.md +261 -0
- package/wiki/references/components/static-asset/errors.md +89 -0
- package/wiki/references/components/static-asset/index.md +617 -0
- package/wiki/references/components/static-asset/usage.md +364 -0
- package/wiki/references/components/swagger.md +390 -110
- package/wiki/references/components/template/api-page.md +125 -0
- package/wiki/references/components/template/errors-page.md +100 -0
- package/wiki/references/components/template/index.md +104 -0
- package/wiki/references/components/template/setup-page.md +134 -0
- package/wiki/references/components/template/single-page.md +132 -0
- package/wiki/references/components/template/usage-page.md +127 -0
- package/wiki/references/components/websocket/api.md +508 -0
- package/wiki/references/components/websocket/errors.md +123 -0
- package/wiki/references/components/websocket/index.md +453 -0
- package/wiki/references/components/websocket/usage.md +475 -0
- package/wiki/references/helpers/cron/index.md +224 -0
- package/wiki/references/helpers/crypto/index.md +537 -0
- package/wiki/references/helpers/env/index.md +214 -0
- package/wiki/references/helpers/error/index.md +232 -0
- package/wiki/references/helpers/index.md +16 -15
- package/wiki/references/helpers/inversion/index.md +608 -0
- package/wiki/references/helpers/logger/index.md +600 -0
- package/wiki/references/helpers/network/api.md +986 -0
- package/wiki/references/helpers/network/index.md +620 -0
- package/wiki/references/helpers/queue/index.md +589 -0
- package/wiki/references/helpers/redis/index.md +495 -0
- package/wiki/references/helpers/socket-io/api.md +497 -0
- package/wiki/references/helpers/socket-io/index.md +513 -0
- package/wiki/references/helpers/storage/api.md +705 -0
- package/wiki/references/helpers/storage/index.md +583 -0
- package/wiki/references/helpers/template/index.md +66 -0
- package/wiki/references/helpers/template/single-page.md +126 -0
- package/wiki/references/helpers/testing/index.md +510 -0
- package/wiki/references/helpers/types/index.md +512 -0
- package/wiki/references/helpers/uid/index.md +272 -0
- package/wiki/references/helpers/websocket/api.md +736 -0
- package/wiki/references/helpers/websocket/index.md +574 -0
- package/wiki/references/helpers/worker-thread/index.md +470 -0
- package/wiki/references/index.md +2 -9
- package/wiki/references/quick-reference.md +3 -18
- package/wiki/references/utilities/jsx.md +1 -8
- package/wiki/references/utilities/statuses.md +0 -7
- package/wiki/references/components/authentication.md +0 -476
- package/wiki/references/components/mail.md +0 -687
- package/wiki/references/components/socket-io.md +0 -562
- package/wiki/references/components/static-asset.md +0 -1277
- package/wiki/references/helpers/cron.md +0 -108
- package/wiki/references/helpers/crypto.md +0 -132
- package/wiki/references/helpers/env.md +0 -83
- package/wiki/references/helpers/error.md +0 -97
- package/wiki/references/helpers/inversion.md +0 -176
- package/wiki/references/helpers/logger.md +0 -296
- package/wiki/references/helpers/network.md +0 -396
- package/wiki/references/helpers/queue.md +0 -150
- package/wiki/references/helpers/redis.md +0 -142
- package/wiki/references/helpers/socket-io.md +0 -932
- package/wiki/references/helpers/storage.md +0 -665
- package/wiki/references/helpers/testing.md +0 -133
- package/wiki/references/helpers/types.md +0 -167
- package/wiki/references/helpers/uid.md +0 -167
- package/wiki/references/helpers/worker-thread.md +0 -178
- package/wiki/references/src-details/boot.md +0 -379
- package/wiki/references/src-details/core.md +0 -263
- package/wiki/references/src-details/dev-configs.md +0 -298
- package/wiki/references/src-details/docs.md +0 -71
- package/wiki/references/src-details/helpers.md +0 -211
- package/wiki/references/src-details/index.md +0 -86
- package/wiki/references/src-details/inversion.md +0 -340
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
# WebSocket -- API Reference
|
|
2
|
+
|
|
3
|
+
> Architecture deep dive, WebSocketEmitter API, and component internals.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
#### Component Lifecycle Diagram
|
|
8
|
+
```
|
|
9
|
+
WebSocketComponent
|
|
10
|
+
+----------------------------------------------+
|
|
11
|
+
| |
|
|
12
|
+
| binding() |
|
|
13
|
+
| |-- RuntimeModules.detect() |
|
|
14
|
+
| | +-- NODE -> throw error |
|
|
15
|
+
| | +-- BUN -> continue |
|
|
16
|
+
| | |
|
|
17
|
+
| |-- resolveBindings() |
|
|
18
|
+
| | |-- SERVER_OPTIONS |
|
|
19
|
+
| | |-- REDIS_CONNECTION |
|
|
20
|
+
| | |-- AUTHENTICATE_HANDLER |
|
|
21
|
+
| | |-- VALIDATE_ROOM_HANDLER |
|
|
22
|
+
| | |-- CLIENT_CONNECTED_HANDLER |
|
|
23
|
+
| | |-- CLIENT_DISCONNECTED_HANDLER |
|
|
24
|
+
| | |-- MESSAGE_HANDLER |
|
|
25
|
+
| | |-- OUTBOUND_TRANSFORMER |
|
|
26
|
+
| | +-- HANDSHAKE_HANDLER |
|
|
27
|
+
| | |
|
|
28
|
+
| +-- registerBunHook(resolved) |
|
|
29
|
+
| |
|
|
30
|
+
| (Post-start hook executes after server) |
|
|
31
|
+
| |-- Creates WebSocketServerHelper |
|
|
32
|
+
| |-- await wsHelper.configure() |
|
|
33
|
+
| |-- Binds to WEBSOCKET_INSTANCE |
|
|
34
|
+
| |-- Creates fetch handler (WS + Hono) |
|
|
35
|
+
| +-- server.reload({ fetch, websocket }) |
|
|
36
|
+
+----------------------------------------------+
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Lifecycle Integration
|
|
40
|
+
|
|
41
|
+
The component uses the **post-start hook** system to solve a fundamental timing problem: WebSocket needs a running Bun server instance, but components are initialized *before* the server starts.
|
|
42
|
+
|
|
43
|
+
#### Application Lifecycle Flow
|
|
44
|
+
```
|
|
45
|
+
Application Lifecycle
|
|
46
|
+
=====================
|
|
47
|
+
|
|
48
|
+
+------------------+
|
|
49
|
+
| preConfigure() | <-- Register WebSocketComponent here
|
|
50
|
+
+--------+---------+
|
|
51
|
+
|
|
|
52
|
+
+--------v---------+
|
|
53
|
+
| initialize() | <-- Component.binding() runs here
|
|
54
|
+
| | Runtime check, resolve bindings, register post-start hook
|
|
55
|
+
+--------+---------+
|
|
56
|
+
|
|
|
57
|
+
+--------v---------+
|
|
58
|
+
| setupMiddlewares |
|
|
59
|
+
+--------+---------+
|
|
60
|
+
|
|
|
61
|
+
+--------v-----------------------+
|
|
62
|
+
| startBunModule() | <-- Bun server starts, instance created
|
|
63
|
+
+--------+-----------------------+
|
|
64
|
+
|
|
|
65
|
+
+--------v--------------------------+
|
|
66
|
+
| executePostStartHooks() | <-- WebSocketServerHelper created HERE
|
|
67
|
+
| +-- websocket-initialize | Server instance is now available
|
|
68
|
+
| |-- new WebSocketServerHelper
|
|
69
|
+
| |-- wsHelper.configure()
|
|
70
|
+
| |-- bind WEBSOCKET_INSTANCE
|
|
71
|
+
| +-- server.reload({ fetch, websocket })
|
|
72
|
+
+-----------------------------------+
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Fetch Handler
|
|
76
|
+
|
|
77
|
+
The component creates a custom `fetch` handler via `createBunFetchHandler()` that routes requests:
|
|
78
|
+
|
|
79
|
+
1. **WebSocket upgrade requests** (`GET /ws` with `Upgrade: websocket` header) are handled by `server.upgrade()` which assigns a `clientId` (via `crypto.randomUUID()`) and passes to Bun's WebSocket handler.
|
|
80
|
+
2. **All other requests** are delegated to the Hono server for normal HTTP routing.
|
|
81
|
+
3. **Failed upgrades** return a `500 WebSocket upgrade failed` response.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Incoming Request
|
|
85
|
+
|
|
|
86
|
+
v
|
|
87
|
+
Is WebSocket upgrade?
|
|
88
|
+
(pathname === wsPath &&
|
|
89
|
+
headers.upgrade === 'websocket')
|
|
90
|
+
|
|
|
91
|
+
+----+----+
|
|
92
|
+
| |
|
|
93
|
+
Yes No
|
|
94
|
+
| |
|
|
95
|
+
v v
|
|
96
|
+
server. honoServer.
|
|
97
|
+
upgrade() fetch(req, server)
|
|
98
|
+
|
|
|
99
|
+
+---> success: return undefined (Bun handles it)
|
|
100
|
+
+---> failure: return Response(500)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## WebSocket Emitter API
|
|
104
|
+
|
|
105
|
+
### Overview
|
|
106
|
+
|
|
107
|
+
`WebSocketEmitter` is a standalone, lightweight Redis-only publisher designed for processes that do not run a `WebSocketServerHelper`. It extends `BaseHelper` and uses a single Redis pub client to publish `IRedisSocketMessage` envelopes.
|
|
108
|
+
|
|
109
|
+
### `IWebSocketEmitterOptions`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface IWebSocketEmitterOptions {
|
|
113
|
+
identifier?: string; // Default: 'WebSocketEmitter' (used as logger scope)
|
|
114
|
+
redisConnection: DefaultRedisHelper; // Required -- same Redis as the server(s)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Constructor
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const emitter = new WebSocketEmitter({
|
|
122
|
+
identifier: 'my-worker', // Optional
|
|
123
|
+
redisConnection: redisHelper, // Required
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The constructor:
|
|
128
|
+
1. Calls `super({ scope })` with `identifier` (or `'WebSocketEmitter'` if not provided)
|
|
129
|
+
2. Validates `redisConnection` is truthy (throws `"Invalid redis connection!"` if not)
|
|
130
|
+
3. Calls `redisConnection.getClient().duplicate()` to create an isolated pub client
|
|
131
|
+
|
|
132
|
+
### `EMITTER_SERVER_ID`
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const EMITTER_SERVER_ID = 'emitter';
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
All messages published by `WebSocketEmitter` use this fixed `serverId`. Since no `WebSocketServerHelper` instance will have a `serverId` of `'emitter'` (they use `crypto.randomUUID()`), all server instances will process emitter messages -- none will self-dedup.
|
|
139
|
+
|
|
140
|
+
### Methods
|
|
141
|
+
|
|
142
|
+
#### `configure()`
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
async configure(): Promise<void>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Prepares the emitter for use:
|
|
149
|
+
1. Registers a Redis `error` event handler (logs errors)
|
|
150
|
+
2. Calls `redisPub.connect()` if the client status is `'wait'` (i.e., lazy-connect mode)
|
|
151
|
+
3. Waits for the Redis client to reach `'ready'` status (30-second timeout)
|
|
152
|
+
|
|
153
|
+
Must be called before any `toClient()`, `toUser()`, `toRoom()`, or `broadcast()` calls.
|
|
154
|
+
|
|
155
|
+
#### `toClient()`
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
async toClient(opts: {
|
|
159
|
+
clientId: string;
|
|
160
|
+
event: string;
|
|
161
|
+
data: unknown;
|
|
162
|
+
}): Promise<void>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Publishes to `ws:client:{clientId}`. The target server that holds this client will deliver the message via `sendToClient()`.
|
|
166
|
+
|
|
167
|
+
#### `toUser()`
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
async toUser(opts: {
|
|
171
|
+
userId: string;
|
|
172
|
+
event: string;
|
|
173
|
+
data: unknown;
|
|
174
|
+
}): Promise<void>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Publishes to `ws:user:{userId}`. All servers with sessions for this user will call `sendToUser()` locally, reaching every session across all instances.
|
|
178
|
+
|
|
179
|
+
#### `toRoom()`
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
async toRoom(opts: {
|
|
183
|
+
room: string;
|
|
184
|
+
event: string;
|
|
185
|
+
data: unknown;
|
|
186
|
+
exclude?: string[];
|
|
187
|
+
}): Promise<void>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Publishes to `ws:room:{room}`. All servers with members in this room will call `sendToRoom()` locally. The optional `exclude` array is forwarded -- servers will skip those client IDs during delivery.
|
|
191
|
+
|
|
192
|
+
#### `broadcast()`
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
async broadcast(opts: {
|
|
196
|
+
event: string;
|
|
197
|
+
data: unknown;
|
|
198
|
+
}): Promise<void>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Publishes to `ws:broadcast`. All servers will call `broadcast()` locally, reaching every authenticated client.
|
|
202
|
+
|
|
203
|
+
#### `shutdown()`
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
async shutdown(): Promise<void>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Gracefully shuts down the emitter by calling `redisPub.quit()`. Always call this when the emitter is no longer needed to release the Redis connection.
|
|
210
|
+
|
|
211
|
+
## Internals
|
|
212
|
+
|
|
213
|
+
### `resolveBindings()`
|
|
214
|
+
|
|
215
|
+
Reads all binding keys from the DI container and validates required ones:
|
|
216
|
+
|
|
217
|
+
| Binding | Validation | Error on Failure |
|
|
218
|
+
|---------|-----------|------------------|
|
|
219
|
+
| `SERVER_OPTIONS` | Optional, merged with `DEFAULT_SERVER_OPTIONS` via `Object.assign()` | -- |
|
|
220
|
+
| `REDIS_CONNECTION` | Must be `instanceof DefaultRedisHelper` | `"Invalid instance of redisConnection"` |
|
|
221
|
+
| `AUTHENTICATE_HANDLER` | Must be truthy (non-null) | `"Invalid authenticateFn to setup WebSocket server!"` |
|
|
222
|
+
| `VALIDATE_ROOM_HANDLER` | Optional, coerced `null` to `undefined` | -- |
|
|
223
|
+
| `CLIENT_CONNECTED_HANDLER` | Optional, coerced `null` to `undefined` | -- |
|
|
224
|
+
| `CLIENT_DISCONNECTED_HANDLER` | Optional, coerced `null` to `undefined` | -- |
|
|
225
|
+
| `MESSAGE_HANDLER` | Optional, coerced `null` to `undefined` | -- |
|
|
226
|
+
| `OUTBOUND_TRANSFORMER` | Optional, coerced `null` to `undefined` | -- |
|
|
227
|
+
| `HANDSHAKE_HANDLER` | Optional, coerced `null` to `undefined` (required if `requireEncryption`) | -- |
|
|
228
|
+
|
|
229
|
+
### `registerBunHook()`
|
|
230
|
+
|
|
231
|
+
Registers a post-start hook that executes the following steps:
|
|
232
|
+
|
|
233
|
+
1. **Get Bun server instance** via `getServerInstance<TBunServerInstance>()`
|
|
234
|
+
2. **Get Hono server** via `getServer()`
|
|
235
|
+
3. **Create WebSocketServerHelper** with all resolved bindings and server options
|
|
236
|
+
4. **Await `wsHelper.configure()`** which connects Redis clients and sets up subscriptions
|
|
237
|
+
5. **Bind the helper** to `WEBSOCKET_INSTANCE` in the DI container
|
|
238
|
+
6. **Create custom `fetch` handler** via `createBunFetchHandler({ wsPath, honoServer })`
|
|
239
|
+
7. **Wire WebSocket into running server** via `serverInstance.reload({ fetch, websocket })`
|
|
240
|
+
|
|
241
|
+
#### Post-Start Hook Code Flow
|
|
242
|
+
```typescript
|
|
243
|
+
// Simplified post-start hook logic
|
|
244
|
+
async () => {
|
|
245
|
+
// Step 1 & 2: Get server instances
|
|
246
|
+
const serverInstance = this.application.getServerInstance<TBunServerInstance>();
|
|
247
|
+
const honoServer = this.application.getServer();
|
|
248
|
+
|
|
249
|
+
if (!serverInstance) {
|
|
250
|
+
throw getError({
|
|
251
|
+
message: '[WebSocketComponent] Bun server instance not available!',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Step 3: Create helper
|
|
256
|
+
const wsHelper = new WebSocketServerHelper({
|
|
257
|
+
identifier: serverOptions.identifier,
|
|
258
|
+
path: serverOptions.path,
|
|
259
|
+
defaultRooms: serverOptions.defaultRooms,
|
|
260
|
+
serverOptions: serverOptions.serverOptions,
|
|
261
|
+
heartbeatInterval: serverOptions.heartbeatInterval,
|
|
262
|
+
heartbeatTimeout: serverOptions.heartbeatTimeout,
|
|
263
|
+
server: serverInstance,
|
|
264
|
+
redisConnection: resolved.redisConnection,
|
|
265
|
+
authenticateFn: resolved.authenticateFn,
|
|
266
|
+
validateRoomFn: resolved.validateRoomFn,
|
|
267
|
+
clientConnectedFn: resolved.clientConnectedFn,
|
|
268
|
+
clientDisconnectedFn: resolved.clientDisconnectedFn,
|
|
269
|
+
messageHandler: resolved.messageHandler,
|
|
270
|
+
outboundTransformer: resolved.outboundTransformer,
|
|
271
|
+
handshakeFn: resolved.handshakeFn,
|
|
272
|
+
requireEncryption: serverOptions.requireEncryption,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Step 4: Configure (Redis + subscriptions + heartbeat timer)
|
|
276
|
+
await wsHelper.configure();
|
|
277
|
+
|
|
278
|
+
// Step 5: Bind to container
|
|
279
|
+
this.application.bind({ key: WebSocketBindingKeys.WEBSOCKET_INSTANCE })
|
|
280
|
+
.toValue(wsHelper);
|
|
281
|
+
|
|
282
|
+
// Step 6 & 7: Create fetch handler and reload server
|
|
283
|
+
serverInstance.reload({
|
|
284
|
+
fetch: createBunFetchHandler({ wsPath, honoServer }),
|
|
285
|
+
websocket: wsHelper.getBunWebSocketHandler(),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `createBunFetchHandler()`
|
|
291
|
+
|
|
292
|
+
The fetch handler is a standalone function (not a method on the component) that returns an async function:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
function createBunFetchHandler(opts: {
|
|
296
|
+
wsPath: string;
|
|
297
|
+
honoServer: OpenAPIHono;
|
|
298
|
+
}): (req: Request, server: TBunServerInstance) => Promise<Response | undefined>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The handler logic:
|
|
302
|
+
1. Parse `new URL(req.url)` to get the pathname
|
|
303
|
+
2. Check if `pathname === wsPath && headers.upgrade === 'websocket'`
|
|
304
|
+
3. If **not** a WebSocket upgrade, delegate to `honoServer.fetch(req, server)` -- note the second argument is the raw `server` instance, not wrapped in an object
|
|
305
|
+
4. If a WebSocket upgrade, call `server.upgrade(req, { data: { clientId: crypto.randomUUID() } })`
|
|
306
|
+
5. If upgrade succeeds, return `undefined` (Bun handles the connection)
|
|
307
|
+
6. If upgrade fails, return `new Response('WebSocket upgrade failed', { status: 500 })`
|
|
308
|
+
|
|
309
|
+
### Runtime Check
|
|
310
|
+
|
|
311
|
+
The component checks the runtime during `binding()`:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
const runtime = RuntimeModules.detect();
|
|
315
|
+
if (runtime === RuntimeModules.NODE) {
|
|
316
|
+
throw getError({
|
|
317
|
+
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
318
|
+
message: '[WebSocketComponent] Node.js runtime is not supported yet. Please use Bun runtime.',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
This check runs at component initialization time (before any hooks are registered), failing fast if the runtime is incompatible.
|
|
324
|
+
|
|
325
|
+
### Bun WebSocket Handler
|
|
326
|
+
|
|
327
|
+
The helper's `getBunWebSocketHandler()` returns an `IBunWebSocketHandler` -- a Bun-native WebSocket handler object with four lifecycle callbacks plus config spread:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
interface IBunWebSocketHandler extends IBunWebSocketConfig {
|
|
331
|
+
open: (socket: IWebSocket) => void; // New connection -- creates client entry, starts auth timer
|
|
332
|
+
message: (socket: IWebSocket, message: string | Buffer) => void; // Incoming message -- routes to handler
|
|
333
|
+
close: (socket: IWebSocket, code: number, reason: string) => void; // Disconnect -- cleanup
|
|
334
|
+
drain: (socket: IWebSocket) => void; // Backpressure cleared -- resets backpressured flag
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The `open` handler (`onClientConnect`):
|
|
339
|
+
1. Checks if clientId already exists (returns early if duplicate)
|
|
340
|
+
2. Creates an `IWebSocketClient` entry in state `UNAUTHORIZED`
|
|
341
|
+
3. Subscribes the socket to its own `clientId` topic (Bun native pub/sub -- enables direct messaging before auth)
|
|
342
|
+
4. Starts an auth timeout timer (`authTimeout`, default 5 s)
|
|
343
|
+
|
|
344
|
+
The `message` handler (`onClientMessage`):
|
|
345
|
+
1. Updates `lastActivity` on the client
|
|
346
|
+
2. Parses JSON -- sends `error` event `"Invalid message format"` if parse fails
|
|
347
|
+
3. Validates `event` field exists -- silently drops if missing (with error log)
|
|
348
|
+
4. Routes by event:
|
|
349
|
+
- `heartbeat`: returns immediately (no-op, `lastActivity` already updated)
|
|
350
|
+
- `authenticate`: delegates to `handleAuthenticate()`
|
|
351
|
+
- Any other event from unauthenticated client: sends `error` event `"Not authenticated"`
|
|
352
|
+
- `join`: delegates to `handleJoin()`
|
|
353
|
+
- `leave`: delegates to `handleLeave()`
|
|
354
|
+
- Custom events: delegates to `messageHandler` callback (if bound), otherwise silently dropped
|
|
355
|
+
|
|
356
|
+
The `close` handler (`onClientDisconnect`):
|
|
357
|
+
1. Clears auth timer if pending
|
|
358
|
+
2. Removes client from `users` index (deletes user entry if last session)
|
|
359
|
+
3. Removes client from all `rooms` entries (deletes room entry if empty)
|
|
360
|
+
4. Deletes from `clients` map
|
|
361
|
+
5. Invokes `clientDisconnectedFn` callback (errors caught and logged)
|
|
362
|
+
|
|
363
|
+
The `drain` handler:
|
|
364
|
+
1. Sets `client.backpressured = false`
|
|
365
|
+
2. Logs a debug message
|
|
366
|
+
|
|
367
|
+
### `deliverToSocket()` Backpressure Handling
|
|
368
|
+
|
|
369
|
+
The `deliverToSocket()` method handles three return values from Bun's `socket.send()`:
|
|
370
|
+
|
|
371
|
+
| Return Value | Meaning | Action |
|
|
372
|
+
|-------------|---------|--------|
|
|
373
|
+
| `> 0` (positive) | Message sent successfully (byte count) | No action |
|
|
374
|
+
| `0` | Message dropped (socket already closed) | Logs warning: `"Message dropped (socket closed)"` |
|
|
375
|
+
| `-1` | Backpressure (Bun's send buffer is full) | Sets `client.backpressured = true`, logs warning. The message is still queued by Bun. When the buffer drains, the `drain` handler fires and resets `backpressured` to `false` |
|
|
376
|
+
|
|
377
|
+
Any exception thrown by `socket.send()` is caught and logged as an error.
|
|
378
|
+
|
|
379
|
+
### `send()` Destination Resolution
|
|
380
|
+
|
|
381
|
+
The `send()` method is the primary public API for sending messages. It resolves the `destination` parameter using the following logic:
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
send({ destination, payload: { topic, data } })
|
|
385
|
+
|
|
|
386
|
+
+-- destination is undefined/null?
|
|
387
|
+
| Yes -> broadcast locally + publishToRedis(BROADCAST)
|
|
388
|
+
|
|
|
389
|
+
+-- destination matches a local clientId?
|
|
390
|
+
| Yes -> sendToClient locally + publishToRedis(CLIENT)
|
|
391
|
+
|
|
|
392
|
+
+-- destination matches a local room name?
|
|
393
|
+
| Yes -> sendToRoom locally + publishToRedis(ROOM)
|
|
394
|
+
|
|
|
395
|
+
+-- destination is unknown locally?
|
|
396
|
+
Yes -> publishToRedis(ROOM, target: destination)
|
|
397
|
+
(assumes it might be a room on another instance)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
> [!IMPORTANT]
|
|
401
|
+
> **No USER type in `send()`.** The `send()` method does not support `userId` as a destination. To send to all sessions of a user, use `sendToUser()` for local-only delivery or `WebSocketEmitter.toUser()` for cross-instance delivery via Redis.
|
|
402
|
+
|
|
403
|
+
> [!NOTE]
|
|
404
|
+
> When the destination is unknown locally, `send()` publishes it as a `ROOM` type to Redis. This is intentional -- if it is a client ID on another server, that server will not find it in its rooms map either, but the `onRedisMessage` handler routes `CLIENT` and `ROOM` messages differently. For reliable cross-instance client targeting, prefer using `WebSocketEmitter.toClient()` which explicitly uses the `CLIENT` message type.
|
|
405
|
+
|
|
406
|
+
### Room Join Validation
|
|
407
|
+
|
|
408
|
+
Room names go through two validation stages:
|
|
409
|
+
|
|
410
|
+
1. **Server-side sanitization** (always applied):
|
|
411
|
+
- Must be a non-empty string (truthy, `typeof r === 'string'`)
|
|
412
|
+
- Must be <= 256 characters
|
|
413
|
+
- Must not start with `ws:` prefix (reserved for internal channels)
|
|
414
|
+
|
|
415
|
+
2. **Application-level validation** (via `validateRoomFn`):
|
|
416
|
+
- Only called if the function is bound
|
|
417
|
+
- Receives the sanitized room list
|
|
418
|
+
- Returns the subset of rooms the client is allowed to join
|
|
419
|
+
- If no `validateRoomFn` is bound, **all join requests are rejected** with a warning log
|
|
420
|
+
|
|
421
|
+
### Room Leave Validation
|
|
422
|
+
|
|
423
|
+
The `handleLeave()` method validates that the client has actually joined the requested rooms before leaving:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
const validRooms = rooms.filter(r => client.rooms.has(r));
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
This prevents clients from unsubscribing from internal topics or rooms they never joined. If no valid rooms remain after filtering, the leave is silently ignored.
|
|
430
|
+
|
|
431
|
+
### Graceful Shutdown
|
|
432
|
+
|
|
433
|
+
Always shut down the WebSocket server before stopping the application:
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
override async stop(): Promise<void> {
|
|
437
|
+
// 1. Shut down WebSocket (disconnects all clients, quits Redis)
|
|
438
|
+
const wsHelper = this.get<WebSocketServerHelper>({
|
|
439
|
+
key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
|
|
440
|
+
isOptional: true,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (wsHelper) {
|
|
444
|
+
await wsHelper.shutdown();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 2. Disconnect Redis helper
|
|
448
|
+
if (this.redisHelper) {
|
|
449
|
+
await this.redisHelper.disconnect();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 3. Stop the Bun server
|
|
453
|
+
await super.stop();
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### Shutdown Sequence Diagram
|
|
458
|
+
```
|
|
459
|
+
wsHelper.shutdown()
|
|
460
|
+
|-- Clear heartbeat timer
|
|
461
|
+
| +-- clearInterval(heartbeatTimer)
|
|
462
|
+
|
|
|
463
|
+
|-- Close all sockets
|
|
464
|
+
| +-- For each client: socket.close(1001, 'Server shutting down')
|
|
465
|
+
| (errors caught per-client -- already-disconnected clients are logged)
|
|
466
|
+
|
|
|
467
|
+
|-- Trigger disconnect callbacks
|
|
468
|
+
| +-- For each client: onClientDisconnect({ clientId })
|
|
469
|
+
| |-- Clear auth timer
|
|
470
|
+
| |-- Remove from users map
|
|
471
|
+
| |-- Remove from rooms map
|
|
472
|
+
| |-- Remove from clients map
|
|
473
|
+
| +-- Invoke clientDisconnectedFn callback
|
|
474
|
+
|
|
|
475
|
+
|-- Clear tracking maps
|
|
476
|
+
| |-- clients.clear()
|
|
477
|
+
| |-- users.clear()
|
|
478
|
+
| +-- rooms.clear()
|
|
479
|
+
|
|
|
480
|
+
+-- Redis cleanup (parallel)
|
|
481
|
+
|-- redisPub.quit()
|
|
482
|
+
+-- redisSub.quit()
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The shutdown sequence ensures:
|
|
486
|
+
- Active connections are gracefully closed with code `1001` ("Going Away")
|
|
487
|
+
- All disconnect callbacks are invoked (so application-level cleanup runs)
|
|
488
|
+
- All internal state is cleared (client/user/room maps)
|
|
489
|
+
- Redis pub/sub clients are properly disconnected
|
|
490
|
+
- No memory leaks from lingering timers or connections
|
|
491
|
+
|
|
492
|
+
### WebSocketEmitter Shutdown
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
emitter.shutdown()
|
|
496
|
+
+-- redisPub.quit()
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The emitter shutdown is simpler since it only has one Redis client and no local state to clean up.
|
|
500
|
+
|
|
501
|
+
## See Also
|
|
502
|
+
|
|
503
|
+
- [Setup & Configuration](./) - Quick reference, imports, setup steps, configuration, and binding keys
|
|
504
|
+
- [Usage & Examples](./usage) - Server-side usage, emitter, wire protocol, client tracking, and delivery strategy
|
|
505
|
+
- [Error Reference](./errors) - Error conditions table and troubleshooting
|
|
506
|
+
- [WebSocketServerHelper](/references/helpers/websocket/) - Helper API documentation
|
|
507
|
+
- [Socket.IO Component](../socket-io/) - Node.js-compatible alternative with Socket.IO
|
|
508
|
+
- [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - Official Bun WebSocket API reference
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# WebSocket -- Error Reference
|
|
2
|
+
|
|
3
|
+
> Error conditions table and troubleshooting guide for the WebSocket component.
|
|
4
|
+
|
|
5
|
+
## Error Conditions
|
|
6
|
+
|
|
7
|
+
The server can send `error` events or close the connection under the following conditions:
|
|
8
|
+
|
|
9
|
+
| Error Message / Close Code | Trigger | Event Type |
|
|
10
|
+
|---------------------------|---------|------------|
|
|
11
|
+
| `"Invalid message format"` | Client sent non-JSON data | `error` event |
|
|
12
|
+
| `"Already authenticated"` | Client sent `authenticate` when state is not `UNAUTHORIZED` | `error` event |
|
|
13
|
+
| `"Not authenticated"` | Client sent a non-`authenticate`, non-`heartbeat` event while unauthenticated | `error` event |
|
|
14
|
+
| `"Authentication failed"` | `authenticateFn` returned `null`/`false` | `error` event + close `4003` |
|
|
15
|
+
| `"Authentication error"` | `authenticateFn` threw an exception | `error` event + close `4003` |
|
|
16
|
+
| `"Encryption handshake failed"` | `handshakeFn` returned `null`/`false` | `error` event + close `4004` |
|
|
17
|
+
| Close `4004` (no error event) | `requireEncryption: true` but no `handshakeFn` configured | close `4004` only |
|
|
18
|
+
| Close `4001` | Auth timeout (initial: no `authenticate` sent) | close `4001` only |
|
|
19
|
+
| Close `4001` | Auth in-progress timeout (`authenticateFn`/`handshakeFn` too slow) | close `4001` only |
|
|
20
|
+
| Close `4002` | Heartbeat timeout (no messages within `heartbeatTimeout`) | close `4002` only |
|
|
21
|
+
| Close `1001` | Server shutdown (`wsHelper.shutdown()`) | close `1001` only |
|
|
22
|
+
| `"Invalid redis connection!"` | Constructor: `redisConnection` is falsy | thrown `Error` (startup) |
|
|
23
|
+
| `"WebSocket upgrade failed"` | `server.upgrade()` returned `false` | HTTP `500` response |
|
|
24
|
+
|
|
25
|
+
## Troubleshooting
|
|
26
|
+
|
|
27
|
+
### "WebSocket not initialized"
|
|
28
|
+
|
|
29
|
+
**Cause**: You are trying to use `WebSocketServerHelper` before the server has started (e.g., during DI construction).
|
|
30
|
+
|
|
31
|
+
**Fix**: Use the lazy getter pattern shown in [Usage & Examples](./usage). Never `@inject` `WEBSOCKET_INSTANCE` directly in a constructor -- it does not exist yet at construction time.
|
|
32
|
+
|
|
33
|
+
### "Invalid instance of redisConnection"
|
|
34
|
+
|
|
35
|
+
**Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `DefaultRedisHelper` (or its subclass `RedisHelper`).
|
|
36
|
+
|
|
37
|
+
**Fix**: Use `RedisHelper` (recommended) or `DefaultRedisHelper`:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// Correct
|
|
41
|
+
this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION })
|
|
42
|
+
.toValue(new RedisHelper({ name: 'websocket', host, port, password }));
|
|
43
|
+
|
|
44
|
+
// Wrong -- raw ioredis client
|
|
45
|
+
this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION })
|
|
46
|
+
.toValue(new Redis(6379)); // This is NOT a DefaultRedisHelper!
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### "Invalid authenticateFn to setup WebSocket server!"
|
|
50
|
+
|
|
51
|
+
**Cause**: No authentication function was bound to `AUTHENTICATE_HANDLER`, or it was bound as `null`.
|
|
52
|
+
|
|
53
|
+
**Fix**: Bind a valid authentication function before registering the component:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
this.bind<TWebSocketAuthenticateFn>({
|
|
57
|
+
key: WebSocketBindingKeys.AUTHENTICATE_HANDLER,
|
|
58
|
+
}).toValue(async (data) => {
|
|
59
|
+
const token = data.token as string;
|
|
60
|
+
const user = await verifyJWT(token);
|
|
61
|
+
return user ? { userId: user.id } : null;
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### "Node.js runtime is not supported yet"
|
|
66
|
+
|
|
67
|
+
**Cause**: Running the application on Node.js. The WebSocket component only supports Bun.
|
|
68
|
+
|
|
69
|
+
**Fix**: Either switch to Bun runtime, or use the [Socket.IO Component](../socket-io/) which supports both Node.js and Bun.
|
|
70
|
+
|
|
71
|
+
### "Bun server instance not available!"
|
|
72
|
+
|
|
73
|
+
**Cause**: The post-start hook executed but could not obtain the Bun server instance. This typically means the server failed to start.
|
|
74
|
+
|
|
75
|
+
**Fix**: Check server startup logs for errors. Ensure `start()` completes successfully before post-start hooks run.
|
|
76
|
+
|
|
77
|
+
### WebSocket connects but messages are not received
|
|
78
|
+
|
|
79
|
+
**Cause**: Clients must send <code v-pre>{ event: 'authenticate', data: { type: '...', token: '...', publicKey?: '...' } }</code> after connecting. Unauthenticated clients are disconnected after the timeout (default: 5 seconds) and cannot receive messages other than `error` events.
|
|
80
|
+
|
|
81
|
+
**Fix**: Ensure your client authenticates immediately after connection:
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
const ws = new WebSocket('wss://example.com/ws');
|
|
85
|
+
|
|
86
|
+
ws.onopen = () => {
|
|
87
|
+
ws.send(JSON.stringify({
|
|
88
|
+
event: 'authenticate',
|
|
89
|
+
data: { type: 'Bearer', token: 'your-jwt-token' },
|
|
90
|
+
}));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
ws.onmessage = (event) => {
|
|
94
|
+
const msg = JSON.parse(event.data);
|
|
95
|
+
if (msg.event === 'connected') {
|
|
96
|
+
console.log('Authenticated! Client ID:', msg.data.id);
|
|
97
|
+
// Now ready to send/receive events
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Client disconnected with code 4002
|
|
103
|
+
|
|
104
|
+
**Cause**: The client did not send any messages (including heartbeat) within the `heartbeatTimeout` period (default: 90 seconds).
|
|
105
|
+
|
|
106
|
+
**Fix**: Implement a heartbeat on the client side:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
setInterval(() => {
|
|
110
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
111
|
+
ws.send(JSON.stringify({ event: 'heartbeat' }));
|
|
112
|
+
}
|
|
113
|
+
}, 30000);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## See Also
|
|
117
|
+
|
|
118
|
+
- [Setup & Configuration](./) - Quick reference, imports, setup steps, configuration, and binding keys
|
|
119
|
+
- [Usage & Examples](./usage) - Server-side usage, emitter, wire protocol, client tracking, and delivery strategy
|
|
120
|
+
- [API Reference](./api) - Architecture, WebSocketEmitter API, and internals
|
|
121
|
+
- [WebSocketServerHelper](/references/helpers/websocket/) - Helper API documentation
|
|
122
|
+
- [Socket.IO Component](../socket-io/) - Node.js-compatible alternative with Socket.IO
|
|
123
|
+
- [Bun WebSocket Documentation](https://bun.sh/docs/api/websockets) - Official Bun WebSocket API reference
|