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