@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.
Files changed (145) hide show
  1. package/content/best-practices/architectural-patterns.md +3 -3
  2. package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
  3. package/content/best-practices/contribution-workflow.md +2 -2
  4. package/content/best-practices/error-handling.md +94 -89
  5. package/content/best-practices/security-guidelines.md +5 -5
  6. package/content/extensions/components/api-reference.md +22 -21
  7. package/content/extensions/components/authentication/api.md +64 -28
  8. package/content/extensions/components/authentication/errors.md +19 -5
  9. package/content/extensions/components/authentication/index.md +19 -18
  10. package/content/extensions/components/authentication/usage.md +55 -30
  11. package/content/extensions/components/authorization/api.md +351 -84
  12. package/content/extensions/components/authorization/errors.md +51 -17
  13. package/content/extensions/components/authorization/getting-started.md +227 -0
  14. package/content/extensions/components/authorization/index.md +24 -16
  15. package/content/extensions/components/authorization/usage.md +45 -21
  16. package/content/extensions/components/health-check.md +15 -9
  17. package/content/extensions/components/index.md +24 -90
  18. package/content/extensions/components/mail/api.md +105 -54
  19. package/content/extensions/components/mail/errors.md +12 -10
  20. package/content/extensions/components/mail/index.md +32 -13
  21. package/content/extensions/components/mail/usage.md +26 -18
  22. package/content/extensions/components/request-tracker.md +18 -14
  23. package/content/extensions/components/socket-io/api.md +377 -882
  24. package/content/extensions/components/socket-io/errors.md +49 -51
  25. package/content/extensions/components/socket-io/index.md +72 -88
  26. package/content/extensions/components/socket-io/usage.md +107 -117
  27. package/content/extensions/components/static-asset/api.md +83 -31
  28. package/content/extensions/components/static-asset/errors.md +18 -7
  29. package/content/extensions/components/static-asset/index.md +18 -11
  30. package/content/extensions/components/static-asset/usage.md +11 -6
  31. package/content/extensions/components/template/index.md +3 -3
  32. package/content/extensions/components/websocket/api.md +58 -27
  33. package/content/extensions/components/websocket/errors.md +3 -3
  34. package/content/extensions/components/websocket/index.md +8 -7
  35. package/content/extensions/components/websocket/usage.md +21 -8
  36. package/content/extensions/helpers/cron/index.md +8 -7
  37. package/content/extensions/helpers/crypto/index.md +16 -8
  38. package/content/extensions/helpers/crypto/reference.md +96 -24
  39. package/content/extensions/helpers/env/index.md +14 -10
  40. package/content/extensions/helpers/error/index.md +99 -23
  41. package/content/extensions/helpers/index.md +61 -47
  42. package/content/extensions/helpers/inversion/index.md +23 -6
  43. package/content/extensions/helpers/inversion/reference.md +30 -22
  44. package/content/extensions/helpers/kafka/admin.md +3 -2
  45. package/content/extensions/helpers/kafka/compile-binary.md +69 -44
  46. package/content/extensions/helpers/kafka/consumer.md +24 -21
  47. package/content/extensions/helpers/kafka/examples.md +22 -234
  48. package/content/extensions/helpers/kafka/index.md +30 -62
  49. package/content/extensions/helpers/kafka/producer.md +31 -26
  50. package/content/extensions/helpers/kafka/schema-registry.md +19 -14
  51. package/content/extensions/helpers/logger/hf-logger.md +49 -22
  52. package/content/extensions/helpers/logger/index.md +37 -12
  53. package/content/extensions/helpers/logger/pino.md +31 -11
  54. package/content/extensions/helpers/logger/reference.md +243 -52
  55. package/content/extensions/helpers/network/api.md +65 -30
  56. package/content/extensions/helpers/network/index.md +32 -11
  57. package/content/extensions/helpers/queue/index.md +17 -6
  58. package/content/extensions/helpers/queue/reference.md +52 -25
  59. package/content/extensions/helpers/redis/index.md +29 -11
  60. package/content/extensions/helpers/redis/reference.md +85 -28
  61. package/content/extensions/helpers/secrets/index.md +82 -12
  62. package/content/extensions/helpers/socket-io/api.md +40 -21
  63. package/content/extensions/helpers/socket-io/index.md +26 -10
  64. package/content/extensions/helpers/storage/api.md +42 -24
  65. package/content/extensions/helpers/storage/index.md +10 -7
  66. package/content/extensions/helpers/types/index.md +20 -7
  67. package/content/extensions/helpers/types/reference.md +53 -14
  68. package/content/extensions/helpers/uid/index.md +166 -10
  69. package/content/extensions/helpers/websocket/api.md +52 -13
  70. package/content/extensions/helpers/websocket/index.md +7 -4
  71. package/content/extensions/helpers/worker-thread/index.md +9 -5
  72. package/content/extensions/helpers/worker-thread/reference.md +20 -20
  73. package/content/extensions/index.md +38 -39
  74. package/content/extensions/src-details/mcp-server.md +96 -548
  75. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  76. package/content/guides/core-concepts/persistent/index.md +5 -1
  77. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  78. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  79. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  80. package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
  81. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  82. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  83. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  84. package/content/guides/get-started/philosophy.md +135 -670
  85. package/content/guides/get-started/setup.md +53 -74
  86. package/content/guides/migrations/redis-helpers-migration.md +4 -3
  87. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  88. package/content/guides/migrations/unified-connectors-migration.md +7 -6
  89. package/content/guides/tutorials/realtime-chat.md +1 -1
  90. package/content/references/base/application.md +63 -21
  91. package/content/references/base/components.md +3 -3
  92. package/content/references/base/connectors.md +14 -14
  93. package/content/references/base/controllers.md +12 -12
  94. package/content/references/base/datasources-reference.md +32 -31
  95. package/content/references/base/datasources.md +6 -6
  96. package/content/references/base/dependency-injection.md +12 -11
  97. package/content/references/base/filter-system/application-usage.md +54 -30
  98. package/content/references/base/filter-system/array-operators.md +24 -46
  99. package/content/references/base/filter-system/comparison-operators.md +47 -67
  100. package/content/references/base/filter-system/default-filter.md +59 -53
  101. package/content/references/base/filter-system/fields-order-pagination.md +92 -146
  102. package/content/references/base/filter-system/index.md +25 -13
  103. package/content/references/base/filter-system/json-filtering.md +45 -184
  104. package/content/references/base/filter-system/list-operators.md +23 -53
  105. package/content/references/base/filter-system/logical-operators.md +63 -121
  106. package/content/references/base/filter-system/null-operators.md +34 -104
  107. package/content/references/base/filter-system/pattern-matching.md +40 -55
  108. package/content/references/base/filter-system/quick-reference.md +86 -198
  109. package/content/references/base/filter-system/range-operators.md +18 -46
  110. package/content/references/base/filter-system/tips.md +6 -6
  111. package/content/references/base/filter-system/use-cases.md +33 -15
  112. package/content/references/base/grpc-controllers.md +53 -17
  113. package/content/references/base/index.md +5 -3
  114. package/content/references/base/middlewares.md +11 -10
  115. package/content/references/base/models-reference.md +17 -17
  116. package/content/references/base/models.md +4 -3
  117. package/content/references/base/providers.md +8 -8
  118. package/content/references/base/repositories/advanced.md +224 -326
  119. package/content/references/base/repositories/index.md +19 -6
  120. package/content/references/base/repositories/mixins.md +5 -5
  121. package/content/references/base/repositories/relations.md +160 -293
  122. package/content/references/base/repositories/soft-deletable.md +16 -6
  123. package/content/references/base/secrets.md +17 -13
  124. package/content/references/base/services.md +6 -4
  125. package/content/references/configuration/environment-variables.md +31 -23
  126. package/content/references/configuration/index.md +6 -4
  127. package/content/references/index.md +1 -1
  128. package/content/references/utilities/duration.md +85 -0
  129. package/content/references/utilities/index.md +5 -1
  130. package/content/references/utilities/jsx-reference.md +11 -11
  131. package/content/references/utilities/jsx.md +2 -2
  132. package/content/references/utilities/module.md +78 -25
  133. package/content/references/utilities/request.md +2 -1
  134. package/content/references/utilities/retry.md +139 -0
  135. package/content/references/utilities/schema.md +2 -2
  136. package/content/references/utilities/statuses-reference.md +4 -4
  137. package/content/references/utilities/statuses.md +5 -5
  138. package/dist/mcp-server/index.js +0 -0
  139. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  140. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  141. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  142. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  143. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  144. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  145. package/package.json +17 -16
