@venizia/ignis-docs 0.2.1-0 → 0.2.1-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/best-practices/architectural-patterns.md +3 -3
- package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
- package/content/best-practices/contribution-workflow.md +2 -2
- package/content/best-practices/error-handling.md +94 -89
- package/content/best-practices/security-guidelines.md +5 -5
- package/content/extensions/components/api-reference.md +22 -21
- package/content/extensions/components/authentication/api.md +64 -28
- package/content/extensions/components/authentication/errors.md +19 -5
- package/content/extensions/components/authentication/index.md +19 -18
- package/content/extensions/components/authentication/usage.md +55 -30
- package/content/extensions/components/authorization/api.md +351 -84
- package/content/extensions/components/authorization/errors.md +51 -17
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +24 -16
- package/content/extensions/components/authorization/usage.md +45 -21
- package/content/extensions/components/health-check.md +15 -9
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +105 -54
- package/content/extensions/components/mail/errors.md +12 -10
- package/content/extensions/components/mail/index.md +32 -13
- package/content/extensions/components/mail/usage.md +26 -18
- package/content/extensions/components/request-tracker.md +18 -14
- package/content/extensions/components/socket-io/api.md +377 -882
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +72 -88
- package/content/extensions/components/socket-io/usage.md +107 -117
- package/content/extensions/components/static-asset/api.md +83 -31
- package/content/extensions/components/static-asset/errors.md +18 -7
- package/content/extensions/components/static-asset/index.md +18 -11
- package/content/extensions/components/static-asset/usage.md +11 -6
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +58 -27
- package/content/extensions/components/websocket/errors.md +3 -3
- package/content/extensions/components/websocket/index.md +8 -7
- package/content/extensions/components/websocket/usage.md +21 -8
- package/content/extensions/helpers/cron/index.md +8 -7
- package/content/extensions/helpers/crypto/index.md +16 -8
- package/content/extensions/helpers/crypto/reference.md +96 -24
- package/content/extensions/helpers/env/index.md +14 -10
- package/content/extensions/helpers/error/index.md +99 -23
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +23 -6
- package/content/extensions/helpers/inversion/reference.md +30 -22
- package/content/extensions/helpers/kafka/admin.md +3 -2
- package/content/extensions/helpers/kafka/compile-binary.md +69 -44
- package/content/extensions/helpers/kafka/consumer.md +24 -21
- package/content/extensions/helpers/kafka/examples.md +22 -234
- package/content/extensions/helpers/kafka/index.md +30 -62
- package/content/extensions/helpers/kafka/producer.md +31 -26
- package/content/extensions/helpers/kafka/schema-registry.md +19 -14
- package/content/extensions/helpers/logger/hf-logger.md +49 -22
- package/content/extensions/helpers/logger/index.md +37 -12
- package/content/extensions/helpers/logger/pino.md +31 -11
- package/content/extensions/helpers/logger/reference.md +243 -52
- package/content/extensions/helpers/network/api.md +65 -30
- package/content/extensions/helpers/network/index.md +32 -11
- package/content/extensions/helpers/queue/index.md +17 -6
- package/content/extensions/helpers/queue/reference.md +52 -25
- package/content/extensions/helpers/redis/index.md +29 -11
- package/content/extensions/helpers/redis/reference.md +85 -28
- package/content/extensions/helpers/secrets/index.md +82 -12
- package/content/extensions/helpers/socket-io/api.md +40 -21
- package/content/extensions/helpers/socket-io/index.md +26 -10
- package/content/extensions/helpers/storage/api.md +42 -24
- package/content/extensions/helpers/storage/index.md +10 -7
- package/content/extensions/helpers/types/index.md +20 -7
- package/content/extensions/helpers/types/reference.md +53 -14
- package/content/extensions/helpers/uid/index.md +166 -10
- package/content/extensions/helpers/websocket/api.md +52 -13
- package/content/extensions/helpers/websocket/index.md +7 -4
- package/content/extensions/helpers/worker-thread/index.md +9 -5
- package/content/extensions/helpers/worker-thread/reference.md +20 -20
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +4 -3
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +7 -6
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +63 -21
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +14 -14
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +32 -31
- package/content/references/base/datasources.md +6 -6
- package/content/references/base/dependency-injection.md +12 -11
- package/content/references/base/filter-system/application-usage.md +54 -30
- package/content/references/base/filter-system/array-operators.md +24 -46
- package/content/references/base/filter-system/comparison-operators.md +47 -67
- package/content/references/base/filter-system/default-filter.md +59 -53
- package/content/references/base/filter-system/fields-order-pagination.md +92 -146
- package/content/references/base/filter-system/index.md +25 -13
- package/content/references/base/filter-system/json-filtering.md +45 -184
- package/content/references/base/filter-system/list-operators.md +23 -53
- package/content/references/base/filter-system/logical-operators.md +63 -121
- package/content/references/base/filter-system/null-operators.md +34 -104
- package/content/references/base/filter-system/pattern-matching.md +40 -55
- package/content/references/base/filter-system/quick-reference.md +86 -198
- package/content/references/base/filter-system/range-operators.md +18 -46
- package/content/references/base/filter-system/tips.md +6 -6
- package/content/references/base/filter-system/use-cases.md +33 -15
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +11 -10
- package/content/references/base/models-reference.md +17 -17
- package/content/references/base/models.md +4 -3
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +224 -326
- package/content/references/base/repositories/index.md +19 -6
- package/content/references/base/repositories/mixins.md +5 -5
- package/content/references/base/repositories/relations.md +160 -293
- package/content/references/base/repositories/soft-deletable.md +16 -6
- package/content/references/base/secrets.md +17 -13
- package/content/references/base/services.md +6 -4
- package/content/references/configuration/environment-variables.md +31 -23
- package/content/references/configuration/index.md +6 -4
- package/content/references/index.md +1 -1
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +5 -1
- package/content/references/utilities/jsx-reference.md +11 -11
- package/content/references/utilities/jsx.md +2 -2
- package/content/references/utilities/module.md +78 -25
- package/content/references/utilities/request.md +2 -1
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +2 -2
- package/content/references/utilities/statuses-reference.md +4 -4
- package/content/references/utilities/statuses.md +5 -5
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +17 -16
|
@@ -1,189 +1,89 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Socket.IO Component - Full Reference
|
|
3
|
+
description: Binding keys, configuration options, event payloads, method signatures, lifecycle diagrams, and internals
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# Socket.IO Component Reference
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Every binding key, configuration option, event payload, and internal mechanism of `SocketIOComponent`, `SocketIOServerHelper`, and `SocketIOClientHelper`. For the task-oriented walkthrough, see [Usage & Examples](./usage).
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
**Files:**
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
```
|
|
13
|
+
- [`packages/core-server/src/components/socket-io/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/component.ts)
|
|
14
|
+
- [`packages/core-server/src/components/socket-io/common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/common/keys.ts)
|
|
15
|
+
- [`packages/core-server/src/components/socket-io/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/common/types.ts)
|
|
16
|
+
- [`packages/core-server/src/components/socket-io/handlers/bun.handler.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/handlers/bun.handler.ts)
|
|
17
|
+
- [`packages/core-server/src/components/socket-io/handlers/node.handler.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/handlers/node.handler.ts)
|
|
18
|
+
- [`packages/helpers/src/modules/socket/socket-io/server/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/socket-io/server/helper.ts)
|
|
19
|
+
- [`packages/helpers/src/modules/socket/socket-io/client/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/socket-io/client/helper.ts)
|
|
20
|
+
- [`packages/helpers/src/modules/socket/socket-io/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/socket-io/common/constants.ts)
|
|
33
21
|
|
|
34
|
-
|
|
22
|
+
## Find what you need
|
|
35
23
|
|
|
36
|
-
|
|
24
|
+
| You want to | Go to |
|
|
25
|
+
|---|---|
|
|
26
|
+
| See every default server option | [Default server options](#default-server-options) |
|
|
27
|
+
| Find a binding key, its type, or whether it's required | [Binding keys](#binding-keys) |
|
|
28
|
+
| Look up an event name or default room | [System events and rooms](#system-events-and-rooms) |
|
|
29
|
+
| Understand the client authentication states | [Client states](#client-states) |
|
|
30
|
+
| Compare Node.js vs Bun behavior | [Runtime-specific behavior](#runtime-specific-behavior) |
|
|
31
|
+
| Call a method on the server helper | [Server helper: public methods](#server-helper-public-methods) |
|
|
32
|
+
| Send a message that reaches another server instance | [Messaging via `send()`](#messaging-via-send) |
|
|
33
|
+
| Call a method on the client helper | [Client helper: public methods](#client-helper-public-methods) |
|
|
34
|
+
| Understand the post-start hook / boot lifecycle | [Lifecycle integration](#lifecycle-integration) |
|
|
35
|
+
| See why one Redis connection becomes three | [Redis 3-client architecture](#redis-3-client-architecture) |
|
|
36
|
+
| Read the full TypeScript type definitions | [Types reference](#types-reference) |
|
|
37
|
+
| Look up an exact error message | [Error Reference](./errors) |
|
|
37
38
|
|
|
38
|
-
|
|
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
|
-
```
|
|
39
|
+
## Quick reference
|
|
66
40
|
|
|
67
|
-
|
|
41
|
+
| Item | Value |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Package | `@venizia/ignis` (core component) + `@venizia/ignis-helpers` (helper classes) |
|
|
44
|
+
| Component class | `SocketIOComponent` |
|
|
45
|
+
| Server helper | [`SocketIOServerHelper`](/extensions/helpers/socket-io/) |
|
|
46
|
+
| Client helper | [`SocketIOClientHelper`](/extensions/helpers/socket-io/) |
|
|
47
|
+
| Runtimes | Node.js (`@hono/node-server`) and Bun (native) |
|
|
48
|
+
| Scaling | `@socket.io/redis-adapter` + `@socket.io/redis-emitter` (`ioredis`) |
|
|
68
49
|
|
|
69
|
-
|
|
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 |
|
|
50
|
+
## Import paths
|
|
80
51
|
|
|
81
|
-
|
|
52
|
+
`SocketIOComponent` and `SocketIOBindingKeys` are exported only from the `@venizia/ignis/socket-io` subpath - never from the `@venizia/ignis` root barrel.
|
|
82
53
|
|
|
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
54
|
```typescript
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
```
|
|
55
|
+
// Core - subpath import only
|
|
56
|
+
import { SocketIOComponent, SocketIOBindingKeys } from '@venizia/ignis/socket-io';
|
|
111
57
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
});
|
|
58
|
+
// Helpers - subpath import
|
|
59
|
+
import { SocketIOServerHelper, SocketIOClientHelper, SocketIOConstants } from '@venizia/ignis-helpers/socket-io';
|
|
60
|
+
import type { TSocketIOAuthenticateFn, TSocketIOValidateRoomFn } from '@venizia/ignis-helpers/socket-io';
|
|
130
61
|
```
|
|
131
62
|
|
|
132
|
-
|
|
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
|
-
```
|
|
63
|
+
## Configuration reference
|
|
161
64
|
|
|
162
|
-
|
|
65
|
+
### Default server options
|
|
163
66
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
The component applies these defaults if `SocketIOBindingKeys.SERVER_OPTIONS` is not bound or partially overridden:
|
|
67
|
+
The component applies these defaults whenever `SocketIOBindingKeys.SERVER_OPTIONS` is unbound or only partially overridden.
|
|
167
68
|
|
|
168
69
|
| Option | Default | Description |
|
|
169
|
-
|
|
70
|
+
|---|---|---|
|
|
170
71
|
| `identifier` | `'SOCKET_IO_SERVER'` | Unique identifier for the helper instance |
|
|
171
|
-
| `path` | `'/io'` | URL path for Socket.IO handshake
|
|
172
|
-
| `cors.origin` | `'*'` | Allowed origins
|
|
72
|
+
| `path` | `'/io'` | URL path for the Socket.IO handshake and polling |
|
|
73
|
+
| `cors.origin` | `'*'` | Allowed origins - restrict this in production |
|
|
173
74
|
| `cors.methods` | `['GET', 'POST']` | Allowed HTTP methods for CORS preflight |
|
|
174
|
-
| `cors.preflightContinue` | `false` | Pass preflight to next handler |
|
|
175
|
-
| `cors.optionsSuccessStatus` | `204` | Status code for successful OPTIONS
|
|
176
|
-
| `cors.credentials` | `true` | Allow cookies
|
|
177
|
-
| `perMessageDeflate.threshold` | `4096` | Minimum message size to compress
|
|
75
|
+
| `cors.preflightContinue` | `false` | Pass preflight to the next handler |
|
|
76
|
+
| `cors.optionsSuccessStatus` | `204` | Status code for a successful OPTIONS request |
|
|
77
|
+
| `cors.credentials` | `true` | Allow cookies and auth headers |
|
|
78
|
+
| `perMessageDeflate.threshold` | `4096` | Minimum message size to compress, in bytes |
|
|
178
79
|
| `perMessageDeflate.concurrencyLimit` | `20` | Max concurrent compression operations |
|
|
179
|
-
| `perMessageDeflate.clientNoContextTakeover` | `true` | Client releases compression context after each message |
|
|
180
|
-
| `perMessageDeflate.serverNoContextTakeover` | `true` | Server releases compression context after each message |
|
|
181
|
-
| `perMessageDeflate.serverMaxWindowBits` | `10` | Server-side
|
|
80
|
+
| `perMessageDeflate.clientNoContextTakeover` | `true` | Client releases its compression context after each message |
|
|
81
|
+
| `perMessageDeflate.serverNoContextTakeover` | `true` | Server releases its compression context after each message |
|
|
82
|
+
| `perMessageDeflate.serverMaxWindowBits` | `10` | Server-side max window size (2^10 = 1 KB) |
|
|
182
83
|
|
|
183
84
|
> [!WARNING]
|
|
184
|
-
> The default `cors.origin: '*'` is
|
|
85
|
+
> The default `cors.origin: '*'` is for development only. Restrict it to your domains in production.
|
|
185
86
|
|
|
186
|
-
#### Full `DEFAULT_SERVER_OPTIONS`
|
|
187
87
|
```typescript
|
|
188
88
|
const DEFAULT_SERVER_OPTIONS: Partial<IServerOptions> = {
|
|
189
89
|
identifier: 'SOCKET_IO_SERVER',
|
|
@@ -207,7 +107,7 @@ const DEFAULT_SERVER_OPTIONS: Partial<IServerOptions> = {
|
|
|
207
107
|
};
|
|
208
108
|
```
|
|
209
109
|
|
|
210
|
-
### Custom
|
|
110
|
+
### Custom configuration
|
|
211
111
|
|
|
212
112
|
Bind custom server options before registering the component:
|
|
213
113
|
|
|
@@ -217,319 +117,212 @@ import type { ServerOptions } from 'socket.io';
|
|
|
217
117
|
|
|
218
118
|
const customOptions: Partial<ServerOptions> = {
|
|
219
119
|
path: '/socket.io',
|
|
220
|
-
cors: {
|
|
221
|
-
origin: ['https://myapp.com', 'https://admin.myapp.com'],
|
|
222
|
-
methods: ['GET', 'POST'],
|
|
223
|
-
credentials: true,
|
|
224
|
-
},
|
|
120
|
+
cors: { origin: ['https://myapp.com'], methods: ['GET', 'POST'], credentials: true },
|
|
225
121
|
pingTimeout: 60000,
|
|
226
122
|
pingInterval: 25000,
|
|
227
123
|
maxHttpBufferSize: 1e6, // 1MB
|
|
228
124
|
};
|
|
229
125
|
|
|
230
|
-
this.bind<Partial<ServerOptions>>({
|
|
231
|
-
key: SocketIOBindingKeys.SERVER_OPTIONS,
|
|
232
|
-
}).toValue(customOptions);
|
|
233
|
-
|
|
126
|
+
this.bind<Partial<ServerOptions>>({ key: SocketIOBindingKeys.SERVER_OPTIONS }).toValue(customOptions);
|
|
234
127
|
this.component(SocketIOComponent);
|
|
235
128
|
```
|
|
236
129
|
|
|
237
130
|
> [!NOTE]
|
|
238
|
-
> `identifier`
|
|
239
|
-
|
|
240
|
-
### Binding Keys
|
|
131
|
+
> `identifier` belongs to the component's `IServerOptions` interface, which extends Socket.IO's native `ServerOptions` - it's not a Socket.IO option itself. Set it by including it in the bound options object.
|
|
241
132
|
|
|
242
|
-
|
|
133
|
+
### Binding keys
|
|
243
134
|
|
|
244
|
-
| Binding
|
|
245
|
-
|
|
246
|
-
| `@app/socket-io/server-options` | `SERVER_OPTIONS` | `Partial<ServerOptions>` | No |
|
|
135
|
+
| Binding key | Constant | Type | Required | Default |
|
|
136
|
+
|---|---|---|---|---|
|
|
137
|
+
| `@app/socket-io/server-options` | `SERVER_OPTIONS` | `Partial<ServerOptions>` | No | [Default server options](#default-server-options) |
|
|
247
138
|
| `@app/socket-io/redis-connection` | `REDIS_CONNECTION` | `IRedisHelper` (`RedisSingleHelper` / `RedisClusterHelper` / `RedisSentinelHelper`) | **Yes** | `null` |
|
|
248
139
|
| `@app/socket-io/authenticate-handler` | `AUTHENTICATE_HANDLER` | `TSocketIOAuthenticateFn` | **Yes** | `null` |
|
|
249
140
|
| `@app/socket-io/validate-room-handler` | `VALIDATE_ROOM_HANDLER` | `TSocketIOValidateRoomFn` | No | `null` |
|
|
250
141
|
| `@app/socket-io/client-connected-handler` | `CLIENT_CONNECTED_HANDLER` | `TSocketIOClientConnectedFn` | No | `null` |
|
|
251
|
-
| `@app/socket-io/instance` | `SOCKET_IO_INSTANCE` | `SocketIOServerHelper` |
|
|
142
|
+
| `@app/socket-io/instance` | `SOCKET_IO_INSTANCE` | `SocketIOServerHelper` | - | Set by the component |
|
|
252
143
|
|
|
253
144
|
> [!NOTE]
|
|
254
|
-
> `SOCKET_IO_INSTANCE` is
|
|
145
|
+
> `SOCKET_IO_INSTANCE` is never bound by application code. The component binds it automatically after the server starts. Inject it lazily - see [Inject the helper in a service or controller](./usage#inject-the-helper-in-a-service-or-controller).
|
|
255
146
|
|
|
256
|
-
### System
|
|
147
|
+
### System events and rooms
|
|
257
148
|
|
|
258
|
-
|
|
149
|
+
Exported from `@venizia/ignis-helpers/socket-io` as `SocketIOConstants`. Used internally by both the component and the helper.
|
|
259
150
|
|
|
260
151
|
| Constant | Value | Description |
|
|
261
|
-
|
|
262
|
-
| `EVENT_PING` | `'ping'` | Keep-alive ping emitted at `pingInterval` (default: 30s) |
|
|
263
|
-
| `EVENT_CONNECT` | `'connection'` | New client connected (server-side event) |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `EVENT_PING` | `'ping'` | Keep-alive ping, emitted at `pingInterval` (default: 30s) |
|
|
154
|
+
| `EVENT_CONNECT` | `'connection'` | New client connected (server-side event name) |
|
|
264
155
|
| `EVENT_DISCONNECT` | `'disconnect'` | Client disconnected |
|
|
265
156
|
| `EVENT_JOIN` | `'join'` | Client requests to join room(s) |
|
|
266
157
|
| `EVENT_LEAVE` | `'leave'` | Client requests to leave room(s) |
|
|
267
158
|
| `EVENT_AUTHENTICATE` | `'authenticate'` | Client sends auth credentials |
|
|
268
|
-
| `EVENT_AUTHENTICATED` | `'authenticated'` | Auth success response sent to client |
|
|
269
|
-
| `EVENT_UNAUTHENTICATE` | `'unauthenticated'` | Auth failure response sent to client |
|
|
270
|
-
| `ROOM_DEFAULT` | `'io-default'` | Default room
|
|
159
|
+
| `EVENT_AUTHENTICATED` | `'authenticated'` | Auth success response sent to the client |
|
|
160
|
+
| `EVENT_UNAUTHENTICATE` | `'unauthenticated'` | Auth failure response sent to the client |
|
|
161
|
+
| `ROOM_DEFAULT` | `'io-default'` | Default room every authenticated client joins |
|
|
271
162
|
| `ROOM_NOTIFICATION` | `'io-notification'` | Notification broadcast room |
|
|
272
163
|
|
|
273
164
|
> [!TIP]
|
|
274
|
-
> Override the default rooms
|
|
275
|
-
|
|
276
|
-
#### Internal Constants (Server Helper)
|
|
165
|
+
> Override the default rooms with the `defaultRooms` option on `SocketIOServerHelper` - the values above are only the fallback.
|
|
277
166
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
| Constant | Value | Description |
|
|
281
|
-
|----------|-------|--------------|
|
|
282
|
-
| `CLIENT_AUTHENTICATE_TIMEOUT` | `10_000` (10s) | Time allowed for a client to authenticate before forced disconnect |
|
|
283
|
-
| `CLIENT_PING_INTERVAL` | `30_000` (30s) | Interval between server-to-client ping emissions |
|
|
167
|
+
Two more constants govern default behavior. They're defined at module scope in the server helper, not exported, but overridable through constructor options:
|
|
284
168
|
|
|
285
|
-
|
|
169
|
+
| Constant | Value | Overridable via |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `CLIENT_AUTHENTICATE_TIMEOUT` | `10_000` (10s) | `authenticateTimeout` |
|
|
172
|
+
| `CLIENT_PING_INTERVAL` | `30_000` (30s) | `pingInterval` |
|
|
286
173
|
|
|
287
|
-
### Client
|
|
174
|
+
### Client states
|
|
288
175
|
|
|
289
|
-
Each connected client tracks an authentication state that governs what
|
|
176
|
+
Each connected client tracks an authentication state that governs what it can do.
|
|
290
177
|
|
|
291
178
|
| State | Constant | Description |
|
|
292
|
-
|
|
293
|
-
| `unauthorized` | `SocketIOClientStates.UNAUTHORIZED` | Initial state
|
|
294
|
-
| `authenticating` | `SocketIOClientStates.AUTHENTICATING` | Auth in progress
|
|
295
|
-
| `authenticated` | `SocketIOClientStates.AUTHENTICATED` | Auth
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `unauthorized` | `SocketIOClientStates.UNAUTHORIZED` | Initial state - the client must emit `authenticate` within the timeout (default: 10s) |
|
|
181
|
+
| `authenticating` | `SocketIOClientStates.AUTHENTICATING` | Auth in progress - `authenticateFn` is running |
|
|
182
|
+
| `authenticated` | `SocketIOClientStates.AUTHENTICATED` | Auth succeeded - the client can send/receive events and join rooms |
|
|
296
183
|
|
|
297
|
-
#### State Machine Diagram
|
|
298
184
|
```
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
+---+----------+---+
|
|
306
|
-
success | | failure
|
|
307
|
-
+---------v--+ +-------v-----------+
|
|
308
|
-
|authenticated| | unauthorized |--> disconnect
|
|
309
|
-
+-------------+ +------------------+
|
|
310
|
-
^
|
|
311
|
-
timeout (10s)
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
#### `SocketIOClientStates` Source
|
|
315
|
-
```typescript
|
|
316
|
-
export class SocketIOClientStates {
|
|
317
|
-
static readonly UNAUTHORIZED = 'unauthorized';
|
|
318
|
-
static readonly AUTHENTICATING = 'authenticating';
|
|
319
|
-
static readonly AUTHENTICATED = 'authenticated';
|
|
320
|
-
|
|
321
|
-
static readonly SCHEME_SET = new Set([
|
|
322
|
-
this.UNAUTHORIZED,
|
|
323
|
-
this.AUTHENTICATING,
|
|
324
|
-
this.AUTHENTICATED,
|
|
325
|
-
]);
|
|
326
|
-
|
|
327
|
-
static isValid(input: string): input is TConstValue<typeof SocketIOClientStates> {
|
|
328
|
-
return this.SCHEME_SET.has(input);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
## Server Helper API Reference
|
|
334
|
-
|
|
335
|
-
### `SocketIOServerHelper` Constructor
|
|
336
|
-
|
|
337
|
-
The helper uses a **discriminated union** for its constructor options, keyed on `runtime`:
|
|
338
|
-
|
|
339
|
-
#### `TSocketIOServerOptions` Type
|
|
340
|
-
```typescript
|
|
341
|
-
interface ISocketIOServerBaseOptions {
|
|
342
|
-
identifier: string;
|
|
343
|
-
serverOptions: Partial<ServerOptions>;
|
|
344
|
-
redisConnection: IRedisHelper;
|
|
345
|
-
defaultRooms?: string[]; // Default: ['io-default', 'io-notification']
|
|
346
|
-
authenticateTimeout?: number; // Default: 10_000 (10 seconds)
|
|
347
|
-
pingInterval?: number; // Default: 30_000 (30 seconds)
|
|
348
|
-
|
|
349
|
-
authenticateFn: TSocketIOAuthenticateFn;
|
|
350
|
-
validateRoomFn?: TSocketIOValidateRoomFn;
|
|
351
|
-
clientConnectedFn?: TSocketIOClientConnectedFn;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
|
|
355
|
-
runtime: typeof RuntimeModules.NODE;
|
|
356
|
-
server: HTTPServer; // node:http.Server instance
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
|
|
360
|
-
runtime: typeof RuntimeModules.BUN;
|
|
361
|
-
engine: any; // @socket.io/bun-engine Server instance
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
|
|
185
|
+
connect ----------> unauthorized --(emit 'authenticate')--> authenticating
|
|
186
|
+
^ |
|
|
187
|
+
| success failure
|
|
188
|
+
timeout (10s) | |
|
|
189
|
+
| v v
|
|
190
|
+
disconnect <---------------- authenticated unauthorized -> disconnect
|
|
365
191
|
```
|
|
366
192
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
1. Sets `identifier`, `runtime`, `serverOptions`, callback functions
|
|
370
|
-
2. Sets defaults: `authenticateTimeout` = 10s, `pingInterval` = 30s, `defaultRooms` = `['io-default', 'io-notification']`
|
|
371
|
-
3. Calls `setRuntime()` -- validates and stores the server or engine
|
|
372
|
-
4. Calls `initRedisClients()` -- creates 3 duplicated Redis clients from the connection
|
|
373
|
-
|
|
374
|
-
> [!IMPORTANT]
|
|
375
|
-
> 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 `RedisSingleHelper` connection is not consumed.
|
|
193
|
+
## Architecture
|
|
376
194
|
|
|
377
|
-
###
|
|
195
|
+
### Lifecycle integration
|
|
378
196
|
|
|
379
|
-
|
|
197
|
+
Socket.IO needs a running server, but components initialize before the server starts. The application's **post-start hook** system bridges that gap.
|
|
380
198
|
|
|
381
199
|
```
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
> 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.
|
|
396
|
-
|
|
397
|
-
### Public Methods
|
|
398
|
-
|
|
399
|
-
#### `getIOServer()`
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
getIOServer(): IOServer
|
|
200
|
+
preConfigure() <- register SocketIOComponent here
|
|
201
|
+
|
|
|
202
|
+
initialize() <- component.binding() runs: resolve bindings, register post-start hook
|
|
203
|
+
|
|
|
204
|
+
setupMiddlewares()
|
|
205
|
+
|
|
|
206
|
+
startBunModule() / startNodeModule() <- server starts, instance created
|
|
207
|
+
|
|
|
208
|
+
executePostStartHooks() <- 'socket-io-initialize' hook runs:
|
|
209
|
+
| new SocketIOServerHelper(...)
|
|
210
|
+
| await socketIOHelper.configure()
|
|
211
|
+
| bind SOCKET_IO_INSTANCE
|
|
212
|
+
| (Bun only) server.reload({ fetch, websocket })
|
|
403
213
|
```
|
|
404
214
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
215
|
+
- Hooks run **sequentially**, in registration order, each timed with `performance.now()` for diagnostics.
|
|
216
|
+
- If a hook throws, the error propagates to `start()` and the server fails to start.
|
|
217
|
+
- The hook identifier is `'socket-io-initialize'` for both runtimes - only one runtime path executes per application.
|
|
408
218
|
|
|
409
219
|
```typescript
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
#### `getClients()`
|
|
220
|
+
// Register a hook, during the binding phase
|
|
221
|
+
application.registerPostStartHook({
|
|
222
|
+
identifier: string,
|
|
223
|
+
hook: () => ValueOrPromise<void>,
|
|
224
|
+
});
|
|
416
225
|
|
|
417
|
-
|
|
418
|
-
|
|
226
|
+
// Get the server instance, available only after start
|
|
227
|
+
application.getServerInstance<T>(): T | undefined;
|
|
419
228
|
```
|
|
420
229
|
|
|
421
|
-
|
|
230
|
+
### Runtime-specific behavior
|
|
422
231
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
232
|
+
| Aspect | Node.js | Bun |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| Server type | `node:http.Server` | `Bun.Server` |
|
|
235
|
+
| IO server init | `new IOServer(httpServer, opts)` | `new IOServer()` then `io.bind(engine)` |
|
|
236
|
+
| Engine | Built in (`socket.io`) | `@socket.io/bun-engine` (optional peer dependency) |
|
|
237
|
+
| Request routing | Socket.IO attaches to the HTTP server automatically | `server.reload({ fetch, websocket })` wires the engine into Bun's request loop |
|
|
238
|
+
| WebSocket upgrade | Handled by `node:http.Server`'s upgrade event | Handled by Bun's `websocket` handler |
|
|
239
|
+
| Dynamic import | None needed | `await import('@socket.io/bun-engine')` at runtime |
|
|
240
|
+
| Fetch handler | Not needed - the HTTP server handles upgrades | A custom fetch wraps Hono's fetch and routes WS upgrades to the engine |
|
|
241
|
+
| CORS | Handled by Socket.IO's own CORS options | Handled by Bun engine options, via explicit field bridging |
|
|
431
242
|
|
|
432
|
-
|
|
243
|
+
### Bun runtime details
|
|
433
244
|
|
|
434
|
-
|
|
245
|
+
A custom fetch function intercepts WebSocket upgrade requests before they reach Hono:
|
|
435
246
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
247
|
+
1. Checks whether the request path starts with the Socket.IO path (`serverOptions.path`, default `'/io'`).
|
|
248
|
+
2. If it matches, delegates to `@socket.io/bun-engine` via `engine.handleRequest(req, server)`.
|
|
249
|
+
3. If it doesn't, delegates to Hono's normal `server.fetch(req, server)` handler.
|
|
439
250
|
|
|
440
|
-
|
|
251
|
+
Socket.IO and `@socket.io/bun-engine` define CORS slightly differently, so the component extracts each field explicitly instead of casting with `as any`. See [`createBunEngine()`](#post-start-hooks) in Internals for the full CORS bridging code.
|
|
441
252
|
|
|
442
|
-
|
|
443
|
-
- If client is not found in the client map, logs and returns
|
|
444
|
-
- If `doIgnoreAuth` is `false` and the client is not `authenticated`, disconnects the client
|
|
445
|
-
- If `doIgnoreAuth` is `true`, sends the ping regardless of auth state
|
|
253
|
+
### Node.js runtime details
|
|
446
254
|
|
|
447
|
-
|
|
255
|
+
Node mode is simpler - Socket.IO attaches to `node:http.Server` natively. The handler builds `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and passes the HTTP server instance directly. See [`createNodeSocketIOHelper()`](#post-start-hooks) in Internals.
|
|
448
256
|
|
|
449
|
-
|
|
257
|
+
## Server helper API reference
|
|
450
258
|
|
|
451
|
-
|
|
452
|
-
disconnect(opts: { socket: IOSocket }): void
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
Disconnects a specific client and cleans up resources:
|
|
259
|
+
### Constructor
|
|
456
260
|
|
|
457
|
-
|
|
458
|
-
2. Clears the authentication timeout
|
|
459
|
-
3. Removes the client from the `clients` map
|
|
460
|
-
4. Calls `socket.disconnect()` on the underlying Socket.IO socket
|
|
261
|
+
`new SocketIOServerHelper(opts: TSocketIOServerOptions)` - see [server types](#server-types) for the full discriminated union.
|
|
461
262
|
|
|
462
|
-
|
|
263
|
+
| Step | What happens |
|
|
264
|
+
|---|---|
|
|
265
|
+
| 1 | Sets `identifier`, `runtime`, `serverOptions`, and the callback functions |
|
|
266
|
+
| 2 | Applies defaults: `authenticateTimeout` = 10s, `pingInterval` = 30s, `defaultRooms` = `['io-default', 'io-notification']` |
|
|
267
|
+
| 3 | `setRuntime()` validates and stores the server or engine - see [runtime validation](#setruntime-runtime-validation) |
|
|
268
|
+
| 4 | `initRedisClients()` creates 3 duplicated Redis clients - see [Redis 3-client architecture](#redis-3-client-architecture) |
|
|
463
269
|
|
|
464
|
-
|
|
270
|
+
> [!IMPORTANT]
|
|
271
|
+
> Redis clients are duplicated from the parent connection via `redisConnection.duplicateClient()`. The helper owns 3 independent connections (pub, sub, emitter) that inherit config from the parent but keep separate state. The parent connection is never consumed.
|
|
465
272
|
|
|
466
|
-
|
|
467
|
-
onClientConnect(opts: { socket: IOSocket }): void
|
|
468
|
-
```
|
|
273
|
+
### `configure()`
|
|
469
274
|
|
|
470
|
-
|
|
275
|
+
The only async method on the helper. It waits for all 3 Redis clients to reach `ready` before building the IO server.
|
|
471
276
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
277
|
+
1. Registers an `error` handler on each of the 3 Redis clients.
|
|
278
|
+
2. Connects any client still in `wait` status (lazy-connect mode).
|
|
279
|
+
3. Awaits all 3 clients reaching `ready`.
|
|
280
|
+
4. Builds the IO server - `new IOServer(httpServer, serverOptions)` on Node.js, or `new IOServer()` + `io.bind(bunEngine)` on Bun.
|
|
281
|
+
5. Wires the Redis adapter: `io.adapter(createAdapter(redisPub, redisSub))`.
|
|
282
|
+
6. Creates the Redis emitter: `emitter = new Emitter(redisEmitter)`.
|
|
283
|
+
7. Registers the `connection` handler, which calls `onClientConnect()` for every new socket.
|
|
479
284
|
|
|
480
|
-
|
|
285
|
+
> [!NOTE]
|
|
286
|
+
> If any Redis client fails to connect, the error propagates and the server does not start.
|
|
481
287
|
|
|
482
|
-
|
|
483
|
-
onClientAuthenticated(opts: { socket: IOSocket }): void
|
|
484
|
-
```
|
|
288
|
+
### Server helper: public methods
|
|
485
289
|
|
|
486
|
-
|
|
290
|
+
| Method | Signature | Behavior |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `getIOServer()` | `(): IOServer` | Returns the underlying `socket.io` `Server` - for APIs the helper doesn't expose, like `io.of()` or `io.fetchSockets()` |
|
|
293
|
+
| `getEngine()` | `(): any` | Returns the `@socket.io/bun-engine` instance. Throws on Node.js runtime |
|
|
294
|
+
| `getClients()` | `(opts?: { id? }): ISocketIOClient \| Map<string, ISocketIOClient> \| undefined` | Without `id`, returns the full client map. With `id`, returns that client or `undefined` |
|
|
295
|
+
| `on()` | `(opts: { topic; handler }): void` | Registers a server-level event handler. Throws if `topic` is empty, `handler` is falsy, or the IO server isn't initialized |
|
|
296
|
+
| `ping()` | `(opts: { socket; doIgnoreAuth }): void` | Sends `{ time }` to one client. No-op if `socket` or the client entry is missing. Disconnects the client if `doIgnoreAuth` is `false` and it isn't authenticated |
|
|
297
|
+
| `disconnect()` | `(opts: { socket }): void` | Clears the client's ping interval and auth timeout, removes it from the client map, then calls `socket.disconnect()`. No-op if `socket` is falsy |
|
|
298
|
+
| `onClientConnect()` | `(opts: { socket }): void` | Runs on every new connection: skips duplicates, creates the client entry (state `UNAUTHORIZED`), starts the auth timeout, and registers the `disconnect` and `authenticate` handlers |
|
|
299
|
+
| `onClientAuthenticated()` | `(opts: { socket }): void` | Runs after successful auth: sets state `AUTHENTICATED`, sends the initial ping, joins default rooms, registers room handlers, starts the ping interval, and emits `authenticated` |
|
|
487
300
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
3. Sends an initial ping
|
|
492
|
-
4. Joins default rooms (`io-default`, `io-notification`)
|
|
493
|
-
5. Registers room handlers (`join`, `leave`)
|
|
494
|
-
6. Starts the ping interval
|
|
495
|
-
7. Emits `authenticated` event to the client with `{ id, time }`
|
|
496
|
-
8. Invokes the `clientConnectedFn` callback (if configured)
|
|
301
|
+
- **`ping()` powers the keep-alive interval.** `doIgnoreAuth: true` covers both the initial post-auth ping and the recurring interval.
|
|
302
|
+
- **`onClientConnect()` and `onClientAuthenticated()` are public** so tests and custom connection routing can call them directly.
|
|
303
|
+
- **`clientConnectedFn` runs through a safety wrapper.** A synchronous throw inside your callback is caught and logged - it never crashes the process.
|
|
497
304
|
|
|
498
305
|
### Messaging via `send()`
|
|
499
306
|
|
|
500
|
-
The `send()` method uses the Redis emitter for message delivery, enabling cross-instance broadcasting:
|
|
501
|
-
|
|
502
307
|
```typescript
|
|
503
308
|
send(opts: {
|
|
504
309
|
destination?: string; // Socket ID, room name, or omit for broadcast
|
|
505
|
-
payload: {
|
|
506
|
-
topic: string; // Event name
|
|
507
|
-
data: any; // Event payload
|
|
508
|
-
};
|
|
310
|
+
payload: { topic: string; data: any };
|
|
509
311
|
doLog?: boolean; // Log the emission (default: false)
|
|
510
312
|
callback?: () => void; // Executed via setImmediate after emit
|
|
511
313
|
})
|
|
512
314
|
```
|
|
513
315
|
|
|
514
|
-
|
|
316
|
+
`send()` delivers through the Redis emitter, so it works even when the destination client is connected to a different server instance.
|
|
515
317
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
- Logging is opt-in (`doLog: true`) to avoid noise in high-throughput scenarios
|
|
318
|
+
| `destination` | Behavior |
|
|
319
|
+
|---|---|
|
|
320
|
+
| A socket ID or room name | `sender.to(destination).emit(topic, data)` |
|
|
321
|
+
| Omitted or empty | Broadcasts to all connected clients: `sender.emit(topic, data)` |
|
|
521
322
|
|
|
522
|
-
|
|
323
|
+
Every message is compressed via `emitter.compress(true)`. `callback` runs through `setImmediate()` - it confirms the emit call ran, not that a client received the message. Logging is opt-in (`doLog: true`) to avoid noise at high throughput.
|
|
523
324
|
|
|
524
|
-
|
|
525
|
-
- `payload` is falsy
|
|
526
|
-
- `payload.topic` is falsy
|
|
527
|
-
- `payload.data` is falsy
|
|
528
|
-
|
|
529
|
-
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.
|
|
530
|
-
|
|
531
|
-
> [!TIP]
|
|
532
|
-
> 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.
|
|
325
|
+
`send()` returns silently, with no error and no log, when `payload`, `payload.topic`, or `payload.data` is falsy. That's deliberate: a fire-and-forget caller doesn't need to know a message was dropped for a missing field.
|
|
533
326
|
|
|
534
327
|
### Shutdown
|
|
535
328
|
|
|
@@ -537,17 +330,14 @@ This is a deliberate design choice for fire-and-forget messaging patterns where
|
|
|
537
330
|
shutdown(): Promise<void>
|
|
538
331
|
```
|
|
539
332
|
|
|
540
|
-
|
|
333
|
+
1. Clears every tracked client's ping interval and auth timeout, then disconnects each socket.
|
|
334
|
+
2. Clears the client map.
|
|
335
|
+
3. Closes the IO server (`io.close()`, wrapped in a promise).
|
|
336
|
+
4. Quits all 3 Redis connections (`redisPub`, `redisSub`, `redisEmitter`).
|
|
541
337
|
|
|
542
|
-
|
|
543
|
-
2. Disconnects each client socket
|
|
544
|
-
3. Clears the client map
|
|
545
|
-
4. Closes the IO server (async, wrapped in a Promise)
|
|
546
|
-
5. Quits all 3 Redis connections (`redisPub`, `redisSub`, `redisEmitter`)
|
|
338
|
+
## Client helper API reference
|
|
547
339
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
`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.
|
|
340
|
+
`SocketIOClientHelper` extends `BaseHelper` and wraps `socket.io-client` with lifecycle callbacks, error-safe event subscription, and authentication state tracking.
|
|
551
341
|
|
|
552
342
|
### Constructor
|
|
553
343
|
|
|
@@ -555,121 +345,59 @@ Gracefully shuts down the server:
|
|
|
555
345
|
constructor(opts: ISocketIOClientOptions)
|
|
556
346
|
```
|
|
557
347
|
|
|
558
|
-
|
|
348
|
+
See [client types](#client-types) for the full interface. `IOptions` extends `socket.io-client`'s `SocketOptions` with two required fields:
|
|
559
349
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
options: IOptions;
|
|
350
|
+
| Field | Purpose |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `path` | Must match the server's `path` |
|
|
353
|
+
| `extraHeaders` | Commonly used for the `authorization` token |
|
|
565
354
|
|
|
566
|
-
|
|
567
|
-
onConnected?: () => ValueOrPromise<void>;
|
|
568
|
-
onDisconnected?: (reason: string) => ValueOrPromise<void>;
|
|
569
|
-
onError?: (error: Error) => ValueOrPromise<void>;
|
|
570
|
-
onAuthenticated?: () => ValueOrPromise<void>;
|
|
571
|
-
onUnauthenticated?: (message: string) => ValueOrPromise<void>;
|
|
572
|
-
}
|
|
573
|
-
```
|
|
355
|
+
Construction, in order:
|
|
574
356
|
|
|
575
|
-
|
|
357
|
+
1. Calls `super({ scope: opts.identifier })` to set up `BaseHelper` with scoped logging.
|
|
358
|
+
2. Stores `identifier`, `host`, `options`, and every lifecycle callback.
|
|
359
|
+
3. Calls `configure()` immediately to create the socket and register handlers.
|
|
576
360
|
|
|
577
|
-
|
|
578
|
-
interface IOptions extends SocketOptions {
|
|
579
|
-
path: string;
|
|
580
|
-
extraHeaders: Record<string | symbol | number, any>;
|
|
581
|
-
}
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
`IOptions` extends `SocketOptions` from `socket.io-client` with two required fields:
|
|
585
|
-
- `path` -- the Socket.IO endpoint path (must match the server's `path` option, e.g., `'/io'`)
|
|
586
|
-
- `extraHeaders` -- headers sent with every request, commonly used for `authorization` tokens
|
|
587
|
-
|
|
588
|
-
#### Constructor Behavior
|
|
589
|
-
|
|
590
|
-
1. Calls `super({ scope: opts.identifier })` to initialize `BaseHelper` with scoped logging
|
|
591
|
-
2. Stores the `identifier`, `host`, `options`, and all lifecycle callbacks
|
|
592
|
-
3. Immediately calls `configure()` to create the socket and register internal handlers
|
|
593
|
-
|
|
594
|
-
### `configure()`
|
|
361
|
+
### Client `configure()` event handlers
|
|
595
362
|
|
|
596
363
|
```typescript
|
|
597
364
|
configure(): void
|
|
598
365
|
```
|
|
599
366
|
|
|
600
|
-
Creates the `socket.io-client` `Socket` instance and registers
|
|
601
|
-
|
|
602
|
-
Registered handlers:
|
|
603
|
-
|
|
604
|
-
| Event | Internal Behavior |
|
|
605
|
-
|-------|-------------------|
|
|
606
|
-
| `connect` | Logs connection, invokes `onConnected` callback |
|
|
607
|
-
| `disconnect` | Logs disconnection with reason, resets state to `unauthorized`, invokes `onDisconnected` callback |
|
|
608
|
-
| `connect_error` | Logs the error, invokes `onError` callback |
|
|
609
|
-
| `authenticated` | Logs auth data, sets state to `authenticated`, invokes `onAuthenticated` callback |
|
|
610
|
-
| `unauthenticated` | Logs warning with auth data, resets state to `unauthorized`, invokes `onUnauthenticated` callback with the message |
|
|
611
|
-
| `ping` | Logs debug-level ping received |
|
|
612
|
-
|
|
613
|
-
All lifecycle callbacks are wrapped in `Promise.resolve(...).catch(...)` to prevent callback errors from crashing the client.
|
|
614
|
-
|
|
615
|
-
### `getState()`
|
|
616
|
-
|
|
617
|
-
```typescript
|
|
618
|
-
getState(): TSocketIOClientState
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
Returns the current authentication state: `'unauthorized'`, `'authenticating'`, or `'authenticated'`.
|
|
622
|
-
|
|
623
|
-
#### `TSocketIOClientState` Type
|
|
624
|
-
|
|
625
|
-
```typescript
|
|
626
|
-
type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
|
|
627
|
-
// Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
### `getSocketClient()`
|
|
631
|
-
|
|
632
|
-
```typescript
|
|
633
|
-
getSocketClient(): Socket
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
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`).
|
|
637
|
-
|
|
638
|
-
### `authenticate()`
|
|
639
|
-
|
|
640
|
-
```typescript
|
|
641
|
-
authenticate(): void
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
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`.
|
|
645
|
-
|
|
646
|
-
Guard conditions (no-op with warning log):
|
|
647
|
-
- Socket is not connected (`!this.client?.connected`)
|
|
648
|
-
- Current state is not `unauthorized` (prevents double-auth or re-auth while authenticating)
|
|
367
|
+
Creates the `socket.io-client` `Socket` instance and registers every internal handler. If `configure()` already ran, it logs a message and returns early.
|
|
649
368
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
369
|
+
| Event | Internal behavior |
|
|
370
|
+
|---|---|
|
|
371
|
+
| `connect` | Logs the connection, invokes `onConnected` |
|
|
372
|
+
| `disconnect` | Logs the disconnection with a reason, resets state to `unauthorized`, invokes `onDisconnected` |
|
|
373
|
+
| `connect_error` | Logs the error, invokes `onError` |
|
|
374
|
+
| `authenticated` | Logs the auth data, sets state to `authenticated`, invokes `onAuthenticated` |
|
|
375
|
+
| `unauthenticated` | Logs a warning with the auth data, resets state to `unauthorized`, invokes `onUnauthenticated` with the message |
|
|
376
|
+
| `ping` | Logs a debug-level "ping received" |
|
|
653
377
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
```typescript
|
|
657
|
-
subscribe<T = unknown>(opts: {
|
|
658
|
-
event: string;
|
|
659
|
-
handler: TSocketIOEventHandler<T>;
|
|
660
|
-
ignoreDuplicate?: boolean; // default: true
|
|
661
|
-
}): void
|
|
662
|
-
```
|
|
378
|
+
Every lifecycle callback runs inside `Promise.resolve(...).catch(...)`, so a callback error never crashes the client.
|
|
663
379
|
|
|
664
|
-
|
|
380
|
+
### Client helper: public methods
|
|
665
381
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
382
|
+
| Method | Signature | Behavior |
|
|
383
|
+
|---|---|---|
|
|
384
|
+
| `getState()` | `(): TSocketIOClientState` | Returns `'unauthorized'`, `'authenticating'`, or `'authenticated'` |
|
|
385
|
+
| `getSocketClient()` | `(): Socket` | Returns the raw `socket.io-client` `Socket` - for APIs the helper doesn't expose, like `socket.id` |
|
|
386
|
+
| `authenticate()` | `(): void` | Emits `authenticate`. No-op with a warning log unless connected and in state `unauthorized` |
|
|
387
|
+
| `subscribe()` | `<T>(opts: { event; handler; ignoreDuplicate? }): void` | Subscribes with error-safe wrapping - see [below](#subscribe-error-safe-wrapping) |
|
|
388
|
+
| `subscribeMany()` | `(opts: { events; ignoreDuplicate? }): void` | Calls `subscribe()` for every entry in `events` |
|
|
389
|
+
| `unsubscribe()` | `(opts: { event; handler? }): void` | Removes one handler, or all handlers for the event if `handler` is omitted |
|
|
390
|
+
| `unsubscribeMany()` | `(opts: { events: string[] }): void` | Calls `unsubscribe()` for every event in the array |
|
|
391
|
+
| `connect()` | `(): void` | Manually connects. No-op with an info log if the client isn't initialized |
|
|
392
|
+
| `disconnect()` | `(): void` | Manually disconnects. No-op with an info log if the client isn't initialized |
|
|
393
|
+
| `emit()` | `<T>(opts: { topic; data; doLog?; callback? }): void` | Emits an event. Throws if not connected or `topic` is falsy - see [throw conditions](#emit-throw-conditions) |
|
|
394
|
+
| `joinRooms()` | `(opts: { rooms: string[] }): void` | Emits `join` with `{ rooms }`. No-op with a warning log if not connected |
|
|
395
|
+
| `leaveRooms()` | `(opts: { rooms: string[] }): void` | Emits `leave` with `{ rooms }`. No-op with a warning log if not connected |
|
|
396
|
+
| `shutdown()` | `(): void` | Removes all listeners, disconnects if connected, resets state to `unauthorized` |
|
|
669
397
|
|
|
670
|
-
####
|
|
398
|
+
#### `subscribe()` error-safe wrapping
|
|
671
399
|
|
|
672
|
-
|
|
400
|
+
Every handler is wrapped in a dual try-catch, so a broken handler never crashes the client:
|
|
673
401
|
|
|
674
402
|
```typescript
|
|
675
403
|
const wrappedHandler = (data: T) => {
|
|
@@ -683,337 +411,234 @@ const wrappedHandler = (data: T) => {
|
|
|
683
411
|
};
|
|
684
412
|
```
|
|
685
413
|
|
|
686
|
-
The outer `try-catch` handles synchronous throws
|
|
687
|
-
|
|
688
|
-
#### `TSocketIOEventHandler<T>` Type
|
|
689
|
-
|
|
690
|
-
```typescript
|
|
691
|
-
type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
Handlers can be synchronous (`void`) or asynchronous (`Promise<void>`). Both are handled correctly by the wrapping pattern.
|
|
695
|
-
|
|
696
|
-
### `subscribeMany()`
|
|
697
|
-
|
|
698
|
-
```typescript
|
|
699
|
-
subscribeMany(opts: {
|
|
700
|
-
events: Record<string, TSocketIOEventHandler>;
|
|
701
|
-
ignoreDuplicate?: boolean;
|
|
702
|
-
}): void
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
Batch subscribes to multiple events. Iterates over the `events` record and calls `subscribe()` for each entry.
|
|
706
|
-
|
|
707
|
-
### `unsubscribe()`
|
|
708
|
-
|
|
709
|
-
```typescript
|
|
710
|
-
unsubscribe(opts: { event: string; handler?: TSocketIOEventHandler }): void
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
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)`.
|
|
714
|
-
|
|
715
|
-
No-op if the socket has no listeners for the event.
|
|
716
|
-
|
|
717
|
-
### `unsubscribeMany()`
|
|
718
|
-
|
|
719
|
-
```typescript
|
|
720
|
-
unsubscribeMany(opts: { events: string[] }): void
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
Removes all handlers for each event in the array. Calls `unsubscribe({ event })` for each entry.
|
|
724
|
-
|
|
725
|
-
### `connect()`
|
|
726
|
-
|
|
727
|
-
```typescript
|
|
728
|
-
connect(): void
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
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.
|
|
732
|
-
|
|
733
|
-
### `disconnect()`
|
|
734
|
-
|
|
735
|
-
```typescript
|
|
736
|
-
disconnect(): void
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
Manually disconnects the socket. No-op with an info log if the client is not initialized.
|
|
740
|
-
|
|
741
|
-
### `emit()`
|
|
742
|
-
|
|
743
|
-
```typescript
|
|
744
|
-
emit<T = unknown>(opts: {
|
|
745
|
-
topic: string;
|
|
746
|
-
data: T;
|
|
747
|
-
doLog?: boolean; // default: false
|
|
748
|
-
callback?: () => void;
|
|
749
|
-
}): void
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
Emits an event to the server.
|
|
753
|
-
|
|
754
|
-
**Throws** (via `getError()`) if:
|
|
755
|
-
- The socket is not connected (`statusCode: 400`, message: `"Invalid socket client state to emit"`)
|
|
756
|
-
- The `topic` is falsy (`statusCode: 400`, message: `"Topic is required to emit"`)
|
|
757
|
-
|
|
758
|
-
If `callback` is provided, it is executed via `setImmediate()` (asynchronously, not after server acknowledgment). If `doLog` is `true`, logs the topic and data.
|
|
759
|
-
|
|
760
|
-
### `joinRooms()`
|
|
761
|
-
|
|
762
|
-
```typescript
|
|
763
|
-
joinRooms(opts: { rooms: string[] }): void
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
Emits a `join` event to the server with `{ rooms }`. The server will validate via `validateRoomFn` and perform the actual join.
|
|
767
|
-
|
|
768
|
-
No-op with warning log if the socket is not connected.
|
|
769
|
-
|
|
770
|
-
### `leaveRooms()`
|
|
771
|
-
|
|
772
|
-
```typescript
|
|
773
|
-
leaveRooms(opts: { rooms: string[] }): void
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
Emits a `leave` event to the server with `{ rooms }`. The server performs the actual leave without validation.
|
|
414
|
+
The outer `try-catch` handles synchronous throws. The `.catch()` on `Promise.resolve()` handles async rejections.
|
|
777
415
|
|
|
778
|
-
|
|
416
|
+
#### `emit()` throw conditions
|
|
779
417
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
```
|
|
785
|
-
|
|
786
|
-
Clean shutdown of the client:
|
|
787
|
-
|
|
788
|
-
1. Calls `removeAllListeners()` on the underlying socket to prevent memory leaks
|
|
789
|
-
2. Disconnects if still connected
|
|
790
|
-
3. Resets state to `unauthorized`
|
|
418
|
+
| Condition | statusCode | Message |
|
|
419
|
+
|---|---|---|
|
|
420
|
+
| Socket not connected | `400` | `"Invalid socket client state to emit"` |
|
|
421
|
+
| `topic` is falsy | `400` | `"Topic is required to emit"` |
|
|
791
422
|
|
|
792
423
|
## Internals
|
|
793
424
|
|
|
794
425
|
### `resolveBindings()`
|
|
795
426
|
|
|
796
|
-
Reads
|
|
427
|
+
Reads every binding key from the DI container and validates the required ones.
|
|
797
428
|
|
|
798
|
-
| Binding | Validation | Error on
|
|
799
|
-
|
|
800
|
-
| `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` |
|
|
801
|
-
| `REDIS_CONNECTION` | Must
|
|
802
|
-
| `AUTHENTICATE_HANDLER` | Must be a function
|
|
803
|
-
| `VALIDATE_ROOM_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` |
|
|
804
|
-
| `CLIENT_CONNECTED_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` |
|
|
429
|
+
| Binding | Validation | Error on failure |
|
|
430
|
+
|---|---|---|
|
|
431
|
+
| `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` | - |
|
|
432
|
+
| `REDIS_CONNECTION` | Must pass `isRedisHelper()` | `"Invalid instance of redisConnection..."` |
|
|
433
|
+
| `AUTHENTICATE_HANDLER` | Must be a truthy function | `"[DANGER][SocketIOComponent] Invalid authenticateFn to setup io socket server!"` |
|
|
434
|
+
| `VALIDATE_ROOM_HANDLER` | Optional, resolved from the container, `null` coerced to `undefined` | - |
|
|
435
|
+
| `CLIENT_CONNECTED_HANDLER` | Optional, resolved from the container, `null` coerced to `undefined` | - |
|
|
805
436
|
|
|
806
|
-
###
|
|
437
|
+
### Post-start hooks
|
|
807
438
|
|
|
808
|
-
|
|
439
|
+
Both runtimes register the same hook identifier, `'socket-io-initialize'`, but wire the helper differently.
|
|
809
440
|
|
|
810
|
-
|
|
811
|
-
2. Creates `SocketIOServerHelper` with `runtime: RuntimeModules.BUN`
|
|
812
|
-
3. Awaits `socketIOHelper.configure()` which waits for all Redis connections to be ready before initializing the adapter and emitter
|
|
813
|
-
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
814
|
-
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
|
|
441
|
+
**Bun** (`registerBunHook()`):
|
|
815
442
|
|
|
816
|
-
|
|
443
|
+
1. Calls `createBunEngine({ serverOptions })`, which dynamically imports `@socket.io/bun-engine` and builds a `BunEngine` instance with CORS bridging.
|
|
444
|
+
2. Constructs `SocketIOServerHelper` with `runtime: RuntimeModules.BUN`.
|
|
445
|
+
3. Awaits `socketIOHelper.configure()`.
|
|
446
|
+
4. Binds the helper to `SOCKET_IO_INSTANCE`.
|
|
447
|
+
5. Calls `serverInstance.reload({ fetch, websocket })` to wire the engine into the running Bun server.
|
|
817
448
|
|
|
818
449
|
```typescript
|
|
819
450
|
async function createBunEngine(opts: {
|
|
820
451
|
serverOptions: Partial<ServerOptions>;
|
|
821
|
-
}): Promise<{ engine: any; engineHandler: any }>
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
452
|
+
}): Promise<{ engine: any; engineHandler: any }> {
|
|
453
|
+
const { serverOptions } = opts;
|
|
454
|
+
const { Server: BunEngine } = await import('@socket.io/bun-engine');
|
|
455
|
+
|
|
456
|
+
const corsConfig = typeof serverOptions.cors === 'object' ? serverOptions.cors : undefined;
|
|
457
|
+
const engine = new BunEngine({
|
|
458
|
+
path: serverOptions.path ?? '/socket.io/',
|
|
459
|
+
...(corsConfig && {
|
|
460
|
+
cors: {
|
|
461
|
+
origin: corsConfig.origin as string | RegExp | (string | RegExp)[] | undefined,
|
|
462
|
+
methods: corsConfig.methods,
|
|
463
|
+
credentials: corsConfig.credentials,
|
|
464
|
+
allowedHeaders: corsConfig.allowedHeaders,
|
|
465
|
+
exposedHeaders: corsConfig.exposedHeaders,
|
|
466
|
+
maxAge: corsConfig.maxAge,
|
|
467
|
+
},
|
|
468
|
+
}),
|
|
469
|
+
});
|
|
827
470
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
engine: any;
|
|
831
|
-
enginePath: string;
|
|
832
|
-
honoServer: OpenAPIHono;
|
|
833
|
-
}): (req: Request, server: TBunServerInstance) => Response | Promise<Response>
|
|
471
|
+
return { engine, engineHandler: engine.handler() };
|
|
472
|
+
}
|
|
834
473
|
```
|
|
835
474
|
|
|
836
|
-
|
|
837
|
-
- If `url.pathname` starts with `enginePath`, delegates to `engine.handleRequest(req, server)` (returns 404 Response if `handleRequest` returns nullish)
|
|
838
|
-
- Otherwise, delegates to `honoServer.fetch(req, server)` for normal Hono routing
|
|
475
|
+
**Node.js** (`registerNodeHook()`):
|
|
839
476
|
|
|
840
|
-
|
|
477
|
+
1. Gets the HTTP server via `getServerInstance()`. Throws `"[SocketIOComponent] HTTP server not available for Node.js runtime!"` if it's missing.
|
|
478
|
+
2. Calls `createNodeSocketIOHelper()`, which constructs `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`.
|
|
479
|
+
3. Binds the helper to `SOCKET_IO_INSTANCE`.
|
|
841
480
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
1. Gets the HTTP server instance via `getServerInstance()`
|
|
845
|
-
2. Validates the server instance exists (throws `"[SocketIOComponent] HTTP server not available for Node.js runtime!"` if not)
|
|
846
|
-
3. Calls `createNodeSocketIOHelper()` which creates `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`
|
|
847
|
-
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
848
|
-
|
|
849
|
-
Node mode is simpler because Socket.IO natively attaches to `node:http.Server`.
|
|
850
|
-
|
|
851
|
-
### Redis 3-Client Architecture
|
|
852
|
-
|
|
853
|
-
The server helper creates 3 independent Redis connections from a single `AbstractRedisHelper` instance:
|
|
481
|
+
### Redis 3-client architecture
|
|
854
482
|
|
|
855
483
|
```
|
|
856
|
-
RedisSingleHelper (parent
|
|
484
|
+
RedisSingleHelper (parent - NOT consumed)
|
|
857
485
|
|
|
|
858
|
-
+--
|
|
486
|
+
+-- duplicateClient() --> redisPub (Redis adapter - publishes)
|
|
859
487
|
|
|
|
860
|
-
+--
|
|
488
|
+
+-- duplicateClient() --> redisSub (Redis adapter - subscribes)
|
|
861
489
|
|
|
|
862
|
-
+--
|
|
490
|
+
+-- duplicateClient() --> redisEmitter (Redis emitter - message delivery)
|
|
863
491
|
```
|
|
864
492
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
-
|
|
868
|
-
-
|
|
493
|
+
Three clients, not one, for two reasons:
|
|
494
|
+
|
|
495
|
+
- **`@socket.io/redis-adapter` needs separate pub and sub clients.** A Redis connection in subscribe mode can't run other commands.
|
|
496
|
+
- **`@socket.io/redis-emitter` needs its own client.** It emits messages independently of the adapter, so a process with no local Socket.IO server can still broadcast.
|
|
497
|
+
|
|
498
|
+
The parent connection stays independent and unconsumed - reuse it for caching, sessions, or anything else.
|
|
869
499
|
|
|
870
|
-
**`TRedisClient` type:**
|
|
871
500
|
```typescript
|
|
872
501
|
type TRedisClient = Redis | Cluster;
|
|
873
502
|
```
|
|
874
503
|
|
|
875
|
-
This
|
|
876
|
-
|
|
877
|
-
### `setRuntime()` -- Runtime Validation
|
|
878
|
-
|
|
879
|
-
The private `setRuntime()` method validates the constructor options based on the `runtime` discriminant:
|
|
504
|
+
This alias covers both single-instance and `Cluster` connections from `ioredis`, so the helper is transparent to the Redis deployment topology.
|
|
880
505
|
|
|
881
|
-
|
|
882
|
-
|---------|---------------|------------------|
|
|
883
|
-
| `RuntimeModules.NODE` | `opts.server` (HTTPServer) | `"Invalid HTTP server for Node.js runtime!"` |
|
|
884
|
-
| `RuntimeModules.BUN` | `opts.engine` (BunEngine) | `"Invalid @socket.io/bun-engine instance for Bun runtime!"` |
|
|
885
|
-
| Other | -- | `"Unsupported runtime!"` |
|
|
506
|
+
### `setRuntime()` runtime validation
|
|
886
507
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
508
|
+
| Runtime | Required field | Error on missing |
|
|
509
|
+
|---|---|---|
|
|
510
|
+
| `RuntimeModules.NODE` | `opts.server` (`HTTPServer`) | `"[SocketIOServerHelper] Invalid HTTP server for Node.js runtime!"` |
|
|
511
|
+
| `RuntimeModules.BUN` | `opts.engine` (`BunEngine`) | `"[SocketIOServerHelper] Invalid @socket.io/bun-engine instance for Bun runtime!"` |
|
|
512
|
+
| Other | - | `"[SocketIOServerHelper] Unsupported runtime!"` |
|
|
892
513
|
|
|
893
|
-
|
|
514
|
+
### `initRedisClients()`
|
|
894
515
|
|
|
895
|
-
Creates 3 duplicated clients from the parent connection
|
|
516
|
+
Creates the 3 duplicated clients from the parent connection. Throws `"Invalid redis connection to config socket.io adapter!"` if `redisConnection` is falsy.
|
|
896
517
|
|
|
897
|
-
### `initIOServer()`
|
|
518
|
+
### `initIOServer()`
|
|
898
519
|
|
|
899
|
-
Called during `configure()
|
|
520
|
+
Called during `configure()`, after the Redis connections are ready.
|
|
900
521
|
|
|
901
522
|
| Runtime | Initialization |
|
|
902
|
-
|
|
523
|
+
|---|---|
|
|
903
524
|
| `RuntimeModules.NODE` | `this.io = new IOServer(this.server, this.serverOptions)` |
|
|
904
525
|
| `RuntimeModules.BUN` | `this.io = new IOServer()` then `this.io.bind(this.bunEngine)` |
|
|
905
526
|
| Other | Throws `"Unsupported runtime: <runtime>"` |
|
|
906
527
|
|
|
907
|
-
|
|
908
|
-
- Node.js without `this.server`: `"[DANGER] Invalid HTTP server instance to init Socket.io server!"`
|
|
909
|
-
- Bun without `this.bunEngine`: `"[DANGER] Invalid @socket.io/bun-engine instance to init Socket.io server!"`
|
|
528
|
+
Two more guards run inside each branch:
|
|
910
529
|
|
|
911
|
-
|
|
530
|
+
| Runtime | Missing field | Error |
|
|
531
|
+
|---|---|---|
|
|
532
|
+
| Node.js | `this.server` | `"[DANGER] Invalid HTTP server instance to init Socket.io server!"` |
|
|
533
|
+
| Bun | `this.bunEngine` | `"[DANGER] Invalid @socket.io/bun-engine instance to init Socket.io server!"` |
|
|
912
534
|
|
|
913
|
-
|
|
535
|
+
### Connection lifecycle
|
|
914
536
|
|
|
915
537
|
```
|
|
916
538
|
Client connects
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
| +-- Validate socket exists and not duplicate
|
|
920
|
-
| +-- Create ISocketIOClient entry (state: UNAUTHORIZED)
|
|
921
|
-
| +-- Start authenticateTimeout (10s default)
|
|
922
|
-
| +-- Register 'disconnect' handler
|
|
923
|
-
| +-- Register 'authenticate' handler
|
|
924
|
-
|
|
|
925
|
-
+-- Client emits 'authenticate'
|
|
926
|
-
| +-- Validate client exists and state is UNAUTHORIZED
|
|
927
|
-
| +-- Set state to AUTHENTICATING
|
|
928
|
-
| +-- Call authenticateFn(handshake)
|
|
929
|
-
| +-- Success -> onClientAuthenticated()
|
|
930
|
-
| | +-- Set state to AUTHENTICATED
|
|
931
|
-
| | +-- Send initial ping
|
|
932
|
-
| | +-- Join default rooms (io-default, io-notification)
|
|
933
|
-
| | +-- Register 'join' and 'leave' room handlers
|
|
934
|
-
| | +-- Start ping interval (30s default)
|
|
935
|
-
| | +-- Emit 'authenticated' with { id, time }
|
|
936
|
-
| | +-- Call clientConnectedFn({ socket }) if provided
|
|
937
|
-
| +-- Failure -> emit 'unauthenticated' -> disconnect
|
|
938
|
-
|
|
|
939
|
-
+-- Timeout (10s) -> disconnect if not AUTHENTICATED
|
|
940
|
-
```
|
|
539
|
+
-> onClientConnect(): validate socket, create client (state UNAUTHORIZED),
|
|
540
|
+
start authenticateTimeout (10s default), register 'disconnect' + 'authenticate' handlers
|
|
941
541
|
|
|
942
|
-
|
|
542
|
+
Client emits 'authenticate'
|
|
543
|
+
-> validate client exists and state is UNAUTHORIZED, set state AUTHENTICATING
|
|
544
|
+
-> call authenticateFn(handshake)
|
|
545
|
+
success -> onClientAuthenticated(): state AUTHENTICATED, send initial ping,
|
|
546
|
+
join default rooms, register 'join'/'leave' handlers,
|
|
547
|
+
start ping interval, emit 'authenticated', call clientConnectedFn()
|
|
548
|
+
failure -> emit 'unauthenticated', disconnect
|
|
943
549
|
|
|
944
|
-
|
|
550
|
+
Timeout (10s) -> disconnect if not yet AUTHENTICATED
|
|
551
|
+
```
|
|
945
552
|
|
|
946
|
-
|
|
947
|
-
- Sets client state back to `UNAUTHORIZED`
|
|
948
|
-
- Sends `unauthenticated` event with message: `"Invalid token to authenticate! Please login again!"`
|
|
949
|
-
- Disconnects after send via `setImmediate` callback
|
|
950
|
-
- No error logging (this is an expected outcome)
|
|
553
|
+
#### Authentication failure paths
|
|
951
554
|
|
|
952
|
-
|
|
953
|
-
- Sets client state back to `UNAUTHORIZED`
|
|
954
|
-
- Logs the error at error level
|
|
955
|
-
- Sends `unauthenticated` event with message: `"Failed to authenticate connection! Please login again!"`
|
|
956
|
-
- Sets `doLog: true` on the send call (unlike Path 1)
|
|
957
|
-
- Disconnects after send via `setImmediate` callback
|
|
555
|
+
`registerAuthHandler()` resolves an auth failure through one of two paths. Both check `this.clients.has(id)` first, in case the client disconnected mid-authentication.
|
|
958
556
|
|
|
959
|
-
|
|
557
|
+
| Path | Trigger | Actions |
|
|
558
|
+
|---|---|---|
|
|
559
|
+
| Rejected | `authenticateFn` resolves `false` | Reset state to `UNAUTHORIZED`. Send `unauthenticated` with `"Invalid token to authenticate! Please login again!"`. Disconnect after send (`setImmediate`). No error logged - this is an expected outcome. |
|
|
560
|
+
| Threw | `authenticateFn` throws | Reset state to `UNAUTHORIZED`. Log the error. Send `unauthenticated` with `"Failed to authenticate connection! Please login again!"`. Disconnect after send (`setImmediate`). |
|
|
960
561
|
|
|
961
|
-
#### `ISocketIOClient` Interface
|
|
962
562
|
```typescript
|
|
963
563
|
interface ISocketIOClient {
|
|
964
564
|
id: string;
|
|
965
565
|
socket: IOSocket;
|
|
966
566
|
state: TSocketIOClientState; // 'unauthorized' | 'authenticating' | 'authenticated'
|
|
967
|
-
interval?: NodeJS.Timeout; // Ping interval
|
|
968
|
-
authenticateTimeout?: NodeJS.Timeout; // Auth deadline
|
|
567
|
+
interval?: NodeJS.Timeout; // Ping interval, set after auth
|
|
568
|
+
authenticateTimeout?: NodeJS.Timeout; // Auth deadline, undefined once cleared
|
|
969
569
|
}
|
|
970
570
|
```
|
|
971
571
|
|
|
972
|
-
### Room
|
|
973
|
-
|
|
974
|
-
Room join/leave handlers are registered after successful authentication:
|
|
572
|
+
### Room handlers
|
|
975
573
|
|
|
976
|
-
|
|
977
|
-
- **`leave`**: Client emits `{ rooms: string[] }`. Leave is always allowed -- no validation function needed.
|
|
574
|
+
Registered after successful authentication.
|
|
978
575
|
|
|
979
|
-
|
|
576
|
+
| Handler | Behavior |
|
|
577
|
+
|---|---|
|
|
578
|
+
| `join` | Client sends `{ rooms }`. If `validateRoomFn` is bound, only the rooms it returns get joined. If it isn't bound, the join is rejected with a warning log. |
|
|
579
|
+
| `leave` | Client sends `{ rooms }`. Always allowed - no validation function needed. |
|
|
980
580
|
|
|
981
|
-
|
|
581
|
+
Both handlers parse the payload defensively: `const { rooms = [] } = payload || { rooms: [] }`. An empty array is silently ignored. A join error is caught and logged - it never disconnects the client.
|
|
982
582
|
|
|
983
583
|
> [!WARNING]
|
|
984
|
-
> Without a `validateRoomFn` bound, clients **cannot** join any custom
|
|
584
|
+
> Without a `validateRoomFn` bound, clients **cannot** join any custom room - they stay in the default rooms only. This is security-by-default.
|
|
985
585
|
|
|
986
|
-
|
|
586
|
+
### Graceful shutdown
|
|
987
587
|
|
|
988
|
-
|
|
588
|
+
Shut down Socket.IO before stopping the application:
|
|
989
589
|
|
|
990
590
|
```typescript
|
|
991
|
-
|
|
591
|
+
override async stop(): Promise<void> {
|
|
592
|
+
// 1. Shut down Socket.IO (disconnects all clients, closes the IO server, quits Redis)
|
|
593
|
+
const socketIOHelper = this.get<SocketIOServerHelper>({
|
|
594
|
+
key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
|
|
595
|
+
isOptional: true,
|
|
596
|
+
});
|
|
597
|
+
if (socketIOHelper) {
|
|
598
|
+
await socketIOHelper.shutdown();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 2. Disconnect the Redis helper
|
|
602
|
+
if (this.redisHelper) {
|
|
603
|
+
await this.redisHelper.disconnect();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 3. Stop the HTTP/Bun server
|
|
607
|
+
await super.stop();
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
`socketIOHelper.shutdown()` runs the 4 steps under [Shutdown](#shutdown) above. `clientHelper.shutdown()` does the client-side equivalent: remove listeners, disconnect if connected, reset state.
|
|
612
|
+
|
|
613
|
+
## Types reference
|
|
614
|
+
|
|
615
|
+
### Server types
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// Server constructor options - discriminated union on 'runtime'
|
|
992
619
|
type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
|
|
993
620
|
|
|
994
|
-
// Base options shared by both runtimes
|
|
995
621
|
interface ISocketIOServerBaseOptions {
|
|
996
622
|
identifier: string;
|
|
997
623
|
serverOptions: Partial<ServerOptions>;
|
|
998
624
|
redisConnection: IRedisHelper;
|
|
999
|
-
defaultRooms?: string[];
|
|
1000
|
-
authenticateTimeout?: number;
|
|
1001
|
-
pingInterval?: number;
|
|
625
|
+
defaultRooms?: string[]; // Default: ['io-default', 'io-notification']
|
|
626
|
+
authenticateTimeout?: number; // Default: 10_000 (10 seconds)
|
|
627
|
+
pingInterval?: number; // Default: 30_000 (30 seconds)
|
|
628
|
+
|
|
1002
629
|
authenticateFn: TSocketIOAuthenticateFn;
|
|
1003
630
|
validateRoomFn?: TSocketIOValidateRoomFn;
|
|
1004
631
|
clientConnectedFn?: TSocketIOClientConnectedFn;
|
|
1005
632
|
}
|
|
1006
633
|
|
|
1007
|
-
// Node.js runtime variant
|
|
1008
634
|
interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
|
|
1009
635
|
runtime: typeof RuntimeModules.NODE;
|
|
1010
|
-
server: HTTPServer;
|
|
636
|
+
server: HTTPServer; // node:http.Server instance
|
|
1011
637
|
}
|
|
1012
638
|
|
|
1013
|
-
// Bun runtime variant
|
|
1014
639
|
interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
|
|
1015
640
|
runtime: typeof RuntimeModules.BUN;
|
|
1016
|
-
engine: any;
|
|
641
|
+
engine: any; // @socket.io/bun-engine Server instance
|
|
1017
642
|
}
|
|
1018
643
|
|
|
1019
644
|
// Tracked client entry (server-side)
|
|
@@ -1025,11 +650,10 @@ interface ISocketIOClient {
|
|
|
1025
650
|
authenticateTimeout?: NodeJS.Timeout;
|
|
1026
651
|
}
|
|
1027
652
|
|
|
1028
|
-
// Redis client type alias
|
|
1029
653
|
type TRedisClient = Redis | Cluster;
|
|
1030
654
|
```
|
|
1031
655
|
|
|
1032
|
-
### Client
|
|
656
|
+
### Client types
|
|
1033
657
|
|
|
1034
658
|
```typescript
|
|
1035
659
|
// Client constructor options
|
|
@@ -1037,6 +661,8 @@ interface ISocketIOClientOptions {
|
|
|
1037
661
|
identifier: string;
|
|
1038
662
|
host: string;
|
|
1039
663
|
options: IOptions;
|
|
664
|
+
|
|
665
|
+
// Lifecycle callbacks (all optional)
|
|
1040
666
|
onConnected?: () => ValueOrPromise<void>;
|
|
1041
667
|
onDisconnected?: (reason: string) => ValueOrPromise<void>;
|
|
1042
668
|
onError?: (error: Error) => ValueOrPromise<void>;
|
|
@@ -1050,15 +676,13 @@ interface IOptions extends SocketOptions {
|
|
|
1050
676
|
extraHeaders: Record<string | symbol | number, any>;
|
|
1051
677
|
}
|
|
1052
678
|
|
|
1053
|
-
// Event handler type (supports sync and async)
|
|
1054
679
|
type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
|
|
1055
680
|
|
|
1056
|
-
// Client state type
|
|
1057
681
|
type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
|
|
1058
682
|
// Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'
|
|
1059
683
|
```
|
|
1060
684
|
|
|
1061
|
-
### Callback
|
|
685
|
+
### Callback types
|
|
1062
686
|
|
|
1063
687
|
```typescript
|
|
1064
688
|
// Handshake payload passed to the authenticate handler
|
|
@@ -1074,28 +698,21 @@ interface IHandshake {
|
|
|
1074
698
|
auth: { [key: string]: any };
|
|
1075
699
|
}
|
|
1076
700
|
|
|
1077
|
-
// Server authentication handler
|
|
1078
701
|
type TSocketIOAuthenticateFn = (args: IHandshake) => ValueOrPromise<boolean>;
|
|
1079
702
|
|
|
1080
|
-
// Server room validation handler
|
|
1081
703
|
type TSocketIOValidateRoomFn = (opts: {
|
|
1082
704
|
socket: IOSocket;
|
|
1083
705
|
rooms: string[];
|
|
1084
706
|
}) => ValueOrPromise<string[]>;
|
|
1085
707
|
|
|
1086
|
-
// Server client connected handler
|
|
1087
708
|
type TSocketIOClientConnectedFn = (opts: { socket: IOSocket }) => ValueOrPromise<void>;
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
### Component Types
|
|
1091
709
|
|
|
1092
|
-
```typescript
|
|
1093
710
|
// Extended ServerOptions with identifier
|
|
1094
711
|
interface IServerOptions extends ServerOptions {
|
|
1095
712
|
identifier: string;
|
|
1096
713
|
}
|
|
1097
714
|
|
|
1098
|
-
// Resolved binding values from DI container
|
|
715
|
+
// Resolved binding values from the DI container
|
|
1099
716
|
interface IResolvedBindings {
|
|
1100
717
|
redisConnection: IRedisHelper;
|
|
1101
718
|
authenticateFn: TSocketIOAuthenticateFn;
|
|
@@ -1104,130 +721,8 @@ interface IResolvedBindings {
|
|
|
1104
721
|
}
|
|
1105
722
|
```
|
|
1106
723
|
|
|
1107
|
-
##
|
|
1108
|
-
|
|
1109
|
-
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.
|
|
1110
|
-
|
|
1111
|
-
The component relies on `AbstractApplication`'s post-start hook system:
|
|
1112
|
-
|
|
1113
|
-
#### API
|
|
1114
|
-
|
|
1115
|
-
```typescript
|
|
1116
|
-
// Register a hook (during binding phase)
|
|
1117
|
-
application.registerPostStartHook({
|
|
1118
|
-
identifier: string, // Unique name for logging
|
|
1119
|
-
hook: () => ValueOrPromise<void>, // Async function to execute
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
// Get the server instance (available after start)
|
|
1123
|
-
application.getServerInstance<T>(): T | undefined;
|
|
1124
|
-
```
|
|
1125
|
-
|
|
1126
|
-
#### Hook Execution Flow
|
|
1127
|
-
|
|
1128
|
-
```
|
|
1129
|
-
Application.start()
|
|
1130
|
-
|
|
|
1131
|
-
+-- Bun.serve() / serve() <-- Server created
|
|
1132
|
-
|
|
|
1133
|
-
+-- executePostStartHooks() <-- Hooks run here
|
|
1134
|
-
|
|
|
1135
|
-
+-- SocketIOComponent hook:
|
|
1136
|
-
1. Get server instance via getServerInstance()
|
|
1137
|
-
2. Create SocketIOServerHelper with runtime-specific options
|
|
1138
|
-
3. Call helper.configure() to initialize Socket.IO server
|
|
1139
|
-
4. Bind the helper instance for injection
|
|
1140
|
-
```
|
|
1141
|
-
|
|
1142
|
-
#### Detailed Hook Timing
|
|
1143
|
-
```
|
|
1144
|
-
executePostStartHooks()
|
|
1145
|
-
|-- Hook 1: "socket-io-initialize"
|
|
1146
|
-
| |-- performance.now() -> start
|
|
1147
|
-
| |-- await hook()
|
|
1148
|
-
| +-- log: "Executed hook | identifier: socket-io-initialize | took: 12.5 (ms)"
|
|
1149
|
-
|-- Hook 2: "another-hook"
|
|
1150
|
-
| +-- ...
|
|
1151
|
-
+-- (hooks run sequentially in registration order)
|
|
1152
|
-
```
|
|
1153
|
-
|
|
1154
|
-
- Hooks run **sequentially** (not parallel) to guarantee ordering
|
|
1155
|
-
- Each hook is timed with `performance.now()` for diagnostics
|
|
1156
|
-
- If a hook throws, it propagates to `start()` and the server fails to start
|
|
1157
|
-
|
|
1158
|
-
#### What Happens Inside the Hook
|
|
1159
|
-
|
|
1160
|
-
For **Bun runtime**, the hook:
|
|
1161
|
-
|
|
1162
|
-
1. Calls `createBunEngine({ serverOptions })` which dynamically imports `@socket.io/bun-engine` and creates a `BunEngine` instance with CORS config bridging
|
|
1163
|
-
2. Creates `SocketIOServerHelper` with `runtime: RuntimeModules.BUN` and the engine
|
|
1164
|
-
3. Awaits `socketIOHelper.configure()` which connects Redis pub/sub/emitter clients, initializes the `IOServer`, and sets up the Redis adapter
|
|
1165
|
-
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
1166
|
-
5. Gets the Bun server instance and Hono server
|
|
1167
|
-
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`
|
|
1168
|
-
|
|
1169
|
-
For **Node.js runtime**, the hook:
|
|
1170
|
-
|
|
1171
|
-
1. Gets the HTTP server instance via `getServerInstance()`
|
|
1172
|
-
2. Validates the server instance exists (throws if not)
|
|
1173
|
-
3. Calls `createNodeSocketIOHelper()` which creates `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`
|
|
1174
|
-
4. Binds the helper to `SOCKET_IO_INSTANCE`
|
|
1175
|
-
|
|
1176
|
-
> [!NOTE]
|
|
1177
|
-
> The hook identifier is `'socket-io-initialize'` for both runtimes. Only one runtime path executes per application.
|
|
1178
|
-
|
|
1179
|
-
### Graceful Shutdown
|
|
1180
|
-
|
|
1181
|
-
Always shut down the Socket.IO server before stopping the application:
|
|
1182
|
-
|
|
1183
|
-
#### Shutdown Implementation
|
|
1184
|
-
```typescript
|
|
1185
|
-
override async stop(): Promise<void> {
|
|
1186
|
-
// 1. Shut down Socket.IO (disconnects all clients, closes IO server, quits Redis)
|
|
1187
|
-
const socketIOHelper = this.get<SocketIOServerHelper>({
|
|
1188
|
-
key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
|
|
1189
|
-
isOptional: true,
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
if (socketIOHelper) {
|
|
1193
|
-
await socketIOHelper.shutdown();
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
// 2. Disconnect Redis helper
|
|
1197
|
-
if (this.redisHelper) {
|
|
1198
|
-
await this.redisHelper.disconnect();
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// 3. Stop the HTTP/Bun server
|
|
1202
|
-
await super.stop();
|
|
1203
|
-
}
|
|
1204
|
-
```
|
|
1205
|
-
|
|
1206
|
-
#### Shutdown Flow
|
|
1207
|
-
```
|
|
1208
|
-
socketIOHelper.shutdown()
|
|
1209
|
-
|-- Disconnect all tracked clients
|
|
1210
|
-
| |-- clearInterval(ping)
|
|
1211
|
-
| |-- clearTimeout(authenticateTimeout)
|
|
1212
|
-
| +-- socket.disconnect()
|
|
1213
|
-
|-- clients.clear()
|
|
1214
|
-
|-- io.close() -- closes the Socket.IO server (async)
|
|
1215
|
-
+-- Redis cleanup
|
|
1216
|
-
|-- redisPub.quit()
|
|
1217
|
-
|-- redisSub.quit()
|
|
1218
|
-
+-- redisEmitter.quit()
|
|
1219
|
-
```
|
|
1220
|
-
|
|
1221
|
-
Client helper shutdown:
|
|
1222
|
-
```
|
|
1223
|
-
clientHelper.shutdown()
|
|
1224
|
-
|-- removeAllListeners() -- prevents memory leaks
|
|
1225
|
-
|-- disconnect() -- if still connected
|
|
1226
|
-
+-- state = UNAUTHORIZED
|
|
1227
|
-
```
|
|
1228
|
-
|
|
1229
|
-
## See Also
|
|
724
|
+
## See also
|
|
1230
725
|
|
|
1231
|
-
- [
|
|
1232
|
-
- [Usage & Examples](./usage)
|
|
1233
|
-
- [Error Reference](./errors)
|
|
726
|
+
- [Overview](./) - quick start, imports, common configuration tasks
|
|
727
|
+
- [Usage & Examples](./usage) - full setup steps, server-side usage, client helper, advanced patterns
|
|
728
|
+
- [Error Reference](./errors) - error conditions and troubleshooting
|