@venizia/ignis-docs 0.0.4 → 0.0.5
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 +2 -2
- package/wiki/best-practices/code-style-standards/function-patterns.md +31 -5
- package/wiki/best-practices/performance-optimization.md +39 -0
- package/wiki/best-practices/troubleshooting-tips.md +24 -3
- package/wiki/guides/tutorials/realtime-chat.md +724 -460
- package/wiki/references/base/filter-system/json-filtering.md +4 -2
- package/wiki/references/base/middlewares.md +24 -72
- package/wiki/references/base/repositories/advanced.md +83 -0
- package/wiki/references/base/repositories/index.md +1 -1
- package/wiki/references/components/socket-io.md +495 -78
- package/wiki/references/helpers/logger.md +222 -43
- package/wiki/references/helpers/network.md +273 -31
- package/wiki/references/helpers/socket-io.md +881 -71
- package/wiki/references/quick-reference.md +3 -5
- package/wiki/references/src-details/core.md +1 -2
- package/wiki/references/utilities/statuses.md +4 -2
|
@@ -1,145 +1,562 @@
|
|
|
1
1
|
# Socket.IO Component
|
|
2
2
|
|
|
3
|
-
Real-time, bidirectional, event-based communication using Socket.IO.
|
|
3
|
+
Real-time, bidirectional, event-based communication using Socket.IO — with automatic runtime detection for both **Node.js** and **Bun**.
|
|
4
4
|
|
|
5
5
|
## Quick Reference
|
|
6
6
|
|
|
7
|
-
|
|
|
8
|
-
|
|
9
|
-
| **
|
|
10
|
-
| **
|
|
11
|
-
| **
|
|
12
|
-
| **
|
|
7
|
+
| Item | Value |
|
|
8
|
+
|------|-------|
|
|
9
|
+
| **Package** | `@venizia/ignis` (core) |
|
|
10
|
+
| **Class** | `SocketIOComponent` |
|
|
11
|
+
| **Helper** | [`SocketIOServerHelper`](/references/helpers/socket-io) |
|
|
12
|
+
| **Runtimes** | Node.js (`@hono/node-server`) and Bun (native) |
|
|
13
|
+
| **Scaling** | `@socket.io/redis-adapter` + `@socket.io/redis-emitter` |
|
|
13
14
|
|
|
14
|
-
###
|
|
15
|
+
### Binding Keys
|
|
15
16
|
|
|
16
|
-
| Binding Key | Type |
|
|
17
|
-
|
|
18
|
-
| `SERVER_OPTIONS` | `
|
|
19
|
-
| `REDIS_CONNECTION` | `DefaultRedisHelper` |
|
|
20
|
-
| `AUTHENTICATE_HANDLER` | `
|
|
21
|
-
| `
|
|
17
|
+
| Binding Key | Constant | Type | Required | Default |
|
|
18
|
+
|------------|----------|------|----------|---------|
|
|
19
|
+
| `@app/socket-io/server-options` | `SocketIOBindingKeys.SERVER_OPTIONS` | `Partial<IServerOptions>` | No | See [Default Options](#default-server-options) |
|
|
20
|
+
| `@app/socket-io/redis-connection` | `SocketIOBindingKeys.REDIS_CONNECTION` | `RedisHelper` / `DefaultRedisHelper` | **Yes** | `null` |
|
|
21
|
+
| `@app/socket-io/authenticate-handler` | `SocketIOBindingKeys.AUTHENTICATE_HANDLER` | `TSocketIOAuthenticateFn` | **Yes** | `null` |
|
|
22
|
+
| `@app/socket-io/validate-room-handler` | `SocketIOBindingKeys.VALIDATE_ROOM_HANDLER` | `TSocketIOValidateRoomFn` | No | `null` |
|
|
23
|
+
| `@app/socket-io/client-connected-handler` | `SocketIOBindingKeys.CLIENT_CONNECTED_HANDLER` | `TSocketIOClientConnectedFn` | No | `null` |
|
|
24
|
+
| `@app/socket-io/instance` | `SocketIOBindingKeys.SOCKET_IO_INSTANCE` | `SocketIOServerHelper` | — | *Set by component* |
|
|
25
|
+
|
|
26
|
+
> [!NOTE]
|
|
27
|
+
> `SOCKET_IO_INSTANCE` is **not** set by you — the component creates and binds it automatically after the server starts. Inject it in services/controllers to interact with Socket.IO.
|
|
22
28
|
|
|
23
29
|
### Use Cases
|
|
24
30
|
|
|
25
|
-
- Live notifications
|
|
26
|
-
- Real-time chat
|
|
27
|
-
- Collaborative editing
|
|
28
|
-
- Live data streams
|
|
31
|
+
- Live notifications and alerts
|
|
32
|
+
- Real-time chat and messaging
|
|
33
|
+
- Collaborative editing (docs, whiteboards)
|
|
34
|
+
- Live data streams (dashboards, monitoring)
|
|
35
|
+
- Multiplayer game state synchronization
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
---
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
- **`SocketIOServerHelper`**: Wraps Socket.IO server, provides interaction methods
|
|
34
|
-
- **`@socket.io/redis-adapter`**: Scales Socket.IO with Redis
|
|
35
|
-
- **`@socket.io/redis-emitter`**: Emits events from external processes
|
|
36
|
-
- **Integration**: Works with HTTP server and authentication system
|
|
39
|
+
## Architecture Overview
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
```
|
|
42
|
+
SocketIOComponent
|
|
43
|
+
┌──────────────────────────────────────────┐
|
|
44
|
+
│ │
|
|
45
|
+
│ binding() │
|
|
46
|
+
│ ├── resolveBindings() │
|
|
47
|
+
│ │ ├── SERVER_OPTIONS │
|
|
48
|
+
│ │ ├── REDIS_CONNECTION │
|
|
49
|
+
│ │ ├── AUTHENTICATE_HANDLER │
|
|
50
|
+
│ │ ├── VALIDATE_ROOM_HANDLER │
|
|
51
|
+
│ │ └── CLIENT_CONNECTED_HANDLER │
|
|
52
|
+
│ │ │
|
|
53
|
+
│ └── RuntimeModules.detect() │
|
|
54
|
+
│ ├── BUN → registerBunHook() │
|
|
55
|
+
│ └── NODE → registerNodeHook() │
|
|
56
|
+
│ │
|
|
57
|
+
│ (Post-start hooks execute after server) │
|
|
58
|
+
│ ├── Creates SocketIOServerHelper │
|
|
59
|
+
│ ├── await socketIOHelper.configure() │
|
|
60
|
+
│ ├── Binds to SOCKET_IO_INSTANCE │
|
|
61
|
+
│ └── Wires into server (runtime-specific)│
|
|
62
|
+
└──────────────────────────────────────────┘
|
|
63
|
+
```
|
|
39
64
|
|
|
40
|
-
###
|
|
65
|
+
### Lifecycle Integration
|
|
41
66
|
|
|
42
|
-
-
|
|
43
|
-
- **`@socket.io/redis-adapter`**
|
|
44
|
-
- **`@socket.io/redis-emitter`**
|
|
45
|
-
- **`ioredis`** (if using Redis)
|
|
67
|
+
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.
|
|
46
68
|
|
|
47
|
-
|
|
69
|
+
```
|
|
70
|
+
Application Lifecycle
|
|
71
|
+
═════════════════════
|
|
72
|
+
|
|
73
|
+
┌─────────────────┐
|
|
74
|
+
│ preConfigure() │ ← Register SocketIOComponent here
|
|
75
|
+
└────────┬────────┘
|
|
76
|
+
│
|
|
77
|
+
┌────────▼────────┐
|
|
78
|
+
│ initialize() │ ← Component.binding() runs here
|
|
79
|
+
│ │ Resolves bindings, registers post-start hook
|
|
80
|
+
└────────┬────────┘
|
|
81
|
+
│
|
|
82
|
+
┌────────▼────────┐
|
|
83
|
+
│ setupMiddlewares │
|
|
84
|
+
└────────┬────────┘
|
|
85
|
+
│
|
|
86
|
+
┌────────▼──────────────┐
|
|
87
|
+
│ startBunModule() OR │ ← Server starts, instance created
|
|
88
|
+
│ startNodeModule() │
|
|
89
|
+
└────────┬──────────────┘
|
|
90
|
+
│
|
|
91
|
+
┌────────▼──────────────────┐
|
|
92
|
+
│ executePostStartHooks() │ ← SocketIOServerHelper created HERE
|
|
93
|
+
│ └── socket-io-initialize│ Server instance is now available
|
|
94
|
+
└───────────────────────────┘
|
|
95
|
+
```
|
|
48
96
|
|
|
49
|
-
|
|
97
|
+
### Runtime-Specific Behavior
|
|
50
98
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
| Aspect | Node.js | Bun |
|
|
100
|
+
|--------|---------|-----|
|
|
101
|
+
| **Server Type** | `node:http.Server` | `Bun.Server` |
|
|
102
|
+
| **IO Server Init** | `new IOServer(httpServer, opts)` | `new IOServer()` + `io.bind(engine)` |
|
|
103
|
+
| **Engine** | Built-in (`socket.io`) | `@socket.io/bun-engine` (optional peer dep) |
|
|
104
|
+
| **Request Routing** | Socket.IO attaches to HTTP server automatically | `server.reload({ fetch, websocket })` wires engine into Bun's request loop |
|
|
105
|
+
| **WebSocket Upgrade** | Handled by `node:http.Server` upgrade event | Handled by Bun's `websocket` handler |
|
|
106
|
+
| **Dynamic Import** | None needed | `await import('@socket.io/bun-engine')` at runtime |
|
|
55
107
|
|
|
56
|
-
|
|
108
|
+
---
|
|
57
109
|
|
|
58
|
-
|
|
110
|
+
## Setup Guide
|
|
111
|
+
|
|
112
|
+
### Step 1: Install Dependencies
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Core dependency (already included via @venizia/ignis)
|
|
116
|
+
# ioredis is required for the Redis adapter
|
|
117
|
+
|
|
118
|
+
# For Bun runtime only — optional peer dependency
|
|
119
|
+
bun add @socket.io/bun-engine
|
|
120
|
+
```
|
|
59
121
|
|
|
60
|
-
|
|
122
|
+
### Step 2: Bind Required Services
|
|
123
|
+
|
|
124
|
+
In your application's `preConfigure()` method, bind the required services and register the component:
|
|
61
125
|
|
|
62
126
|
```typescript
|
|
63
127
|
import {
|
|
128
|
+
BaseApplication,
|
|
64
129
|
SocketIOComponent,
|
|
65
130
|
SocketIOBindingKeys,
|
|
66
|
-
RedisHelper,
|
|
67
|
-
|
|
131
|
+
RedisHelper,
|
|
132
|
+
TSocketIOAuthenticateFn,
|
|
133
|
+
TSocketIOValidateRoomFn,
|
|
134
|
+
TSocketIOClientConnectedFn,
|
|
68
135
|
ValueOrPromise,
|
|
69
|
-
IHandshake,
|
|
70
136
|
} from '@venizia/ignis';
|
|
71
137
|
|
|
72
|
-
// ...
|
|
73
|
-
|
|
74
138
|
export class Application extends BaseApplication {
|
|
75
|
-
|
|
139
|
+
private redisHelper: RedisHelper;
|
|
76
140
|
|
|
77
141
|
preConfigure(): ValueOrPromise<void> {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
142
|
+
this.setupSocketIO();
|
|
143
|
+
// ... other setup
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setupSocketIO() {
|
|
147
|
+
// 1. Redis connection (required for adapter + emitter)
|
|
148
|
+
this.redisHelper = new RedisHelper({
|
|
149
|
+
name: 'socket-io-redis',
|
|
150
|
+
host: process.env.REDIS_HOST ?? 'localhost',
|
|
151
|
+
port: +(process.env.REDIS_PORT ?? 6379),
|
|
152
|
+
password: process.env.REDIS_PASSWORD,
|
|
153
|
+
autoConnect: false,
|
|
86
154
|
});
|
|
87
|
-
this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION }).toValue(redis);
|
|
88
155
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
156
|
+
this.bind<RedisHelper>({
|
|
157
|
+
key: SocketIOBindingKeys.REDIS_CONNECTION,
|
|
158
|
+
}).toValue(this.redisHelper);
|
|
159
|
+
|
|
160
|
+
// 2. Authentication handler (required)
|
|
161
|
+
const authenticateFn: TSocketIOAuthenticateFn = handshake => {
|
|
162
|
+
const token = handshake.headers.authorization;
|
|
163
|
+
// Implement your auth logic — JWT verification, session check, etc.
|
|
94
164
|
return !!token;
|
|
95
165
|
};
|
|
96
|
-
this.bind({ key: SocketIOBindingKeys.AUTHENTICATE_HANDLER }).toValue(authenticateFn);
|
|
97
166
|
|
|
98
|
-
|
|
167
|
+
this.bind<TSocketIOAuthenticateFn>({
|
|
168
|
+
key: SocketIOBindingKeys.AUTHENTICATE_HANDLER,
|
|
169
|
+
}).toValue(authenticateFn);
|
|
170
|
+
|
|
171
|
+
// 3. Room validation handler (optional — joins rejected without this)
|
|
172
|
+
const validateRoomFn: TSocketIOValidateRoomFn = ({ socket, rooms }) => {
|
|
173
|
+
// Return the rooms that the client is allowed to join
|
|
174
|
+
const allowedRooms = rooms.filter(room => room.startsWith('public-'));
|
|
175
|
+
return allowedRooms;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
this.bind<TSocketIOValidateRoomFn>({
|
|
179
|
+
key: SocketIOBindingKeys.VALIDATE_ROOM_HANDLER,
|
|
180
|
+
}).toValue(validateRoomFn);
|
|
181
|
+
|
|
182
|
+
// 4. Client connected handler (optional)
|
|
183
|
+
const clientConnectedFn: TSocketIOClientConnectedFn = ({ socket }) => {
|
|
184
|
+
console.log('Client connected:', socket.id);
|
|
185
|
+
// Register custom event handlers on the socket
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
this.bind<TSocketIOClientConnectedFn>({
|
|
189
|
+
key: SocketIOBindingKeys.CLIENT_CONNECTED_HANDLER,
|
|
190
|
+
}).toValue(clientConnectedFn);
|
|
191
|
+
|
|
192
|
+
// 5. Register the component — that's it!
|
|
99
193
|
this.component(SocketIOComponent);
|
|
100
194
|
}
|
|
101
|
-
|
|
102
|
-
// ...
|
|
103
195
|
}
|
|
104
196
|
```
|
|
105
197
|
|
|
106
|
-
|
|
198
|
+
### Step 3: Use in Services/Controllers
|
|
107
199
|
|
|
108
|
-
|
|
200
|
+
Inject `SocketIOServerHelper` to interact with Socket.IO:
|
|
109
201
|
|
|
110
202
|
```typescript
|
|
111
|
-
import {
|
|
203
|
+
import {
|
|
204
|
+
BaseService,
|
|
205
|
+
inject,
|
|
206
|
+
SocketIOBindingKeys,
|
|
207
|
+
SocketIOServerHelper,
|
|
208
|
+
CoreBindings,
|
|
209
|
+
BaseApplication,
|
|
210
|
+
} from '@venizia/ignis';
|
|
211
|
+
|
|
212
|
+
export class NotificationService extends BaseService {
|
|
213
|
+
// Lazy getter pattern — helper is bound AFTER server starts
|
|
214
|
+
private _io: SocketIOServerHelper | null = null;
|
|
215
|
+
|
|
216
|
+
constructor(
|
|
217
|
+
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
|
|
218
|
+
private application: BaseApplication,
|
|
219
|
+
) {
|
|
220
|
+
super({ scope: NotificationService.name });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private get io(): SocketIOServerHelper {
|
|
224
|
+
if (!this._io) {
|
|
225
|
+
this._io = this.application.get<SocketIOServerHelper>({
|
|
226
|
+
key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
|
|
227
|
+
isOptional: true,
|
|
228
|
+
}) ?? null;
|
|
229
|
+
}
|
|
112
230
|
|
|
113
|
-
|
|
231
|
+
if (!this._io) {
|
|
232
|
+
throw new Error('SocketIO not initialized');
|
|
233
|
+
}
|
|
114
234
|
|
|
115
|
-
|
|
116
|
-
|
|
235
|
+
return this._io;
|
|
236
|
+
}
|
|
117
237
|
|
|
118
|
-
|
|
238
|
+
// Send to a specific client
|
|
239
|
+
notifyUser(opts: { userId: string; message: string }) {
|
|
119
240
|
this.io.send({
|
|
120
|
-
destination: userId,
|
|
241
|
+
destination: opts.userId,
|
|
121
242
|
payload: {
|
|
122
243
|
topic: 'notification',
|
|
123
|
-
data: { message },
|
|
244
|
+
data: { message: opts.message, time: new Date().toISOString() },
|
|
124
245
|
},
|
|
125
246
|
});
|
|
126
247
|
}
|
|
248
|
+
|
|
249
|
+
// Send to a room
|
|
250
|
+
notifyRoom(opts: { room: string; message: string }) {
|
|
251
|
+
this.io.send({
|
|
252
|
+
destination: opts.room,
|
|
253
|
+
payload: {
|
|
254
|
+
topic: 'room:update',
|
|
255
|
+
data: { message: opts.message },
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Broadcast to all clients
|
|
261
|
+
broadcastAnnouncement(opts: { message: string }) {
|
|
262
|
+
this.io.send({
|
|
263
|
+
payload: {
|
|
264
|
+
topic: 'system:announcement',
|
|
265
|
+
data: { message: opts.message },
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
127
270
|
```
|
|
128
271
|
|
|
272
|
+
> [!IMPORTANT]
|
|
273
|
+
> **Lazy getter pattern**: Since `SocketIOServerHelper` is bound via a post-start hook, it's not available during DI construction. Use a lazy getter that resolves from the application container on first access.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Default Server Options
|
|
278
|
+
|
|
279
|
+
The component applies these defaults if `SocketIOBindingKeys.SERVER_OPTIONS` is not bound or partially overridden:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const DEFAULT_SERVER_OPTIONS = {
|
|
283
|
+
identifier: 'SOCKET_IO_SERVER',
|
|
284
|
+
path: '/io',
|
|
285
|
+
cors: {
|
|
286
|
+
origin: '*',
|
|
287
|
+
methods: ['GET', 'POST'],
|
|
288
|
+
preflightContinue: false,
|
|
289
|
+
optionsSuccessStatus: 204,
|
|
290
|
+
credentials: true,
|
|
291
|
+
},
|
|
292
|
+
perMessageDeflate: {
|
|
293
|
+
threshold: 4096,
|
|
294
|
+
zlibDeflateOptions: { chunkSize: 10 * 1024 },
|
|
295
|
+
zlibInflateOptions: { windowBits: 12, memLevel: 8 },
|
|
296
|
+
clientNoContextTakeover: true,
|
|
297
|
+
serverNoContextTakeover: true,
|
|
298
|
+
serverMaxWindowBits: 10,
|
|
299
|
+
concurrencyLimit: 20,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
| Option | Default | Description |
|
|
305
|
+
|--------|---------|-------------|
|
|
306
|
+
| `identifier` | `'SOCKET_IO_SERVER'` | Unique identifier for the helper instance |
|
|
307
|
+
| `path` | `'/io'` | URL path for Socket.IO handshake/polling |
|
|
308
|
+
| `cors.origin` | `'*'` | Allowed origins (restrict in production!) |
|
|
309
|
+
| `cors.credentials` | `true` | Allow cookies/auth headers |
|
|
310
|
+
| `perMessageDeflate` | Enabled | WebSocket compression settings |
|
|
311
|
+
|
|
312
|
+
### Custom Server Options
|
|
313
|
+
|
|
314
|
+
Override defaults by binding custom options:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
this.bind<Partial<IServerOptions>>({
|
|
318
|
+
key: SocketIOBindingKeys.SERVER_OPTIONS,
|
|
319
|
+
}).toValue({
|
|
320
|
+
identifier: 'my-app-socket',
|
|
321
|
+
path: '/socket.io',
|
|
322
|
+
cors: {
|
|
323
|
+
origin: ['https://myapp.com', 'https://admin.myapp.com'],
|
|
324
|
+
methods: ['GET', 'POST'],
|
|
325
|
+
credentials: true,
|
|
326
|
+
},
|
|
327
|
+
pingTimeout: 60000,
|
|
328
|
+
pingInterval: 25000,
|
|
329
|
+
maxHttpBufferSize: 1e6, // 1MB
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Component Internals
|
|
336
|
+
|
|
337
|
+
### `resolveBindings()`
|
|
338
|
+
|
|
339
|
+
Reads all binding keys from the DI container and validates required ones:
|
|
340
|
+
|
|
341
|
+
| Binding | Validation | Error on Failure |
|
|
342
|
+
|---------|-----------|------------------|
|
|
343
|
+
| `SERVER_OPTIONS` | Optional, merged with defaults | — |
|
|
344
|
+
| `REDIS_CONNECTION` | Must be `instanceof DefaultRedisHelper` | `"Invalid instance of redisConnection"` |
|
|
345
|
+
| `AUTHENTICATE_HANDLER` | Must be a function (non-null) | `"Invalid authenticateFn"` |
|
|
346
|
+
| `VALIDATE_ROOM_HANDLER` | Optional, checked via `isBound()` | — |
|
|
347
|
+
| `CLIENT_CONNECTED_HANDLER` | Optional, checked via `isBound()` | — |
|
|
348
|
+
|
|
349
|
+
### `registerBunHook()`
|
|
350
|
+
|
|
351
|
+
Registers a post-start hook that:
|
|
352
|
+
|
|
353
|
+
1. Dynamically imports `@socket.io/bun-engine`
|
|
354
|
+
2. Creates a `BunEngine` instance with CORS config bridging
|
|
355
|
+
3. Creates `SocketIOServerHelper` with `runtime: RuntimeModules.BUN`
|
|
356
|
+
4. Awaits `socketIOHelper.configure()` which waits for all Redis connections to be ready before initializing the adapter and emitter
|
|
357
|
+
5. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
358
|
+
6. Calls `serverInstance.reload()` to wire the engine's `fetch` and `websocket` handlers into the running Bun server
|
|
359
|
+
|
|
360
|
+
**CORS type bridging**: Socket.IO and `@socket.io/bun-engine` have slightly different CORS type definitions. The component extracts individual fields explicitly to avoid type mismatches without using `as any`.
|
|
361
|
+
|
|
362
|
+
### `registerNodeHook()`
|
|
363
|
+
|
|
364
|
+
Registers a post-start hook that:
|
|
365
|
+
|
|
366
|
+
1. Gets the HTTP server instance via `getServerInstance()`
|
|
367
|
+
2. Creates `SocketIOServerHelper` with `runtime: RuntimeModules.NODE`
|
|
368
|
+
3. Awaits `socketIOHelper.configure()` which waits for all Redis connections to be ready before initializing the adapter and emitter
|
|
369
|
+
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
370
|
+
|
|
371
|
+
Node mode is simpler because Socket.IO natively attaches to `node:http.Server`.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Post-Start Hook System
|
|
376
|
+
|
|
377
|
+
The component relies on `AbstractApplication`'s post-start hook system:
|
|
378
|
+
|
|
379
|
+
### API
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// Register a hook (during binding phase)
|
|
383
|
+
application.registerPostStartHook({
|
|
384
|
+
identifier: string, // Unique name for logging
|
|
385
|
+
hook: () => ValueOrPromise<void>, // Async function to execute
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Get the server instance (available after start)
|
|
389
|
+
application.getServerInstance<T>(): T | undefined;
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### How Hooks Execute
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
executePostStartHooks()
|
|
396
|
+
├── Hook 1: "socket-io-initialize"
|
|
397
|
+
│ ├── performance.now() → start
|
|
398
|
+
│ ├── await hook()
|
|
399
|
+
│ └── log: "Executed hook | identifier: socket-io-initialize | took: 12.5 (ms)"
|
|
400
|
+
├── Hook 2: "another-hook"
|
|
401
|
+
│ └── ...
|
|
402
|
+
└── (hooks run sequentially in registration order)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- Hooks run **sequentially** (not parallel) to guarantee ordering
|
|
406
|
+
- Each hook is timed with `performance.now()` for diagnostics
|
|
407
|
+
- If a hook throws, it propagates to `start()` and the server fails to start
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Graceful Shutdown
|
|
412
|
+
|
|
413
|
+
Always shut down the Socket.IO server before stopping the application:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
override async stop(): Promise<void> {
|
|
417
|
+
// 1. Shut down Socket.IO (disconnects all clients, closes IO server, quits Redis)
|
|
418
|
+
const socketIOHelper = this.get<SocketIOServerHelper>({
|
|
419
|
+
key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
|
|
420
|
+
isOptional: true,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
if (socketIOHelper) {
|
|
424
|
+
await socketIOHelper.shutdown();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 2. Disconnect Redis helper
|
|
428
|
+
if (this.redisHelper) {
|
|
429
|
+
await this.redisHelper.disconnect();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 3. Stop the HTTP/Bun server
|
|
433
|
+
await super.stop();
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Shutdown Sequence
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
socketIOHelper.shutdown()
|
|
441
|
+
├── Disconnect all tracked clients
|
|
442
|
+
│ ├── clearInterval(ping)
|
|
443
|
+
│ ├── clearTimeout(authenticateTimeout)
|
|
444
|
+
│ └── socket.disconnect()
|
|
445
|
+
├── clients.clear()
|
|
446
|
+
├── io.close() — closes the Socket.IO server
|
|
447
|
+
└── Redis cleanup
|
|
448
|
+
├── redisPub.quit()
|
|
449
|
+
├── redisSub.quit()
|
|
450
|
+
└── redisEmitter.quit()
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Complete Example
|
|
456
|
+
|
|
457
|
+
A full working example is available at `examples/socket-io-test/`. It demonstrates:
|
|
458
|
+
|
|
459
|
+
| Feature | Implementation |
|
|
460
|
+
|---------|---------------|
|
|
461
|
+
| Application setup | `src/application.ts` — bindings, component registration, graceful shutdown |
|
|
462
|
+
| REST endpoints | `src/controllers/socket-test.controller.ts` — 9 endpoints for Socket.IO management |
|
|
463
|
+
| Event handling | `src/services/socket-event.service.ts` — chat, echo, room management |
|
|
464
|
+
| Automated test client | `client.ts` — 15+ test cases covering all features |
|
|
465
|
+
|
|
466
|
+
### REST API Endpoints
|
|
467
|
+
|
|
468
|
+
| Method | Path | Description |
|
|
469
|
+
|--------|------|-------------|
|
|
470
|
+
| `GET` | `/socket/info` | Server status + connected client count |
|
|
471
|
+
| `GET` | `/socket/clients` | List all connected client IDs |
|
|
472
|
+
| `GET` | `/socket/health` | Health check (is SocketIO ready?) |
|
|
473
|
+
| `POST` | `/socket/broadcast` | Broadcast `{ topic, data }` to all clients |
|
|
474
|
+
| `POST` | `/socket/room/{roomId}/send` | Send `{ topic, data }` to a room |
|
|
475
|
+
| `POST` | `/socket/client/{clientId}/send` | Send `{ topic, data }` to a specific client |
|
|
476
|
+
| `POST` | `/socket/client/{clientId}/join` | Join client to `{ rooms: string[] }` |
|
|
477
|
+
| `POST` | `/socket/client/{clientId}/leave` | Remove client from `{ rooms: string[] }` |
|
|
478
|
+
| `GET` | `/socket/client/{clientId}/rooms` | List rooms a client belongs to |
|
|
479
|
+
|
|
480
|
+
### Running the Example
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
# Start the server
|
|
484
|
+
cd examples/socket-io-test
|
|
485
|
+
bun run server:dev
|
|
486
|
+
|
|
487
|
+
# In another terminal — run automated tests
|
|
488
|
+
bun client.ts
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Troubleshooting
|
|
494
|
+
|
|
495
|
+
### "SocketIO not initialized"
|
|
496
|
+
|
|
497
|
+
**Cause**: You're trying to use `SocketIOServerHelper` before the server has started (e.g., during DI construction).
|
|
498
|
+
|
|
499
|
+
**Fix**: Use the lazy getter pattern shown in [Step 3](#step-3-use-in-servicescontrollers). Never `@inject` `SOCKET_IO_INSTANCE` directly in a constructor — it doesn't exist yet at construction time.
|
|
500
|
+
|
|
501
|
+
### "Invalid instance of redisConnection"
|
|
502
|
+
|
|
503
|
+
**Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `DefaultRedisHelper` (or its subclass `RedisHelper`).
|
|
504
|
+
|
|
505
|
+
**Fix**: Use `RedisHelper` (recommended) or `DefaultRedisHelper`:
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
// Correct
|
|
509
|
+
this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION })
|
|
510
|
+
.toValue(new RedisHelper({ name: 'socket-io', host, port, password }));
|
|
511
|
+
|
|
512
|
+
// Wrong — raw ioredis client
|
|
513
|
+
this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION })
|
|
514
|
+
.toValue(new Redis(6379)); // This is NOT a DefaultRedisHelper!
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### "Cannot find module '@socket.io/bun-engine'"
|
|
518
|
+
|
|
519
|
+
**Cause**: Running on Bun runtime without the optional peer dependency installed.
|
|
520
|
+
|
|
521
|
+
**Fix**: `bun add @socket.io/bun-engine`
|
|
522
|
+
|
|
523
|
+
### Socket.IO connects but events aren't received
|
|
524
|
+
|
|
525
|
+
**Cause**: Clients must emit `authenticate` after connecting. Unauthenticated clients are disconnected after the timeout (default: 10 seconds).
|
|
526
|
+
|
|
527
|
+
**Fix**: Ensure your client emits the authenticate event:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
socket.on('connect', () => {
|
|
531
|
+
socket.emit('authenticate');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
socket.on('authenticated', (data) => {
|
|
535
|
+
// Now ready to send/receive events
|
|
536
|
+
});
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
129
541
|
## See Also
|
|
130
542
|
|
|
131
543
|
- **Related Concepts:**
|
|
132
|
-
- [Components Overview](/guides/core-concepts/components)
|
|
133
|
-
- [Application](/guides/core-concepts/application/)
|
|
544
|
+
- [Components Overview](/guides/core-concepts/components) — Component system basics
|
|
545
|
+
- [Application](/guides/core-concepts/application/) — Registering components
|
|
134
546
|
|
|
135
547
|
- **Other Components:**
|
|
136
|
-
- [Components Index](./index)
|
|
548
|
+
- [Components Index](./index) — All built-in components
|
|
137
549
|
|
|
138
550
|
- **References:**
|
|
139
|
-
- [Socket.IO Helper](/references/helpers/socket-io)
|
|
551
|
+
- [Socket.IO Helper](/references/helpers/socket-io) — Full `SocketIOServerHelper` + `SocketIOClientHelper` API reference
|
|
140
552
|
|
|
141
553
|
- **External Resources:**
|
|
142
|
-
- [Socket.IO Documentation](https://socket.io/docs/)
|
|
554
|
+
- [Socket.IO Documentation](https://socket.io/docs/) — Official docs
|
|
555
|
+
- [Socket.IO Redis Adapter](https://socket.io/docs/v4/redis-adapter/) — Horizontal scaling guide
|
|
556
|
+
- [@socket.io/bun-engine](https://github.com/socketio/bun-engine) — Bun runtime support
|
|
143
557
|
|
|
144
558
|
- **Tutorials:**
|
|
145
|
-
- [Real-Time Chat](/guides/tutorials/realtime-chat)
|
|
559
|
+
- [Real-Time Chat](/guides/tutorials/realtime-chat) — Building a chat app with Socket.IO
|
|
560
|
+
|
|
561
|
+
- **Changelog:**
|
|
562
|
+
- [2026-02-06: Socket.IO Integration Fix](/changelogs/2026-02-06-socket-io-integration-fix) — Lifecycle timing fix + Bun runtime support
|