@@ -1,189 +1,89 @@
1
- # Socket.IO -- API Reference
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
- > Architecture deep dive, method signatures, internals, and type definitions.
7
+ # Socket.IO Component Reference
4
8
 
5
- ## Architecture
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
- The component integrates Socket.IO into the IGNIS application lifecycle with runtime-specific initialization (Node.js vs Bun).
11
+ **Files:**
8
12
 
9
- #### Architecture Diagram
10
- ```
11
- SocketIOComponent
12
- +----------------------------------------------+
13
- | |
14
- | binding() |
15
- | |-- resolveBindings() |
16
- | | |-- SERVER_OPTIONS |
17
- | | |-- REDIS_CONNECTION |
18
- | | |-- AUTHENTICATE_HANDLER |
19
- | | |-- VALIDATE_ROOM_HANDLER |
20
- | | +-- CLIENT_CONNECTED_HANDLER |
21
- | | |
22
- | +-- RuntimeModules.detect() |
23
- | |-- BUN -> registerBunHook() |
24
- | +-- NODE -> registerNodeHook() |
25
- | |
26
- | (Post-start hooks execute after server) |
27
- | |-- Creates SocketIOServerHelper |
28
- | |-- await socketIOHelper.configure() |
29
- | |-- Binds to SOCKET_IO_INSTANCE |
30
- | +-- Wires into server (runtime-specific) |
31
- +----------------------------------------------+
32
- ```
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
- ### Lifecycle Integration
22
+ ## Find what you need
35
23
 
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.
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
- #### Application Lifecycle Diagram
39
- ```
40
- Application Lifecycle
41
- =====================
42
-
43
- +------------------+
44
- | preConfigure() | <-- Register SocketIOComponent here
45
- +--------+---------+
46
- |
47
- +--------v---------+
48
- | initialize() | <-- Component.binding() runs here
49
- | | Resolves bindings, registers post-start hook
50
- +--------+---------+
51
- |
52
- +--------v---------+
53
- | setupMiddlewares |
54
- +--------+---------+
55
- |
56
- +--------v-----------------------+
57
- | startBunModule() OR | <-- Server starts, instance created
58
- | startNodeModule() |
59
- +--------+-----------------------+
60
- |
61
- +--------v--------------------------+
62
- | executePostStartHooks() | <-- SocketIOServerHelper created HERE
63
- | +-- socket-io-initialize | Server instance is now available
64
- +-----------------------------------+
65
- ```
39
+ ## Quick reference
66
40
 
