@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.
- package/README.md +229 -2
- package/index.mjs +23 -0
- package/package.json +71 -4
- package/source/application/Kernel/index.mjs +1137 -0
- package/source/application/Server/index.mjs +1201 -0
- package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
- package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
- package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
- package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
- package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
- package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
- package/source/application/resolvers/TransportResolver/index.mjs +119 -0
- package/source/application/use-cases/CloseServer/index.mjs +85 -0
- package/source/application/use-cases/ListenServer/index.mjs +98 -0
- package/source/core/EventSystem/index.mjs +110 -0
- package/source/core/Exchange/ExchangeContext.mjs +291 -0
- package/source/core/Exchange/Telemetry.mjs +193 -0
- package/source/core/Exchange/index.mjs +604 -0
- package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
- package/source/core/IPCMain/IPCMain.mjs +507 -0
- package/source/core/IPCMain/IPCProtocol.mjs +441 -0
- package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
- package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
- package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
- package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
- package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
- package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
- package/source/core/Request/RequestHeaders.mjs +177 -0
- package/source/core/Request/index.mjs +805 -0
- package/source/core/Response/ResponseHeaders.mjs +402 -0
- package/source/core/Response/index.mjs +1370 -0
- package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
- package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
- package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
- package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
- package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
- package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
- package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
- package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
- package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
- package/source/core/routing/ExchangeLayer/index.mjs +254 -0
- package/source/core/routing/Pipeline/index.mjs +157 -0
- package/source/core/routing/Router/Route.mjs +184 -0
- package/source/core/routing/Router/index.mjs +523 -0
- package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
- package/source/core/routing/RouterRegister/index.mjs +150 -0
- package/source/core/routing/RouterSystem/index.mjs +225 -0
- package/source/core/value-objects/Address/index.mjs +281 -0
- package/source/core/value-objects/Endpoint/index.mjs +293 -0
- package/source/index.mjs +62 -0
- package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
- package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
- package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
- package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
- package/source/shared/Identifier/index.mjs +671 -0
- package/source/shared/constants/symbols/brands/index.mjs +45 -0
- package/source/shared/domain/UUIDV7/index.mjs +346 -0
- package/source/shared/errors/BaseError/index.mjs +157 -0
- package/source/shared/errors/IPCError/index.mjs +97 -0
- package/source/shared/errors/MiddlewareError/index.mjs +106 -0
- package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
- package/source/shared/errors/RequestError/index.mjs +116 -0
- package/source/shared/errors/ResponseError/index.mjs +140 -0
- package/source/shared/errors/RouterError/index.mjs +90 -0
- package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
- package/source/shared/errors/ServerError/index.mjs +127 -0
- package/source/shared/errors/WebSocketError/index.mjs +58 -0
- package/source/shared/outcome/ErrorSchema.mjs +361 -0
- package/source/shared/outcome/Result/index.mjs +239 -0
- package/source/shared/outcome/Validation/index.mjs +208 -0
- package/source/shared/validations/Middleware/index.mjs +160 -0
- package/source/shared/validations/PathnamePattern/index.mjs +256 -0
- package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
- package/source/shared/validations/Server/Router/index.mjs +68 -0
- package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
- package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
- package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
- package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
- package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
- package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
- package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
- package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
- package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
- package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
- package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
- package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
- package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
- 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;
|