@boo-labs/server 0.0.0-stage → 1.0.0

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 (114) hide show
  1. package/README.md +229 -2
  2. package/index.mjs +23 -0
  3. package/package.json +71 -4
  4. package/source/application/Kernel/index.mjs +1137 -0
  5. package/source/application/Server/index.mjs +1201 -0
  6. package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
  7. package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
  8. package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
  9. package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
  10. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
  11. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
  12. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
  13. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
  14. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
  15. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
  16. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
  17. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
  18. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
  19. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
  20. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
  21. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
  22. package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
  23. package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
  24. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
  25. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
  26. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
  27. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
  28. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
  29. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
  30. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
  31. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
  32. package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
  33. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
  34. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
  35. package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
  36. package/source/application/resolvers/TransportResolver/index.mjs +119 -0
  37. package/source/application/use-cases/CloseServer/index.mjs +85 -0
  38. package/source/application/use-cases/ListenServer/index.mjs +98 -0
  39. package/source/core/EventSystem/index.mjs +110 -0
  40. package/source/core/Exchange/ExchangeContext.mjs +291 -0
  41. package/source/core/Exchange/Telemetry.mjs +193 -0
  42. package/source/core/Exchange/index.mjs +604 -0
  43. package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
  44. package/source/core/IPCMain/IPCMain.mjs +507 -0
  45. package/source/core/IPCMain/IPCProtocol.mjs +441 -0
  46. package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
  47. package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
  48. package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
  49. package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
  50. package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
  51. package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
  52. package/source/core/Request/RequestHeaders.mjs +177 -0
  53. package/source/core/Request/index.mjs +805 -0
  54. package/source/core/Response/ResponseHeaders.mjs +402 -0
  55. package/source/core/Response/index.mjs +1370 -0
  56. package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
  57. package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
  58. package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
  59. package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
  60. package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
  61. package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
  62. package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
  63. package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
  64. package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
  65. package/source/core/routing/ExchangeLayer/index.mjs +254 -0
  66. package/source/core/routing/Pipeline/index.mjs +157 -0
  67. package/source/core/routing/Router/Route.mjs +184 -0
  68. package/source/core/routing/Router/index.mjs +523 -0
  69. package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
  70. package/source/core/routing/RouterRegister/index.mjs +150 -0
  71. package/source/core/routing/RouterSystem/index.mjs +225 -0
  72. package/source/core/value-objects/Address/index.mjs +281 -0
  73. package/source/core/value-objects/Endpoint/index.mjs +293 -0
  74. package/source/index.mjs +62 -0
  75. package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
  76. package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
  77. package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
  78. package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
  79. package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
  80. package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
  81. package/source/shared/Identifier/index.mjs +671 -0
  82. package/source/shared/constants/symbols/brands/index.mjs +45 -0
  83. package/source/shared/domain/UUIDV7/index.mjs +346 -0
  84. package/source/shared/errors/BaseError/index.mjs +157 -0
  85. package/source/shared/errors/IPCError/index.mjs +97 -0
  86. package/source/shared/errors/MiddlewareError/index.mjs +106 -0
  87. package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
  88. package/source/shared/errors/RequestError/index.mjs +116 -0
  89. package/source/shared/errors/ResponseError/index.mjs +140 -0
  90. package/source/shared/errors/RouterError/index.mjs +90 -0
  91. package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
  92. package/source/shared/errors/ServerError/index.mjs +127 -0
  93. package/source/shared/errors/WebSocketError/index.mjs +58 -0
  94. package/source/shared/outcome/ErrorSchema.mjs +361 -0
  95. package/source/shared/outcome/Result/index.mjs +239 -0
  96. package/source/shared/outcome/Validation/index.mjs +208 -0
  97. package/source/shared/validations/Middleware/index.mjs +160 -0
  98. package/source/shared/validations/PathnamePattern/index.mjs +256 -0
  99. package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
  100. package/source/shared/validations/Server/Router/index.mjs +68 -0
  101. package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
  102. package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
  103. package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
  104. package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
  105. package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
  106. package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
  107. package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
  108. package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
  109. package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
  110. package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
  111. package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
  112. package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
  113. package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
  114. package/types/index.d.ts +719 -0
@@ -0,0 +1,133 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Defines the exchange layer builder.
4
+ *
5
+ * @description Builds exchange layer definitions from middleware options.
6
+ *
7
+ * @module ExchangeLayerBuilder
8
+ */
9
+
10
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11
+ // » IMPORT LOCAL MODULES
12
+ import StaticMiddleware from '#middlewares/StaticMiddleware';
13
+ import CORSMiddleware from '#middlewares/CORSMiddleware';
14
+ import RateLimitMiddleware from '#middlewares/RateLimitMiddleware';
15
+ import SecurityHeadersMiddleware from '#middlewares/SecurityHeadersMiddleware';
16
+ import CookieParserMiddleware from '#middlewares/CookieParserMiddleware';
17
+ import CookieSessionMiddleware from '#middlewares/CookieSessionMiddleware';
18
+ import CSRFMiddleware from '#middlewares/CSRFMiddleware';
19
+ import CompressMiddleware from '#middlewares/CompressMiddleware';
20
+
21
+ // ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22
+ /**
23
+ * Represents a single HTTP transaction.
24
+ *
25
+ * @typedef {import('#Exchange').default} Exchange
26
+ */
27
+
28
+ /**
29
+ * Represents an HTTP method supported by the routing layer.
30
+ *
31
+ * @typedef {import('#routing/ExchangeLayer').HTTPMethod} HTTPMethod
32
+ */
33
+
34
+ /**
35
+ * Represents an exchange middleware function.
36
+ *
37
+ * @typedef {import('#routing/ExchangeLayer').MiddlewareFunction} MiddlewareFunction
38
+ */
39
+
40
+ /**
41
+ * Defines the criteria and middleware used by an exchange layer.
42
+ *
43
+ * @typedef {import('#routing/ExchangeLayer').ExchangeLayerOptions} ExchangeLayerOptions
44
+ */
45
+
46
+ /**
47
+ * Represents middleware options used to build an exchange layer.
48
+ *
49
+ * @typedef {object} MiddlewareOptions
50
+ * @property {string} name - Middleware name.
51
+ * @property {string|undefined} pathnamePattern - URL pathname pattern to match.
52
+ * @property {HTTPMethod|undefined} method - HTTP method to match.
53
+ * @property {unknown} options - Middleware configuration options.
54
+ */
55
+
56
+ /**
57
+ * Represents a middleware constructor.
58
+ *
59
+ * @typedef {new (options: any) => {handle: MiddlewareFunction}} MiddlewareConstructor
60
+ */
61
+
62
+ // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63
+ /**
64
+ * Builds exchange layer definitions from middleware options.
65
+ *
66
+ * @class
67
+ */
68
+ class ExchangeLayerBuilder {
69
+ /**
70
+ * Prevents direct instantiation of the builder.
71
+ *
72
+ * @abstract
73
+ * @throws {Error} Always thrown because `ExchangeLayerBuilder` exposes only static behavior.
74
+ */
75
+ constructor() {
76
+ throw new Error('ExchangeLayerBuilder is a static class and cannot be instantiated');
77
+ }
78
+
79
+ /**
80
+ * Builds exchange layer definitions from middleware options.
81
+ *
82
+ * @param {MiddlewareOptions[]} middlewareOptions - Middleware options to build.
83
+ * @returns {ExchangeLayerOptions[]} The resulting exchange layer definitions.
84
+ * @throws {Error} When the middleware is not supported.
85
+ * @example
86
+ * ```js
87
+ * const layers = ExchangeLayerBuilder.build(middlewareOptions);
88
+ * ```
89
+ */
90
+ static build(middlewareOptions) {
91
+ return middlewareOptions.map(({ name, options, pathnamePattern, method }) => {
92
+ const Middleware = ExchangeLayerBuilder.#middlewares.get(name);
93
+
94
+ if (!Middleware) {
95
+ throw new Error(`Unsupported middleware: ${name}`);
96
+ }
97
+
98
+ const middleware = new Middleware(options);
99
+
100
+ return {
101
+ method,
102
+ pathnamePattern,
103
+ middleware: middleware.handle.bind(middleware),
104
+ };
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Middleware constructors supported by the builder.
110
+ *
111
+ * @type {Array<[string, MiddlewareConstructor]>}
112
+ */
113
+ static #MIDDLEWARES = [
114
+ ['cors', CORSMiddleware],
115
+ ['static', StaticMiddleware],
116
+ ['rateLimit', RateLimitMiddleware],
117
+ ['securityHeaders', SecurityHeadersMiddleware],
118
+ ['cookieParser', CookieParserMiddleware],
119
+ ['cookieSession', CookieSessionMiddleware],
120
+ ['csrf', CSRFMiddleware],
121
+ ['compress', CompressMiddleware],
122
+ ];
123
+
124
+ /**
125
+ * Middleware constructors indexed by middleware name.
126
+ *
127
+ * @type {Map<string, MiddlewareConstructor>}
128
+ */
129
+ static #middlewares = new Map(ExchangeLayerBuilder.#MIDDLEWARES);
130
+ }
131
+
132
+ // ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
133
+ export default ExchangeLayerBuilder;
@@ -0,0 +1,507 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Owns the IPC subsystem and its application-facing API.
4
+ *
5
+ * @description IPCMain owns IPC handlers, event listeners, active WebSocket
6
+ * connections, lifecycle state, and the default IPC endpoint.
7
+ *
8
+ * It resolves the transport's WebSocket capability through WebSocketAdapter
9
+ * and creates one IPCServerChannel for each accepted connection.
10
+ *
11
+ * IPCMain does not expose transport details, WebSocket framing, or IPC wire
12
+ * serialization to its consumers.
13
+ *
14
+ * @module IPCMain
15
+ */
16
+
17
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18
+ // » IMPORT NATIVE NODEJS MODULES
19
+
20
+ // » IMPORT THIRD PARTY MODULES
21
+
22
+ // » IMPORT LOCAL MODULES
23
+ import WebSocketAdapter from './WebSocketAdapter.mjs';
24
+ import IPCServerChannel from './IPCServerChannel.mjs';
25
+ import IPCError from '#shared/errors/IPCError';
26
+
27
+ // ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
28
+ /**
29
+ * Represents a permitted transport identity.
30
+ *
31
+ * @typedef {import('#adapters/TransportAdapter').TransportIdentity} TransportIdentity
32
+ */
33
+
34
+ /**
35
+ * Represents a transport adapter capable of managing a permitted transport.
36
+ *
37
+ * @typedef {import('#adapters/TransportAdapter').default<TransportIdentity>} TransportAdapter
38
+ */
39
+
40
+ /**
41
+ * Represents a decoded IPC request.
42
+ *
43
+ * @typedef {import('./IPCProtocol.mjs').IPCRequest} IPCRequest
44
+ */
45
+
46
+ /**
47
+ * Represents a decoded IPC invocation request.
48
+ *
49
+ * @typedef {import('./IPCProtocol.mjs').IPCInvokeRequest} IPCInvokeRequest
50
+ */
51
+
52
+ /**
53
+ * Represents an established WebSocket connection contract.
54
+ *
55
+ * @typedef {import('#adapters/TransportAdapter').WebSocketConnection} WebSocketConnection
56
+ */
57
+
58
+ /**
59
+ * Listener for incoming WebSocket messages.
60
+ *
61
+ * @callback WebSocketMessageListener
62
+ * @param {string|Buffer} data - Incoming text or binary message.
63
+ * @returns {void}
64
+ */
65
+
66
+ /**
67
+ * Listener for WebSocket close events.
68
+ *
69
+ * @callback WebSocketCloseListener
70
+ * @returns {void}
71
+ */
72
+
73
+ /**
74
+ * Listener for WebSocket error events.
75
+ *
76
+ * @callback WebSocketErrorListener
77
+ * @param {Error} error - Error reported by the WebSocket connection.
78
+ * @returns {void}
79
+ */
80
+
81
+ /**
82
+ * Registers a WebSocket event listener.
83
+ *
84
+ * @typedef {(
85
+ * ((event: 'message', listener: WebSocketMessageListener) => void) &
86
+ * ((event: 'close', listener: WebSocketCloseListener) => void) &
87
+ * ((event: 'error', listener: WebSocketErrorListener) => void)
88
+ * )} WebSocketOn
89
+ */
90
+
91
+ /**
92
+ * Handles an IPC invoke request.
93
+ *
94
+ * @callback IPCHandler
95
+ * @param {unknown} data
96
+ * @returns {unknown|Promise<unknown>}
97
+ */
98
+
99
+ /**
100
+ * Handles an IPC event.
101
+ *
102
+ * @callback IPCEventListener
103
+ * @param {unknown} data
104
+ * @returns {void}
105
+ */
106
+
107
+ /**
108
+ * Configuration for IPCMain.
109
+ *
110
+ * @typedef {object} IPCMainOptions
111
+ * @property {string} [path='/ipc'] - WebSocket endpoint owned by the IPC subsystem.
112
+ */
113
+
114
+ /**
115
+ * Represents a response payload supplied by the IPC host.
116
+ *
117
+ * @typedef {import('./IPCServerChannel.mjs').IPCResponsePayload} IPCResponsePayload
118
+ */
119
+
120
+ /**
121
+ * Internal response capability provided to IPCServerChannel.
122
+ *
123
+ * Errors remain raw until IPCServerChannel passes them to IPCProtocol for wire
124
+ * normalization.
125
+ *
126
+ * @callback IPCRespond
127
+ * @param {IPCResponsePayload} payload
128
+ * @returns {void}
129
+ */
130
+
131
+ // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
132
+ /**
133
+ * Owns the IPC subsystem and its application-facing API.
134
+ *
135
+ * @classdesc IPCMain owns IPC handlers, event listeners, active WebSocket
136
+ * connections, lifecycle state, and the default IPC endpoint.
137
+ */
138
+ class IPCMain {
139
+ /**
140
+ * Registered invoke handlers indexed by channel name.
141
+ *
142
+ * @type {Map<string, IPCHandler>}
143
+ */
144
+ #handlers = new Map();
145
+
146
+ /**
147
+ * Registered event listeners indexed by channel name.
148
+ *
149
+ * @type {Map<string, Set<IPCEventListener>>}
150
+ */
151
+ #listeners = new Map();
152
+
153
+ /**
154
+ * WebSocket connections currently managed by the IPC subsystem.
155
+ *
156
+ * @type {Set<WebSocketConnection>}
157
+ */
158
+ #connections = new Set();
159
+
160
+ /**
161
+ * WebSocket adapter used by the IPC subsystem.
162
+ *
163
+ * @type {WebSocketAdapter}
164
+ */
165
+ #webSocket;
166
+
167
+ /**
168
+ * WebSocket endpoint path owned by the IPC subsystem.
169
+ *
170
+ * @type {string}
171
+ */
172
+ #path;
173
+
174
+ /**
175
+ * Whether the IPC subsystem has been closed.
176
+ *
177
+ * @type {boolean}
178
+ */
179
+ #closed = false;
180
+
181
+ /**
182
+ * Creates the IPC subsystem for a transport.
183
+ *
184
+ * @param {TransportAdapter} transport - Transport used to provide WebSocket connections for the IPC subsystem.
185
+ * @param {IPCMainOptions} [options] - IPC subsystem configuration.
186
+ * @example
187
+ * ```js
188
+ * const ipcMain = new IPCMain(transport);
189
+ * ```
190
+ */
191
+ constructor(transport, options = {}) {
192
+ this.#path =
193
+ typeof options.path === 'string' && options.path.length > 0 ? options.path : '/ipc';
194
+
195
+ this.#webSocket = new WebSocketAdapter(transport, {
196
+ accept: url => this.#matchesPath(url),
197
+ });
198
+
199
+ this.#webSocket.onConnection(connection => {
200
+ this.#onConnection(connection);
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Registers or replaces an invoke handler for a channel.
206
+ *
207
+ * @param {string} channel - Name of the IPC channel.
208
+ * @param {IPCHandler} handler - Function invoked when a request targets the channel.
209
+ * @returns {this} This IPCMain instance.
210
+ * @throws {IPCError} If IPCMain has been closed.
211
+ * @throws {IPCError} If channel or handler is invalid.
212
+ * @example
213
+ * ```js
214
+ * ipcMain.handle('get-user', async (id) => {
215
+ * return getUser(id);
216
+ * });
217
+ * ```
218
+ */
219
+ handle(channel, handler) {
220
+ this.#assertOpen();
221
+ this.#assertChannel(channel);
222
+ this.#assertFunction(handler, 'handler');
223
+
224
+ this.#handlers.set(channel, handler);
225
+
226
+ return this;
227
+ }
228
+
229
+ /**
230
+ * Removes the invoke handler registered for a channel.
231
+ *
232
+ * @param {string} channel - Name of the IPC channel whose handler is removed.
233
+ * @returns {this} This IPCMain instance.
234
+ * @throws {IPCError} If IPCMain has been closed.
235
+ * @throws {IPCError} If channel is not a non-empty string.
236
+ * @example
237
+ * ```js
238
+ * ipcMain.removeHandler('get-user');
239
+ * ```
240
+ */
241
+ removeHandler(channel) {
242
+ this.#assertOpen();
243
+ this.#assertChannel(channel);
244
+ this.#handlers.delete(channel);
245
+
246
+ return this;
247
+ }
248
+
249
+ /**
250
+ * Registers an event listener for a channel.
251
+ *
252
+ * Multiple listeners may be registered for the same channel.
253
+ *
254
+ * @param {string} channel - Name of the IPC event channel.
255
+ * @param {IPCEventListener} listener - Function invoked when an event is emitted for the channel.
256
+ * @returns {this} This IPCMain instance.
257
+ * @throws {IPCError} If IPCMain has been closed.
258
+ * @throws {IPCError} If channel or listener is invalid.
259
+ * @example
260
+ * ```js
261
+ * ipcMain.on('notification', (data) => {
262
+ * console.log(data);
263
+ * });
264
+ * ```
265
+ */
266
+ on(channel, listener) {
267
+ this.#assertOpen();
268
+ this.#assertChannel(channel);
269
+ this.#assertFunction(listener, 'listener');
270
+
271
+ this.#ensureListenerSet(channel).add(listener);
272
+
273
+ return this;
274
+ }
275
+
276
+ /**
277
+ * Removes an event listener from a channel.
278
+ *
279
+ * @param {string} channel - Name of the IPC event channel.
280
+ * @param {IPCEventListener} listener - Listener previously registered for the channel.
281
+ * @returns {this} This IPCMain instance.
282
+ * @throws {IPCError} If IPCMain has been closed.
283
+ * @throws {IPCError} If channel or listener is invalid.
284
+ * @example
285
+ * ```js
286
+ * ipcMain.off('notification', listener);
287
+ * ```
288
+ */
289
+ off(channel, listener) {
290
+ this.#assertOpen();
291
+ this.#assertChannel(channel);
292
+ this.#assertFunction(listener, 'listener');
293
+
294
+ const set = this.#listeners.get(channel);
295
+
296
+ if (set) {
297
+ set.delete(listener);
298
+
299
+ if (set.size === 0) {
300
+ this.#listeners.delete(channel);
301
+ }
302
+ }
303
+
304
+ return this;
305
+ }
306
+
307
+ /**
308
+ * Closes the IPC subsystem and all active connections.
309
+ *
310
+ * This is an internal lifecycle operation invoked by Kernel and is not part
311
+ * of the public Server IPC API.
312
+ *
313
+ * @example
314
+ * ```js
315
+ * ipcMain.close();
316
+ * ```
317
+ */
318
+ close() {
319
+ if (!this.#closed) {
320
+ this.#closed = true;
321
+ this.#handlers.clear();
322
+ this.#listeners.clear();
323
+
324
+ for (const connection of this.#connections) {
325
+ try {
326
+ connection.close();
327
+ } catch {
328
+ // Ignore transport close failures.
329
+ }
330
+ }
331
+
332
+ this.#connections.clear();
333
+ this.#webSocket.close();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Returns the listener set for a channel, creating it when absent.
339
+ *
340
+ * @param {string} channel - Channel name whose listener set is resolved.
341
+ * @returns {Set<IPCEventListener>} The listener set associated with the channel.
342
+ */
343
+ #ensureListenerSet(channel) {
344
+ const existing = this.#listeners.get(channel);
345
+
346
+ if (existing) {
347
+ return existing;
348
+ }
349
+
350
+ const set = new Set();
351
+ this.#listeners.set(channel, set);
352
+
353
+ return set;
354
+ }
355
+
356
+ /**
357
+ * Registers one established WebSocket connection with the IPC subsystem.
358
+ *
359
+ * @param {WebSocketConnection} connection - Established WebSocket connection assigned to the IPC subsystem.
360
+ */
361
+ #onConnection(connection) {
362
+ if (this.#closed) {
363
+ try {
364
+ connection.close();
365
+ } catch {
366
+ // Ignore transport close failures.
367
+ }
368
+ } else {
369
+ this.#connections.add(connection);
370
+
371
+ connection.on('close', () => {
372
+ this.#connections.delete(connection);
373
+ });
374
+
375
+ connection.on('error', () => {
376
+ this.#connections.delete(connection);
377
+ });
378
+
379
+ new IPCServerChannel(connection, {
380
+ dispatch: (request, respond) => this.#dispatch(request, respond),
381
+ });
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Dispatches one decoded IPC request to the appropriate application
387
+ * operation.
388
+ *
389
+ * @param {IPCRequest} request - Decoded IPC request to dispatch.
390
+ * @param {IPCRespond} respond - Function used to send the operation result.
391
+ */
392
+ async #dispatch(request, respond) {
393
+ if (!this.#closed) {
394
+ if (request.type === 'event') {
395
+ this.#emit(request.channel, request.data);
396
+ } else {
397
+ await this.#invoke(request, respond);
398
+ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Emits an IPC event to every listener registered for its channel.
404
+ *
405
+ * Listener failures are isolated so one listener cannot prevent the
406
+ * remaining listeners from receiving the event.
407
+ *
408
+ * @param {string} channel - Name of the IPC event channel.
409
+ * @param {unknown} data - Event data delivered to registered listeners.
410
+ */
411
+ #emit(channel, data) {
412
+ const set = this.#listeners.get(channel);
413
+
414
+ if (set) {
415
+ for (const listener of set) {
416
+ try {
417
+ listener(data);
418
+ } catch {
419
+ // Listener failures are intentionally isolated.
420
+ }
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Executes the handler associated with an IPC invoke request.
427
+ *
428
+ * Invokes are intentionally processed concurrently. IPCMain does not
429
+ * serialize requests through a queue or mutex.
430
+ *
431
+ * @param {IPCInvokeRequest} request - Decoded IPC invoke request.
432
+ * @param {IPCRespond} respond - Function used to send the handler result or error back to the requesting connection.
433
+ */
434
+ async #invoke(request, respond) {
435
+ const handler = this.#handlers.get(request.channel);
436
+
437
+ if (handler) {
438
+ try {
439
+ const result = await handler(request.data);
440
+ respond({ result });
441
+ } catch (error) {
442
+ // IPCProtocol owns normalization of raw handler errors.
443
+ respond({ error });
444
+ }
445
+ } else {
446
+ respond({
447
+ error: {
448
+ code: 'IPC_CHANNEL_NOT_FOUND',
449
+ message: `No handler registered for channel "${request.channel}"`,
450
+ },
451
+ });
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Determines whether a WebSocket upgrade targets the IPC endpoint.
457
+ *
458
+ * @param {string} url - URL presented by the WebSocket upgrade request.
459
+ * @returns {boolean} Whether the URL targets the configured IPC endpoint.
460
+ */
461
+ #matchesPath(url) {
462
+ try {
463
+ return new URL(url, 'http://localhost').pathname === this.#path;
464
+ } catch {
465
+ return url === this.#path || url.startsWith(`${this.#path}?`);
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Validates an IPC channel name.
471
+ *
472
+ * @param {string} channel - Channel name to validate.
473
+ * @throws {IPCError} If channel is not a non-empty string.
474
+ */
475
+ #assertChannel(channel) {
476
+ if (typeof channel !== 'string' || channel.length === 0) {
477
+ throw new IPCError('IPC_INVALID_CHANNEL');
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Validates a callable IPC API argument.
483
+ *
484
+ * @param {unknown} value - Value to validate as a function.
485
+ * @param {'handler'|'listener'} name - Argument name used in the validation error.
486
+ * @throws {IPCError} If value is not a function.
487
+ */
488
+ #assertFunction(value, name) {
489
+ if (typeof value !== 'function') {
490
+ throw new IPCError(name === 'handler' ? 'IPC_INVALID_HANDLER' : 'IPC_INVALID_LISTENER');
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Ensures IPCMain is still available for lifecycle-sensitive operations.
496
+ *
497
+ * @throws {IPCError} If IPCMain has already been closed.
498
+ */
499
+ #assertOpen() {
500
+ if (this.#closed) {
501
+ throw new IPCError('IPC_CLOSED');
502
+ }
503
+ }
504
+ }
505
+
506
+ // ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
507
+ export default IPCMain;