67
- ### Runtime-Specific Behavior
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
- | 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 |
50
+ ## Import paths
80
51
 
81
- ### Runtime Differences -- Deep Dive
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
- function createBunFetchHandler(opts: {
94
- engine: any;
95
- enginePath: string;
96
- honoServer: OpenAPIHono;
97
- }): (req: Request, server: TBunServerInstance) => Response | Promise<Response> {
98
- const { engine, enginePath, honoServer } = opts;
99
-
100
- return (req: Request, server: TBunServerInstance): Response | Promise<Response> => {
101
- const url = new URL(req.url);
102
-
103
- if (!url.pathname.startsWith(enginePath)) {
104
- return honoServer.fetch(req, server);
105
- }
106
-
107
- return engine.handleRequest(req, server) ?? new Response(null, { status: 404 });
108
- };
109
- }
110
- ```
55
+ // Core - subpath import only
56
+ import { SocketIOComponent, SocketIOBindingKeys } from '@venizia/ignis/socket-io';
111
57
 
112
- **CORS type bridging**: Socket.IO and `@socket.io/bun-engine` have slightly different CORS type definitions. The component extracts individual CORS fields explicitly to avoid type mismatches without using `as any`:
113
-
114
- #### Bun Engine CORS Bridging
115
- ```typescript
116
- const corsConfig = typeof serverOptions.cors === 'object' ? serverOptions.cors : undefined;
117
- const engine = new BunEngine({
118
- path: serverOptions.path ?? '/socket.io/',
119
- ...(corsConfig && {
120
- cors: {
121
- origin: corsConfig.origin as string | RegExp | (string | RegExp)[] | undefined,
122
- methods: corsConfig.methods,
123
- credentials: corsConfig.credentials,
124
- allowedHeaders: corsConfig.allowedHeaders,
125
- exposedHeaders: corsConfig.exposedHeaders,
126
- maxAge: corsConfig.maxAge,
127
- },
128
- }),
129
- });
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
- #### Node.js Runtime
133
-
134
- Node mode is simpler because Socket.IO natively attaches to `node:http.Server`. The handler creates a `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and passes the HTTP server instance directly:
135
-
136
- #### Node.js Handler Source
137
- ```typescript
138
- async function createNodeSocketIOHelper(opts: {
139
- serverOptions: Partial<IServerOptions>;
140
- httpServer: TNodeServerInstance;
141
- resolvedBindings: IResolvedBindings;
142
- }): Promise<SocketIOServerHelper> {
143
- const { serverOptions, httpServer, resolvedBindings } = opts;
144
- const { redisConnection, authenticateFn, validateRoomFn, clientConnectedFn } = resolvedBindings;
145
-
146
- const socketIOHelper = new SocketIOServerHelper({
147
- runtime: RuntimeModules.NODE,
148
- identifier: serverOptions.identifier!,
149
- server: httpServer,
150
- serverOptions,
151
- redisConnection,
152
- authenticateFn,
153
- validateRoomFn,
154
- clientConnectedFn,
155
- });
156
- await socketIOHelper.configure();
157
-
158
- return socketIOHelper;
159
- }
160
- ```
63
+ ## Configuration reference
161
64
 
162
- ## Configuration Reference
65
+ ### Default server options
163
66
 
164
- ### Default Server Options
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/polling |
172
- | `cors.origin` | `'*'` | Allowed origins (restrict in production!) |
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 requests |
176
- | `cors.credentials` | `true` | Allow cookies/auth headers |
177
- | `perMessageDeflate.threshold` | `4096` | Minimum message size to compress (bytes) |
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 maximum window size (2^10 = 1KB) |
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 suitable for development only. In production, restrict this to your specific domains.
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 Configuration
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` is part of the component's `IServerOptions` interface (which extends `ServerOptions`), not Socket.IO's native options. Set it by including it in the bound options object.
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
- All binding keys are available on `SocketIOBindingKeys`:
133
+ ### Binding keys
243
134
 
244
- | Binding Key | Constant | Type | Required | Default |
245
- |------------|----------|------|----------|---------|
246
- | `@app/socket-io/server-options` | `SERVER_OPTIONS` | `Partial<ServerOptions>` | No | See defaults above |
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` | -- | *Set by component* |
142
+ | `@app/socket-io/instance` | `SOCKET_IO_INSTANCE` | `SocketIOServerHelper` | - | Set by the component |
252
143
 
253
144
  > [!NOTE]
254
- > `SOCKET_IO_INSTANCE` is **not** set by you -- the component creates and binds it automatically after the server starts. Inject it in services/controllers via a lazy getter (see [Usage & Examples](./usage#server-side-usage)).
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 Events and Rooms
147
+ ### System events and rooms
257
148
 
258
- Constants are exported from `@venizia/ignis-helpers/socket-io` as `SocketIOConstants` and used internally by both the component and the helper.
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 all authenticated clients join |
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 via the `defaultRooms` option on `SocketIOServerHelper` -- the values above are only the fallback.
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
- Defined at module scope in the server helper, not exported, but they govern default behavior:
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
- Both are overridable via the `authenticateTimeout` and `pingInterval` constructor options on `SocketIOServerHelper`.
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 States
174
+ ### Client states
288
175
 
289
- Each connected client tracks an authentication state that governs what actions are permitted:
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 -- client must emit `authenticate` within the timeout (default: 10s) |
294
- | `authenticating` | `SocketIOClientStates.AUTHENTICATING` | Auth in progress -- `authenticateFn` is executing |
295
- | `authenticated` | `SocketIOClientStates.AUTHENTICATED` | Auth successful -- client can send/receive events and join rooms |
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
- connect ---------->| unauthorized |
301
- +--------+---------+
302
- | emit('authenticate')
303
- +--------v---------+
304
- | authenticating |
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
- During construction:
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
- ### `configure()` -- Server Initialization
195
+ ### Lifecycle integration
378
196
 
379
- The `configure()` method is the main initialization entry point, called after construction:
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
- configure()
383
- |-- Register error handlers on all 3 Redis clients
384
- |-- Connect any clients in 'wait' status (lazyConnect mode)
385
- |-- await Promise.all([redisPub.ready, redisSub.ready, redisEmitter.ready])
386
- |-- initIOServer()
387
- | |-- NODE: new IOServer(httpServer, serverOptions)
388
- | +-- BUN: new IOServer() -> io.bind(bunEngine)
389
- |-- io.adapter(createAdapter(redisPub, redisSub))
390
- |-- emitter = new Emitter(redisEmitter)
391
- +-- io.on('connection', onClientConnect)
392
- ```
393
-
394
- > [!NOTE]
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
- Returns the underlying `socket.io` `Server` instance. Use this for direct access to Socket.IO APIs not exposed by the helper (e.g., `io.of('/namespace')`, `io.fetchSockets()`).
406
-
407
- #### `getEngine()`
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
- getEngine(): any
411
- ```
412
-
413
- Returns the `@socket.io/bun-engine` instance. **Throws** if the runtime is Node.js (`"Engine is only available for Bun runtime!"`).
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
- ```typescript
418
- getClients(opts?: { id?: string }): ISocketIOClient | Map<string, ISocketIOClient> | undefined
226
+ // Get the server instance, available only after start
227
+ application.getServerInstance<T>(): T | undefined;
419
228
  ```
420
229
 
421
- When called without arguments, returns the full client map. When called with `{ id }`, returns the specific client entry or `undefined` if not found.
230
+ ### Runtime-specific behavior
422
231
 
423
- #### `on()`
424
-
425
- ```typescript
426
- on<HandlerArgsType extends unknown[] = unknown[], HandlerReturnType = void>(opts: {
427
- topic: string;
428
- handler: (...args: HandlerArgsType) => ValueOrPromise<HandlerReturnType>;
429
- }): void
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
- Registers a server-level event handler on the IO server. **Throws** if `topic` is empty, `handler` is falsy, or the IO server is not initialized.
243
+ ### Bun runtime details
433
244
 
