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