@bymax-one/nest-core 1.3.2 → 1.5.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/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Catch, Inject, Optional, Injectable, ConfigurableModuleBuilder, Module, HttpException, Logger, Get, Res, Controller, Req, HttpStatus, UnauthorizedException, NotFoundException } from '@nestjs/common';
2
- import { HttpAdapterHost, DiscoveryService, Reflector, BaseExceptionFilter, APP_FILTER, APP_INTERCEPTOR, DiscoveryModule } from '@nestjs/core';
3
- import { tap, catchError, throwError } from 'rxjs';
2
+ import { HttpAdapterHost, DiscoveryService, Reflector, BaseExceptionFilter, APP_FILTER, DiscoveryModule } from '@nestjs/core';
4
3
  import { createHash, timingSafeEqual } from 'crypto';
4
+ import { AsyncResource } from 'async_hooks';
5
+ import { tap, catchError, throwError } from 'rxjs';
5
6
 
6
7
  var __defProp = Object.defineProperty;
7
8
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -110,6 +111,9 @@ function resolveOpenApi(raw) {
110
111
  // otherwise reach into objects the consumer still holds a reference to.
111
112
  security: structuredClone(raw?.security ?? []),
112
113
  operationSecurity: structuredClone(raw?.operationSecurity ?? {}),
114
+ // Carried by reference, not cloned: it is a function the consumer owns, and
115
+ // `structuredClone` cannot copy one at all.
116
+ ...raw?.operationIdFactory === void 0 ? {} : { operationIdFactory: raw.operationIdFactory },
113
117
  includeCoreSchemas: raw?.includeCoreSchemas ?? true
114
118
  };
115
119
  }
@@ -590,141 +594,6 @@ BymaxExceptionFilter = __decorateClass([
590
594
  __decorateParam(3, Optional()),
591
595
  __decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
592
596
  ], BymaxExceptionFilter);