434
- #### `ping()`
245
+ A custom fetch function intercepts WebSocket upgrade requests before they reach Hono:
435
246
 
436
- ```typescript
437
- ping(opts: { socket: IOSocket; doIgnoreAuth: boolean }): void
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
- Sends a `ping` event to a specific client with `{ time: <ISO string> }`. Behavior:
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
- - If `socket` is undefined, logs and returns
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
- Used internally for the keep-alive interval after authentication. The `doIgnoreAuth: true` flag is used for the initial post-auth ping and the recurring interval.
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
- #### `disconnect()`
257
+ ## Server helper API reference
450
258
 
451
- ```typescript
452
- disconnect(opts: { socket: IOSocket }): void
453
- ```
454
-
455
- Disconnects a specific client and cleans up resources:
259
+ ### Constructor
456
260
 
457
- 1. Clears the ping interval (if set)
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
- If `socket` is falsy, the method returns immediately -- no cleanup, no `socket.disconnect()` call. If the socket exists but is not tracked in the `clients` map, steps 1-3 are skipped but `socket.disconnect()` is still called.
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
- #### `onClientConnect()`
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
- ```typescript
467
- onClientConnect(opts: { socket: IOSocket }): void
468
- ```
273
+ ### `configure()`
469
274
 
470
- Handles a new socket connection. Called by the `connection` event handler on the IO server. This method is public so it can be invoked externally for testing or custom connection routing.
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
- Behavior:
473
- 1. Validates the socket exists (returns if `null`/`undefined`)
474
- 2. Checks for duplicate connections by socket ID (returns if already tracked)
475
- 3. Creates an `ISocketIOClient` entry with state `UNAUTHORIZED`
476
- 4. Starts the authentication timeout (`authenticateTimeout` ms)
477
- 5. Registers `disconnect` handler on the socket
478
- 6. Registers `authenticate` handler via `registerAuthHandler()`
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
- #### `onClientAuthenticated()`
285
+ > [!NOTE]
286
+ > If any Redis client fails to connect, the error propagates and the server does not start.
481
287
 
482
- ```typescript
483
- onClientAuthenticated(opts: { socket: IOSocket }): void
484
- ```
288
+ ### Server helper: public methods
485
289
 
486
- Called after successful authentication. This method is public so it can be invoked externally for testing or custom auth flows.
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
- Behavior:
489
- 1. Validates the socket and client entry exist
490
- 2. Sets client state to `AUTHENTICATED`
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
- Key behaviors:
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
- - All messages are **compressed** via `emitter.compress(true)`
517
- - If `destination` is provided and non-empty, sends via `sender.to(destination).emit(topic, data)`
518
- - If `destination` is omitted/empty, broadcasts to **all** connected clients via `sender.emit(topic, data)`
519
- - `callback` is executed asynchronously via `setImmediate()`, not after delivery confirmation
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
- #### `send()` Silent Failure Behavior
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
- The `send()` method silently returns (no error, no log) in these cases:
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
- Gracefully shuts down the server:
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
- 1. Iterates all tracked clients and clears their intervals/timeouts
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
- ## Client Helper API Reference
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
- #### `ISocketIOClientOptions` Interface
348
+ See [client types](#client-types) for the full interface. `IOptions` extends `socket.io-client`'s `SocketOptions` with two required fields:
559
349
 
560
- ```typescript
561
- interface ISocketIOClientOptions {
562
- identifier: string;
563
- host: string;
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
- // Lifecycle callbacks (all optional)
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
- #### `IOptions` Interface
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
- ```typescript
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 all internal event handlers. If the client is already established (i.e., `configure()` was already called), logs a message and returns early.
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
- On call:
651
- 1. Sets state to `authenticating`
652
- 2. Emits `SocketIOConstants.EVENT_AUTHENTICATE` (value: `'authenticate'`)
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
- ### `subscribe()`
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
- Subscribes to a Socket.IO event with automatic error safety.
380
+ ### Client helper: public methods
665
381
 
666
- Guard conditions (no-op with warning log):
667
- - `handler` is falsy
668
- - `ignoreDuplicate` is `true` (default) and the event already has listeners
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
- #### Handler Wrapping Pattern
398
+ #### `subscribe()` error-safe wrapping
671
399
 
672
- 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:
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 from the handler. The `.catch()` on `Promise.resolve()` handles async rejections. This ensures handler errors never crash the client.
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
- No-op with warning log if the socket is not connected.
416
+ #### `emit()` throw conditions
779
417
 
780
- ### `shutdown()`
781
-
782
- ```typescript
783
- shutdown(): void
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 all binding keys from the DI container and validates required ones:
427
+ Reads every binding key from the DI container and validates the required ones.
797
428
 
798
- | Binding | Validation | Error on Failure |
799
- |---------|-----------|------------------|
800
- | `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` | -- |
801
- | `REDIS_CONNECTION` | Must be `instanceof AbstractRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisSingleHelper (single), RedisClusterHelper (cluster), or RedisSentinelHelper (sentinel)"` |
802
- | `AUTHENTICATE_HANDLER` | Must be a function (non-null) | `"[DANGER][SocketIOComponent] Invalid authenticateFn to setup io socket server!"` |
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
- ### `registerBunHook()`
437
+ ### Post-start hooks
807
438
 
808
- Registers a post-start hook that:
439
+ Both runtimes register the same hook identifier, `'socket-io-initialize'`, but wire the helper differently.
809
440
 
810
- 1. Calls `createBunEngine({ serverOptions })` which dynamically imports `@socket.io/bun-engine` and creates a `BunEngine` instance with CORS config bridging
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
- ### `createBunEngine()` Function
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
- Dynamically imports `@socket.io/bun-engine`, creates a `BunEngine` with CORS bridging, and returns both the `engine` and the `engineHandler` (from `engine.handler()`). The `engineHandler` provides the `websocket` handler that Bun's server needs.
825
-
826
- ### `createBunFetchHandler()` Function
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
- ```typescript
829
- function createBunFetchHandler(opts: {
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
- Returns a fetch handler function that routes requests:
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
- ### `registerNodeHook()`
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
- Registers a post-start hook that:
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 -- NOT consumed)
484
+ RedisSingleHelper (parent - NOT consumed)
857
485
  |
858
- +-- client.duplicate() --> redisPub (for Redis adapter -- publishes)
486
+ +-- duplicateClient() --> redisPub (Redis adapter - publishes)
859
487
  |
860
- +-- client.duplicate() --> redisSub (for Redis adapter -- subscribes)
488
+ +-- duplicateClient() --> redisSub (Redis adapter - subscribes)
861
489
  |
862
- +-- client.duplicate() --> redisEmitter (for @socket.io/redis-emitter -- message delivery)
490
+ +-- duplicateClient() --> redisEmitter (Redis emitter - message delivery)
863
491
  ```
864
492
 
865
- **Why 3 clients?**
866
- - `@socket.io/redis-adapter` requires separate pub and sub clients because a Redis connection in subscribe mode cannot execute other commands
867
- - `@socket.io/redis-emitter` uses its own client to emit messages independently of the adapter, enabling cross-instance broadcasting even from contexts without a direct Socket.IO reference
868
- - The parent `RedisSingleHelper` connection remains independent and is not consumed -- it can be used for other purposes (e.g., caching, sessions)
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 supports both single-instance `Redis` and `Cluster` connections from ioredis, making the helper transparent to the Redis deployment topology.
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
- | Runtime | Required Field | Error on Missing |
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
- ### `initRedisClients()` -- Redis Initialization
888
-
889
- ```typescript
890
- private initRedisClients(redisConnection: TSocketIOServerOptions['redisConnection']): void
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
- **Throws** if `redisConnection` is falsy: `"Invalid redis connection to config socket.io adapter!"`
514
+ ### `initRedisClients()`
894
515
 
895
- Creates 3 duplicated clients from the parent connection's underlying ioredis client.
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()` -- IO Server Initialization
518
+ ### `initIOServer()`
898
519
 
899
- Called during `configure()` after Redis connections are ready:
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
- Additional validation errors:
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
- ### Connection Lifecycle
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
- When a client connects, the server manages a strict authentication flow:
535
+ ### Connection lifecycle
914
536
 
915
537
  ```
916
538
  Client connects
917
- |
918
- +-- onClientConnect({ socket })
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
- #### Authentication Failure -- Two Code Paths
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
- The `registerAuthHandler()` method handles authentication results through two distinct code paths:
550
+ Timeout (10s) -> disconnect if not yet AUTHENTICATED
551
+ ```
945
552
 
946
- **Path 1: `authenticateFn` returns `false`** (`.then()` handler):
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
- **Path 2: `authenticateFn` throws an error** (`.catch()` handler):
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
- Both paths also handle the edge case where the client disconnected *during* authentication -- they check `this.clients.has(id)` before proceeding.
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 (set after auth)
968
- authenticateTimeout?: NodeJS.Timeout; // Auth deadline (undefined after it's cleared)
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 Handlers
973
-
974
- Room join/leave handlers are registered after successful authentication:
572
+ ### Room handlers
975
573
 
976
- - **`join`**: Client emits `{ rooms: string[] }`. If `validateRoomFn` is configured, only the rooms it returns are joined. If `validateRoomFn` is **not** configured, join is silently rejected with a warning log.
977
- - **`leave`**: Client emits `{ rooms: string[] }`. Leave is always allowed -- no validation function needed.
574
+ Registered after successful authentication.
978
575
 
979
- Both handlers parse the payload defensively: `const { rooms = [] } = payload || { rooms: [] }`. Empty `rooms` arrays are silently ignored.
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
- Join handler validation errors are caught and logged but do not disconnect the client.
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 rooms. They will only be in the default rooms (`io-default`, `io-notification`). This is a security-by-default design.
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
- ## Types Reference
586
+ ### Graceful shutdown
987
587
 
988
- ### Server Types
588
+ Shut down Socket.IO before stopping the application:
989
589
 
990
590
  ```typescript
991
- // Server constructor options -- discriminated union on 'runtime'
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 Types
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 Types
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
- ## Post-Start Hook System
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
- - [Setup & Configuration](./) -- Quick reference, required bindings, how it works
1232
- - [Usage & Examples](./usage) -- Full setup steps, server-side usage, client helper, advanced patterns
1233
- - [Error Reference](./errors) -- Error conditions and troubleshooting
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