@horizon-republic/nestjs-jetstream 2.10.0 → 2.11.1

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.js CHANGED
@@ -13,10 +13,10 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
13
13
  // src/jetstream.module.ts
14
14
  import {
15
15
  Global,
16
- Inject,
17
- Logger as Logger18,
18
- Module,
19
- Optional
16
+ Inject as Inject2,
17
+ Logger as Logger20,
18
+ Module as Module2,
19
+ Optional as Optional2
20
20
  } from "@nestjs/common";
21
21
 
22
22
  // src/client/jetstream.client.ts
@@ -46,6 +46,10 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
46
46
  TransportEvent2["ShutdownStart"] = "shutdownStart";
47
47
  TransportEvent2["ShutdownComplete"] = "shutdownComplete";
48
48
  TransportEvent2["DeadLetter"] = "deadLetter";
49
+ TransportEvent2["ConsumerRecovered"] = "consumerRecovered";
50
+ TransportEvent2["HandlerCompleted"] = "handlerCompleted";
51
+ TransportEvent2["Published"] = "published";
52
+ TransportEvent2["RpcCompleted"] = "rpcCompleted";
49
53
  return TransportEvent2;
50
54
  })(TransportEvent || {});
51
55
 
@@ -442,6 +446,8 @@ var resolveCaptureBody = (option) => {
442
446
  };
443
447
  };
444
448
  var resolveOtelOptions = (options = {}) => {
449
+ if (options === true) options = {};
450
+ if (options === false) options = { enabled: false };
445
451
  return {
446
452
  enabled: options.enabled ?? true,
447
453
  traces: expandTracesOption(options.traces),
@@ -523,7 +529,7 @@ var extractContext = (ctx, carrier, getter) => propagation.extract(ctx, carrier,
523
529
 
524
530
  // src/otel/tracer.ts
525
531
  import { trace } from "@opentelemetry/api";
526
- var PACKAGE_VERSION = true ? "2.10.0" : "0.0.0";
532
+ var PACKAGE_VERSION = true ? "2.11.1" : "0.0.0";
527
533
  var getTracer = () => trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
528
534
 
529
535
  // src/otel/carrier.ts
@@ -1325,6 +1331,17 @@ var detectEventKind = (pattern) => {
1325
1331
  if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
1326
1332
  return "event" /* Event */;
1327
1333
  };
1334
+ var declaredEventPattern = (pattern) => {
1335
+ if (pattern.startsWith("broadcast:" /* Broadcast */))
1336
+ return pattern.slice("broadcast:" /* Broadcast */.length);
1337
+ if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
1338
+ return pattern;
1339
+ };
1340
+ var eventStreamKind = (kind) => {
1341
+ if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
1342
+ if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
1343
+ return "ev" /* Event */;
1344
+ };
1328
1345
  var JetstreamClient = class extends ClientProxy {
1329
1346
  constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
1330
1347
  super();
@@ -1440,50 +1457,60 @@ var JetstreamClient = class extends ClientProxy {
1440
1457
  const encoded = this.codec.encode(data);
1441
1458
  const effectiveMsgId = messageId ?? nuid.next();
1442
1459
  const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
1443
- await withPublishSpan(
1444
- {
1445
- subject: publishSubject,
1446
- pattern: packet.pattern,
1447
- record,
1448
- kind: detectEventKind(packet.pattern),
1449
- payloadBytes: encoded.length,
1450
- payload: encoded,
1451
- messageId: effectiveMsgId,
1452
- headers: msgHeaders,
1453
- serviceName: this.callerName,
1454
- endpoint: this.serverEndpoint,
1455
- scheduleTarget: schedule ? eventSubject : void 0
1456
- },
1457
- this.otel,
1458
- async () => {
1459
- const warnIfDuplicate = (kindLabel, ack2) => {
1460
- if (ack2.duplicate) {
1461
- this.logger.warn(
1462
- `Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
1463
- );
1460
+ const publishKind = detectEventKind(packet.pattern);
1461
+ const declaredPattern = declaredEventPattern(packet.pattern);
1462
+ const streamKind = eventStreamKind(publishKind);
1463
+ const startedAt = performance.now();
1464
+ try {
1465
+ await withPublishSpan(
1466
+ {
1467
+ subject: publishSubject,
1468
+ pattern: packet.pattern,
1469
+ record,
1470
+ kind: publishKind,
1471
+ payloadBytes: encoded.length,
1472
+ payload: encoded,
1473
+ messageId: effectiveMsgId,
1474
+ headers: msgHeaders,
1475
+ serviceName: this.callerName,
1476
+ endpoint: this.serverEndpoint,
1477
+ scheduleTarget: schedule ? eventSubject : void 0
1478
+ },
1479
+ this.otel,
1480
+ async () => {
1481
+ const warnIfDuplicate = (kindLabel, ack2) => {
1482
+ if (ack2.duplicate) {
1483
+ this.logger.warn(
1484
+ `Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
1485
+ );
1486
+ }
1487
+ };
1488
+ if (schedule) {
1489
+ const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1490
+ headers: msgHeaders,
1491
+ msgID: effectiveMsgId,
1492
+ ttl,
1493
+ schedule: {
1494
+ specification: schedule.at,
1495
+ target: eventSubject
1496
+ }
1497
+ });
1498
+ warnIfDuplicate("scheduled", ack2);
1499
+ return;
1464
1500
  }
1465
- };
1466
- if (schedule) {
1467
- const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1501
+ const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1468
1502
  headers: msgHeaders,
1469
1503
  msgID: effectiveMsgId,
1470
- ttl,
1471
- schedule: {
1472
- specification: schedule.at,
1473
- target: eventSubject
1474
- }
1504
+ ttl
1475
1505
  });
1476
- warnIfDuplicate("scheduled", ack2);
1477
- return;
1506
+ warnIfDuplicate("event", ack);
1478
1507
  }
1479
- const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1480
- headers: msgHeaders,
1481
- msgID: effectiveMsgId,
1482
- ttl
1483
- });
1484
- warnIfDuplicate("event", ack);
1485
- }
1486
- );
1508
+ );
1509
+ this.reportPublished(declaredPattern, streamKind, startedAt, "success");
1510
+ } catch (err) {
1511
+ this.reportPublished(declaredPattern, streamKind, startedAt, "error");
1512
+ throw err;
1513
+ }
1487
1514
  return void 0;
1488
1515
  }
1489
1516
  /**
@@ -1511,14 +1538,17 @@ var JetstreamClient = class extends ClientProxy {
1511
1538
  };
1512
1539
  let jetStreamCorrelationId = null;
1513
1540
  if (this.isCoreMode) {
1514
- this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
1541
+ this.publishCoreRpc(subject, data, hdrs, timeout, callback, packet.pattern).catch(
1542
+ onUnhandled
1543
+ );
1515
1544
  } else {
1516
1545
  jetStreamCorrelationId = nuid.next();
1517
1546
  this.publishJetStreamRpc(subject, data, callback, {
1518
1547
  headers: hdrs,
1519
1548
  timeout,
1520
1549
  correlationId: jetStreamCorrelationId,
1521
- messageId
1550
+ messageId,
1551
+ declaredPattern: packet.pattern
1522
1552
  }).catch(onUnhandled);
1523
1553
  }
1524
1554
  return () => {
@@ -1533,7 +1563,7 @@ var JetstreamClient = class extends ClientProxy {
1533
1563
  };
1534
1564
  }
1535
1565
  /** Core mode: nc.request() with timeout. */
1536
- async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
1566
+ async publishCoreRpc(subject, data, customHeaders, timeout, callback, declaredPattern) {
1537
1567
  const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
1538
1568
  const hdrs = this.buildHeaders(customHeaders, { subject });
1539
1569
  const encoded = this.codec.encode(data);
@@ -1548,6 +1578,7 @@ var JetstreamClient = class extends ClientProxy {
1548
1578
  },
1549
1579
  this.otel
1550
1580
  );
1581
+ const startedAt = performance.now();
1551
1582
  try {
1552
1583
  const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
1553
1584
  const response = await context6.with(
@@ -1560,9 +1591,13 @@ var JetstreamClient = class extends ClientProxy {
1560
1591
  const decoded = this.codec.decode(response.data);
1561
1592
  if (response.headers?.get("x-error" /* Error */)) {
1562
1593
  spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: decoded });
1594
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1595
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1563
1596
  callback({ err: decoded, response: null, isDisposed: true });
1564
1597
  } else {
1565
1598
  spanHandle.finish({ kind: "ok" /* Ok */, reply: decoded });
1599
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1600
+ this.reportRpcCompleted(declaredPattern, startedAt, "success");
1566
1601
  callback({ err: null, response: decoded, isDisposed: true });
1567
1602
  }
1568
1603
  } catch (err) {
@@ -1570,16 +1605,20 @@ var JetstreamClient = class extends ClientProxy {
1570
1605
  if (error instanceof TimeoutError) {
1571
1606
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1572
1607
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, "");
1608
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1609
+ this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1573
1610
  } else {
1574
1611
  spanHandle.finish({ kind: "error" /* Error */, error });
1575
1612
  this.eventBus.emit("error" /* Error */, error, "client-rpc");
1613
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1614
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1576
1615
  }
1577
1616
  callback({ err: error, response: null, isDisposed: true });
1578
1617
  }
1579
1618
  }
1580
1619
  /** JetStream mode: publish to stream + wait for inbox response. */
1581
1620
  async publishJetStreamRpc(subject, data, callback, options) {
1582
- const { headers: customHeaders, correlationId, messageId } = options;
1621
+ const { headers: customHeaders, correlationId, messageId, declaredPattern } = options;
1583
1622
  const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
1584
1623
  const hdrs = this.buildHeaders(customHeaders, {
1585
1624
  subject,
@@ -1600,6 +1639,7 @@ var JetstreamClient = class extends ClientProxy {
1600
1639
  },
1601
1640
  this.otel
1602
1641
  );
1642
+ const startedAt = performance.now();
1603
1643
  this.pendingMessages.set(correlationId, (packet) => {
1604
1644
  if (packet.err) {
1605
1645
  if (packet.err instanceof Error) {
@@ -1607,8 +1647,10 @@ var JetstreamClient = class extends ClientProxy {
1607
1647
  } else {
1608
1648
  spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: packet.err });
1609
1649
  }
1650
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1610
1651
  } else {
1611
1652
  spanHandle.finish({ kind: "ok" /* Ok */, reply: packet.response });
1653
+ this.reportRpcCompleted(declaredPattern, startedAt, "success");
1612
1654
  }
1613
1655
  callback(packet);
1614
1656
  });
@@ -1618,6 +1660,7 @@ var JetstreamClient = class extends ClientProxy {
1618
1660
  this.pendingMessages.delete(correlationId);
1619
1661
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1620
1662
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
1663
+ this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1621
1664
  callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
1622
1665
  }, effectiveTimeout);
1623
1666
  this.pendingTimeouts.set(correlationId, timeoutId);
@@ -1630,6 +1673,8 @@ var JetstreamClient = class extends ClientProxy {
1630
1673
  this.pendingMessages.delete(correlationId);
1631
1674
  const inboxError = new Error("Inbox not initialized");
1632
1675
  spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
1676
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1677
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1633
1678
  callback({
1634
1679
  err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
1635
1680
  response: null,
@@ -1644,6 +1689,7 @@ var JetstreamClient = class extends ClientProxy {
1644
1689
  msgID: messageId ?? nuid.next()
1645
1690
  })
1646
1691
  );
1692
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1647
1693
  } catch (err) {
1648
1694
  const existingTimeout = this.pendingTimeouts.get(correlationId);
1649
1695
  if (existingTimeout) {
@@ -1655,9 +1701,32 @@ var JetstreamClient = class extends ClientProxy {
1655
1701
  const error = err instanceof Error ? err : new Error("Unknown error");
1656
1702
  spanHandle.finish({ kind: "error" /* Error */, error });
1657
1703
  this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
1704
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1705
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1658
1706
  callback({ err: error, response: null, isDisposed: true });
1659
1707
  }
1660
1708
  }
1709
+ // hasHook is per-emit so late subscribers (JetstreamMetricsService during
1710
+ // OnApplicationBootstrap) still receive events.
1711
+ reportPublished(declaredPattern, kind, startedAt, status) {
1712
+ if (!this.eventBus.hasHook("published" /* Published */)) return;
1713
+ this.eventBus.emit(
1714
+ "published" /* Published */,
1715
+ declaredPattern,
1716
+ kind,
1717
+ performance.now() - startedAt,
1718
+ status
1719
+ );
1720
+ }
1721
+ reportRpcCompleted(declaredPattern, startedAt, status) {
1722
+ if (!this.eventBus.hasHook("rpcCompleted" /* RpcCompleted */)) return;
1723
+ this.eventBus.emit(
1724
+ "rpcCompleted" /* RpcCompleted */,
1725
+ declaredPattern,
1726
+ performance.now() - startedAt,
1727
+ status
1728
+ );
1729
+ }
1661
1730
  /** Fail-fast all pending JetStream RPC callbacks on connection loss. */
1662
1731
  handleDisconnect() {
1663
1732
  this.rejectPendingRpcs(new Error("Connection lost"));
@@ -2068,43 +2137,53 @@ var ConnectionProvider = class {
2068
2137
  var EventBus = class {
2069
2138
  hooks;
2070
2139
  logger;
2140
+ subscribers = /* @__PURE__ */ new Map();
2071
2141
  constructor(logger5, hooks) {
2072
2142
  this.logger = logger5;
2073
2143
  this.hooks = hooks ?? {};
2074
2144
  }
2075
2145
  /**
2076
- * Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
2077
- *
2078
- * @param event - The {@link TransportEvent} to emit.
2079
- * @param args - Arguments matching the hook signature for this event.
2146
+ * Subscribe to a transport event. Used by built-in observers (e.g. metrics).
2147
+ * Multiple subscribers per event are supported; each is called independently.
2148
+ */
2149
+ subscribe(event, handler) {
2150
+ const list = this.subscribers.get(event) ?? [];
2151
+ list.push(handler);
2152
+ this.subscribers.set(event, list);
2153
+ }
2154
+ /**
2155
+ * Emit a lifecycle event. Dispatches to all internal subscribers and the
2156
+ * registered user hook (if any).
2080
2157
  */
2081
2158
  emit(event, ...args) {
2082
- const hook = this.hooks[event];
2083
- if (!hook) return;
2084
- this.callHook(event, hook, ...args);
2159
+ this.dispatch(event, args);
2085
2160
  }
2086
2161
  /**
2087
2162
  * Hot-path optimized emit for MessageRouted events.
2088
2163
  * Avoids rest/spread overhead of the generic `emit()`.
2089
2164
  */
2090
2165
  emitMessageRouted(subject, kind) {
2091
- const hook = this.hooks["messageRouted" /* MessageRouted */];
2092
- if (!hook) return;
2093
- this.callHook(
2094
- "messageRouted" /* MessageRouted */,
2095
- hook,
2096
- subject,
2097
- kind
2098
- );
2166
+ this.dispatch("messageRouted" /* MessageRouted */, [subject, kind]);
2099
2167
  }
2100
2168
  /**
2101
- * Check whether a hook is registered for the given event.
2102
- *
2103
- * Used by the routing hot path to elide the emit call entirely when the
2104
- * transport owner did not register a listener.
2169
+ * Check whether any listener (user hook or internal subscriber) is registered
2170
+ * for the given event. Used by routing hot path to elide the emit call when
2171
+ * no one is listening.
2105
2172
  */
2106
2173
  hasHook(event) {
2107
- return this.hooks[event] !== void 0;
2174
+ return this.hooks[event] !== void 0 || (this.subscribers.get(event)?.length ?? 0) > 0;
2175
+ }
2176
+ dispatch(event, args) {
2177
+ const subs = this.subscribers.get(event);
2178
+ if (subs?.length) {
2179
+ for (const sub of [...subs]) {
2180
+ this.callHook(event, sub, ...args);
2181
+ }
2182
+ }
2183
+ const hook = this.hooks[event];
2184
+ if (hook) {
2185
+ this.callHook(event, hook, ...args);
2186
+ }
2108
2187
  }
2109
2188
  callHook(event, hook, ...args) {
2110
2189
  try {
@@ -2183,12 +2262,697 @@ var JetstreamHealthIndicator = class {
2183
2262
  isHealthCheckError: true
2184
2263
  });
2185
2264
  }
2186
- return { [key]: details };
2265
+ return { [key]: details };
2266
+ }
2267
+ };
2268
+ JetstreamHealthIndicator = __decorateClass([
2269
+ Injectable()
2270
+ ], JetstreamHealthIndicator);
2271
+
2272
+ // src/metrics/metrics.module.ts
2273
+ import { Module } from "@nestjs/common";
2274
+
2275
+ // src/server/routing/pattern-registry.ts
2276
+ import { Logger as Logger8 } from "@nestjs/common";
2277
+ var HANDLER_LABELS = {
2278
+ ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
2279
+ ["ordered" /* Ordered */]: "ordered" /* Ordered */,
2280
+ ["ev" /* Event */]: "event" /* Event */,
2281
+ ["cmd" /* Command */]: "rpc" /* Rpc */
2282
+ };
2283
+ var PatternRegistry = class {
2284
+ constructor(options) {
2285
+ this.options = options;
2286
+ }
2287
+ logger = new Logger8("Jetstream:PatternRegistry");
2288
+ registry = /* @__PURE__ */ new Map();
2289
+ // Cached after registerHandlers() — the registry is immutable from that point
2290
+ cachedPatterns = null;
2291
+ _hasEvents = false;
2292
+ _hasCommands = false;
2293
+ _hasBroadcasts = false;
2294
+ _hasOrdered = false;
2295
+ _hasMetadata = false;
2296
+ /**
2297
+ * Register all handlers from the NestJS strategy.
2298
+ *
2299
+ * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
2300
+ */
2301
+ registerHandlers(handlers) {
2302
+ const serviceName = this.options.name;
2303
+ for (const [pattern, handler] of handlers) {
2304
+ const extras = handler.extras;
2305
+ const isEvent = handler.isEventHandler ?? false;
2306
+ const isBroadcast = !!extras?.broadcast;
2307
+ const isOrdered = !!extras?.ordered;
2308
+ const meta = extras?.meta;
2309
+ if (isBroadcast && isOrdered) {
2310
+ throw new Error(
2311
+ `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
2312
+ );
2313
+ }
2314
+ let kind;
2315
+ if (isBroadcast) kind = "broadcast" /* Broadcast */;
2316
+ else if (isOrdered) kind = "ordered" /* Ordered */;
2317
+ else if (isEvent) kind = "ev" /* Event */;
2318
+ else kind = "cmd" /* Command */;
2319
+ const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
2320
+ this.registry.set(fullSubject, {
2321
+ handler,
2322
+ pattern,
2323
+ isEvent: isEvent && !isOrdered,
2324
+ isBroadcast,
2325
+ isOrdered,
2326
+ meta
2327
+ });
2328
+ this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
2329
+ }
2330
+ this.cachedPatterns = this.buildPatternsByKind();
2331
+ this._hasEvents = this.cachedPatterns.events.length > 0;
2332
+ this._hasCommands = this.cachedPatterns.commands.length > 0;
2333
+ this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
2334
+ this._hasOrdered = this.cachedPatterns.ordered.length > 0;
2335
+ this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
2336
+ this.logSummary();
2337
+ }
2338
+ /** Find handler for a full NATS subject. */
2339
+ getHandler(subject) {
2340
+ return this.registry.get(subject)?.handler ?? null;
2341
+ }
2342
+ /**
2343
+ * Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
2344
+ *
2345
+ * Returns `null` when the subject is not registered. The declared pattern is
2346
+ * the value the user passed to `@EventPattern`/`@MessagePattern` — stable and
2347
+ * bounded, suitable for use as a Prometheus label without cardinality risk.
2348
+ */
2349
+ resolveDeclared(subject) {
2350
+ const entry = this.registry.get(subject);
2351
+ if (!entry) return null;
2352
+ return { pattern: entry.pattern, kind: this.resolveStreamKind(entry) };
2353
+ }
2354
+ /** Get all registered broadcast patterns (for consumer filter_subject setup). */
2355
+ getBroadcastPatterns() {
2356
+ return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
2357
+ }
2358
+ hasBroadcastHandlers() {
2359
+ return this._hasBroadcasts;
2360
+ }
2361
+ hasRpcHandlers() {
2362
+ return this._hasCommands;
2363
+ }
2364
+ hasEventHandlers() {
2365
+ return this._hasEvents;
2366
+ }
2367
+ hasOrderedHandlers() {
2368
+ return this._hasOrdered;
2369
+ }
2370
+ /** Get fully-qualified NATS subjects for ordered handlers. */
2371
+ getOrderedSubjects() {
2372
+ return this.getPatternsByKind().ordered.map(
2373
+ (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2374
+ );
2375
+ }
2376
+ /** Check if any registered handler has metadata. */
2377
+ hasMetadata() {
2378
+ return this._hasMetadata;
2379
+ }
2380
+ /**
2381
+ * Get handler metadata entries for KV publishing.
2382
+ *
2383
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
2384
+ * Key format: `{serviceName}.{kind}.{pattern}`.
2385
+ */
2386
+ getMetadataEntries() {
2387
+ const entries = /* @__PURE__ */ new Map();
2388
+ for (const entry of this.registry.values()) {
2389
+ if (!entry.meta) continue;
2390
+ const kind = this.resolveStreamKind(entry);
2391
+ const key = metadataKey(this.options.name, kind, entry.pattern);
2392
+ entries.set(key, entry.meta);
2393
+ }
2394
+ return entries;
2395
+ }
2396
+ /** Get patterns grouped by kind (cached after registration). */
2397
+ getPatternsByKind() {
2398
+ const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
2399
+ return {
2400
+ events: [...patterns.events],
2401
+ commands: [...patterns.commands],
2402
+ broadcasts: [...patterns.broadcasts],
2403
+ ordered: [...patterns.ordered]
2404
+ };
2405
+ }
2406
+ /** Normalize a full NATS subject back to the user-facing pattern. */
2407
+ normalizeSubject(subject) {
2408
+ const name = internalName(this.options.name);
2409
+ const prefixes = [
2410
+ `${name}.${"cmd" /* Command */}.`,
2411
+ `${name}.${"ev" /* Event */}.`,
2412
+ `${name}.${"ordered" /* Ordered */}.`,
2413
+ `${"broadcast" /* Broadcast */}.`
2414
+ ];
2415
+ for (const prefix of prefixes) {
2416
+ if (subject.startsWith(prefix)) {
2417
+ return subject.slice(prefix.length);
2418
+ }
2419
+ }
2420
+ return subject;
2421
+ }
2422
+ buildPatternsByKind() {
2423
+ const events = [];
2424
+ const commands = [];
2425
+ const broadcasts = [];
2426
+ const ordered = [];
2427
+ for (const entry of this.registry.values()) {
2428
+ if (entry.isBroadcast) broadcasts.push(entry.pattern);
2429
+ else if (entry.isOrdered) ordered.push(entry.pattern);
2430
+ else if (entry.isEvent) events.push(entry.pattern);
2431
+ else commands.push(entry.pattern);
2432
+ }
2433
+ return { events, commands, broadcasts, ordered };
2434
+ }
2435
+ resolveStreamKind(entry) {
2436
+ if (entry.isBroadcast) return "broadcast" /* Broadcast */;
2437
+ if (entry.isOrdered) return "ordered" /* Ordered */;
2438
+ if (entry.isEvent) return "ev" /* Event */;
2439
+ return "cmd" /* Command */;
2440
+ }
2441
+ logSummary() {
2442
+ const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
2443
+ const parts = [
2444
+ `${commands.length} RPC`,
2445
+ `${events.length} events`,
2446
+ `${broadcasts.length} broadcasts`
2447
+ ];
2448
+ if (ordered.length > 0) {
2449
+ parts.push(`${ordered.length} ordered`);
2450
+ }
2451
+ this.logger.log(`Registered handlers: ${parts.join(", ")}`);
2452
+ }
2453
+ };
2454
+
2455
+ // src/metrics/metrics.constants.ts
2456
+ var JETSTREAM_METRICS_CONFIG = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_CONFIG");
2457
+ var JETSTREAM_METRICS_REGISTRY = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_REGISTRY");
2458
+ var JETSTREAM_METRICS_PROM_CLIENT = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_PROM_CLIENT");
2459
+ var DEFAULT_METRICS_PREFIX = "jetstream_";
2460
+ var DEFAULT_POLL_INTERVAL_MS = 15e3;
2461
+ var DEFAULT_HISTOGRAM_BUCKETS = {
2462
+ handlerDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
2463
+ publishDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
2464
+ rpcDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
2465
+ };
2466
+ var ERROR_CONTEXT_PREFIXES = [
2467
+ ["connection", "connection"],
2468
+ ["codec", "codec"],
2469
+ ["client-rpc", "publish"],
2470
+ ["jetstream-rpc-publish", "publish"],
2471
+ ["publish", "publish"],
2472
+ ["message-provider", "consume"],
2473
+ ["consume", "consume"],
2474
+ ["core-rpc-handler", "handler"],
2475
+ ["rpc-handler", "handler"],
2476
+ // EventRouter formats contexts as `${StreamKind.*}-handler:...` — the enum
2477
+ // uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
2478
+ ["ev-handler", "handler"],
2479
+ ["event-handler", "handler"],
2480
+ ["broadcast-handler", "handler"],
2481
+ ["ordered-handler", "handler"],
2482
+ ["handler", "handler"],
2483
+ ["shutdown", "shutdown"]
2484
+ ];
2485
+ var UNMATCHED_SUBJECT_LABEL = "<unmatched>";
2486
+ var STREAM_KIND_LABEL = {
2487
+ ["ev" /* Event */]: "event",
2488
+ ["cmd" /* Command */]: "command",
2489
+ ["broadcast" /* Broadcast */]: "broadcast",
2490
+ ["ordered" /* Ordered */]: "ordered"
2491
+ };
2492
+
2493
+ // src/metrics/metrics.service.ts
2494
+ import {
2495
+ Inject,
2496
+ Injectable as Injectable2,
2497
+ Logger as Logger10,
2498
+ Optional
2499
+ } from "@nestjs/common";
2500
+
2501
+ // src/metrics/error-context-mapper.ts
2502
+ var mapErrorContext = (context7) => {
2503
+ if (!context7) return "other";
2504
+ for (const [prefix, mapped] of ERROR_CONTEXT_PREFIXES) {
2505
+ if (context7 === prefix || context7.startsWith(`${prefix}:`)) {
2506
+ return mapped;
2507
+ }
2508
+ }
2509
+ return "other";
2510
+ };
2511
+
2512
+ // src/metrics/metrics.factory.ts
2513
+ var createMetrics = (opts) => {
2514
+ const { register, promClient } = opts;
2515
+ const prefix = opts.prefix ?? DEFAULT_METRICS_PREFIX;
2516
+ const buckets = {
2517
+ handlerDuration: opts.buckets?.handlerDuration ?? DEFAULT_HISTOGRAM_BUCKETS.handlerDuration,
2518
+ publishDuration: opts.buckets?.publishDuration ?? DEFAULT_HISTOGRAM_BUCKETS.publishDuration,
2519
+ rpcDuration: opts.buckets?.rpcDuration ?? DEFAULT_HISTOGRAM_BUCKETS.rpcDuration
2520
+ };
2521
+ if (opts.defaultLabels && Object.keys(opts.defaultLabels).length > 0) {
2522
+ register.setDefaultLabels(opts.defaultLabels);
2523
+ }
2524
+ const counter = (name, help, labelNames) => new promClient.Counter({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
2525
+ const histogram = (name, help, labelNames, bucketArr) => new promClient.Histogram({
2526
+ name: `${prefix}${name}`,
2527
+ help,
2528
+ labelNames,
2529
+ buckets: bucketArr,
2530
+ registers: [register]
2531
+ });
2532
+ const gauge = (name, help, labelNames) => new promClient.Gauge({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
2533
+ return {
2534
+ messagesReceivedTotal: counter(
2535
+ "messages_received_total",
2536
+ "Total messages routed to a handler.",
2537
+ ["stream", "subject", "kind"]
2538
+ ),
2539
+ messagesProcessedTotal: counter(
2540
+ "messages_processed_total",
2541
+ "Total messages whose handler completed.",
2542
+ ["stream", "subject", "kind", "status"]
2543
+ ),
2544
+ messagesUnhandledTotal: counter(
2545
+ "messages_unhandled_total",
2546
+ "Messages received but not matching any registered handler.",
2547
+ ["subject"]
2548
+ ),
2549
+ messagesDeadLetterTotal: counter(
2550
+ "messages_dead_letter_total",
2551
+ "Messages routed to dead-letter after exhausting redelivery attempts.",
2552
+ ["stream", "subject"]
2553
+ ),
2554
+ publishTotal: counter(
2555
+ "publish_total",
2556
+ "Total publish/send operations performed by the client.",
2557
+ ["subject", "kind", "status"]
2558
+ ),
2559
+ rpcTimeoutTotal: counter("rpc_timeout_total", "RPC calls that exceeded the timeout deadline.", [
2560
+ "subject"
2561
+ ]),
2562
+ consumerRecoveredTotal: counter(
2563
+ "consumer_recovered_total",
2564
+ "Self-healing recoveries after consume-loop failures.",
2565
+ ["kind"]
2566
+ ),
2567
+ errorsTotal: counter("errors_total", "Transport-level errors emitted on the EventBus.", [
2568
+ "context"
2569
+ ]),
2570
+ handlerDurationSeconds: histogram(
2571
+ "handler_duration_seconds",
2572
+ "Wall-clock duration of handler execution.",
2573
+ ["stream", "subject", "kind", "status"],
2574
+ buckets.handlerDuration
2575
+ ),
2576
+ publishDurationSeconds: histogram(
2577
+ "publish_duration_seconds",
2578
+ "Wall-clock duration of client publish/send operations.",
2579
+ ["subject", "kind", "status"],
2580
+ buckets.publishDuration
2581
+ ),
2582
+ rpcDurationSeconds: histogram(
2583
+ "rpc_duration_seconds",
2584
+ "Wall-clock duration of RPC round-trips from client perspective.",
2585
+ ["subject", "status"],
2586
+ buckets.rpcDuration
2587
+ ),
2588
+ consumerNumPending: gauge(
2589
+ "consumer_num_pending",
2590
+ "Messages not yet delivered to this consumer.",
2591
+ ["stream", "consumer", "kind"]
2592
+ ),
2593
+ consumerNumAckPending: gauge(
2594
+ "consumer_num_ack_pending",
2595
+ "Messages delivered but not yet acked.",
2596
+ ["stream", "consumer", "kind"]
2597
+ ),
2598
+ consumerNumRedelivered: gauge(
2599
+ "consumer_num_redelivered",
2600
+ "Messages currently in redelivery state.",
2601
+ ["stream", "consumer", "kind"]
2602
+ ),
2603
+ consumerNumWaiting: gauge(
2604
+ "consumer_num_waiting",
2605
+ "Pull-request waiting count for this consumer.",
2606
+ ["stream", "consumer", "kind"]
2607
+ ),
2608
+ streamMessages: gauge("stream_messages", "Total messages stored in this stream.", ["stream"]),
2609
+ streamBytes: gauge("stream_bytes", "Total bytes stored in this stream.", ["stream"]),
2610
+ connectionUp: gauge("connection_up", "NATS connection state (1 connected, 0 disconnected).", [
2611
+ "server"
2612
+ ]),
2613
+ metricsPollErrorsTotal: counter(
2614
+ "metrics_poll_errors_total",
2615
+ "Errors encountered while polling JetStreamManager for gauge data.",
2616
+ ["target"]
2617
+ )
2618
+ };
2619
+ };
2620
+
2621
+ // src/metrics/poll-runner.ts
2622
+ import { Logger as Logger9 } from "@nestjs/common";
2623
+ var PollRunner = class {
2624
+ constructor(opts) {
2625
+ this.opts = opts;
2626
+ }
2627
+ logger = new Logger9("Jetstream:Metrics:Poll");
2628
+ timer = null;
2629
+ inFlight = null;
2630
+ start() {
2631
+ if (this.timer !== null) return;
2632
+ if (this.opts.intervalMs <= 0) return;
2633
+ if (this.opts.targets.length === 0) return;
2634
+ this.timer = setInterval(() => {
2635
+ if (this.inFlight !== null) {
2636
+ this.logger.warn("Skipping poll tick \u2014 previous cycle still in flight");
2637
+ return;
2638
+ }
2639
+ this.inFlight = this.tick().finally(() => {
2640
+ this.inFlight = null;
2641
+ });
2642
+ }, this.opts.intervalMs);
2643
+ }
2644
+ async stop() {
2645
+ if (this.timer !== null) {
2646
+ clearInterval(this.timer);
2647
+ this.timer = null;
2648
+ }
2649
+ if (this.inFlight !== null) await this.inFlight;
2650
+ }
2651
+ /** @internal Visible for tests. Runs one poll cycle. */
2652
+ async tick() {
2653
+ let jsm;
2654
+ try {
2655
+ jsm = await this.opts.jsmFactory();
2656
+ } catch {
2657
+ this.recordPollError("jsm.connect");
2658
+ return;
2659
+ }
2660
+ await Promise.all([this.pollConsumers(jsm), this.pollStreams(jsm)]);
2661
+ }
2662
+ async pollConsumers(jsm) {
2663
+ for (const target of this.opts.targets) {
2664
+ try {
2665
+ const info = await jsm.consumers.info(target.stream, target.consumer);
2666
+ const labels = {
2667
+ stream: target.stream,
2668
+ consumer: target.consumer,
2669
+ kind: STREAM_KIND_LABEL[target.kind]
2670
+ };
2671
+ this.opts.metrics.consumerNumPending.labels(labels).set(info.num_pending);
2672
+ this.opts.metrics.consumerNumAckPending.labels(labels).set(info.num_ack_pending);
2673
+ this.opts.metrics.consumerNumRedelivered.labels(labels).set(info.num_redelivered);
2674
+ this.opts.metrics.consumerNumWaiting.labels(labels).set(info.num_waiting);
2675
+ } catch {
2676
+ this.recordPollError("consumer.info");
2677
+ }
2678
+ }
2679
+ }
2680
+ async pollStreams(jsm) {
2681
+ const uniqueStreams = new Set(this.opts.targets.map((t) => t.stream));
2682
+ for (const stream of uniqueStreams) {
2683
+ try {
2684
+ const info = await jsm.streams.info(stream);
2685
+ this.opts.metrics.streamMessages.labels({ stream }).set(info.state.messages);
2686
+ this.opts.metrics.streamBytes.labels({ stream }).set(info.state.bytes);
2687
+ } catch {
2688
+ this.recordPollError("stream.info");
2689
+ }
2690
+ }
2691
+ }
2692
+ recordPollError(target) {
2693
+ this.opts.metrics.metricsPollErrorsTotal.labels({ target }).inc();
2694
+ }
2695
+ };
2696
+
2697
+ // src/metrics/metrics.service.ts
2698
+ var JetstreamMetricsService = class {
2699
+ constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
2700
+ this.eventBus = eventBus;
2701
+ this.config = config;
2702
+ this.promClient = promClient;
2703
+ this.options = options;
2704
+ this.patternRegistry = patternRegistry;
2705
+ this.connection = connection;
2706
+ }
2707
+ logger = new Logger10("Jetstream:Metrics");
2708
+ metrics = null;
2709
+ pollRunner = null;
2710
+ activeServers = /* @__PURE__ */ new Set();
2711
+ async onApplicationBootstrap() {
2712
+ if (this.metrics !== null) return;
2713
+ if (!this.options.metrics || !this.config || !this.promClient) return;
2714
+ if (!this.config.register) {
2715
+ throw new Error(
2716
+ "JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
2717
+ );
2718
+ }
2719
+ this.metrics = createMetrics({
2720
+ register: this.config.register,
2721
+ promClient: this.promClient,
2722
+ prefix: this.config.prefix,
2723
+ defaultLabels: this.config.defaultLabels,
2724
+ buckets: this.config.buckets
2725
+ });
2726
+ this.subscribeToEvents();
2727
+ this.syncInitialConnectionState();
2728
+ this.startPolling();
2729
+ this.logger.log(
2730
+ `Metrics enabled (prefix=${this.config.prefix ?? DEFAULT_METRICS_PREFIX}, poll=${this.getEffectivePollInterval()}ms)`
2731
+ );
2732
+ }
2733
+ async onModuleDestroy() {
2734
+ await this.pollRunner?.stop();
2735
+ this.pollRunner = null;
2736
+ }
2737
+ /** @internal Visible for tests. `0` disables polling. */
2738
+ getEffectivePollInterval() {
2739
+ return this.config?.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
2740
+ }
2741
+ /**
2742
+ * NATS connects during early bootstrap, before this service subscribes to
2743
+ * the EventBus — the initial `Connect` emission misses us. Mirror the
2744
+ * current state here so `connection_up` reflects reality the moment metrics
2745
+ * come online; later disconnects/reconnects update it normally.
2746
+ */
2747
+ syncInitialConnectionState() {
2748
+ const nc = this.connection?.unwrap;
2749
+ if (!nc) return;
2750
+ const server = nc.getServer();
2751
+ this.activeServers.add(server);
2752
+ this.metrics?.connectionUp.labels({ server }).set(1);
2753
+ }
2754
+ /** Skips polling for publisher-only deployments and when no kinds are active. */
2755
+ startPolling() {
2756
+ const interval = this.getEffectivePollInterval();
2757
+ const connection = this.connection;
2758
+ if (interval <= 0 || !this.patternRegistry || !connection || !this.metrics) return;
2759
+ const targets = this.buildPollTargets();
2760
+ if (targets.length === 0) return;
2761
+ this.pollRunner = new PollRunner({
2762
+ intervalMs: interval,
2763
+ jsmFactory: async () => connection.getJetStreamManager(),
2764
+ metrics: this.metrics,
2765
+ targets
2766
+ });
2767
+ this.pollRunner.start();
2768
+ }
2769
+ buildPollTargets() {
2770
+ const registry = this.patternRegistry;
2771
+ if (!registry) return [];
2772
+ const targets = [];
2773
+ if (registry.hasEventHandlers()) {
2774
+ targets.push({
2775
+ kind: "ev" /* Event */,
2776
+ stream: streamName(this.options.name, "ev" /* Event */),
2777
+ consumer: consumerName(this.options.name, "ev" /* Event */)
2778
+ });
2779
+ }
2780
+ if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
2781
+ targets.push({
2782
+ kind: "cmd" /* Command */,
2783
+ stream: streamName(this.options.name, "cmd" /* Command */),
2784
+ consumer: consumerName(this.options.name, "cmd" /* Command */)
2785
+ });
2786
+ }
2787
+ if (registry.hasBroadcastHandlers()) {
2788
+ targets.push({
2789
+ kind: "broadcast" /* Broadcast */,
2790
+ stream: streamName(this.options.name, "broadcast" /* Broadcast */),
2791
+ consumer: consumerName(this.options.name, "broadcast" /* Broadcast */)
2792
+ });
2793
+ }
2794
+ return targets;
2795
+ }
2796
+ subscribeToEvents() {
2797
+ this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
2798
+ this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
2799
+ this.eventBus.subscribe("reconnect" /* Reconnect */, this.onReconnect);
2800
+ this.eventBus.subscribe("error" /* Error */, this.onError);
2801
+ this.eventBus.subscribe("rpcTimeout" /* RpcTimeout */, this.onRpcTimeout);
2802
+ this.eventBus.subscribe("messageRouted" /* MessageRouted */, this.onMessageRouted);
2803
+ this.eventBus.subscribe("deadLetter" /* DeadLetter */, this.onDeadLetter);
2804
+ this.eventBus.subscribe("consumerRecovered" /* ConsumerRecovered */, this.onConsumerRecovered);
2805
+ this.eventBus.subscribe("handlerCompleted" /* HandlerCompleted */, this.onHandlerCompleted);
2806
+ this.eventBus.subscribe("published" /* Published */, this.onPublished);
2807
+ this.eventBus.subscribe("rpcCompleted" /* RpcCompleted */, this.onRpcCompleted);
2808
+ }
2809
+ onConnect = (server) => {
2810
+ this.activeServers.add(server);
2811
+ this.metrics?.connectionUp.labels({ server }).set(1);
2812
+ };
2813
+ onReconnect = (server) => {
2814
+ this.activeServers.add(server);
2815
+ this.metrics?.connectionUp.labels({ server }).set(1);
2816
+ };
2817
+ onDisconnect = () => {
2818
+ for (const server of this.activeServers) {
2819
+ this.metrics?.connectionUp.labels({ server }).set(0);
2820
+ }
2821
+ };
2822
+ onError = (_err, context7) => {
2823
+ this.metrics?.errorsTotal.labels({ context: mapErrorContext(context7) }).inc();
2824
+ };
2825
+ onRpcTimeout = (subject, _correlationId) => {
2826
+ const declared = this.resolveDeclared(subject);
2827
+ const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2828
+ this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
2829
+ };
2830
+ // `_kind` collapses broadcast/ordered into MessageKind.Event — we use
2831
+ // declared.kind from PatternRegistry for the precise label instead.
2832
+ onMessageRouted = (subject, _kind) => {
2833
+ if (!this.metrics) return;
2834
+ const declared = this.resolveDeclared(subject);
2835
+ if (!declared) {
2836
+ this.metrics.messagesUnhandledTotal.labels({ subject: UNMATCHED_SUBJECT_LABEL }).inc();
2837
+ return;
2838
+ }
2839
+ this.metrics.messagesReceivedTotal.labels({
2840
+ stream: streamName(this.options.name, declared.kind),
2841
+ subject: declared.pattern,
2842
+ kind: STREAM_KIND_LABEL[declared.kind]
2843
+ }).inc();
2844
+ };
2845
+ onDeadLetter = (info) => {
2846
+ const declared = this.resolveDeclared(info.subject);
2847
+ const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2848
+ this.metrics?.messagesDeadLetterTotal.labels({ stream: info.stream, subject: subjectLabel }).inc();
2849
+ };
2850
+ onConsumerRecovered = (label, _attempts) => {
2851
+ const kindLabel = STREAM_KIND_LABEL[label] ?? String(label);
2852
+ this.metrics?.consumerRecoveredTotal.labels({ kind: kindLabel }).inc();
2853
+ };
2854
+ onHandlerCompleted = (pattern, kind, durationMs, status) => {
2855
+ if (!this.metrics) return;
2856
+ const stream = streamName(this.options.name, kind);
2857
+ const kindLabel = STREAM_KIND_LABEL[kind];
2858
+ const labels = { stream, subject: pattern, kind: kindLabel, status };
2859
+ this.metrics.messagesProcessedTotal.labels(labels).inc();
2860
+ this.metrics.handlerDurationSeconds.labels(labels).observe(durationMs / 1e3);
2861
+ };
2862
+ onPublished = (pattern, kind, durationMs, status) => {
2863
+ if (!this.metrics) return;
2864
+ const labels = { subject: pattern, kind: STREAM_KIND_LABEL[kind], status };
2865
+ this.metrics.publishTotal.labels(labels).inc();
2866
+ this.metrics.publishDurationSeconds.labels(labels).observe(durationMs / 1e3);
2867
+ };
2868
+ onRpcCompleted = (pattern, durationMs, status) => {
2869
+ this.metrics?.rpcDurationSeconds.labels({ subject: pattern, status }).observe(durationMs / 1e3);
2870
+ };
2871
+ resolveDeclared(subject) {
2872
+ return this.patternRegistry?.resolveDeclared(subject) ?? null;
2187
2873
  }
2188
2874
  };
2189
- JetstreamHealthIndicator = __decorateClass([
2190
- Injectable()
2191
- ], JetstreamHealthIndicator);
2875
+ JetstreamMetricsService = __decorateClass([
2876
+ Injectable2(),
2877
+ __decorateParam(1, Inject(JETSTREAM_METRICS_CONFIG)),
2878
+ __decorateParam(2, Inject(JETSTREAM_METRICS_PROM_CLIENT)),
2879
+ __decorateParam(3, Inject(JETSTREAM_OPTIONS)),
2880
+ __decorateParam(4, Optional()),
2881
+ __decorateParam(5, Optional()),
2882
+ __decorateParam(5, Inject(JETSTREAM_CONNECTION))
2883
+ ], JetstreamMetricsService);
2884
+
2885
+ // src/metrics/metrics.module.ts
2886
+ var PROM_CLIENT_INSTALL_MESSAGE = "prom-client is required when JetstreamModule.forRoot({ metrics: ... }) is enabled. Install it with: pnpm add prom-client";
2887
+ var resolvePromClient = async () => {
2888
+ try {
2889
+ return await import("prom-client");
2890
+ } catch {
2891
+ throw new Error(PROM_CLIENT_INSTALL_MESSAGE);
2892
+ }
2893
+ };
2894
+ var normalizeMetricsConfig = (option, promClient) => {
2895
+ const user = option && option !== true ? option : {};
2896
+ return {
2897
+ register: user.register ?? promClient.register,
2898
+ prefix: user.prefix ?? DEFAULT_METRICS_PREFIX,
2899
+ defaultLabels: user.defaultLabels,
2900
+ pollInterval: user.pollInterval ?? DEFAULT_POLL_INTERVAL_MS,
2901
+ buckets: user.buckets
2902
+ };
2903
+ };
2904
+ var JetstreamMetricsModule = class {
2905
+ static forFeature() {
2906
+ const promClientProvider = {
2907
+ provide: JETSTREAM_METRICS_PROM_CLIENT,
2908
+ inject: [JETSTREAM_OPTIONS],
2909
+ useFactory: async (opts) => {
2910
+ if (!opts.metrics) return null;
2911
+ const mod = await resolvePromClient();
2912
+ return { Counter: mod.Counter, Histogram: mod.Histogram, Gauge: mod.Gauge };
2913
+ }
2914
+ };
2915
+ const configProvider = {
2916
+ provide: JETSTREAM_METRICS_CONFIG,
2917
+ inject: [JETSTREAM_OPTIONS],
2918
+ useFactory: async (opts) => {
2919
+ if (!opts.metrics) return null;
2920
+ const mod = await resolvePromClient();
2921
+ return normalizeMetricsConfig(opts.metrics, mod);
2922
+ }
2923
+ };
2924
+ const registryProvider = {
2925
+ provide: JETSTREAM_METRICS_REGISTRY,
2926
+ inject: [JETSTREAM_METRICS_CONFIG],
2927
+ useFactory: (cfg) => cfg?.register ?? null
2928
+ };
2929
+ const serviceProvider = {
2930
+ provide: JetstreamMetricsService,
2931
+ inject: [
2932
+ JETSTREAM_EVENT_BUS,
2933
+ JETSTREAM_METRICS_CONFIG,
2934
+ JETSTREAM_METRICS_PROM_CLIENT,
2935
+ JETSTREAM_OPTIONS,
2936
+ { token: PatternRegistry, optional: true },
2937
+ { token: JETSTREAM_CONNECTION, optional: true }
2938
+ ],
2939
+ useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(eventBus, cfg, runtime, opts, patternRegistry, connection)
2940
+ };
2941
+ return {
2942
+ module: JetstreamMetricsModule,
2943
+ providers: [promClientProvider, configProvider, registryProvider, serviceProvider],
2944
+ exports: [
2945
+ JetstreamMetricsService,
2946
+ JETSTREAM_METRICS_CONFIG,
2947
+ JETSTREAM_METRICS_REGISTRY,
2948
+ JETSTREAM_METRICS_PROM_CLIENT
2949
+ ]
2950
+ };
2951
+ }
2952
+ };
2953
+ JetstreamMetricsModule = __decorateClass([
2954
+ Module({})
2955
+ ], JetstreamMetricsModule);
2192
2956
 
2193
2957
  // src/server/strategy.ts
2194
2958
  import { Server } from "@nestjs/microservices";
@@ -2264,6 +3028,26 @@ var JetstreamStrategy = class extends Server {
2264
3028
  this.messageProvider.destroy();
2265
3029
  this.started = false;
2266
3030
  }
3031
+ /**
3032
+ * Override NestJS `Server.addHandler` to fail-fast on duplicate pattern registration.
3033
+ *
3034
+ * The base class silently overwrites duplicate RPC handlers (last wins) and appends
3035
+ * duplicate event handlers to a linked list. Both behaviors are hazardous in a
3036
+ * JetStream context: silent overwrite drops a handler the user wrote, and double
3037
+ * event dispatch double-acks/double-processes the same JetStream message.
3038
+ *
3039
+ * We treat any pattern collision as a fatal misconfiguration so it surfaces at
3040
+ * bootstrap instead of in production traffic.
3041
+ */
3042
+ addHandler(pattern, callback, isEventHandler = false, extras = {}) {
3043
+ const normalizedPattern = this.normalizePattern(pattern);
3044
+ if (this.messageHandlers.has(normalizedPattern)) {
3045
+ throw new Error(
3046
+ `Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice \u2014 find and remove the second declaration.`
3047
+ );
3048
+ }
3049
+ super.addHandler(pattern, callback, isEventHandler, extras);
3050
+ }
2267
3051
  /**
2268
3052
  * Register event listener (required by Server base class).
2269
3053
  *
@@ -2336,7 +3120,7 @@ var JetstreamStrategy = class extends Server {
2336
3120
  };
2337
3121
 
2338
3122
  // src/server/core-rpc.server.ts
2339
- import { Logger as Logger8 } from "@nestjs/common";
3123
+ import { Logger as Logger11 } from "@nestjs/common";
2340
3124
  import { headers as natsHeaders2 } from "@nats-io/transport-node";
2341
3125
 
2342
3126
  // src/context/rpc.context.ts
@@ -2605,7 +3389,7 @@ var CoreRpcServer = class {
2605
3389
  this.serviceName = derived.serviceName;
2606
3390
  this.serverEndpoint = derived.serverEndpoint;
2607
3391
  }
2608
- logger = new Logger8("Jetstream:CoreRpc");
3392
+ logger = new Logger11("Jetstream:CoreRpc");
2609
3393
  subscription = null;
2610
3394
  otel;
2611
3395
  serviceName;
@@ -2658,6 +3442,7 @@ var CoreRpcServer = class {
2658
3442
  return;
2659
3443
  }
2660
3444
  const ctx = new RpcContext([msg]);
3445
+ const startedAt = performance.now();
2661
3446
  try {
2662
3447
  const raw = await withConsumeSpan(
2663
3448
  {
@@ -2676,6 +3461,7 @@ var CoreRpcServer = class {
2676
3461
  }
2677
3462
  );
2678
3463
  msg.respond(this.codec.encode(raw));
3464
+ this.reportHandlerCompleted(msg, startedAt, "success");
2679
3465
  } catch (err) {
2680
3466
  this.eventBus.emit(
2681
3467
  "error" /* Error */,
@@ -2683,7 +3469,23 @@ var CoreRpcServer = class {
2683
3469
  `core-rpc-handler:${msg.subject}`
2684
3470
  );
2685
3471
  this.respondWithError(msg, err);
2686
- }
3472
+ this.reportHandlerCompleted(msg, startedAt, "error");
3473
+ }
3474
+ }
3475
+ // See EventRouter.reportHandlerCompleted for the rationale on declared
3476
+ // pattern + per-emit hasHook check.
3477
+ reportHandlerCompleted(msg, startedAt, status) {
3478
+ if (!this.eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
3479
+ const declared = this.patternRegistry.resolveDeclared(msg.subject);
3480
+ const pattern = declared?.pattern ?? msg.subject;
3481
+ const kind = declared?.kind ?? "cmd" /* Command */;
3482
+ this.eventBus.emit(
3483
+ "handlerCompleted" /* HandlerCompleted */,
3484
+ pattern,
3485
+ kind,
3486
+ performance.now() - startedAt,
3487
+ status
3488
+ );
2687
3489
  }
2688
3490
  /** Send an error response back to the caller with x-error header. */
2689
3491
  respondWithError(msg, error) {
@@ -2698,7 +3500,7 @@ var CoreRpcServer = class {
2698
3500
  };
2699
3501
 
2700
3502
  // src/server/infrastructure/stream.provider.ts
2701
- import { Logger as Logger10 } from "@nestjs/common";
3503
+ import { Logger as Logger13 } from "@nestjs/common";
2702
3504
  import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
2703
3505
 
2704
3506
  // src/server/infrastructure/nats-error-codes.ts
@@ -2765,7 +3567,7 @@ var isEqual = (a, b) => {
2765
3567
  };
2766
3568
 
2767
3569
  // src/server/infrastructure/stream-migration.ts
2768
- import { Logger as Logger9 } from "@nestjs/common";
3570
+ import { Logger as Logger12 } from "@nestjs/common";
2769
3571
  import { JetStreamApiError } from "@nats-io/jetstream";
2770
3572
  var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
2771
3573
  var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
@@ -2774,7 +3576,7 @@ var StreamMigration = class {
2774
3576
  constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
2775
3577
  this.sourcingTimeoutMs = sourcingTimeoutMs;
2776
3578
  }
2777
- logger = new Logger9("Jetstream:Stream");
3579
+ logger = new Logger12("Jetstream:Stream");
2778
3580
  async migrate(jsm, streamName2, newConfig) {
2779
3581
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
2780
3582
  const startTime = Date.now();
@@ -2861,7 +3663,7 @@ var StreamProvider = class {
2861
3663
  this.otelServiceName = derived.serviceName;
2862
3664
  this.otelEndpoint = derived.serverEndpoint;
2863
3665
  }
2864
- logger = new Logger10("Jetstream:Stream");
3666
+ logger = new Logger13("Jetstream:Stream");
2865
3667
  migration = new StreamMigration();
2866
3668
  otel;
2867
3669
  otelServiceName;
@@ -3116,7 +3918,7 @@ var StreamProvider = class {
3116
3918
  };
3117
3919
 
3118
3920
  // src/server/infrastructure/consumer.provider.ts
3119
- import { Logger as Logger11 } from "@nestjs/common";
3921
+ import { Logger as Logger14 } from "@nestjs/common";
3120
3922
  import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
3121
3923
  var ConsumerProvider = class {
3122
3924
  constructor(options, connection, streamProvider, patternRegistry) {
@@ -3129,7 +3931,7 @@ var ConsumerProvider = class {
3129
3931
  this.otelServiceName = derived.serviceName;
3130
3932
  this.otelEndpoint = derived.serverEndpoint;
3131
3933
  }
3132
- logger = new Logger11("Jetstream:Consumer");
3934
+ logger = new Logger14("Jetstream:Consumer");
3133
3935
  otel;
3134
3936
  otelServiceName;
3135
3937
  otelEndpoint;
@@ -3340,7 +4142,7 @@ var ConsumerProvider = class {
3340
4142
  };
3341
4143
 
3342
4144
  // src/server/infrastructure/message.provider.ts
3343
- import { Logger as Logger12 } from "@nestjs/common";
4145
+ import { Logger as Logger15 } from "@nestjs/common";
3344
4146
  import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
3345
4147
  import {
3346
4148
  catchError,
@@ -3350,7 +4152,6 @@ import {
3350
4152
  repeat,
3351
4153
  Subject,
3352
4154
  takeUntil,
3353
- tap,
3354
4155
  timer
3355
4156
  } from "rxjs";
3356
4157
  var MessageProvider = class {
@@ -3360,7 +4161,7 @@ var MessageProvider = class {
3360
4161
  this.consumeOptionsMap = consumeOptionsMap;
3361
4162
  this.consumerRecoveryFn = consumerRecoveryFn;
3362
4163
  }
3363
- logger = new Logger12("Jetstream:Message");
4164
+ logger = new Logger15("Jetstream:Message");
3364
4165
  activeIterators = /* @__PURE__ */ new Set();
3365
4166
  orderedReadyResolve = null;
3366
4167
  orderedReadyReject = null;
@@ -3458,10 +4259,13 @@ var MessageProvider = class {
3458
4259
  /** Create a self-healing consumer flow for a specific kind. */
3459
4260
  createFlow(kind, info) {
3460
4261
  const target$ = this.getTargetSubject(kind);
3461
- return this.createSelfHealingFlow(() => this.consumeOnce(kind, info, target$), info.name);
4262
+ return this.createSelfHealingFlow(
4263
+ (onConnected) => this.consumeOnce(kind, info, target$, onConnected),
4264
+ info.name
4265
+ );
3462
4266
  }
3463
4267
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
3464
- async consumeOnce(kind, info, target$) {
4268
+ async consumeOnce(kind, info, target$, onConnected) {
3465
4269
  const js = this.connection.getJetStreamClient();
3466
4270
  let consumer;
3467
4271
  let consumerName2 = info.name;
@@ -3489,6 +4293,7 @@ var MessageProvider = class {
3489
4293
  });
3490
4294
  this.activeIterators.add(messages);
3491
4295
  this.monitorConsumerHealth(messages, consumerName2);
4296
+ onConnected();
3492
4297
  try {
3493
4298
  await messages.closed();
3494
4299
  } finally {
@@ -3543,7 +4348,7 @@ var MessageProvider = class {
3543
4348
  /** Create a self-healing ordered consumer flow. */
3544
4349
  createOrderedFlow(streamName2, consumerOpts) {
3545
4350
  return this.createSelfHealingFlow(
3546
- () => this.consumeOrderedOnce(streamName2, consumerOpts),
4351
+ (onConnected) => this.consumeOrderedOnce(streamName2, consumerOpts, onConnected),
3547
4352
  "ordered" /* Ordered */,
3548
4353
  (err) => {
3549
4354
  if (this.orderedReadyReject) {
@@ -3557,10 +4362,15 @@ var MessageProvider = class {
3557
4362
  /** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
3558
4363
  createSelfHealingFlow(source, label, onFirstError) {
3559
4364
  let consecutiveFailures = 0;
3560
- return defer2(source).pipe(
3561
- tap(() => {
3562
- consecutiveFailures = 0;
3563
- }),
4365
+ const onConnected = () => {
4366
+ if (consecutiveFailures > 0) {
4367
+ const attempts = consecutiveFailures;
4368
+ this.logger.log(`Consumer ${label} recovered after ${attempts} failed attempt(s)`);
4369
+ this.eventBus.emit("consumerRecovered" /* ConsumerRecovered */, label, attempts);
4370
+ }
4371
+ consecutiveFailures = 0;
4372
+ };
4373
+ return defer2(() => source(onConnected)).pipe(
3564
4374
  catchError((err) => {
3565
4375
  consecutiveFailures++;
3566
4376
  this.logger.error(`Consumer ${label} error, will restart:`, err);
@@ -3583,7 +4393,7 @@ var MessageProvider = class {
3583
4393
  );
3584
4394
  }
3585
4395
  /** Single iteration: create ordered consumer -> push messages into the subject. */
3586
- async consumeOrderedOnce(streamName2, consumerOpts) {
4396
+ async consumeOrderedOnce(streamName2, consumerOpts, onConnected) {
3587
4397
  const js = this.connection.getJetStreamClient();
3588
4398
  const consumer = await js.consumers.get(streamName2, consumerOpts);
3589
4399
  const orderedMessages$ = this.orderedMessages$;
@@ -3598,6 +4408,7 @@ var MessageProvider = class {
3598
4408
  this.orderedReadyReject = null;
3599
4409
  }
3600
4410
  this.activeIterators.add(messages);
4411
+ onConnected();
3601
4412
  try {
3602
4413
  await messages.closed();
3603
4414
  } finally {
@@ -3607,7 +4418,7 @@ var MessageProvider = class {
3607
4418
  };
3608
4419
 
3609
4420
  // src/server/infrastructure/metadata.provider.ts
3610
- import { Logger as Logger13 } from "@nestjs/common";
4421
+ import { Logger as Logger16 } from "@nestjs/common";
3611
4422
  import { Kvm } from "@nats-io/kv";
3612
4423
  var MetadataProvider = class {
3613
4424
  constructor(options, connection) {
@@ -3616,7 +4427,7 @@ var MetadataProvider = class {
3616
4427
  this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
3617
4428
  this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
3618
4429
  }
3619
- logger = new Logger13("Jetstream:Metadata");
4430
+ logger = new Logger16("Jetstream:Metadata");
3620
4431
  bucketName;
3621
4432
  replicas;
3622
4433
  ttl;
@@ -3708,176 +4519,8 @@ var MetadataProvider = class {
3708
4519
  }
3709
4520
  };
3710
4521
 
3711
- // src/server/routing/pattern-registry.ts
3712
- import { Logger as Logger14 } from "@nestjs/common";
3713
- var HANDLER_LABELS = {
3714
- ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
3715
- ["ordered" /* Ordered */]: "ordered" /* Ordered */,
3716
- ["ev" /* Event */]: "event" /* Event */,
3717
- ["cmd" /* Command */]: "rpc" /* Rpc */
3718
- };
3719
- var PatternRegistry = class {
3720
- constructor(options) {
3721
- this.options = options;
3722
- }
3723
- logger = new Logger14("Jetstream:PatternRegistry");
3724
- registry = /* @__PURE__ */ new Map();
3725
- // Cached after registerHandlers() — the registry is immutable from that point
3726
- cachedPatterns = null;
3727
- _hasEvents = false;
3728
- _hasCommands = false;
3729
- _hasBroadcasts = false;
3730
- _hasOrdered = false;
3731
- _hasMetadata = false;
3732
- /**
3733
- * Register all handlers from the NestJS strategy.
3734
- *
3735
- * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
3736
- */
3737
- registerHandlers(handlers) {
3738
- const serviceName = this.options.name;
3739
- for (const [pattern, handler] of handlers) {
3740
- const extras = handler.extras;
3741
- const isEvent = handler.isEventHandler ?? false;
3742
- const isBroadcast = !!extras?.broadcast;
3743
- const isOrdered = !!extras?.ordered;
3744
- const meta = extras?.meta;
3745
- if (isBroadcast && isOrdered) {
3746
- throw new Error(
3747
- `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
3748
- );
3749
- }
3750
- let kind;
3751
- if (isBroadcast) kind = "broadcast" /* Broadcast */;
3752
- else if (isOrdered) kind = "ordered" /* Ordered */;
3753
- else if (isEvent) kind = "ev" /* Event */;
3754
- else kind = "cmd" /* Command */;
3755
- const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
3756
- this.registry.set(fullSubject, {
3757
- handler,
3758
- pattern,
3759
- isEvent: isEvent && !isOrdered,
3760
- isBroadcast,
3761
- isOrdered,
3762
- meta
3763
- });
3764
- this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
3765
- }
3766
- this.cachedPatterns = this.buildPatternsByKind();
3767
- this._hasEvents = this.cachedPatterns.events.length > 0;
3768
- this._hasCommands = this.cachedPatterns.commands.length > 0;
3769
- this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
3770
- this._hasOrdered = this.cachedPatterns.ordered.length > 0;
3771
- this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
3772
- this.logSummary();
3773
- }
3774
- /** Find handler for a full NATS subject. */
3775
- getHandler(subject) {
3776
- return this.registry.get(subject)?.handler ?? null;
3777
- }
3778
- /** Get all registered broadcast patterns (for consumer filter_subject setup). */
3779
- getBroadcastPatterns() {
3780
- return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
3781
- }
3782
- hasBroadcastHandlers() {
3783
- return this._hasBroadcasts;
3784
- }
3785
- hasRpcHandlers() {
3786
- return this._hasCommands;
3787
- }
3788
- hasEventHandlers() {
3789
- return this._hasEvents;
3790
- }
3791
- hasOrderedHandlers() {
3792
- return this._hasOrdered;
3793
- }
3794
- /** Get fully-qualified NATS subjects for ordered handlers. */
3795
- getOrderedSubjects() {
3796
- return this.getPatternsByKind().ordered.map(
3797
- (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
3798
- );
3799
- }
3800
- /** Check if any registered handler has metadata. */
3801
- hasMetadata() {
3802
- return this._hasMetadata;
3803
- }
3804
- /**
3805
- * Get handler metadata entries for KV publishing.
3806
- *
3807
- * Returns a map of KV key -> metadata object for all handlers that have `meta`.
3808
- * Key format: `{serviceName}.{kind}.{pattern}`.
3809
- */
3810
- getMetadataEntries() {
3811
- const entries = /* @__PURE__ */ new Map();
3812
- for (const entry of this.registry.values()) {
3813
- if (!entry.meta) continue;
3814
- const kind = this.resolveStreamKind(entry);
3815
- const key = metadataKey(this.options.name, kind, entry.pattern);
3816
- entries.set(key, entry.meta);
3817
- }
3818
- return entries;
3819
- }
3820
- /** Get patterns grouped by kind (cached after registration). */
3821
- getPatternsByKind() {
3822
- const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
3823
- return {
3824
- events: [...patterns.events],
3825
- commands: [...patterns.commands],
3826
- broadcasts: [...patterns.broadcasts],
3827
- ordered: [...patterns.ordered]
3828
- };
3829
- }
3830
- /** Normalize a full NATS subject back to the user-facing pattern. */
3831
- normalizeSubject(subject) {
3832
- const name = internalName(this.options.name);
3833
- const prefixes = [
3834
- `${name}.${"cmd" /* Command */}.`,
3835
- `${name}.${"ev" /* Event */}.`,
3836
- `${name}.${"ordered" /* Ordered */}.`,
3837
- `${"broadcast" /* Broadcast */}.`
3838
- ];
3839
- for (const prefix of prefixes) {
3840
- if (subject.startsWith(prefix)) {
3841
- return subject.slice(prefix.length);
3842
- }
3843
- }
3844
- return subject;
3845
- }
3846
- buildPatternsByKind() {
3847
- const events = [];
3848
- const commands = [];
3849
- const broadcasts = [];
3850
- const ordered = [];
3851
- for (const entry of this.registry.values()) {
3852
- if (entry.isBroadcast) broadcasts.push(entry.pattern);
3853
- else if (entry.isOrdered) ordered.push(entry.pattern);
3854
- else if (entry.isEvent) events.push(entry.pattern);
3855
- else commands.push(entry.pattern);
3856
- }
3857
- return { events, commands, broadcasts, ordered };
3858
- }
3859
- resolveStreamKind(entry) {
3860
- if (entry.isBroadcast) return "broadcast" /* Broadcast */;
3861
- if (entry.isOrdered) return "ordered" /* Ordered */;
3862
- if (entry.isEvent) return "ev" /* Event */;
3863
- return "cmd" /* Command */;
3864
- }
3865
- logSummary() {
3866
- const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
3867
- const parts = [
3868
- `${commands.length} RPC`,
3869
- `${events.length} events`,
3870
- `${broadcasts.length} broadcasts`
3871
- ];
3872
- if (ordered.length > 0) {
3873
- parts.push(`${ordered.length} ordered`);
3874
- }
3875
- this.logger.log(`Registered handlers: ${parts.join(", ")}`);
3876
- }
3877
- };
3878
-
3879
4522
  // src/server/routing/event.router.ts
3880
- import { Logger as Logger15 } from "@nestjs/common";
4523
+ import { Logger as Logger17 } from "@nestjs/common";
3881
4524
  import { headers as natsHeaders3 } from "@nats-io/transport-node";
3882
4525
  var eventConsumeKindFor = (kind) => {
3883
4526
  if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
@@ -3906,7 +4549,7 @@ var EventRouter = class {
3906
4549
  this.serverEndpoint = null;
3907
4550
  }
3908
4551
  }
3909
- logger = new Logger15("Jetstream:EventRouter");
4552
+ logger = new Logger17("Jetstream:EventRouter");
3910
4553
  subscriptions = [];
3911
4554
  otel;
3912
4555
  serviceName;
@@ -3950,7 +4593,14 @@ var EventRouter = class {
3950
4593
  const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
3951
4594
  const concurrency = this.getConcurrency(kind);
3952
4595
  const hasDlqCheck = deadLetterConfig !== void 0;
3953
- const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
4596
+ const reportHandlerCompleted = (msg, startedAt, status) => {
4597
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
4598
+ const declared = patternRegistry.resolveDeclared(msg.subject);
4599
+ const pattern = declared?.pattern ?? msg.subject;
4600
+ const declaredKind = declared?.kind ?? kind;
4601
+ const durationMs = performance.now() - startedAt;
4602
+ eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
4603
+ };
3954
4604
  const isDeadLetter = (msg) => {
3955
4605
  if (!hasDlqCheck) return false;
3956
4606
  const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
@@ -3987,7 +4637,7 @@ var EventRouter = class {
3987
4637
  logger5.error(`Decode error for ${subject}:`, err);
3988
4638
  return null;
3989
4639
  }
3990
- if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
4640
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
3991
4641
  return { handler, data };
3992
4642
  } catch (err) {
3993
4643
  logger5.error(`Unexpected error in ${kind} event router`, err);
@@ -3999,12 +4649,18 @@ var EventRouter = class {
3999
4649
  return null;
4000
4650
  }
4001
4651
  };
4652
+ const statusForContext = (ctx) => {
4653
+ if (ctx.shouldTerminate) return "terminated";
4654
+ if (ctx.shouldRetry) return "retried";
4655
+ return "success";
4656
+ };
4002
4657
  const handleSafe = (msg) => {
4003
4658
  const resolved = resolveEvent(msg);
4004
4659
  if (resolved === null) return void 0;
4005
4660
  const { handler, data } = resolved;
4006
4661
  const ctx = new RpcContext([msg]);
4007
4662
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
4663
+ const startedAt = performance.now();
4008
4664
  let pending;
4009
4665
  try {
4010
4666
  pending = withConsumeSpan(
@@ -4027,18 +4683,21 @@ var EventRouter = class {
4027
4683
  err instanceof Error ? err : new Error(String(err)),
4028
4684
  `${kind}-handler:${msg.subject}`
4029
4685
  );
4686
+ reportHandlerCompleted(msg, startedAt, "error");
4030
4687
  return settleFailure(msg, data, err).finally(() => {
4031
4688
  if (stopAckExtension !== null) stopAckExtension();
4032
4689
  });
4033
4690
  }
4034
4691
  if (!isPromiseLike2(pending)) {
4035
4692
  settleSuccess(msg, ctx);
4693
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4036
4694
  if (stopAckExtension !== null) stopAckExtension();
4037
4695
  return void 0;
4038
4696
  }
4039
4697
  return pending.then(
4040
4698
  () => {
4041
4699
  settleSuccess(msg, ctx);
4700
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4042
4701
  if (stopAckExtension !== null) stopAckExtension();
4043
4702
  },
4044
4703
  async (err) => {
@@ -4047,6 +4706,7 @@ var EventRouter = class {
4047
4706
  err instanceof Error ? err : new Error(String(err)),
4048
4707
  `${kind}-handler:${msg.subject}`
4049
4708
  );
4709
+ reportHandlerCompleted(msg, startedAt, "error");
4050
4710
  try {
4051
4711
  await settleFailure(msg, data, err);
4052
4712
  } finally {
@@ -4071,7 +4731,7 @@ var EventRouter = class {
4071
4731
  logger5.error(`Decode error for ${subject}:`, err);
4072
4732
  return void 0;
4073
4733
  }
4074
- if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
4734
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
4075
4735
  } catch (err) {
4076
4736
  logger5.error(`Ordered handler error (${subject}):`, err);
4077
4737
  return void 0;
@@ -4084,6 +4744,7 @@ var EventRouter = class {
4084
4744
  );
4085
4745
  }
4086
4746
  };
4747
+ const startedAt = performance.now();
4087
4748
  let pending;
4088
4749
  try {
4089
4750
  pending = withConsumeSpan(
@@ -4102,15 +4763,24 @@ var EventRouter = class {
4102
4763
  );
4103
4764
  } catch (err) {
4104
4765
  logger5.error(`Ordered handler error (${subject}):`, err);
4766
+ reportHandlerCompleted(msg, startedAt, "error");
4105
4767
  return void 0;
4106
4768
  }
4107
4769
  if (!isPromiseLike2(pending)) {
4108
4770
  warnIfSettlementAttempted();
4771
+ reportHandlerCompleted(msg, startedAt, "success");
4109
4772
  return void 0;
4110
4773
  }
4111
- return pending.then(warnIfSettlementAttempted, (err) => {
4112
- logger5.error(`Ordered handler error (${subject}):`, err);
4113
- });
4774
+ return pending.then(
4775
+ () => {
4776
+ warnIfSettlementAttempted();
4777
+ reportHandlerCompleted(msg, startedAt, "success");
4778
+ },
4779
+ (err) => {
4780
+ logger5.error(`Ordered handler error (${subject}):`, err);
4781
+ reportHandlerCompleted(msg, startedAt, "error");
4782
+ }
4783
+ );
4114
4784
  };
4115
4785
  const route = isOrdered ? handleOrderedSafe : handleSafe;
4116
4786
  const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
@@ -4296,7 +4966,7 @@ var EventRouter = class {
4296
4966
  };
4297
4967
 
4298
4968
  // src/server/routing/rpc.router.ts
4299
- import { Logger as Logger16 } from "@nestjs/common";
4969
+ import { Logger as Logger18 } from "@nestjs/common";
4300
4970
  import { headers } from "@nats-io/transport-node";
4301
4971
  var RpcRouter = class {
4302
4972
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
@@ -4320,7 +4990,7 @@ var RpcRouter = class {
4320
4990
  this.serverEndpoint = null;
4321
4991
  }
4322
4992
  }
4323
- logger = new Logger16("Jetstream:RpcRouter");
4993
+ logger = new Logger18("Jetstream:RpcRouter");
4324
4994
  timeout;
4325
4995
  concurrency;
4326
4996
  resolvedAckExtensionInterval;
@@ -4356,6 +5026,19 @@ var RpcRouter = class {
4356
5026
  const emitRpcTimeout = (subject, correlationId) => {
4357
5027
  eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
4358
5028
  };
5029
+ const reportHandlerCompleted = (msg, startedAt, status) => {
5030
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
5031
+ const declared = patternRegistry.resolveDeclared(msg.subject);
5032
+ const pattern = declared?.pattern ?? msg.subject;
5033
+ const declaredKind = declared?.kind ?? "cmd" /* Command */;
5034
+ eventBus.emit(
5035
+ "handlerCompleted" /* HandlerCompleted */,
5036
+ pattern,
5037
+ declaredKind,
5038
+ performance.now() - startedAt,
5039
+ status
5040
+ );
5041
+ };
4359
5042
  const publishReply = (replyTo, correlationId, payload) => {
4360
5043
  try {
4361
5044
  const hdrs = headers();
@@ -4419,6 +5102,7 @@ var RpcRouter = class {
4419
5102
  const subject = msg.subject;
4420
5103
  const ctx = new RpcContext([msg]);
4421
5104
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
5105
+ const startedAt = performance.now();
4422
5106
  const reportHandlerError = (err) => {
4423
5107
  eventBus.emit(
4424
5108
  "error" /* Error */,
@@ -4449,12 +5133,14 @@ var RpcRouter = class {
4449
5133
  } catch (err) {
4450
5134
  if (stopAckExtension !== null) stopAckExtension();
4451
5135
  reportHandlerError(err);
5136
+ reportHandlerCompleted(msg, startedAt, "error");
4452
5137
  return void 0;
4453
5138
  }
4454
5139
  if (!isPromiseLike2(pending)) {
4455
5140
  if (stopAckExtension !== null) stopAckExtension();
4456
5141
  msg.ack();
4457
5142
  publishReply(replyTo, correlationId, pending);
5143
+ reportHandlerCompleted(msg, startedAt, "success");
4458
5144
  return void 0;
4459
5145
  }
4460
5146
  let settled = false;
@@ -4465,6 +5151,7 @@ var RpcRouter = class {
4465
5151
  abortController.abort();
4466
5152
  emitRpcTimeout(subject, correlationId);
4467
5153
  msg.term("Handler timeout");
5154
+ reportHandlerCompleted(msg, startedAt, "terminated");
4468
5155
  }, timeout);
4469
5156
  return pending.then(
4470
5157
  (result) => {
@@ -4474,6 +5161,7 @@ var RpcRouter = class {
4474
5161
  if (stopAckExtension !== null) stopAckExtension();
4475
5162
  msg.ack();
4476
5163
  publishReply(replyTo, correlationId, result);
5164
+ reportHandlerCompleted(msg, startedAt, "success");
4477
5165
  },
4478
5166
  (err) => {
4479
5167
  if (settled) return;
@@ -4481,6 +5169,7 @@ var RpcRouter = class {
4481
5169
  clearTimeout(timeoutId);
4482
5170
  if (stopAckExtension !== null) stopAckExtension();
4483
5171
  reportHandlerError(err);
5172
+ reportHandlerCompleted(msg, startedAt, "error");
4484
5173
  }
4485
5174
  );
4486
5175
  };
@@ -4540,14 +5229,14 @@ var RpcRouter = class {
4540
5229
  };
4541
5230
 
4542
5231
  // src/shutdown/shutdown.manager.ts
4543
- import { Logger as Logger17 } from "@nestjs/common";
5232
+ import { Logger as Logger19 } from "@nestjs/common";
4544
5233
  var ShutdownManager = class {
4545
5234
  constructor(connection, eventBus, timeout) {
4546
5235
  this.connection = connection;
4547
5236
  this.eventBus = eventBus;
4548
5237
  this.timeout = timeout;
4549
5238
  }
4550
- logger = new Logger17("Jetstream:Shutdown");
5239
+ logger = new Logger19("Jetstream:Shutdown");
4551
5240
  shutdownPromise;
4552
5241
  /**
4553
5242
  * Execute the full shutdown sequence.
@@ -4601,12 +5290,14 @@ var JetstreamModule = class {
4601
5290
  return {
4602
5291
  module: JetstreamModule,
4603
5292
  global: true,
5293
+ imports: [JetstreamMetricsModule.forFeature()],
4604
5294
  providers,
4605
5295
  exports: [
4606
5296
  JETSTREAM_CONNECTION,
4607
5297
  JETSTREAM_CODEC,
4608
5298
  JETSTREAM_EVENT_BUS,
4609
5299
  JETSTREAM_OPTIONS,
5300
+ PatternRegistry,
4610
5301
  ShutdownManager,
4611
5302
  JetstreamStrategy,
4612
5303
  JetstreamHealthIndicator
@@ -4628,13 +5319,14 @@ var JetstreamModule = class {
4628
5319
  return {
4629
5320
  module: JetstreamModule,
4630
5321
  global: true,
4631
- imports: asyncOptions.imports ?? [],
5322
+ imports: [...asyncOptions.imports ?? [], JetstreamMetricsModule.forFeature()],
4632
5323
  providers: [...asyncProviders, ...coreProviders],
4633
5324
  exports: [
4634
5325
  JETSTREAM_CONNECTION,
4635
5326
  JETSTREAM_CODEC,
4636
5327
  JETSTREAM_EVENT_BUS,
4637
5328
  JETSTREAM_OPTIONS,
5329
+ PatternRegistry,
4638
5330
  ShutdownManager,
4639
5331
  JetstreamStrategy,
4640
5332
  JetstreamHealthIndicator
@@ -4683,7 +5375,7 @@ var JetstreamModule = class {
4683
5375
  provide: JETSTREAM_EVENT_BUS,
4684
5376
  inject: [JETSTREAM_OPTIONS],
4685
5377
  useFactory: (options) => {
4686
- const logger5 = new Logger18("Jetstream:Module");
5378
+ const logger5 = new Logger20("Jetstream:Module");
4687
5379
  return new EventBus(logger5, options.hooks);
4688
5380
  }
4689
5381
  },
@@ -4978,11 +5670,11 @@ var JetstreamModule = class {
4978
5670
  };
4979
5671
  JetstreamModule = __decorateClass([
4980
5672
  Global(),
4981
- Module({}),
4982
- __decorateParam(0, Optional()),
4983
- __decorateParam(0, Inject(ShutdownManager)),
4984
- __decorateParam(1, Optional()),
4985
- __decorateParam(1, Inject(JetstreamStrategy))
5673
+ Module2({}),
5674
+ __decorateParam(0, Optional2()),
5675
+ __decorateParam(0, Inject2(ShutdownManager)),
5676
+ __decorateParam(1, Optional2()),
5677
+ __decorateParam(1, Inject2(JetstreamStrategy))
4986
5678
  ], JetstreamModule);
4987
5679
  export {
4988
5680
  ConsumeKind,