@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,604 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Defines the Exchange abstraction.
4
+ *
5
+ * @description An Exchange represents a single HTTP transaction. It
6
+ * composes a `Request`, a `Response`, and a private `ExchangeContext`
7
+ * that carries runtime-only execution data such as telemetry, cancellation,
8
+ * and correlation identifiers. It also exposes mutable per-request state
9
+ * shared between middleware layers.
10
+ *
11
+ * The exchange is the unit of work processed by the pipeline. It is
12
+ * created by the runtime from the incoming request and outgoing
13
+ * response objects and does not expose those objects to consumers.
14
+ *
15
+ * @module Exchange
16
+ */
17
+
18
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19
+ // » IMPORT LOCAL MODULES
20
+ import Request from '#Request';
21
+ import Response from '#Response';
22
+ import ExchangeContext from './ExchangeContext.mjs';
23
+
24
+ // ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25
+ /**
26
+ * Response operation error.
27
+ *
28
+ * @typedef {import('#shared/errors/ResponseError').default} ResponseError
29
+ */
30
+
31
+ /**
32
+ * Represents an incoming HTTP/1.1 or HTTPS/1.1 request message from Node.js.
33
+ *
34
+ * @typedef {import('node:http').IncomingMessage} HttpIncomingMessage
35
+ */
36
+
37
+ /**
38
+ * Represents an incoming HTTP/2 request message from Node.js.
39
+ *
40
+ * @typedef {import('node:http2').Http2ServerRequest} Http2IncomingMessage
41
+ */
42
+
43
+ /**
44
+ * Represents an incoming request message supported by the server.
45
+ *
46
+ * @typedef {HttpIncomingMessage|Http2IncomingMessage} IncomingRequest
47
+ */
48
+
49
+ /**
50
+ * Represents a native Node.js HTTP/1.1 server response.
51
+ *
52
+ * @typedef {import('node:http').ServerResponse} HttpServerResponse
53
+ */
54
+
55
+ /**
56
+ * Represents a native Node.js HTTP/2 server response.
57
+ *
58
+ * @typedef {import('node:http2').Http2ServerResponse} Http2ServerResponse
59
+ */
60
+
61
+ /**
62
+ * Represents a native Node.js server response.
63
+ *
64
+ * @typedef {HttpServerResponse|Http2ServerResponse} ServerResponse
65
+ */
66
+
67
+ /**
68
+ * Represents a native Node.js transform stream.
69
+ *
70
+ * @typedef {import('node:stream').Transform} NodeTransform
71
+ */
72
+
73
+ /**
74
+ * Options forwarded to the `Request` constructor.
75
+ *
76
+ * @typedef {object} ExchangeRequestOptions
77
+ * @property {number} [bodyLimit] - Maximum number of bytes allowed for the request body.
78
+ * @property {boolean|number} [trustProxy] - Whether (and how much) to trust proxy headers.
79
+ */
80
+
81
+ /**
82
+ * Options forwarded to the `Response` constructor.
83
+ *
84
+ * @typedef {object} ExchangeResponseOptions
85
+ * @property {boolean} [etag] - Whether to generate a weak ETag for buffered bodies.
86
+ * @property {string} [defaultCharset] - Charset appended to eligible Content-Type headers when none is set.
87
+ */
88
+
89
+ /**
90
+ * Callback invoked when response commitment produces an error.
91
+ *
92
+ * @typedef {(error: Error, exchange: Exchange) => void} ExchangeCommitErrorHook
93
+ */
94
+
95
+ /**
96
+ * Defines the options supported by an exchange.
97
+ *
98
+ * @typedef {object} ExchangeOptions
99
+ * @property {ExchangeRequestOptions} [request] - Options forwarded to the `Request` constructor.
100
+ * @property {ExchangeResponseOptions} [response] - Options forwarded to the `Response` constructor.
101
+ * @property {ExchangeCommitErrorHook} [onCommitError] - Optional callback invoked when response commitment produces an error.
102
+ */
103
+
104
+ /**
105
+ * Represents the processing outcome of an HTTP exchange.
106
+ *
107
+ * @typedef {'pending'|'success'|'error'} ExchangeOutcome
108
+ */
109
+
110
+ /**
111
+ * Represents timing information captured when exchange processing starts.
112
+ *
113
+ * @typedef {object} ExchangeStartTiming
114
+ * @property {number} startedAt - Monotonic timestamp captured when processing started.
115
+ * @property {undefined} endedAt - Timestamp is unavailable before processing ends.
116
+ * @property {undefined} duration - Duration is unavailable before processing ends.
117
+ */
118
+
119
+ /**
120
+ * Represents timing information captured for a non-terminal exchange error.
121
+ *
122
+ * @typedef {object} ExchangeErrorTiming
123
+ * @property {number} startedAt - Monotonic timestamp captured when processing started.
124
+ * @property {undefined} endedAt - Not available in a non-terminal error snapshot.
125
+ * @property {undefined} duration - Not available in a non-terminal error snapshot.
126
+ */
127
+
128
+ /**
129
+ * Represents timing information captured when exchange processing ends.
130
+ *
131
+ * @typedef {object} ExchangeEndTiming
132
+ * @property {number} startedAt - Monotonic timestamp captured when processing started.
133
+ * @property {number} endedAt - Monotonic timestamp captured when processing ended.
134
+ * @property {number} duration - Elapsed processing time in milliseconds, rounded.
135
+ */
136
+
137
+ /**
138
+ * Contains a snapshot of the relevant HTTP request information.
139
+ *
140
+ * @typedef {object} ExchangeRequestInfo
141
+ * @property {string|undefined} method The HTTP method used by the request, or `undefined` if unavailable.
142
+ * @property {string} pathname The URL pathname requested by the client.
143
+ * @property {string|undefined} ip The IP address associated with the request, or `undefined` if unavailable.
144
+ * @property {string|undefined} userAgent The value of the request's `User-Agent` header, or `undefined` if the header is absent.
145
+ */
146
+
147
+ /**
148
+ * Contains a snapshot of the relevant HTTP response information.
149
+ *
150
+ * @typedef {object} ExchangeResponseInfo
151
+ * @property {number} statusCode The HTTP status code assigned to the response.
152
+ */
153
+
154
+ /**
155
+ * Provides information about an exchange when processing starts.
156
+ *
157
+ * @typedef {object} ExchangeStartInfo
158
+ * @property {string} id - Unique identifier assigned to the exchange.
159
+ * @property {string} traceId - Identifier used to correlate related operations.
160
+ * @property {ExchangeStartTiming} timing - Timing information captured at processing start.
161
+ * @property {ExchangeRequestInfo} request - Request information captured for the exchange.
162
+ * @property {ExchangeResponseInfo} response - Response information captured for the exchange.
163
+ * @property {'pending'} outcome - Processing outcome at the time of the snapshot.
164
+ * @property {undefined} error - No processing error has been recorded in this snapshot.
165
+ */
166
+
167
+ /**
168
+ * Provides information about an exchange when processing fails.
169
+ *
170
+ * @typedef {object} ExchangeErrorInfo
171
+ * @property {string} id - Unique identifier assigned to the exchange.
172
+ * @property {string} traceId - Identifier used to correlate related operations.
173
+ * @property {ExchangeErrorTiming} timing - Timing information captured when processing ended.
174
+ * @property {ExchangeRequestInfo} request - Request information captured for the exchange.
175
+ * @property {ExchangeResponseInfo} response - Response information captured for the exchange.
176
+ * @property {'error'} outcome - Indicates that exchange processing failed.
177
+ * @property {Error} error - Error that caused exchange processing to fail.
178
+ */
179
+
180
+ /**
181
+ * Provides information about an exchange when processing completes.
182
+ *
183
+ * @typedef {object} ExchangeEndInfo
184
+ * @property {string} id - Unique identifier assigned to the exchange.
185
+ * @property {string} traceId - Identifier used to correlate related operations.
186
+ * @property {ExchangeEndTiming} timing - Timing information captured when processing ended.
187
+ * @property {ExchangeRequestInfo} request - Request information captured for the exchange.
188
+ * @property {ExchangeResponseInfo} response - Response information captured for the exchange.
189
+ * @property {'success'|'error'} outcome - Indicates whether exchange processing succeeded or failed.
190
+ * @property {Error|undefined} error - Error associated with the exchange, if one occurred.
191
+ */
192
+
193
+ /**
194
+ * Represents an exchange snapshot for any supported processing outcome.
195
+ *
196
+ * @typedef {ExchangeStartInfo|ExchangeErrorInfo|ExchangeEndInfo} ExchangeInfo
197
+ */
198
+
199
+ // ━━ CONSTANTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
200
+ /**
201
+ * Header name used to expose the W3C trace context on the response.
202
+ *
203
+ * @type {string}
204
+ */
205
+ const HEADER_TRACEPARENT = 'traceparent';
206
+
207
+ /**
208
+ * Header name used to expose the request identifier on the response.
209
+ *
210
+ * @type {string}
211
+ */
212
+ const HEADER_REQUEST_ID = 'x-request-id';
213
+
214
+ // ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
215
+ /**
216
+ * Discards the uncommitted response state.
217
+ *
218
+ * @private
219
+ * @param {Response} response - Response whose state is discarded.
220
+ * @throws {TypeError} If `response` is not a `Response` instance.
221
+ * @throws {ResponseError} If the response has already been committed.
222
+ */
223
+ const discardResponseState = response => {
224
+ Response.discardUncommittedState(response);
225
+ };
226
+
227
+ /**
228
+ * Wraps the response body with a transform stream.
229
+ *
230
+ * @private
231
+ * @param {Response} response - Response whose body is wrapped.
232
+ * @param {NodeTransform} transform - Transform applied to the response body.
233
+ * @throws {TypeError} If the response is not a `Response` instance.
234
+ * @throws {ResponseError} If the response cannot be wrapped (see `Response.wrapBody`).
235
+ */
236
+ const wrapResponseBody = (response, transform) => {
237
+ Response.wrapBody(response, transform);
238
+ };
239
+
240
+ // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
241
+ /**
242
+ * Represents a single HTTP transaction.
243
+ *
244
+ * @classdesc An exchange couples an incoming request with its outgoing
245
+ * response and carries the internal context needed to process it.
246
+ */
247
+ class Exchange {
248
+ /**
249
+ * Incoming request abstraction.
250
+ *
251
+ * @type {Request}
252
+ */
253
+ #request;
254
+
255
+ /**
256
+ * Outgoing response abstraction.
257
+ *
258
+ * @type {Response}
259
+ */
260
+ #response;
261
+
262
+ /**
263
+ * Internal execution context.
264
+ *
265
+ * @type {ExchangeContext}
266
+ */
267
+ #context;
268
+
269
+ /**
270
+ * Per-request mutable state shared between middleware layers.
271
+ *
272
+ * @type {Record<string, unknown>}
273
+ */
274
+ #state;
275
+
276
+ /**
277
+ * Optional callback invoked when response commitment produces an error.
278
+ *
279
+ * @type {ExchangeCommitErrorHook|null}
280
+ */
281
+ #onCommitError = null;
282
+
283
+ /**
284
+ * Creates an exchange from the incoming request and outgoing response.
285
+ *
286
+ * @param {IncomingRequest} incomingMessage - Incoming HTTP request message.
287
+ * @param {ServerResponse} serverResponse - Native Node.js server response.
288
+ * @param {ExchangeOptions} [options] - The exchange options.
289
+ * @example
290
+ * ```js
291
+ * const exchange = new Exchange(incomingMessage, serverResponse, {
292
+ * request: { bodyLimit: 1_000_000 },
293
+ * response: { etag: true, defaultCharset: 'utf-8' },
294
+ * events,
295
+ * onCommitError: (error, exchange) => console.error(error, exchange.id),
296
+ * });
297
+ * ```
298
+ */
299
+ constructor(incomingMessage, serverResponse, options = {}) {
300
+ this.#onCommitError =
301
+ typeof options.onCommitError === 'function' ? options.onCommitError : null;
302
+ this.#state = Object.create(null);
303
+
304
+ this.#request = new Request(incomingMessage, options.request);
305
+
306
+ const traceparent = this.#request.getHeader(HEADER_TRACEPARENT);
307
+ const validTraceparent =
308
+ typeof traceparent === 'string'
309
+ ? traceparent
310
+ : Array.isArray(traceparent)
311
+ ? traceparent[0]
312
+ : undefined;
313
+
314
+ this.#context = new ExchangeContext({
315
+ traceparent: validTraceparent,
316
+ });
317
+
318
+ this.#response = new Response(serverResponse, {
319
+ ...options.response,
320
+ onCommit: response => this.#handleCommit(response),
321
+ onCommitError: error => this.#handleCommitError(error),
322
+ });
323
+
324
+ this.#bindNativeLifecycle(incomingMessage, serverResponse);
325
+ }
326
+
327
+ /**
328
+ * The incoming request.
329
+ *
330
+ * @type {Request}
331
+ */
332
+ get request() {
333
+ return this.#request;
334
+ }
335
+
336
+ /**
337
+ * The outgoing response.
338
+ *
339
+ * @type {Response}
340
+ */
341
+ get response() {
342
+ return this.#response;
343
+ }
344
+
345
+ /**
346
+ * The mutable per-request state shared between middleware layers.
347
+ *
348
+ * @type {Record<string, unknown>}
349
+ */
350
+ get state() {
351
+ return this.#state;
352
+ }
353
+
354
+ /**
355
+ * The unique identifier for the transaction.
356
+ *
357
+ * @type {string}
358
+ */
359
+ get id() {
360
+ return this.#context.id;
361
+ }
362
+
363
+ /**
364
+ * The W3C trace identifier for the transaction.
365
+ *
366
+ * @type {string}
367
+ */
368
+ get traceId() {
369
+ return this.#context.traceId;
370
+ }
371
+
372
+ /**
373
+ * The W3C span identifier for the transaction.
374
+ *
375
+ * @type {string}
376
+ */
377
+ get spanId() {
378
+ return this.#context.spanId;
379
+ }
380
+
381
+ /**
382
+ * The cancellation signal for the transaction.
383
+ *
384
+ * @type {AbortSignal}
385
+ */
386
+ get signal() {
387
+ return this.#context.signal;
388
+ }
389
+
390
+ /**
391
+ * Monotonic timestamp captured when the exchange was created.
392
+ *
393
+ * @type {number}
394
+ */
395
+ get startedAt() {
396
+ return this.#context.startedAt;
397
+ }
398
+
399
+ /**
400
+ * Aborts the transaction.
401
+ *
402
+ * Signals the cancellation signal associated with the transaction.
403
+ * Subsequent calls have no effect.
404
+ *
405
+ * @returns {void}
406
+ * @example
407
+ * ```js
408
+ * exchange.abort();
409
+ * ```
410
+ */
411
+ abort() {
412
+ this.#context.abort();
413
+ }
414
+
415
+ /**
416
+ * Binds native transport lifecycle events to the exchange.
417
+ *
418
+ * If the socket is already destroyed when the exchange is created,
419
+ * the exchange is aborted immediately. Otherwise a one-shot listener
420
+ * aborts the exchange when the incoming request closes before the
421
+ * response has finished writing.
422
+ *
423
+ * @param {IncomingRequest} incomingMessage - Native incoming HTTP message whose close event is observed.
424
+ * @param {ServerResponse} serverResponse - Native outgoing HTTP response used to determine whether writing has already ended.
425
+ * @returns {void}
426
+ */
427
+ #bindNativeLifecycle(incomingMessage, serverResponse) {
428
+ if (incomingMessage.destroyed) {
429
+ this.abort();
430
+ } else {
431
+ const onClose = () => {
432
+ if (!serverResponse.writableEnded) {
433
+ this.abort();
434
+ }
435
+ };
436
+
437
+ incomingMessage.once('close', onClose);
438
+
439
+ this.#context.signal.addEventListener(
440
+ 'abort',
441
+ () => {
442
+ incomingMessage.removeListener('close', onClose);
443
+ },
444
+ { once: true },
445
+ );
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Runs the commit hook associated with the response.
451
+ *
452
+ * Adds the telemetry headers, the W3C trace context, and the request
453
+ * identifier to the response before it is materialized. Headers already
454
+ * present on the response are preserved.
455
+ *
456
+ * @param {Response} response - The response being committed.
457
+ */
458
+ #handleCommit(response) {
459
+ this.#context.telemetry.markServerSentAt();
460
+ this.#context.telemetry.markProcessDuration();
461
+
462
+ for (const [name, value] of this.#context.telemetry.entries()) {
463
+ if (!response.hasHeader(name)) {
464
+ response.setHeader(name, value);
465
+ }
466
+ }
467
+
468
+ if (!response.hasHeader(HEADER_TRACEPARENT)) {
469
+ response.setHeader(HEADER_TRACEPARENT, this.#context.traceparent());
470
+ }
471
+
472
+ if (!response.hasHeader(HEADER_REQUEST_ID)) {
473
+ response.setHeader(HEADER_REQUEST_ID, this.#context.id);
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Forwards a commit error to the configured error callback and event bus.
479
+ *
480
+ * @param {Error} error - The error produced while committing the response.
481
+ */
482
+ #handleCommitError(error) {
483
+ if (this.#onCommitError) {
484
+ this.#onCommitError(error, this);
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Builds an immutable snapshot of this exchange.
490
+ *
491
+ * @param {ExchangeOutcome} outcome - Outcome to record.
492
+ * @param {number|undefined} endedAt - Monotonic end timestamp, or `undefined` for a non-terminal snapshot.
493
+ * @param {Error|undefined} error - Error raised during the exchange, when any.
494
+ * @returns {ExchangeInfo} An immutable snapshot of the exchange.
495
+ * @throws {TypeError} If `outcome` does not match any supported processing state.
496
+ */
497
+ #createSnapshot(outcome, endedAt, error) {
498
+ const startedAt = this.#context.startedAt;
499
+ const userAgent = this.#request.getHeader('user-agent');
500
+ const request = Object.freeze({
501
+ method: this.#request.method,
502
+ pathname: this.#request.pathname,
503
+ ip: this.#request.ip,
504
+ userAgent: Array.isArray(userAgent) ? userAgent[0] : userAgent,
505
+ });
506
+ const response = Object.freeze({
507
+ statusCode: this.#response.statusCode,
508
+ });
509
+
510
+ if (outcome === 'pending') {
511
+ return Object.freeze({
512
+ id: this.#context.id,
513
+ traceId: this.#context.traceId,
514
+ timing: Object.freeze({ startedAt, endedAt: undefined, duration: undefined }),
515
+ request,
516
+ response,
517
+ outcome,
518
+ error: undefined,
519
+ });
520
+ }
521
+
522
+ if (outcome === 'error' && endedAt === undefined && error) {
523
+ return Object.freeze({
524
+ id: this.#context.id,
525
+ traceId: this.#context.traceId,
526
+ timing: Object.freeze({ startedAt, endedAt: undefined, duration: undefined }),
527
+ request,
528
+ response,
529
+ outcome,
530
+ error,
531
+ });
532
+ }
533
+
534
+ if (outcome === 'error' && endedAt !== undefined && error) {
535
+ return Object.freeze({
536
+ id: this.#context.id,
537
+ traceId: this.#context.traceId,
538
+ timing: Object.freeze({
539
+ startedAt,
540
+ endedAt,
541
+ duration: Math.round(endedAt - startedAt),
542
+ }),
543
+ request,
544
+ response,
545
+ outcome,
546
+ error,
547
+ });
548
+ }
549
+
550
+ if (outcome === 'success' && endedAt !== undefined) {
551
+ return Object.freeze({
552
+ id: this.#context.id,
553
+ traceId: this.#context.traceId,
554
+ timing: Object.freeze({
555
+ startedAt,
556
+ endedAt,
557
+ duration: Math.round(endedAt - startedAt),
558
+ }),
559
+ request,
560
+ response,
561
+ outcome,
562
+ error: undefined,
563
+ });
564
+ }
565
+
566
+ throw new TypeError('Unreachable: unexpected exchange outcome');
567
+ }
568
+
569
+ /**
570
+ * Creates an immutable snapshot of an exchange.
571
+ *
572
+ * The `outcome` value indicates the processing state captured in the snapshot:
573
+ *
574
+ * - `'pending'` — processing has not finished.
575
+ * - `'success'` — processing finished without an error.
576
+ * - `'error'` — an error occurred during processing.
577
+ *
578
+ * When `endedAt` is `undefined`, the snapshot is non-terminal and `duration`
579
+ * is also `undefined`. When `endedAt` is provided, `duration` is computed as
580
+ * the rounded elapsed time since `startedAt`.
581
+ *
582
+ * @param {Exchange} exchange - Exchange to snapshot.
583
+ * @param {ExchangeOutcome} outcome - Outcome to record.
584
+ * @param {number|undefined} endedAt - Monotonic end timestamp, or `undefined` for a non-terminal snapshot.
585
+ * @param {Error|undefined} error - Error raised during the exchange, when any.
586
+ * @returns {ExchangeInfo} An immutable snapshot of the exchange.
587
+ * @throws {TypeError} If `exchange` is not an `Exchange` instance.
588
+ * @example
589
+ * ```js
590
+ * const info = Exchange.createSnapshot(exchange, 'success', performance.now(), undefined);
591
+ * ```
592
+ */
593
+ static createSnapshot(exchange, outcome, endedAt, error) {
594
+ if (!(exchange instanceof Exchange)) {
595
+ throw new TypeError('Expected an Exchange instance');
596
+ }
597
+
598
+ return exchange.#createSnapshot(outcome, endedAt, error);
599
+ }
600
+ }
601
+
602
+ // ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
603
+ export default Exchange;
604
+ export { discardResponseState, wrapResponseBody };