593
-
594
- // src/timing/request-info.accessor.ts
595
- function stripQueryString(rawUrl) {
596
- const queryIndex = rawUrl.indexOf("?");
597
- return queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
598
- }
599
- function readExpressTemplate(request) {
600
- const path = request.route?.path;
601
- return path === void 0 ? void 0 : `${request.baseUrl ?? ""}${path}`;
602
- }
603
- function readFastifyTemplate(request) {
604
- return request.routeOptions?.url;
605
- }
606
- function extractRequestInfo(context) {
607
- const request = context.switchToHttp().getRequest();
608
- const template = readExpressTemplate(request) ?? readFastifyTemplate(request);
609
- const rawUrl = request.originalUrl ?? request.url ?? "";
610
- return { method: request.method ?? "", route: template ?? stripQueryString(rawUrl) };
611
- }
612
-
613
- // src/timing/timing.interceptor.ts
614
- var DEFAULT_SUCCESS_STATUS = 200;
615
- var UNKNOWN_ERROR_STATUS = 500;
616
- var TimingInterceptor = class {
617
- /**
618
- * @param options - Resolved core options; supplies `slowRequestThresholdMs`.
619
- * @param sink - The bound timing sink; its `record` failures are swallowed.
620
- * Injected with `@Optional()`: `BymaxCoreModule` binds no local default for
621
- * this token on the sync path when the metrics bridge is not registered, so
622
- * a consumer's own `BYMAX_TIMING_SINK` binding is not shadowed by one; when
623
- * nothing resolves, this falls back to a no-op sink.
624
- * @param clock - Monotonic clock seam; defaults to `performance.now()`, and
625
- * is bound explicitly through {@link BYMAX_TIMING_CLOCK} so tests inject a
626
- * stub advancing by controlled amounts.
627
- * @param traceContext - Reads the active span's identifiers. Injected with
628
- * `@Optional()` so this interceptor stays constructible on its own; when
629
- * nothing resolves, a no-op resolves no trace and the sample simply omits
630
- * the fields.
631
- */
632
- constructor(options, sink, clock = DEFAULT_MONOTONIC_CLOCK, traceContext) {
633
- this.options = options;
634
- this.clock = clock;
635
- this.sink = sink ?? new NoopTimingSink();
636
- this.traceContext = traceContext ?? new NoopTraceContextProvider();
637
- }
638
- /**
639
- * Measure the handler chain and record exactly one sample per completed
640
- * request, on the success path and on the error path alike.
641
- *
642
- * @param context - The execution context of the current request.
643
- * @param next - The next handler in the chain.
644
- * @returns The downstream response stream, unmodified beyond the measurement.
645
- */
646
- intercept(context, next) {
647
- if (context.getType() !== "http") {
648
- return next.handle();
649
- }
650
- const start = this.clock.now();
651
- const { method, route } = extractRequestInfo(context);
652
- return next.handle().pipe(
653
- tap({
654
- complete: () => {
655
- this.recordSample(method, route, this.readSuccessStatus(context), start);
656
- }
657
- }),
658
- catchError((error) => {
659
- this.recordSample(method, route, this.readErrorStatus(error), start);
660
- return throwError(() => error);
661
- })
662
- );
663
- }
664
- /**
665
- * Read the final status code from the response object on the success path.
666
- *
667
- * @param context - The execution context of the current request.
668
- * @returns The response's status code, or the default success status when absent.
669
- */
670
- readSuccessStatus(context) {
671
- const response = context.switchToHttp().getResponse();
672
- return response.statusCode ?? DEFAULT_SUCCESS_STATUS;
673
- }
674
- /**
675
- * Derive the final status code for an error that escaped the handler.
676
- *
677
- * @param error - The error propagated by the handler chain.
678
- * @returns The `HttpException` status, or the generic 500 for anything else.
679
- */
680
- readErrorStatus(error) {
681
- return error instanceof HttpException ? error.getStatus() : UNKNOWN_ERROR_STATUS;
682
- }
683
- /**
684
- * Build the sample, compute the slow flag, and deliver it to the sink inside
685
- * a try/catch that silences any failure: a throwing sink must never affect
686
- * the request it is observing.
687
- *
688
- * @param method - HTTP method of the request.
689
- * @param route - Route template of the request.
690
- * @param statusCode - Final status code, success or error.
691
- * @param start - Monotonic start timestamp captured before the handler ran.
692
- */
693
- recordSample(method, route, statusCode, start) {
694
- const durationMs = this.clock.now() - start;
695
- const threshold = this.options.timing.slowRequestThresholdMs;
696
- const slow = threshold !== void 0 && durationMs > threshold;
697
- let trace;
698
- try {
699
- trace = this.traceContext.getTraceContext();
700
- } catch {
701
- }
702
- try {
703
- this.sink.record({
704
- method,
705
- route,
706
- statusCode,
707
- durationMs,
708
- slow,
709
- // Spread rather than assigned: an absent trace must leave the keys off
710
- // the sample entirely, so a sink cannot mistake `undefined` for an id.
711
- ...trace !== void 0 ? { traceId: trace.traceId, spanId: trace.spanId } : {}
712
- });
713
- } catch {
714
- }
715
- }
716
- };
717
- TimingInterceptor = __decorateClass([
718
- Injectable(),
719
- __decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
720
- __decorateParam(1, Optional()),
721
- __decorateParam(1, Inject(BYMAX_TIMING_SINK)),
722
- __decorateParam(2, Inject(BYMAX_TIMING_CLOCK)),
723
- __decorateParam(3, Optional()),
724
- __decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
725
- ], TimingInterceptor);
726
-
727
- // src/passthrough.providers.ts
728
597
  var PassThroughExceptionFilter = class {
729
598
  constructor(adapterHost) {
730
599
  this.adapterHost = adapterHost;
@@ -743,18 +612,6 @@ var PassThroughExceptionFilter = class {
743
612
  PassThroughExceptionFilter = __decorateClass([
744
613
  Catch()
745
614
  ], PassThroughExceptionFilter);
746
- var PassThroughInterceptor = class {
747
- /**
748
- * Forward the request to the next handler unchanged.
749
- *
750
- * @param _context - The execution context; unused by a transparent forwarder.
751
- * @param next - The next handler in the chain.
752
- * @returns The downstream response stream, unmodified.
753
- */
754
- intercept(_context, next) {
755
- return next.handle();
756
- }
757
- };
758
615
  function assertAsyncFeatureEnabled(feature, enabled) {
759
616
  if (!enabled) {
760
617
  throw new NotFoundException(
@@ -765,9 +622,6 @@ function assertAsyncFeatureEnabled(feature, enabled) {
765
622
  function selectAsyncExceptionFilter(options, correlation, adapterHost) {
766
623
  return options.envelope.enabled ? new BymaxExceptionFilter(options, correlation, adapterHost) : new PassThroughExceptionFilter(adapterHost);
767
624
  }
768
- function selectAsyncTimingInterceptor(options, sink, clock) {
769
- return options.timing.enabled ? new TimingInterceptor(options, sink, clock) : new PassThroughInterceptor();
770
- }
771
625
 
772
626
  // src/discovery.ts
773
627
  function labelFor(className, token) {
@@ -1223,12 +1077,12 @@ var TimingMetricsSink = class {
1223
1077
  this.histogram = getOrCreateHistogram(promClient, registry);
1224
1078
  }
1225
1079
  /**
1226
- * Record one completed request: increment the counter once and observe the
1080
+ * Record one closed request: increment the counter once and observe the
1227
1081
  * duration in seconds, both under the bounded label set. Any failure is
1228
1082
  * swallowed so a metrics backend problem can never break the request being
1229
1083
  * observed.
1230
1084
  *
1231
- * @param sample - The timing sample for a completed request.
1085
+ * @param sample - The timing sample for a closed request, however it ended.
1232
1086
  */
1233
1087
  record(sample) {
1234
1088
  const labels = {
@@ -1280,6 +1134,148 @@ function buildMetricsTimingSinkProvider() {
1280
1134
  };
1281
1135
  }
1282
1136
 
1137
+ // src/timing/fastify-route.bridge.ts
1138
+ function bridgeFastifyRouteMetadata(adapter) {
1139
+ if (adapter?.getType?.() !== "fastify") {
1140
+ return false;
1141
+ }
1142
+ const instance = adapter.getInstance?.();
1143
+ if (!isFastifyInstance(instance)) {
1144
+ return false;
1145
+ }
1146
+ instance.addHook("onRequest", (request, _reply, done) => {
1147
+ if (request.raw !== void 0 && request.routeOptions !== void 0) {
1148
+ request.raw["routeOptions"] = request.routeOptions;
1149
+ }
1150
+ done();
1151
+ });
1152
+ return true;
1153
+ }
1154
+ function isFastifyInstance(instance) {
1155
+ return typeof instance?.addHook === "function";
1156
+ }
1157
+
1158
+ // src/timing/request-info.accessor.ts
1159
+ var UNMATCHED_ROUTE = "<unmatched>";
1160
+ function readExpressTemplate(request) {
1161
+ const path = request.route?.path;
1162
+ return path === void 0 ? void 0 : `${request.baseUrl ?? ""}${path}`;
1163
+ }
1164
+ function readFastifyTemplate(request) {
1165
+ return request.routeOptions?.url;
1166
+ }
1167
+ function readRequestInfo(request) {
1168
+ const template = readExpressTemplate(request) ?? readFastifyTemplate(request);
1169
+ return { method: request.method ?? "", route: template ?? UNMATCHED_ROUTE };
1170
+ }
1171
+ function extractRequestInfo(context) {
1172
+ return readRequestInfo(context.switchToHttp().getRequest());
1173
+ }
1174
+
1175
+ // src/timing/timing.sample.ts
1176
+ function readTraceContext(traceContext) {
1177
+ try {
1178
+ return traceContext.getTraceContext();
1179
+ } catch {
1180
+ return void 0;
1181
+ }
1182
+ }
1183
+ function buildTimingSample(input) {
1184
+ const { method, route, statusCode, durationMs, threshold, trace } = input;
1185
+ return {
1186
+ method,
1187
+ route,
1188
+ statusCode,
1189
+ durationMs,
1190
+ slow: threshold !== void 0 && durationMs > threshold,
1191
+ // Spread rather than assigned: an absent trace must leave the keys off the
1192
+ // sample entirely, so a sink cannot mistake `undefined` for an id.
1193
+ ...trace !== void 0 ? { traceId: trace.traceId, spanId: trace.spanId } : {}
1194
+ };
1195
+ }
1196
+
1197
+ // src/timing/timing.middleware.ts
1198
+ var BymaxTimingMiddleware = class {
1199
+ /**
1200
+ * @param options - The resolved options, read for the slow-request threshold.
1201
+ * @param sink - The bound timing sink; its failures are swallowed, because a
1202
+ * throwing sink must never affect the request it is observing.
1203
+ * @param clock - Monotonic clock seam, so a test advances time by controlled
1204
+ * amounts instead of sleeping.
1205
+ * @param traceContext - Reads the active span's identifiers. Optional: the
1206
+ * middleware stays constructible on its own, and a no-op resolves nothing.
1207
+ */
1208
+ constructor(options, sink, clock = DEFAULT_MONOTONIC_CLOCK, traceContext) {
1209
+ this.options = options;
1210
+ this.clock = clock;
1211
+ this.sink = sink ?? new NoopTimingSink();
1212
+ this.traceContext = traceContext ?? new NoopTraceContextProvider();
1213
+ }
1214
+ /**
1215
+ * Start the measurement and arrange for the sample to be recorded once the
1216
+ * connection closes, then hand the request straight on.
1217
+ *
1218
+ * The route is read inside the listener rather than here: at middleware time
1219
+ * the router has not matched yet, so `req.route` is still empty. By the time
1220
+ * the connection closes it is populated — including for a request a guard
1221
+ * rejected, since matching happens before guards run.
1222
+ *
1223
+ * The trace lookup is tried in the live context first and in this moment's
1224
+ * captured context second; see this file's header for the measurements that
1225
+ * decided that order.
1226
+ *
1227
+ * @param request - The framework request object.
1228
+ * @param response - The framework response object.
1229
+ * @param next - Continues the chain; called synchronously and unconditionally.
1230
+ */
1231
+ use(request, response, next) {
1232
+ const start = this.clock.now();
1233
+ const readCapturedTrace = AsyncResource.bind(() => readTraceContext(this.traceContext));
1234
+ response.on("close", () => {
1235
+ this.record(
1236
+ request,
1237
+ response,
1238
+ start,
1239
+ readTraceContext(this.traceContext) ?? readCapturedTrace()
1240
+ );
1241
+ });
1242
+ next();
1243
+ }
1244
+ /**
1245
+ * Build the sample and hand it to the sink, guarding both steps.
1246
+ *
1247
+ * @param request - The framework request object.
1248
+ * @param response - The framework response object.
1249
+ * @param start - Monotonic timestamp captured before the chain ran.
1250
+ * @param trace - The span identifiers already resolved by the caller, which
1251
+ * owns the choice of which context to read them from.
1252
+ */
1253
+ record(request, response, start, trace) {
1254
+ const { method, route } = readRequestInfo(request);
1255
+ const sample = buildTimingSample({
1256
+ method,
1257
+ route,
1258
+ statusCode: response.statusCode ?? 0,
1259
+ durationMs: this.clock.now() - start,
1260
+ threshold: this.options.timing.slowRequestThresholdMs,
1261
+ trace
1262
+ });
1263
+ try {
1264
+ this.sink.record(sample);
1265
+ } catch {
1266
+ }
1267
+ }
1268
+ };
1269
+ BymaxTimingMiddleware = __decorateClass([
1270
+ Injectable(),
1271
+ __decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
1272
+ __decorateParam(1, Optional()),
1273
+ __decorateParam(1, Inject(BYMAX_TIMING_SINK)),
1274
+ __decorateParam(2, Inject(BYMAX_TIMING_CLOCK)),
1275
+ __decorateParam(3, Optional()),
1276
+ __decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
1277
+ ], BymaxTimingMiddleware);
1278
+
1283
1279
  // src/core.module.ts
1284
1280
  var {
1285
1281
  ConfigurableModuleClass: BymaxCoreModuleBase,
@@ -1298,7 +1294,7 @@ function buildSyncProviders(resolved) {
1298
1294
  providers.push({ provide: APP_FILTER, useClass: BymaxExceptionFilter });
1299
1295
  }
1300
1296
  if (resolved.timing.enabled) {
1301
- providers.push({ provide: APP_INTERCEPTOR, useClass: TimingInterceptor });
1297
+ providers.push(BymaxTimingMiddleware);
1302
1298
  }
1303
1299
  if (resolved.health.enabled) {
1304
1300
  providers.push(HealthService);
@@ -1335,11 +1331,6 @@ function buildAsyncSlots() {
1335
1331
  { token: BYMAX_CORRELATION_PROVIDER, optional: true },
1336
1332
  HttpAdapterHost
1337
1333
  ]
1338
- },
1339
- {
1340
- provide: APP_INTERCEPTOR,
1341
- useFactory: (options, sink, clock) => selectAsyncTimingInterceptor(options, sink, clock),
1342
- inject: [BYMAX_CORE_OPTIONS, BYMAX_TIMING_SINK, BYMAX_TIMING_CLOCK]
1343
1334
  }
1344
1335
  ];
1345
1336
  }
@@ -1354,6 +1345,77 @@ function augmentModule(base, providers, controllers, exportTokens = [BYMAX_CORE_
1354
1345
  };
1355
1346
  }
1356
1347
  var BymaxCoreModule = class extends BymaxCoreModuleBase {
1348
+ /**
1349
+ * @param options - The resolved snapshot, read to decide whether the timing
1350
+ * middleware is applied at all.
1351
+ */
1352
+ constructor(resolved, adapterHost) {
1353
+ super();
1354
+ this.resolved = resolved;
1355
+ this.adapterHost = adapterHost;
1356
+ }
1357
+ /**
1358
+ * Apply the request-timing middleware to every route.
1359
+ *
1360
+ * Middleware rather than the interceptor this replaced, because guards run
1361
+ * before interceptors: a request rejected by an authentication, authorization
1362
+ * or throttling guard never reached the interceptor, and one matching no
1363
+ * route never reached a controller at all. A deployment could therefore be
1364
+ * under a credential-stuffing run — a flood of 401s — with a flat error
1365
+ * graph, which is why this is a security fix rather than a metrics
1366
+ * improvement.
1367
+ *
1368
+ * Applied on both registration paths, and only when timing is enabled: the
1369
+ * middleware is the sole recorder now, since a second one would count every
1370
+ * matched request twice.
1371
+ *
1372
+ * There is no single pattern that covers both adapters, which is the whole
1373
+ * reason this reads the adapter first. Measured, requesting the root, a
1374
+ * parameterised route, a nested path and an unmatched path, with and without
1375
+ * `setGlobalPrefix('api')`:
1376
+ *
1377
+ * | `forRoutes(...)` | Express | Fastify |
1378
+ * | ---------------- | ---------------- | -------------------- |
1379
+ * | `'*splat'` | skips the root | — |
1380
+ * | `'{*splat}'` | skips `/api` | every path |
1381
+ * | `'/'` | every path | matches `/` only |
1382
+ *
1383
+ * On Express `'/'` is a mount and matches everything beneath whatever prefix
1384
+ * it is scoped to, while `'{*splat}'` — the form Nest 11's migration guide
1385
+ * prescribes for "all routes" — stops matching the prefixed root once an
1386
+ * application calls `setGlobalPrefix`. That was reported as nest#14520 and
1387
+ * fixed by nest#14522, whose regression test covers Fastify; on
1388
+ * `@nestjs/core` 11.1.28 with the Express adapter the prefixed root still
1389
+ * reaches no middleware while resolving to `200`.
1390
+ *
1391
+ * On Fastify the same `'/'` is an exact match rather than a mount — one of
1392
+ * three requests reached the middleware — so the wildcard is the only form
1393
+ * that works there.
1394
+ *
1395
+ * A recorder that quietly omits one route is the same class of defect this
1396
+ * middleware exists to fix, so each adapter gets the form that omits none.
1397
+ *
1398
+ * Fastify also needs {@link bridgeFastifyRouteMetadata}, because middie hands
1399
+ * middleware the raw request, which carries no route metadata at all; see
1400
+ * that file for why the label would otherwise be `<unmatched>` for every
1401
+ * request.
1402
+ *
1403
+ * One limit stays and is documented in the README: Nest scopes module
1404
+ * middleware to the global prefix, so with `setGlobalPrefix('api')` a request
1405
+ * to `/nope` — outside the prefix entirely — reaches no middleware and is not
1406
+ * recorded, while `/api/nope` is. No `forRoutes` argument changes that, and
1407
+ * nothing this module can register reaches outside its own scope.
1408
+ *
1409
+ * @param consumer - Nest's middleware consumer.
1410
+ */
1411
+ configure(consumer) {
1412
+ if (!this.resolved.timing.enabled) {
1413
+ return;
1414
+ }
1415
+ const adapter = this.adapterHost?.httpAdapter;
1416
+ const onFastify = bridgeFastifyRouteMetadata(adapter);
1417
+ consumer.apply(BymaxTimingMiddleware).forRoutes(onFastify ? "{*splat}" : "/");
1418
+ }
1357
1419
  /**
1358
1420
  * Register the module synchronously. Options are known now, so disabled
1359
1421
  * features are omitted from the providers and controllers arrays and the
@@ -1379,7 +1441,7 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
1379
1441
  exportTokens.push(BYMAX_TIMING_SINK);
1380
1442
  }
1381
1443
  }
1382
- const scansProviders = resolved.health.enabled && resolved.health.autoDiscover || resolved.metrics.enabled;
1444
+ const scansProviders = resolved.health.enabled && resolved.health.autoDiscover || resolved.metrics.enabled || resolved.openapi.enabled;
1383
1445
  const imports = scansProviders ? [DiscoveryModule] : [];
1384
1446
  return augmentModule(
1385
1447
  super.forRoot(options),
@@ -1418,6 +1480,11 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
1418
1480
  },
1419
1481
  ...buildDefaultProviders(),
1420
1482
  ...buildAsyncSlots(),
1483
+ // Registered unconditionally, applied conditionally: the class must be
1484
+ // resolvable before `configure` can apply it, and whether timing is
1485
+ // enabled is unknown until the consumer's factory has run. An unapplied
1486
+ // middleware provider costs one construction and observes nothing.
1487
+ BymaxTimingMiddleware,
1421
1488
  HealthService,
1422
1489
  buildMetricsRegistryProvider(),
1423
1490
  buildMetricsTimingSinkProvider(),
@@ -1434,7 +1501,113 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
1434
1501
  }
1435
1502
  };
1436
1503
  BymaxCoreModule = __decorateClass([
1437
- Module({})
1504
+ Module({}),
1505
+ __decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
1506
+ __decorateParam(1, Optional()),
1507
+ __decorateParam(1, Inject(HttpAdapterHost))
1438
1508
  ], BymaxCoreModule);
1509
+ var DEFAULT_SUCCESS_STATUS = 200;
1510
+ var UNKNOWN_ERROR_STATUS = 500;
1511
+ var TimingInterceptor = class {
1512
+ /**
1513
+ * @param options - Resolved core options; supplies `slowRequestThresholdMs`.
1514
+ * @param sink - The bound timing sink; its `record` failures are swallowed.
1515
+ * Injected with `@Optional()`: `BymaxCoreModule` binds no local default for
1516
+ * this token on the sync path when the metrics bridge is not registered, so
1517
+ * a consumer's own `BYMAX_TIMING_SINK` binding is not shadowed by one; when
1518
+ * nothing resolves, this falls back to a no-op sink.
1519
+ * @param clock - Monotonic clock seam; defaults to `performance.now()`, and
1520
+ * is bound explicitly through {@link BYMAX_TIMING_CLOCK} so tests inject a
1521
+ * stub advancing by controlled amounts.
1522
+ * @param traceContext - Reads the active span's identifiers. Injected with
1523
+ * `@Optional()` so this interceptor stays constructible on its own; when
1524
+ * nothing resolves, a no-op resolves no trace and the sample simply omits
1525
+ * the fields.
1526
+ */
1527
+ constructor(options, sink, clock = DEFAULT_MONOTONIC_CLOCK, traceContext) {
1528
+ this.options = options;
1529
+ this.clock = clock;
1530
+ this.sink = sink ?? new NoopTimingSink();
1531
+ this.traceContext = traceContext ?? new NoopTraceContextProvider();
1532
+ }
1533
+ /**
1534
+ * Measure the handler chain and record exactly one sample per completed
1535
+ * request, on the success path and on the error path alike.
1536
+ *
1537
+ * @param context - The execution context of the current request.
1538
+ * @param next - The next handler in the chain.
1539
+ * @returns The downstream response stream, unmodified beyond the measurement.
1540
+ */
1541
+ intercept(context, next) {
1542
+ if (context.getType() !== "http") {
1543
+ return next.handle();
1544
+ }
1545
+ const start = this.clock.now();
1546
+ const { method, route } = extractRequestInfo(context);
1547
+ return next.handle().pipe(
1548
+ tap({
1549
+ complete: () => {
1550
+ this.recordSample(method, route, this.readSuccessStatus(context), start);
1551
+ }
1552
+ }),
1553
+ catchError((error) => {
1554
+ this.recordSample(method, route, this.readErrorStatus(error), start);
1555
+ return throwError(() => error);
1556
+ })
1557
+ );
1558
+ }
1559
+ /**
1560
+ * Read the final status code from the response object on the success path.
1561
+ *
1562
+ * @param context - The execution context of the current request.
1563
+ * @returns The response's status code, or the default success status when absent.
1564
+ */
1565
+ readSuccessStatus(context) {
1566
+ const response = context.switchToHttp().getResponse();
1567
+ return response.statusCode ?? DEFAULT_SUCCESS_STATUS;
1568
+ }
1569
+ /**
1570
+ * Derive the final status code for an error that escaped the handler.
1571
+ *
1572
+ * @param error - The error propagated by the handler chain.
1573
+ * @returns The `HttpException` status, or the generic 500 for anything else.
1574
+ */
1575
+ readErrorStatus(error) {
1576
+ return error instanceof HttpException ? error.getStatus() : UNKNOWN_ERROR_STATUS;
1577
+ }
1578
+ /**
1579
+ * Build the sample and deliver it to the sink inside a try/catch that
1580
+ * silences any failure: a throwing sink must never affect the request it is
1581
+ * observing.
1582
+ *
1583
+ * @param method - HTTP method of the request.
1584
+ * @param route - Route template of the request.
1585
+ * @param statusCode - Final status code, success or error.
1586
+ * @param start - Monotonic start timestamp captured before the handler ran.
1587
+ */
1588
+ recordSample(method, route, statusCode, start) {
1589
+ const sample = buildTimingSample({
1590
+ method,
1591
+ route,
1592
+ statusCode,
1593
+ durationMs: this.clock.now() - start,
1594
+ threshold: this.options.timing.slowRequestThresholdMs,
1595
+ trace: readTraceContext(this.traceContext)
1596
+ });
1597
+ try {
1598
+ this.sink.record(sample);
1599
+ } catch {
1600
+ }
1601
+ }
1602
+ };
1603
+ TimingInterceptor = __decorateClass([
1604
+ Injectable(),
1605
+ __decorateParam(0, Inject(BYMAX_CORE_OPTIONS)),
1606
+ __decorateParam(1, Optional()),
1607
+ __decorateParam(1, Inject(BYMAX_TIMING_SINK)),
1608
+ __decorateParam(2, Inject(BYMAX_TIMING_CLOCK)),
1609
+ __decorateParam(3, Optional()),
1610
+ __decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
1611
+ ], TimingInterceptor);
1439
1612
 
1440
- export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, TimingInterceptor, buildErrorEnvelope, codeForStatus };
1613
+ export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };