@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,1051 @@
|
|
|
1
|
+
# Socket.IO -- API Reference
|
|
2
|
+
|
|
3
|
+
> Architecture deep dive, method signatures, internals, and type definitions.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
The component integrates Socket.IO into the Ignis application lifecycle with runtime-specific initialization (Node.js vs Bun).
|
|
8
|
+
|
|
9
|
+
#### Architecture Diagram
|
|
10
|
+
```
|
|
11
|
+
SocketIOComponent
|
|
12
|
+
+----------------------------------------------+
|
|
13
|
+
| |
|
|
14
|
+
| binding() |
|
|
15
|
+
| |-- resolveBindings() |
|
|
16
|
+
| | |-- SERVER_OPTIONS |
|
|
17
|
+
| | |-- REDIS_CONNECTION |
|
|
18
|
+
| | |-- AUTHENTICATE_HANDLER |
|
|
19
|
+
| | |-- VALIDATE_ROOM_HANDLER |
|
|
20
|
+
| | +-- CLIENT_CONNECTED_HANDLER |
|
|
21
|
+
| | |
|
|
22
|
+
| +-- RuntimeModules.detect() |
|
|
23
|
+
| |-- BUN -> registerBunHook() |
|
|
24
|
+
| +-- NODE -> registerNodeHook() |
|
|
25
|
+
| |
|
|
26
|
+
| (Post-start hooks execute after server) |
|
|
27
|
+
| |-- Creates SocketIOServerHelper |
|
|
28
|
+
| |-- await socketIOHelper.configure() |
|
|
29
|
+
| |-- Binds to SOCKET_IO_INSTANCE |
|
|
30
|
+
| +-- Wires into server (runtime-specific) |
|
|
31
|
+
+----------------------------------------------+
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Lifecycle Integration
|
|
35
|
+
|
|
36
|
+
The component uses the **post-start hook** system to solve a fundamental timing problem: Socket.IO needs a running server instance, but components are initialized *before* the server starts.
|
|
37
|
+
|
|
38
|
+
#### Application Lifecycle Diagram
|
|
39
|
+
```
|
|
40
|
+
Application Lifecycle
|
|
41
|
+
=====================
|
|
42
|
+
|
|
43
|
+
+------------------+
|
|
44
|
+
| preConfigure() | <-- Register SocketIOComponent here
|
|
45
|
+
+--------+---------+
|
|
46
|
+
|
|
|
47
|
+
+--------v---------+
|
|
48
|
+
| initialize() | <-- Component.binding() runs here
|
|
49
|
+
| | Resolves bindings, registers post-start hook
|
|
50
|
+
+--------+---------+
|
|
51
|
+
|
|
|
52
|
+
+--------v---------+
|
|
53
|
+
| setupMiddlewares |
|
|
54
|
+
+--------+---------+
|
|
55
|
+
|
|
|
56
|
+
+--------v-----------------------+
|
|
57
|
+
| startBunModule() OR | <-- Server starts, instance created
|
|
58
|
+
| startNodeModule() |
|
|
59
|
+
+--------+-----------------------+
|
|
60
|
+
|
|
|
61
|
+
+--------v--------------------------+
|
|
62
|
+
| executePostStartHooks() | <-- SocketIOServerHelper created HERE
|
|
63
|
+
| +-- socket-io-initialize | Server instance is now available
|
|
64
|
+
+-----------------------------------+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Runtime-Specific Behavior
|
|
68
|
+
|
|
69
|
+
| Aspect | Node.js | Bun |
|
|
70
|
+
|--------|---------|-----|
|
|
71
|
+
| **Server Type** | `node:http.Server` | `Bun.Server` |
|
|
72
|
+
| **IO Server Init** | `new IOServer(httpServer, opts)` | `new IOServer()` + `io.bind(engine)` |
|
|
73
|
+
| **Engine** | Built-in (`socket.io`) | `@socket.io/bun-engine` (optional peer dep) |
|
|
74
|
+
| **Request Routing** | Socket.IO attaches to HTTP server automatically | `server.reload({ fetch, websocket })` wires engine into Bun's request loop |
|
|
75
|
+
| **WebSocket Upgrade** | Handled by `node:http.Server` upgrade event | Handled by Bun's `websocket` handler |
|
|
76
|
+
| **Dynamic Import** | None needed | `await import('@socket.io/bun-engine')` at runtime |
|
|
77
|
+
| **Fetch Handler** | Not needed -- HTTP server handles upgrades | Custom fetch wraps Hono fetch, routes WS upgrades to engine |
|
|
78
|
+
| **CORS** | Handled by `socket.io` CORS options | Handled by Bun engine options (requires explicit field bridging) |
|
|
79
|
+
| **Server Access** | Direct -- Socket.IO attaches to HTTP server | `server.reload({ fetch, websocket })` to hot-swap handlers |
|
|
80
|
+
|
|
81
|
+
### Runtime Differences -- Deep Dive
|
|
82
|
+
|
|
83
|
+
#### Bun Runtime
|
|
84
|
+
|
|
85
|
+
The Bun handler creates a custom fetch function that intercepts WebSocket upgrade requests:
|
|
86
|
+
|
|
87
|
+
1. Checks if the request path matches the Socket.IO path (`serverOptions.path`, default `'/io'`)
|
|
88
|
+
2. If yes, delegates to `@socket.io/bun-engine` via `engine.handleRequest(req, server)` for WebSocket protocol handling
|
|
89
|
+
3. If no, delegates to Hono's normal `server.fetch(req, server)` handler
|
|
90
|
+
|
|
91
|
+
#### Bun Fetch Handler Source
|
|
92
|
+
```typescript
|
|
93
|
+
function createBunFetchHandler(opts: {
|
|
94
|
+
engine: any;
|
|
95
|
+
enginePath: string;
|
|
96
|
+
honoServer: OpenAPIHono;
|
|
97
|
+
}): (req: Request, server: TBunServerInstance) => Response | Promise<Response> {
|
|
98
|
+
const { engine, enginePath, honoServer } = opts;
|
|
99
|
+
|
|
100
|
+
return (req: Request, server: TBunServerInstance): Response | Promise<Response> => {
|
|
101
|
+
const url = new URL(req.url);
|
|
102
|
+
|
|
103
|
+
if (!url.pathname.startsWith(enginePath)) {
|
|
104
|
+
return honoServer.fetch(req, server);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return engine.handleRequest(req, server) ?? new Response(null, { status: 404 });
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**CORS type bridging**: Socket.IO and `@socket.io/bun-engine` have slightly different CORS type definitions. The component extracts individual CORS fields explicitly to avoid type mismatches without using `as any`:
|
|
113
|
+
|
|
114
|
+
#### Bun Engine CORS Bridging
|
|
115
|
+
```typescript
|
|
116
|
+
const corsConfig = typeof serverOptions.cors === 'object' ? serverOptions.cors : undefined;
|
|
117
|
+
const engine = new BunEngine({
|
|
118
|
+
path: serverOptions.path ?? '/socket.io/',
|
|
119
|
+
...(corsConfig && {
|
|
120
|
+
cors: {
|
|
121
|
+
origin: corsConfig.origin as string | RegExp | (string | RegExp)[] | undefined,
|
|
122
|
+
methods: corsConfig.methods,
|
|
123
|
+
credentials: corsConfig.credentials,
|
|
124
|
+
allowedHeaders: corsConfig.allowedHeaders,
|
|
125
|
+
exposedHeaders: corsConfig.exposedHeaders,
|
|
126
|
+
maxAge: corsConfig.maxAge,
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Node.js Runtime
|
|
133
|
+
|
|
134
|
+
Node mode is simpler because Socket.IO natively attaches to `node:http.Server`. The handler creates a `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and passes the HTTP server instance directly:
|
|
135
|
+
|
|
136
|
+
#### Node.js Handler Source
|
|
137
|
+
```typescript
|
|
138
|
+
async function createNodeSocketIOHelper(opts: {
|
|
139
|
+
serverOptions: Partial<IServerOptions>;
|
|
140
|
+
httpServer: TNodeServerInstance;
|
|
141
|
+
resolvedBindings: IResolvedBindings;
|
|
142
|
+
}): Promise<SocketIOServerHelper> {
|
|
143
|
+
const { serverOptions, httpServer, resolvedBindings } = opts;
|
|
144
|
+
const { redisConnection, authenticateFn, validateRoomFn, clientConnectedFn } = resolvedBindings;
|
|
145
|
+
|
|
146
|
+
const socketIOHelper = new SocketIOServerHelper({
|
|
147
|
+
runtime: RuntimeModules.NODE,
|
|
148
|
+
identifier: serverOptions.identifier!,
|
|
149
|
+
server: httpServer,
|
|
150
|
+
serverOptions,
|
|
151
|
+
redisConnection,
|
|
152
|
+
authenticateFn,
|
|
153
|
+
validateRoomFn,
|
|
154
|
+
clientConnectedFn,
|
|
155
|
+
});
|
|
156
|
+
await socketIOHelper.configure();
|
|
157
|
+
|
|
158
|
+
return socketIOHelper;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Server Helper API Reference
|
|
163
|
+
|
|
164
|
+
### `SocketIOServerHelper` Constructor
|
|
165
|
+
|
|
166
|
+
The helper uses a **discriminated union** for its constructor options, keyed on `runtime`:
|
|
167
|
+
|
|
168
|
+
#### `TSocketIOServerOptions` Type
|
|
169
|
+
```typescript
|
|
170
|
+
interface ISocketIOServerBaseOptions {
|
|
171
|
+
identifier: string;
|
|
172
|
+
serverOptions: Partial<ServerOptions>;
|
|
173
|
+
redisConnection: DefaultRedisHelper;
|
|
174
|
+
defaultRooms?: string[]; // Default: ['io-default', 'io-notification']
|
|
175
|
+
authenticateTimeout?: number; // Default: 10_000 (10 seconds)
|
|
176
|
+
pingInterval?: number; // Default: 30_000 (30 seconds)
|
|
177
|
+
|
|
178
|
+
authenticateFn: TSocketIOAuthenticateFn;
|
|
179
|
+
validateRoomFn?: TSocketIOValidateRoomFn;
|
|
180
|
+
clientConnectedFn?: TSocketIOClientConnectedFn;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
|
|
184
|
+
runtime: typeof RuntimeModules.NODE;
|
|
185
|
+
server: HTTPServer; // node:http.Server instance
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
|
|
189
|
+
runtime: typeof RuntimeModules.BUN;
|
|
190
|
+
engine: any; // @socket.io/bun-engine Server instance
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
During construction:
|
|
197
|
+
|
|
198
|
+
1. Sets `identifier`, `runtime`, `serverOptions`, callback functions
|
|
199
|
+
2. Sets defaults: `authenticateTimeout` = 10s, `pingInterval` = 30s, `defaultRooms` = `['io-default', 'io-notification']`
|
|
200
|
+
3. Calls `setRuntime()` -- validates and stores the server or engine
|
|
201
|
+
4. Calls `initRedisClients()` -- creates 3 duplicated Redis clients from the connection
|
|
202
|
+
|
|
203
|
+
> [!IMPORTANT]
|
|
204
|
+
> Redis clients are **duplicated** from the parent connection (`client.duplicate()`). This means the helper uses 3 independent connections (pub, sub, emitter) that inherit config from the parent but maintain separate state. The parent `RedisHelper` connection is not consumed.
|
|
205
|
+
|
|
206
|
+
### `configure()` -- Server Initialization
|
|
207
|
+
|
|
208
|
+
The `configure()` method is the main initialization entry point, called after construction:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
configure()
|
|
212
|
+
|-- Register error handlers on all 3 Redis clients
|
|
213
|
+
|-- Connect any clients in 'wait' status (lazyConnect mode)
|
|
214
|
+
|-- await Promise.all([redisPub.ready, redisSub.ready, redisEmitter.ready])
|
|
215
|
+
|-- initIOServer()
|
|
216
|
+
| |-- NODE: new IOServer(httpServer, serverOptions)
|
|
217
|
+
| +-- BUN: new IOServer() -> io.bind(bunEngine)
|
|
218
|
+
|-- io.adapter(createAdapter(redisPub, redisSub))
|
|
219
|
+
|-- emitter = new Emitter(redisEmitter)
|
|
220
|
+
+-- io.on('connection', onClientConnect)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
> [!NOTE]
|
|
224
|
+
> The `configure()` method is **async** because it waits for all 3 Redis connections to be ready before proceeding. If any Redis client fails to connect, the error propagates and the server will not start.
|
|
225
|
+
|
|
226
|
+
### Public Methods
|
|
227
|
+
|
|
228
|
+
#### `getIOServer()`
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
getIOServer(): IOServer
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Returns the underlying `socket.io` `Server` instance. Use this for direct access to Socket.IO APIs not exposed by the helper (e.g., `io.of('/namespace')`, `io.fetchSockets()`).
|
|
235
|
+
|
|
236
|
+
#### `getEngine()`
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
getEngine(): any
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Returns the `@socket.io/bun-engine` instance. **Throws** if the runtime is Node.js (`"Engine is only available for Bun runtime!"`).
|
|
243
|
+
|
|
244
|
+
#### `getClients()`
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// Overloaded:
|
|
248
|
+
getClients(): Map<string, ISocketIOClient>
|
|
249
|
+
getClients(opts: { id: string }): ISocketIOClient | undefined
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
When called without arguments, returns the full client map. When called with `{ id }`, returns the specific client entry or `undefined` if not found.
|
|
253
|
+
|
|
254
|
+
#### `on()`
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
on<HandlerArgsType extends unknown[] = unknown[], HandlerReturnType = void>(opts: {
|
|
258
|
+
topic: string;
|
|
259
|
+
handler: (...args: HandlerArgsType) => ValueOrPromise<HandlerReturnType>;
|
|
260
|
+
}): void
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Registers a server-level event handler on the IO server. **Throws** if `topic` is empty, `handler` is falsy, or the IO server is not initialized.
|
|
264
|
+
|
|
265
|
+
#### `ping()`
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
ping(opts: { socket: IOSocket; doIgnoreAuth: boolean }): void
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Sends a `ping` event to a specific client with `{ time: <ISO string> }`. Behavior:
|
|
272
|
+
|
|
273
|
+
- If `socket` is undefined, logs and returns
|
|
274
|
+
- If client is not found in the client map, logs and returns
|
|
275
|
+
- If `doIgnoreAuth` is `false` and the client is not `authenticated`, disconnects the client
|
|
276
|
+
- If `doIgnoreAuth` is `true`, sends the ping regardless of auth state
|
|
277
|
+
|
|
278
|
+
Used internally for the keep-alive interval after authentication. The `doIgnoreAuth: true` flag is used for the initial post-auth ping and the recurring interval.
|
|
279
|
+
|
|
280
|
+
#### `disconnect()`
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
disconnect(opts: { socket: IOSocket }): void
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Disconnects a specific client and cleans up resources:
|
|
287
|
+
|
|
288
|
+
1. Clears the ping interval (if set)
|
|
289
|
+
2. Clears the authentication timeout
|
|
290
|
+
3. Removes the client from the `clients` map
|
|
291
|
+
4. Calls `socket.disconnect()` on the underlying Socket.IO socket
|
|
292
|
+
|
|
293
|
+
If the socket is `undefined` or not tracked in the client map, the method still calls `socket.disconnect()` for safety.
|
|
294
|
+
|
|
295
|
+
#### `onClientConnect()`
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
onClientConnect(opts: { socket: IOSocket }): void
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Handles a new socket connection. Called by the `connection` event handler on the IO server. This method is public so it can be invoked externally for testing or custom connection routing.
|
|
302
|
+
|
|
303
|
+
Behavior:
|
|
304
|
+
1. Validates the socket exists (returns if `null`/`undefined`)
|
|
305
|
+
2. Checks for duplicate connections by socket ID (returns if already tracked)
|
|
306
|
+
3. Creates an `ISocketIOClient` entry with state `UNAUTHORIZED`
|
|
307
|
+
4. Starts the authentication timeout (`authenticateTimeout` ms)
|
|
308
|
+
5. Registers `disconnect` handler on the socket
|
|
309
|
+
6. Registers `authenticate` handler via `registerAuthHandler()`
|
|
310
|
+
|
|
311
|
+
#### `onClientAuthenticated()`
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
onClientAuthenticated(opts: { socket: IOSocket }): void
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Called after successful authentication. This method is public so it can be invoked externally for testing or custom auth flows.
|
|
318
|
+
|
|
319
|
+
Behavior:
|
|
320
|
+
1. Validates the socket and client entry exist
|
|
321
|
+
2. Sets client state to `AUTHENTICATED`
|
|
322
|
+
3. Sends an initial ping
|
|
323
|
+
4. Joins default rooms (`io-default`, `io-notification`)
|
|
324
|
+
5. Registers room handlers (`join`, `leave`)
|
|
325
|
+
6. Starts the ping interval
|
|
326
|
+
7. Emits `authenticated` event to the client with `{ id, time }`
|
|
327
|
+
8. Invokes the `clientConnectedFn` callback (if configured)
|
|
328
|
+
|
|
329
|
+
### Messaging via `send()`
|
|
330
|
+
|
|
331
|
+
The `send()` method uses the Redis emitter for message delivery, enabling cross-instance broadcasting:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
send(opts: {
|
|
335
|
+
destination?: string; // Socket ID, room name, or omit for broadcast
|
|
336
|
+
payload: {
|
|
337
|
+
topic: string; // Event name
|
|
338
|
+
data: any; // Event payload
|
|
339
|
+
};
|
|
340
|
+
doLog?: boolean; // Log the emission (default: false)
|
|
341
|
+
cb?: () => void; // Callback executed via setImmediate after emit
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Key behaviors:
|
|
346
|
+
|
|
347
|
+
- All messages are **compressed** via `emitter.compress(true)`
|
|
348
|
+
- If `destination` is provided and non-empty, sends via `sender.to(destination).emit(topic, data)`
|
|
349
|
+
- If `destination` is omitted/empty, broadcasts to **all** connected clients via `sender.emit(topic, data)`
|
|
350
|
+
- Callback (`cb`) is executed asynchronously via `setImmediate()`, not after delivery confirmation
|
|
351
|
+
- Logging is opt-in (`doLog: true`) to avoid noise in high-throughput scenarios
|
|
352
|
+
|
|
353
|
+
#### `send()` Silent Failure Behavior
|
|
354
|
+
|
|
355
|
+
The `send()` method silently returns (no error, no log) in these cases:
|
|
356
|
+
- `payload` is falsy
|
|
357
|
+
- `payload.topic` is falsy
|
|
358
|
+
- `payload.data` is falsy
|
|
359
|
+
|
|
360
|
+
This is a deliberate design choice for fire-and-forget messaging patterns where callers do not need to know if a message was dropped due to missing fields.
|
|
361
|
+
|
|
362
|
+
> [!TIP]
|
|
363
|
+
> The emitter uses the Redis emitter client, so messages are delivered across all server instances in a horizontally-scaled deployment. This works even if the recipient is connected to a different server instance.
|
|
364
|
+
|
|
365
|
+
### Shutdown
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
shutdown(): Promise<void>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Gracefully shuts down the server:
|
|
372
|
+
|
|
373
|
+
1. Iterates all tracked clients and clears their intervals/timeouts
|
|
374
|
+
2. Disconnects each client socket
|
|
375
|
+
3. Clears the client map
|
|
376
|
+
4. Closes the IO server (async, wrapped in a Promise)
|
|
377
|
+
5. Quits all 3 Redis connections (`redisPub`, `redisSub`, `redisEmitter`)
|
|
378
|
+
|
|
379
|
+
## Client Helper API Reference
|
|
380
|
+
|
|
381
|
+
`SocketIOClientHelper` extends `BaseHelper` and provides a managed Socket.IO client. It wraps the `socket.io-client` library with lifecycle callbacks, error-safe event subscription, and authentication state tracking.
|
|
382
|
+
|
|
383
|
+
### Constructor
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
constructor(opts: ISocketIOClientOptions)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### `ISocketIOClientOptions` Interface
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
interface ISocketIOClientOptions {
|
|
393
|
+
identifier: string;
|
|
394
|
+
host: string;
|
|
395
|
+
options: IOptions;
|
|
396
|
+
|
|
397
|
+
// Lifecycle callbacks (all optional)
|
|
398
|
+
onConnected?: () => ValueOrPromise<void>;
|
|
399
|
+
onDisconnected?: (reason: string) => ValueOrPromise<void>;
|
|
400
|
+
onError?: (error: Error) => ValueOrPromise<void>;
|
|
401
|
+
onAuthenticated?: () => ValueOrPromise<void>;
|
|
402
|
+
onUnauthenticated?: (message: string) => ValueOrPromise<void>;
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### `IOptions` Interface
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
interface IOptions extends SocketOptions {
|
|
410
|
+
path: string;
|
|
411
|
+
extraHeaders: Record<string | symbol | number, any>;
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`IOptions` extends `SocketOptions` from `socket.io-client` with two required fields:
|
|
416
|
+
- `path` -- the Socket.IO endpoint path (must match the server's `path` option, e.g., `'/io'`)
|
|
417
|
+
- `extraHeaders` -- headers sent with every request, commonly used for `authorization` tokens
|
|
418
|
+
|
|
419
|
+
#### Constructor Behavior
|
|
420
|
+
|
|
421
|
+
1. Calls `super({ scope: opts.identifier })` to initialize `BaseHelper` with scoped logging
|
|
422
|
+
2. Stores the `identifier`, `host`, `options`, and all lifecycle callbacks
|
|
423
|
+
3. Immediately calls `configure()` to create the socket and register internal handlers
|
|
424
|
+
|
|
425
|
+
### `configure()`
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
configure(): void
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Creates the `socket.io-client` `Socket` instance and registers all internal event handlers. If the client is already established (i.e., `configure()` was already called), logs a message and returns early.
|
|
432
|
+
|
|
433
|
+
Registered handlers:
|
|
434
|
+
|
|
435
|
+
| Event | Internal Behavior |
|
|
436
|
+
|-------|-------------------|
|
|
437
|
+
| `connect` | Logs connection, invokes `onConnected` callback |
|
|
438
|
+
| `disconnect` | Logs disconnection with reason, resets state to `unauthorized`, invokes `onDisconnected` callback |
|
|
439
|
+
| `connect_error` | Logs the error, invokes `onError` callback |
|
|
440
|
+
| `authenticated` | Logs auth data, sets state to `authenticated`, invokes `onAuthenticated` callback |
|
|
441
|
+
| `unauthenticated` | Logs warning with auth data, resets state to `unauthorized`, invokes `onUnauthenticated` callback with the message |
|
|
442
|
+
| `ping` | Logs debug-level ping received |
|
|
443
|
+
|
|
444
|
+
All lifecycle callbacks are wrapped in `Promise.resolve(...).catch(...)` to prevent callback errors from crashing the client.
|
|
445
|
+
|
|
446
|
+
### `getState()`
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
getState(): TSocketIOClientState
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Returns the current authentication state: `'unauthorized'`, `'authenticating'`, or `'authenticated'`.
|
|
453
|
+
|
|
454
|
+
#### `TSocketIOClientState` Type
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
|
|
458
|
+
// Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### `getSocketClient()`
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
getSocketClient(): Socket
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Returns the raw `socket.io-client` `Socket` instance. Use this for direct access to Socket.IO client APIs not exposed by the helper (e.g., `socket.io`, `socket.connected`, `socket.id`).
|
|
468
|
+
|
|
469
|
+
### `authenticate()`
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
authenticate(): void
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Initiates the authentication handshake by emitting the `authenticate` event to the server. The server will validate credentials from the socket handshake (headers, query, `auth` object) and respond with `authenticated` or `unauthenticated`.
|
|
476
|
+
|
|
477
|
+
Guard conditions (no-op with warning log):
|
|
478
|
+
- Socket is not connected (`!this.client?.connected`)
|
|
479
|
+
- Current state is not `unauthorized` (prevents double-auth or re-auth while authenticating)
|
|
480
|
+
|
|
481
|
+
On call:
|
|
482
|
+
1. Sets state to `authenticating`
|
|
483
|
+
2. Emits `SocketIOConstants.EVENT_AUTHENTICATE` (value: `'authenticate'`)
|
|
484
|
+
|
|
485
|
+
### `subscribe()`
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
subscribe<T = unknown>(opts: {
|
|
489
|
+
event: string;
|
|
490
|
+
handler: TSocketIOEventHandler<T>;
|
|
491
|
+
ignoreDuplicate?: boolean; // default: true
|
|
492
|
+
}): void
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Subscribes to a Socket.IO event with automatic error safety.
|
|
496
|
+
|
|
497
|
+
Guard conditions (no-op with warning log):
|
|
498
|
+
- `handler` is falsy
|
|
499
|
+
- `ignoreDuplicate` is `true` (default) and the event already has listeners
|
|
500
|
+
|
|
501
|
+
#### Handler Wrapping Pattern
|
|
502
|
+
|
|
503
|
+
Handlers are wrapped in a **dual try-catch** that catches both synchronous throws and asynchronous rejections:
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
const wrappedHandler = (data: T) => {
|
|
507
|
+
try {
|
|
508
|
+
Promise.resolve(handler(data)).catch(error => {
|
|
509
|
+
logger.error('Handler error | event: %s | error: %s', event, error);
|
|
510
|
+
});
|
|
511
|
+
} catch (error) {
|
|
512
|
+
logger.error('Handler error | event: %s | error: %s', event, error);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
The outer `try-catch` handles synchronous throws from the handler. The `.catch()` on `Promise.resolve()` handles async rejections. This ensures handler errors never crash the client.
|
|
518
|
+
|
|
519
|
+
#### `TSocketIOEventHandler<T>` Type
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Handlers can be synchronous (`void`) or asynchronous (`Promise<void>`). Both are handled correctly by the wrapping pattern.
|
|
526
|
+
|
|
527
|
+
### `subscribeMany()`
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
subscribeMany(opts: {
|
|
531
|
+
events: Record<string, TSocketIOEventHandler>;
|
|
532
|
+
ignoreDuplicate?: boolean;
|
|
533
|
+
}): void
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Batch subscribes to multiple events. Iterates over the `events` record and calls `subscribe()` for each entry.
|
|
537
|
+
|
|
538
|
+
### `unsubscribe()`
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
unsubscribe(opts: { event: string; handler?: TSocketIOEventHandler }): void
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Removes event listeners. If `handler` is provided, removes only that specific handler via `socket.off(event, handler)`. If `handler` is omitted, removes **all** handlers for the event via `socket.off(event)`.
|
|
545
|
+
|
|
546
|
+
No-op if the socket has no listeners for the event.
|
|
547
|
+
|
|
548
|
+
### `unsubscribeMany()`
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
unsubscribeMany(opts: { events: string[] }): void
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Removes all handlers for each event in the array. Calls `unsubscribe({ event })` for each entry.
|
|
555
|
+
|
|
556
|
+
### `connect()`
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
connect(): void
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Manually connects the socket. No-op with an info log if the client is not initialized. Useful when `autoConnect: false` is set in the options.
|
|
563
|
+
|
|
564
|
+
### `disconnect()`
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
disconnect(): void
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Manually disconnects the socket. No-op with an info log if the client is not initialized.
|
|
571
|
+
|
|
572
|
+
### `emit()`
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
emit<T = unknown>(opts: {
|
|
576
|
+
topic: string;
|
|
577
|
+
data: T;
|
|
578
|
+
doLog?: boolean; // default: false
|
|
579
|
+
cb?: () => void;
|
|
580
|
+
}): void
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Emits an event to the server.
|
|
584
|
+
|
|
585
|
+
**Throws** (via `getError()`) if:
|
|
586
|
+
- The socket is not connected (`statusCode: 400`, message: `"Invalid socket client state to emit"`)
|
|
587
|
+
- The `topic` is falsy (`statusCode: 400`, message: `"Topic is required to emit"`)
|
|
588
|
+
|
|
589
|
+
If `cb` is provided, it is executed via `setImmediate()` (asynchronously, not after server acknowledgment). If `doLog` is `true`, logs the topic and data.
|
|
590
|
+
|
|
591
|
+
### `joinRooms()`
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
joinRooms(opts: { rooms: string[] }): void
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Emits a `join` event to the server with `{ rooms }`. The server will validate via `validateRoomFn` and perform the actual join.
|
|
598
|
+
|
|
599
|
+
No-op with warning log if the socket is not connected.
|
|
600
|
+
|
|
601
|
+
### `leaveRooms()`
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
leaveRooms(opts: { rooms: string[] }): void
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Emits a `leave` event to the server with `{ rooms }`. The server performs the actual leave without validation.
|
|
608
|
+
|
|
609
|
+
No-op with warning log if the socket is not connected.
|
|
610
|
+
|
|
611
|
+
### `shutdown()`
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
shutdown(): void
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Clean shutdown of the client:
|
|
618
|
+
|
|
619
|
+
1. Calls `removeAllListeners()` on the underlying socket to prevent memory leaks
|
|
620
|
+
2. Disconnects if still connected
|
|
621
|
+
3. Resets state to `unauthorized`
|
|
622
|
+
|
|
623
|
+
## Internals
|
|
624
|
+
|
|
625
|
+
### `resolveBindings()`
|
|
626
|
+
|
|
627
|
+
Reads all binding keys from the DI container and validates required ones:
|
|
628
|
+
|
|
629
|
+
| Binding | Validation | Error on Failure |
|
|
630
|
+
|---------|-----------|------------------|
|
|
631
|
+
| `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` | -- |
|
|
632
|
+
| `REDIS_CONNECTION` | Must be `instanceof DefaultRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisHelper for single redis connection or RedisClusterHelper for redis cluster mode!"` |
|
|
633
|
+
| `AUTHENTICATE_HANDLER` | Must be a function (non-null) | `"Invalid authenticateFn to setup io socket server!"` |
|
|
634
|
+
| `VALIDATE_ROOM_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` | -- |
|
|
635
|
+
| `CLIENT_CONNECTED_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` | -- |
|
|
636
|
+
|
|
637
|
+
### `registerBunHook()`
|
|
638
|
+
|
|
639
|
+
Registers a post-start hook that:
|
|
640
|
+
|
|
641
|
+
1. Calls `createBunEngine({ serverOptions })` which dynamically imports `@socket.io/bun-engine` and creates a `BunEngine` instance with CORS config bridging
|
|
642
|
+
2. Creates `SocketIOServerHelper` with `runtime: RuntimeModules.BUN`
|
|
643
|
+
3. Awaits `socketIOHelper.configure()` which waits for all Redis connections to be ready before initializing the adapter and emitter
|
|
644
|
+
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
645
|
+
5. Gets the Bun server instance and Hono server, then calls `serverInstance.reload()` to wire the engine's `fetch` and `websocket` handlers into the running Bun server
|
|
646
|
+
|
|
647
|
+
### `createBunEngine()` Function
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
async function createBunEngine(opts: {
|
|
651
|
+
serverOptions: Partial<ServerOptions>;
|
|
652
|
+
}): Promise<{ engine: any; engineHandler: any }>
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Dynamically imports `@socket.io/bun-engine`, creates a `BunEngine` with CORS bridging, and returns both the `engine` and the `engineHandler` (from `engine.handler()`). The `engineHandler` provides the `websocket` handler that Bun's server needs.
|
|
656
|
+
|
|
657
|
+
### `createBunFetchHandler()` Function
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
function createBunFetchHandler(opts: {
|
|
661
|
+
engine: any;
|
|
662
|
+
enginePath: string;
|
|
663
|
+
honoServer: OpenAPIHono;
|
|
664
|
+
}): (req: Request, server: TBunServerInstance) => Response | Promise<Response>
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Returns a fetch handler function that routes requests:
|
|
668
|
+
- If `url.pathname` starts with `enginePath`, delegates to `engine.handleRequest(req, server)` (returns 404 Response if `handleRequest` returns nullish)
|
|
669
|
+
- Otherwise, delegates to `honoServer.fetch(req, server)` for normal Hono routing
|
|
670
|
+
|
|
671
|
+
### `registerNodeHook()`
|
|
672
|
+
|
|
673
|
+
Registers a post-start hook that:
|
|
674
|
+
|
|
675
|
+
1. Gets the HTTP server instance via `getServerInstance()`
|
|
676
|
+
2. Validates the server instance exists (throws `"HTTP server not available for Node.js runtime!"` if not)
|
|
677
|
+
3. Calls `createNodeSocketIOHelper()` which creates `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`
|
|
678
|
+
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
679
|
+
|
|
680
|
+
Node mode is simpler because Socket.IO natively attaches to `node:http.Server`.
|
|
681
|
+
|
|
682
|
+
### Redis 3-Client Architecture
|
|
683
|
+
|
|
684
|
+
The server helper creates 3 independent Redis connections from a single `DefaultRedisHelper`:
|
|
685
|
+
|
|
686
|
+
```
|
|
687
|
+
RedisHelper (parent -- NOT consumed)
|
|
688
|
+
|
|
|
689
|
+
+-- client.duplicate() --> redisPub (for Redis adapter -- publishes)
|
|
690
|
+
|
|
|
691
|
+
+-- client.duplicate() --> redisSub (for Redis adapter -- subscribes)
|
|
692
|
+
|
|
|
693
|
+
+-- client.duplicate() --> redisEmitter (for @socket.io/redis-emitter -- message delivery)
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**Why 3 clients?**
|
|
697
|
+
- `@socket.io/redis-adapter` requires separate pub and sub clients because a Redis connection in subscribe mode cannot execute other commands
|
|
698
|
+
- `@socket.io/redis-emitter` uses its own client to emit messages independently of the adapter, enabling cross-instance broadcasting even from contexts without a direct Socket.IO reference
|
|
699
|
+
- The parent `RedisHelper` connection remains independent and is not consumed -- it can be used for other purposes (e.g., caching, sessions)
|
|
700
|
+
|
|
701
|
+
**`TRedisClient` type:**
|
|
702
|
+
```typescript
|
|
703
|
+
type TRedisClient = Redis | Cluster;
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
This supports both single-instance `Redis` and `Cluster` connections from ioredis, making the helper transparent to the Redis deployment topology.
|
|
707
|
+
|
|
708
|
+
### `setRuntime()` -- Runtime Validation
|
|
709
|
+
|
|
710
|
+
The private `setRuntime()` method validates the constructor options based on the `runtime` discriminant:
|
|
711
|
+
|
|
712
|
+
| Runtime | Required Field | Error on Missing |
|
|
713
|
+
|---------|---------------|------------------|
|
|
714
|
+
| `RuntimeModules.NODE` | `opts.server` (HTTPServer) | `"Invalid HTTP server for Node.js runtime!"` |
|
|
715
|
+
| `RuntimeModules.BUN` | `opts.engine` (BunEngine) | `"Invalid @socket.io/bun-engine instance for Bun runtime!"` |
|
|
716
|
+
| Other | -- | `"Unsupported runtime!"` |
|
|
717
|
+
|
|
718
|
+
### `initRedisClients()` -- Redis Initialization
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
private initRedisClients(redisConnection: TSocketIOServerOptions['redisConnection']): void
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**Throws** if `redisConnection` is falsy: `"Invalid redis connection to config socket.io adapter!"`
|
|
725
|
+
|
|
726
|
+
Creates 3 duplicated clients from the parent connection's underlying ioredis client.
|
|
727
|
+
|
|
728
|
+
### `initIOServer()` -- IO Server Initialization
|
|
729
|
+
|
|
730
|
+
Called during `configure()` after Redis connections are ready:
|
|
731
|
+
|
|
732
|
+
| Runtime | Initialization |
|
|
733
|
+
|---------|---------------|
|
|
734
|
+
| `RuntimeModules.NODE` | `this.io = new IOServer(this.server, this.serverOptions)` |
|
|
735
|
+
| `RuntimeModules.BUN` | `this.io = new IOServer()` then `this.io.bind(this.bunEngine)` |
|
|
736
|
+
| Other | Throws `"Unsupported runtime: <runtime>"` |
|
|
737
|
+
|
|
738
|
+
Additional validation errors:
|
|
739
|
+
- Node.js without `this.server`: `"[DANGER] Invalid HTTP server instance to init Socket.io server!"`
|
|
740
|
+
- Bun without `this.bunEngine`: `"[DANGER] Invalid @socket.io/bun-engine instance to init Socket.io server!"`
|
|
741
|
+
|
|
742
|
+
### Connection Lifecycle
|
|
743
|
+
|
|
744
|
+
When a client connects, the server manages a strict authentication flow:
|
|
745
|
+
|
|
746
|
+
```
|
|
747
|
+
Client connects
|
|
748
|
+
|
|
|
749
|
+
+-- onClientConnect({ socket })
|
|
750
|
+
| +-- Validate socket exists and not duplicate
|
|
751
|
+
| +-- Create ISocketIOClient entry (state: UNAUTHORIZED)
|
|
752
|
+
| +-- Start authenticateTimeout (10s default)
|
|
753
|
+
| +-- Register 'disconnect' handler
|
|
754
|
+
| +-- Register 'authenticate' handler
|
|
755
|
+
|
|
|
756
|
+
+-- Client emits 'authenticate'
|
|
757
|
+
| +-- Validate client exists and state is UNAUTHORIZED
|
|
758
|
+
| +-- Set state to AUTHENTICATING
|
|
759
|
+
| +-- Call authenticateFn(handshake)
|
|
760
|
+
| +-- Success -> onClientAuthenticated()
|
|
761
|
+
| | +-- Set state to AUTHENTICATED
|
|
762
|
+
| | +-- Send initial ping
|
|
763
|
+
| | +-- Join default rooms (io-default, io-notification)
|
|
764
|
+
| | +-- Register 'join' and 'leave' room handlers
|
|
765
|
+
| | +-- Start ping interval (30s default)
|
|
766
|
+
| | +-- Emit 'authenticated' with { id, time }
|
|
767
|
+
| | +-- Call clientConnectedFn({ socket }) if provided
|
|
768
|
+
| +-- Failure -> emit 'unauthenticated' -> disconnect
|
|
769
|
+
|
|
|
770
|
+
+-- Timeout (10s) -> disconnect if not AUTHENTICATED
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
#### Authentication Failure -- Two Code Paths
|
|
774
|
+
|
|
775
|
+
The `registerAuthHandler()` method handles authentication results through two distinct code paths:
|
|
776
|
+
|
|
777
|
+
**Path 1: `authenticateFn` returns `false`** (`.then()` handler):
|
|
778
|
+
- Sets client state back to `UNAUTHORIZED`
|
|
779
|
+
- Sends `unauthenticated` event with message: `"Invalid token to authenticate! Please login again!"`
|
|
780
|
+
- Disconnects after send via `setImmediate` callback
|
|
781
|
+
- No error logging (this is an expected outcome)
|
|
782
|
+
|
|
783
|
+
**Path 2: `authenticateFn` throws an error** (`.catch()` handler):
|
|
784
|
+
- Sets client state back to `UNAUTHORIZED`
|
|
785
|
+
- Logs the error at error level
|
|
786
|
+
- Sends `unauthenticated` event with message: `"Failed to authenticate connection! Please login again!"`
|
|
787
|
+
- Sets `doLog: true` on the send call (unlike Path 1)
|
|
788
|
+
- Disconnects after send via `setImmediate` callback
|
|
789
|
+
|
|
790
|
+
Both paths also handle the edge case where the client disconnected *during* authentication -- they check `this.clients.has(id)` before proceeding.
|
|
791
|
+
|
|
792
|
+
#### `ISocketIOClient` Interface
|
|
793
|
+
```typescript
|
|
794
|
+
interface ISocketIOClient {
|
|
795
|
+
id: string;
|
|
796
|
+
socket: IOSocket;
|
|
797
|
+
state: TSocketIOClientState; // 'unauthorized' | 'authenticating' | 'authenticated'
|
|
798
|
+
interval?: NodeJS.Timeout; // Ping interval (set after auth)
|
|
799
|
+
authenticateTimeout: NodeJS.Timeout; // Auth deadline (cleared on success)
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### Room Handlers
|
|
804
|
+
|
|
805
|
+
Room join/leave handlers are registered after successful authentication:
|
|
806
|
+
|
|
807
|
+
- **`join`**: Client emits `{ rooms: string[] }`. If `validateRoomFn` is configured, only the rooms it returns are joined. If `validateRoomFn` is **not** configured, join is silently rejected with a warning log.
|
|
808
|
+
- **`leave`**: Client emits `{ rooms: string[] }`. Leave is always allowed -- no validation function needed.
|
|
809
|
+
|
|
810
|
+
Both handlers parse the payload defensively: `const { rooms = [] } = payload || { rooms: [] }`. Empty `rooms` arrays are silently ignored.
|
|
811
|
+
|
|
812
|
+
Join handler validation errors are caught and logged but do not disconnect the client.
|
|
813
|
+
|
|
814
|
+
> [!WARNING]
|
|
815
|
+
> Without a `validateRoomFn` bound, clients **cannot** join any custom rooms. They will only be in the default rooms (`io-default`, `io-notification`). This is a security-by-default design.
|
|
816
|
+
|
|
817
|
+
## Types Reference
|
|
818
|
+
|
|
819
|
+
### Server Types
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
// Server constructor options -- discriminated union on 'runtime'
|
|
823
|
+
type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
|
|
824
|
+
|
|
825
|
+
// Base options shared by both runtimes
|
|
826
|
+
interface ISocketIOServerBaseOptions {
|
|
827
|
+
identifier: string;
|
|
828
|
+
serverOptions: Partial<ServerOptions>;
|
|
829
|
+
redisConnection: DefaultRedisHelper;
|
|
830
|
+
defaultRooms?: string[];
|
|
831
|
+
authenticateTimeout?: number;
|
|
832
|
+
pingInterval?: number;
|
|
833
|
+
authenticateFn: TSocketIOAuthenticateFn;
|
|
834
|
+
validateRoomFn?: TSocketIOValidateRoomFn;
|
|
835
|
+
clientConnectedFn?: TSocketIOClientConnectedFn;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Node.js runtime variant
|
|
839
|
+
interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
|
|
840
|
+
runtime: typeof RuntimeModules.NODE;
|
|
841
|
+
server: HTTPServer;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Bun runtime variant
|
|
845
|
+
interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
|
|
846
|
+
runtime: typeof RuntimeModules.BUN;
|
|
847
|
+
engine: any;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Tracked client entry (server-side)
|
|
851
|
+
interface ISocketIOClient {
|
|
852
|
+
id: string;
|
|
853
|
+
socket: IOSocket;
|
|
854
|
+
state: TSocketIOClientState;
|
|
855
|
+
interval?: NodeJS.Timeout;
|
|
856
|
+
authenticateTimeout: NodeJS.Timeout;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Redis client type alias
|
|
860
|
+
type TRedisClient = Redis | Cluster;
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### Client Types
|
|
864
|
+
|
|
865
|
+
```typescript
|
|
866
|
+
// Client constructor options
|
|
867
|
+
interface ISocketIOClientOptions {
|
|
868
|
+
identifier: string;
|
|
869
|
+
host: string;
|
|
870
|
+
options: IOptions;
|
|
871
|
+
onConnected?: () => ValueOrPromise<void>;
|
|
872
|
+
onDisconnected?: (reason: string) => ValueOrPromise<void>;
|
|
873
|
+
onError?: (error: Error) => ValueOrPromise<void>;
|
|
874
|
+
onAuthenticated?: () => ValueOrPromise<void>;
|
|
875
|
+
onUnauthenticated?: (message: string) => ValueOrPromise<void>;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Socket connection options (extends socket.io-client's SocketOptions)
|
|
879
|
+
interface IOptions extends SocketOptions {
|
|
880
|
+
path: string;
|
|
881
|
+
extraHeaders: Record<string | symbol | number, any>;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Event handler type (supports sync and async)
|
|
885
|
+
type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
|
|
886
|
+
|
|
887
|
+
// Client state type
|
|
888
|
+
type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
|
|
889
|
+
// Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
### Callback Types
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
// Server authentication handler
|
|
896
|
+
type TSocketIOAuthenticateFn = (args: IHandshake) => ValueOrPromise<boolean>;
|
|
897
|
+
|
|
898
|
+
// Server room validation handler
|
|
899
|
+
type TSocketIOValidateRoomFn = (opts: {
|
|
900
|
+
socket: IOSocket;
|
|
901
|
+
rooms: string[];
|
|
902
|
+
}) => ValueOrPromise<string[]>;
|
|
903
|
+
|
|
904
|
+
// Server client connected handler
|
|
905
|
+
type TSocketIOClientConnectedFn = (opts: { socket: IOSocket }) => ValueOrPromise<void>;
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### Component Types
|
|
909
|
+
|
|
910
|
+
```typescript
|
|
911
|
+
// Extended ServerOptions with identifier
|
|
912
|
+
interface IServerOptions extends ServerOptions {
|
|
913
|
+
identifier: string;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Resolved binding values from DI container
|
|
917
|
+
interface IResolvedBindings {
|
|
918
|
+
redisConnection: DefaultRedisHelper;
|
|
919
|
+
authenticateFn: TSocketIOAuthenticateFn;
|
|
920
|
+
validateRoomFn?: TSocketIOValidateRoomFn;
|
|
921
|
+
clientConnectedFn?: TSocketIOClientConnectedFn;
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
## Post-Start Hook System
|
|
926
|
+
|
|
927
|
+
The Socket.IO component uses post-start hooks to solve a timing problem: Socket.IO needs a running server, but components initialize before the server starts.
|
|
928
|
+
|
|
929
|
+
The component relies on `AbstractApplication`'s post-start hook system:
|
|
930
|
+
|
|
931
|
+
#### API
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
// Register a hook (during binding phase)
|
|
935
|
+
application.registerPostStartHook({
|
|
936
|
+
identifier: string, // Unique name for logging
|
|
937
|
+
hook: () => ValueOrPromise<void>, // Async function to execute
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Get the server instance (available after start)
|
|
941
|
+
application.getServerInstance<T>(): T | undefined;
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
#### Hook Execution Flow
|
|
945
|
+
|
|
946
|
+
```
|
|
947
|
+
Application.start()
|
|
948
|
+
|
|
|
949
|
+
+-- Bun.serve() / serve() <-- Server created
|
|
950
|
+
|
|
|
951
|
+
+-- executePostStartHooks() <-- Hooks run here
|
|
952
|
+
|
|
|
953
|
+
+-- SocketIOComponent hook:
|
|
954
|
+
1. Get server instance via getServerInstance()
|
|
955
|
+
2. Create SocketIOServerHelper with runtime-specific options
|
|
956
|
+
3. Call helper.configure() to initialize Socket.IO server
|
|
957
|
+
4. Bind the helper instance for injection
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
#### Detailed Hook Timing
|
|
961
|
+
```
|
|
962
|
+
executePostStartHooks()
|
|
963
|
+
|-- Hook 1: "socket-io-initialize"
|
|
964
|
+
| |-- performance.now() -> start
|
|
965
|
+
| |-- await hook()
|
|
966
|
+
| +-- log: "Executed hook | identifier: socket-io-initialize | took: 12.5 (ms)"
|
|
967
|
+
|-- Hook 2: "another-hook"
|
|
968
|
+
| +-- ...
|
|
969
|
+
+-- (hooks run sequentially in registration order)
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
- Hooks run **sequentially** (not parallel) to guarantee ordering
|
|
973
|
+
- Each hook is timed with `performance.now()` for diagnostics
|
|
974
|
+
- If a hook throws, it propagates to `start()` and the server fails to start
|
|
975
|
+
|
|
976
|
+
#### What Happens Inside the Hook
|
|
977
|
+
|
|
978
|
+
For **Bun runtime**, the hook:
|
|
979
|
+
|
|
980
|
+
1. Calls `createBunEngine({ serverOptions })` which dynamically imports `@socket.io/bun-engine` and creates a `BunEngine` instance with CORS config bridging
|
|
981
|
+
2. Creates `SocketIOServerHelper` with `runtime: RuntimeModules.BUN` and the engine
|
|
982
|
+
3. Awaits `socketIOHelper.configure()` which connects Redis pub/sub/emitter clients, initializes the `IOServer`, and sets up the Redis adapter
|
|
983
|
+
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
984
|
+
5. Gets the Bun server instance and Hono server
|
|
985
|
+
6. Calls `serverInstance.reload({ fetch, websocket })` to wire the engine's fetch and websocket handlers into the running Bun server, where `fetch` is the result of `createBunFetchHandler()` and `websocket` is from `engineHandler.websocket`
|
|
986
|
+
|
|
987
|
+
For **Node.js runtime**, the hook:
|
|
988
|
+
|
|
989
|
+
1. Gets the HTTP server instance via `getServerInstance()`
|
|
990
|
+
2. Validates the server instance exists (throws if not)
|
|
991
|
+
3. Calls `createNodeSocketIOHelper()` which creates `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`
|
|
992
|
+
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
993
|
+
|
|
994
|
+
> [!NOTE]
|
|
995
|
+
> The hook identifier is `'socket-io-initialize'` for both runtimes. Only one runtime path executes per application.
|
|
996
|
+
|
|
997
|
+
### Graceful Shutdown
|
|
998
|
+
|
|
999
|
+
Always shut down the Socket.IO server before stopping the application:
|
|
1000
|
+
|
|
1001
|
+
#### Shutdown Implementation
|
|
1002
|
+
```typescript
|
|
1003
|
+
override async stop(): Promise<void> {
|
|
1004
|
+
// 1. Shut down Socket.IO (disconnects all clients, closes IO server, quits Redis)
|
|
1005
|
+
const socketIOHelper = this.get<SocketIOServerHelper>({
|
|
1006
|
+
key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
|
|
1007
|
+
isOptional: true,
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
if (socketIOHelper) {
|
|
1011
|
+
await socketIOHelper.shutdown();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// 2. Disconnect Redis helper
|
|
1015
|
+
if (this.redisHelper) {
|
|
1016
|
+
await this.redisHelper.disconnect();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// 3. Stop the HTTP/Bun server
|
|
1020
|
+
await super.stop();
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
#### Shutdown Flow
|
|
1025
|
+
```
|
|
1026
|
+
socketIOHelper.shutdown()
|
|
1027
|
+
|-- Disconnect all tracked clients
|
|
1028
|
+
| |-- clearInterval(ping)
|
|
1029
|
+
| |-- clearTimeout(authenticateTimeout)
|
|
1030
|
+
| +-- socket.disconnect()
|
|
1031
|
+
|-- clients.clear()
|
|
1032
|
+
|-- io.close() -- closes the Socket.IO server (async)
|
|
1033
|
+
+-- Redis cleanup
|
|
1034
|
+
|-- redisPub.quit()
|
|
1035
|
+
|-- redisSub.quit()
|
|
1036
|
+
+-- redisEmitter.quit()
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
Client helper shutdown:
|
|
1040
|
+
```
|
|
1041
|
+
clientHelper.shutdown()
|
|
1042
|
+
|-- removeAllListeners() -- prevents memory leaks
|
|
1043
|
+
|-- disconnect() -- if still connected
|
|
1044
|
+
+-- state = UNAUTHORIZED
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
## See Also
|
|
1048
|
+
|
|
1049
|
+
- [Setup & Configuration](./) -- Quick reference, installation, bindings, constants
|
|
1050
|
+
- [Usage & Examples](./usage) -- Server-side usage, client helper, advanced patterns
|
|
1051
|
+
- [Error Reference](./errors) -- Error conditions and troubleshooting
|