@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.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from2, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
  var __decorateClass = (decorators, target, key, kind) => {
20
30
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
@@ -85,7 +95,7 @@ __export(index_exports, {
85
95
  module.exports = __toCommonJS(index_exports);
86
96
 
87
97
  // src/jetstream.module.ts
88
- var import_common18 = require("@nestjs/common");
98
+ var import_common21 = require("@nestjs/common");
89
99
 
90
100
  // src/client/jetstream.client.ts
91
101
  var import_common5 = require("@nestjs/common");
@@ -110,6 +120,10 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
110
120
  TransportEvent2["ShutdownStart"] = "shutdownStart";
111
121
  TransportEvent2["ShutdownComplete"] = "shutdownComplete";
112
122
  TransportEvent2["DeadLetter"] = "deadLetter";
123
+ TransportEvent2["ConsumerRecovered"] = "consumerRecovered";
124
+ TransportEvent2["HandlerCompleted"] = "handlerCompleted";
125
+ TransportEvent2["Published"] = "published";
126
+ TransportEvent2["RpcCompleted"] = "rpcCompleted";
113
127
  return TransportEvent2;
114
128
  })(TransportEvent || {});
115
129
 
@@ -498,6 +512,8 @@ var resolveCaptureBody = (option) => {
498
512
  };
499
513
  };
500
514
  var resolveOtelOptions = (options = {}) => {
515
+ if (options === true) options = {};
516
+ if (options === false) options = { enabled: false };
501
517
  return {
502
518
  enabled: options.enabled ?? true,
503
519
  traces: expandTracesOption(options.traces),
@@ -577,7 +593,7 @@ var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ct
577
593
 
578
594
  // src/otel/tracer.ts
579
595
  var import_api2 = require("@opentelemetry/api");
580
- var PACKAGE_VERSION = true ? "2.10.0" : "0.0.0";
596
+ var PACKAGE_VERSION = true ? "2.11.1" : "0.0.0";
581
597
  var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
582
598
 
583
599
  // src/otel/carrier.ts
@@ -1368,6 +1384,17 @@ var detectEventKind = (pattern) => {
1368
1384
  if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
1369
1385
  return "event" /* Event */;
1370
1386
  };
1387
+ var declaredEventPattern = (pattern) => {
1388
+ if (pattern.startsWith("broadcast:" /* Broadcast */))
1389
+ return pattern.slice("broadcast:" /* Broadcast */.length);
1390
+ if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
1391
+ return pattern;
1392
+ };
1393
+ var eventStreamKind = (kind) => {
1394
+ if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
1395
+ if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
1396
+ return "ev" /* Event */;
1397
+ };
1371
1398
  var JetstreamClient = class extends import_microservices.ClientProxy {
1372
1399
  constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
1373
1400
  super();
@@ -1483,50 +1510,60 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1483
1510
  const encoded = this.codec.encode(data);
1484
1511
  const effectiveMsgId = messageId ?? import_nuid.nuid.next();
1485
1512
  const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
1486
- await withPublishSpan(
1487
- {
1488
- subject: publishSubject,
1489
- pattern: packet.pattern,
1490
- record,
1491
- kind: detectEventKind(packet.pattern),
1492
- payloadBytes: encoded.length,
1493
- payload: encoded,
1494
- messageId: effectiveMsgId,
1495
- headers: msgHeaders,
1496
- serviceName: this.callerName,
1497
- endpoint: this.serverEndpoint,
1498
- scheduleTarget: schedule ? eventSubject : void 0
1499
- },
1500
- this.otel,
1501
- async () => {
1502
- const warnIfDuplicate = (kindLabel, ack2) => {
1503
- if (ack2.duplicate) {
1504
- this.logger.warn(
1505
- `Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
1506
- );
1513
+ const publishKind = detectEventKind(packet.pattern);
1514
+ const declaredPattern = declaredEventPattern(packet.pattern);
1515
+ const streamKind = eventStreamKind(publishKind);
1516
+ const startedAt = performance.now();
1517
+ try {
1518
+ await withPublishSpan(
1519
+ {
1520
+ subject: publishSubject,
1521
+ pattern: packet.pattern,
1522
+ record,
1523
+ kind: publishKind,
1524
+ payloadBytes: encoded.length,
1525
+ payload: encoded,
1526
+ messageId: effectiveMsgId,
1527
+ headers: msgHeaders,
1528
+ serviceName: this.callerName,
1529
+ endpoint: this.serverEndpoint,
1530
+ scheduleTarget: schedule ? eventSubject : void 0
1531
+ },
1532
+ this.otel,
1533
+ async () => {
1534
+ const warnIfDuplicate = (kindLabel, ack2) => {
1535
+ if (ack2.duplicate) {
1536
+ this.logger.warn(
1537
+ `Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
1538
+ );
1539
+ }
1540
+ };
1541
+ if (schedule) {
1542
+ const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1543
+ headers: msgHeaders,
1544
+ msgID: effectiveMsgId,
1545
+ ttl,
1546
+ schedule: {
1547
+ specification: schedule.at,
1548
+ target: eventSubject
1549
+ }
1550
+ });
1551
+ warnIfDuplicate("scheduled", ack2);
1552
+ return;
1507
1553
  }
1508
- };
1509
- if (schedule) {
1510
- const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1554
+ const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1511
1555
  headers: msgHeaders,
1512
1556
  msgID: effectiveMsgId,
1513
- ttl,
1514
- schedule: {
1515
- specification: schedule.at,
1516
- target: eventSubject
1517
- }
1557
+ ttl
1518
1558
  });
1519
- warnIfDuplicate("scheduled", ack2);
1520
- return;
1559
+ warnIfDuplicate("event", ack);
1521
1560
  }
1522
- const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1523
- headers: msgHeaders,
1524
- msgID: effectiveMsgId,
1525
- ttl
1526
- });
1527
- warnIfDuplicate("event", ack);
1528
- }
1529
- );
1561
+ );
1562
+ this.reportPublished(declaredPattern, streamKind, startedAt, "success");
1563
+ } catch (err) {
1564
+ this.reportPublished(declaredPattern, streamKind, startedAt, "error");
1565
+ throw err;
1566
+ }
1530
1567
  return void 0;
1531
1568
  }
1532
1569
  /**
@@ -1554,14 +1591,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1554
1591
  };
1555
1592
  let jetStreamCorrelationId = null;
1556
1593
  if (this.isCoreMode) {
1557
- this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
1594
+ this.publishCoreRpc(subject, data, hdrs, timeout, callback, packet.pattern).catch(
1595
+ onUnhandled
1596
+ );
1558
1597
  } else {
1559
1598
  jetStreamCorrelationId = import_nuid.nuid.next();
1560
1599
  this.publishJetStreamRpc(subject, data, callback, {
1561
1600
  headers: hdrs,
1562
1601
  timeout,
1563
1602
  correlationId: jetStreamCorrelationId,
1564
- messageId
1603
+ messageId,
1604
+ declaredPattern: packet.pattern
1565
1605
  }).catch(onUnhandled);
1566
1606
  }
1567
1607
  return () => {
@@ -1576,7 +1616,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1576
1616
  };
1577
1617
  }
1578
1618
  /** Core mode: nc.request() with timeout. */
1579
- async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
1619
+ async publishCoreRpc(subject, data, customHeaders, timeout, callback, declaredPattern) {
1580
1620
  const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
1581
1621
  const hdrs = this.buildHeaders(customHeaders, { subject });
1582
1622
  const encoded = this.codec.encode(data);
@@ -1591,6 +1631,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1591
1631
  },
1592
1632
  this.otel
1593
1633
  );
1634
+ const startedAt = performance.now();
1594
1635
  try {
1595
1636
  const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
1596
1637
  const response = await import_api8.context.with(
@@ -1603,9 +1644,13 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1603
1644
  const decoded = this.codec.decode(response.data);
1604
1645
  if (response.headers?.get("x-error" /* Error */)) {
1605
1646
  spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: decoded });
1647
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1648
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1606
1649
  callback({ err: decoded, response: null, isDisposed: true });
1607
1650
  } else {
1608
1651
  spanHandle.finish({ kind: "ok" /* Ok */, reply: decoded });
1652
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1653
+ this.reportRpcCompleted(declaredPattern, startedAt, "success");
1609
1654
  callback({ err: null, response: decoded, isDisposed: true });
1610
1655
  }
1611
1656
  } catch (err) {
@@ -1613,16 +1658,20 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1613
1658
  if (error instanceof import_transport_node.TimeoutError) {
1614
1659
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1615
1660
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, "");
1661
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1662
+ this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1616
1663
  } else {
1617
1664
  spanHandle.finish({ kind: "error" /* Error */, error });
1618
1665
  this.eventBus.emit("error" /* Error */, error, "client-rpc");
1666
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1667
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1619
1668
  }
1620
1669
  callback({ err: error, response: null, isDisposed: true });
1621
1670
  }
1622
1671
  }
1623
1672
  /** JetStream mode: publish to stream + wait for inbox response. */
1624
1673
  async publishJetStreamRpc(subject, data, callback, options) {
1625
- const { headers: customHeaders, correlationId, messageId } = options;
1674
+ const { headers: customHeaders, correlationId, messageId, declaredPattern } = options;
1626
1675
  const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
1627
1676
  const hdrs = this.buildHeaders(customHeaders, {
1628
1677
  subject,
@@ -1643,6 +1692,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1643
1692
  },
1644
1693
  this.otel
1645
1694
  );
1695
+ const startedAt = performance.now();
1646
1696
  this.pendingMessages.set(correlationId, (packet) => {
1647
1697
  if (packet.err) {
1648
1698
  if (packet.err instanceof Error) {
@@ -1650,8 +1700,10 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1650
1700
  } else {
1651
1701
  spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: packet.err });
1652
1702
  }
1703
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1653
1704
  } else {
1654
1705
  spanHandle.finish({ kind: "ok" /* Ok */, reply: packet.response });
1706
+ this.reportRpcCompleted(declaredPattern, startedAt, "success");
1655
1707
  }
1656
1708
  callback(packet);
1657
1709
  });
@@ -1661,6 +1713,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1661
1713
  this.pendingMessages.delete(correlationId);
1662
1714
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1663
1715
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
1716
+ this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1664
1717
  callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
1665
1718
  }, effectiveTimeout);
1666
1719
  this.pendingTimeouts.set(correlationId, timeoutId);
@@ -1673,6 +1726,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1673
1726
  this.pendingMessages.delete(correlationId);
1674
1727
  const inboxError = new Error("Inbox not initialized");
1675
1728
  spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
1729
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1730
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1676
1731
  callback({
1677
1732
  err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
1678
1733
  response: null,
@@ -1687,6 +1742,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1687
1742
  msgID: messageId ?? import_nuid.nuid.next()
1688
1743
  })
1689
1744
  );
1745
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1690
1746
  } catch (err) {
1691
1747
  const existingTimeout = this.pendingTimeouts.get(correlationId);
1692
1748
  if (existingTimeout) {
@@ -1698,9 +1754,32 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1698
1754
  const error = err instanceof Error ? err : new Error("Unknown error");
1699
1755
  spanHandle.finish({ kind: "error" /* Error */, error });
1700
1756
  this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
1757
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1758
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1701
1759
  callback({ err: error, response: null, isDisposed: true });
1702
1760
  }
1703
1761
  }
1762
+ // hasHook is per-emit so late subscribers (JetstreamMetricsService during
1763
+ // OnApplicationBootstrap) still receive events.
1764
+ reportPublished(declaredPattern, kind, startedAt, status) {
1765
+ if (!this.eventBus.hasHook("published" /* Published */)) return;
1766
+ this.eventBus.emit(
1767
+ "published" /* Published */,
1768
+ declaredPattern,
1769
+ kind,
1770
+ performance.now() - startedAt,
1771
+ status
1772
+ );
1773
+ }
1774
+ reportRpcCompleted(declaredPattern, startedAt, status) {
1775
+ if (!this.eventBus.hasHook("rpcCompleted" /* RpcCompleted */)) return;
1776
+ this.eventBus.emit(
1777
+ "rpcCompleted" /* RpcCompleted */,
1778
+ declaredPattern,
1779
+ performance.now() - startedAt,
1780
+ status
1781
+ );
1782
+ }
1704
1783
  /** Fail-fast all pending JetStream RPC callbacks on connection loss. */
1705
1784
  handleDisconnect() {
1706
1785
  this.rejectPendingRpcs(new Error("Connection lost"));
@@ -2106,43 +2185,53 @@ var ConnectionProvider = class {
2106
2185
  var EventBus = class {
2107
2186
  hooks;
2108
2187
  logger;
2188
+ subscribers = /* @__PURE__ */ new Map();
2109
2189
  constructor(logger5, hooks) {
2110
2190
  this.logger = logger5;
2111
2191
  this.hooks = hooks ?? {};
2112
2192
  }
2113
2193
  /**
2114
- * Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
2115
- *
2116
- * @param event - The {@link TransportEvent} to emit.
2117
- * @param args - Arguments matching the hook signature for this event.
2194
+ * Subscribe to a transport event. Used by built-in observers (e.g. metrics).
2195
+ * Multiple subscribers per event are supported; each is called independently.
2196
+ */
2197
+ subscribe(event, handler) {
2198
+ const list = this.subscribers.get(event) ?? [];
2199
+ list.push(handler);
2200
+ this.subscribers.set(event, list);
2201
+ }
2202
+ /**
2203
+ * Emit a lifecycle event. Dispatches to all internal subscribers and the
2204
+ * registered user hook (if any).
2118
2205
  */
2119
2206
  emit(event, ...args) {
2120
- const hook = this.hooks[event];
2121
- if (!hook) return;
2122
- this.callHook(event, hook, ...args);
2207
+ this.dispatch(event, args);
2123
2208
  }
2124
2209
  /**
2125
2210
  * Hot-path optimized emit for MessageRouted events.
2126
2211
  * Avoids rest/spread overhead of the generic `emit()`.
2127
2212
  */
2128
2213
  emitMessageRouted(subject, kind) {
2129
- const hook = this.hooks["messageRouted" /* MessageRouted */];
2130
- if (!hook) return;
2131
- this.callHook(
2132
- "messageRouted" /* MessageRouted */,
2133
- hook,
2134
- subject,
2135
- kind
2136
- );
2214
+ this.dispatch("messageRouted" /* MessageRouted */, [subject, kind]);
2137
2215
  }
2138
2216
  /**
2139
- * Check whether a hook is registered for the given event.
2140
- *
2141
- * Used by the routing hot path to elide the emit call entirely when the
2142
- * transport owner did not register a listener.
2217
+ * Check whether any listener (user hook or internal subscriber) is registered
2218
+ * for the given event. Used by routing hot path to elide the emit call when
2219
+ * no one is listening.
2143
2220
  */
2144
2221
  hasHook(event) {
2145
- return this.hooks[event] !== void 0;
2222
+ return this.hooks[event] !== void 0 || (this.subscribers.get(event)?.length ?? 0) > 0;
2223
+ }
2224
+ dispatch(event, args) {
2225
+ const subs = this.subscribers.get(event);
2226
+ if (subs?.length) {
2227
+ for (const sub of [...subs]) {
2228
+ this.callHook(event, sub, ...args);
2229
+ }
2230
+ }
2231
+ const hook = this.hooks[event];
2232
+ if (hook) {
2233
+ this.callHook(event, hook, ...args);
2234
+ }
2146
2235
  }
2147
2236
  callHook(event, hook, ...args) {
2148
2237
  try {
@@ -2221,12 +2310,692 @@ var JetstreamHealthIndicator = class {
2221
2310
  isHealthCheckError: true
2222
2311
  });
2223
2312
  }
2224
- return { [key]: details };
2313
+ return { [key]: details };
2314
+ }
2315
+ };
2316
+ JetstreamHealthIndicator = __decorateClass([
2317
+ (0, import_common7.Injectable)()
2318
+ ], JetstreamHealthIndicator);
2319
+
2320
+ // src/metrics/metrics.module.ts
2321
+ var import_common11 = require("@nestjs/common");
2322
+
2323
+ // src/server/routing/pattern-registry.ts
2324
+ var import_common8 = require("@nestjs/common");
2325
+ var HANDLER_LABELS = {
2326
+ ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
2327
+ ["ordered" /* Ordered */]: "ordered" /* Ordered */,
2328
+ ["ev" /* Event */]: "event" /* Event */,
2329
+ ["cmd" /* Command */]: "rpc" /* Rpc */
2330
+ };
2331
+ var PatternRegistry = class {
2332
+ constructor(options) {
2333
+ this.options = options;
2334
+ }
2335
+ logger = new import_common8.Logger("Jetstream:PatternRegistry");
2336
+ registry = /* @__PURE__ */ new Map();
2337
+ // Cached after registerHandlers() — the registry is immutable from that point
2338
+ cachedPatterns = null;
2339
+ _hasEvents = false;
2340
+ _hasCommands = false;
2341
+ _hasBroadcasts = false;
2342
+ _hasOrdered = false;
2343
+ _hasMetadata = false;
2344
+ /**
2345
+ * Register all handlers from the NestJS strategy.
2346
+ *
2347
+ * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
2348
+ */
2349
+ registerHandlers(handlers) {
2350
+ const serviceName = this.options.name;
2351
+ for (const [pattern, handler] of handlers) {
2352
+ const extras = handler.extras;
2353
+ const isEvent = handler.isEventHandler ?? false;
2354
+ const isBroadcast = !!extras?.broadcast;
2355
+ const isOrdered = !!extras?.ordered;
2356
+ const meta = extras?.meta;
2357
+ if (isBroadcast && isOrdered) {
2358
+ throw new Error(
2359
+ `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
2360
+ );
2361
+ }
2362
+ let kind;
2363
+ if (isBroadcast) kind = "broadcast" /* Broadcast */;
2364
+ else if (isOrdered) kind = "ordered" /* Ordered */;
2365
+ else if (isEvent) kind = "ev" /* Event */;
2366
+ else kind = "cmd" /* Command */;
2367
+ const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
2368
+ this.registry.set(fullSubject, {
2369
+ handler,
2370
+ pattern,
2371
+ isEvent: isEvent && !isOrdered,
2372
+ isBroadcast,
2373
+ isOrdered,
2374
+ meta
2375
+ });
2376
+ this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
2377
+ }
2378
+ this.cachedPatterns = this.buildPatternsByKind();
2379
+ this._hasEvents = this.cachedPatterns.events.length > 0;
2380
+ this._hasCommands = this.cachedPatterns.commands.length > 0;
2381
+ this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
2382
+ this._hasOrdered = this.cachedPatterns.ordered.length > 0;
2383
+ this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
2384
+ this.logSummary();
2385
+ }
2386
+ /** Find handler for a full NATS subject. */
2387
+ getHandler(subject) {
2388
+ return this.registry.get(subject)?.handler ?? null;
2389
+ }
2390
+ /**
2391
+ * Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
2392
+ *
2393
+ * Returns `null` when the subject is not registered. The declared pattern is
2394
+ * the value the user passed to `@EventPattern`/`@MessagePattern` — stable and
2395
+ * bounded, suitable for use as a Prometheus label without cardinality risk.
2396
+ */
2397
+ resolveDeclared(subject) {
2398
+ const entry = this.registry.get(subject);
2399
+ if (!entry) return null;
2400
+ return { pattern: entry.pattern, kind: this.resolveStreamKind(entry) };
2401
+ }
2402
+ /** Get all registered broadcast patterns (for consumer filter_subject setup). */
2403
+ getBroadcastPatterns() {
2404
+ return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
2405
+ }
2406
+ hasBroadcastHandlers() {
2407
+ return this._hasBroadcasts;
2408
+ }
2409
+ hasRpcHandlers() {
2410
+ return this._hasCommands;
2411
+ }
2412
+ hasEventHandlers() {
2413
+ return this._hasEvents;
2414
+ }
2415
+ hasOrderedHandlers() {
2416
+ return this._hasOrdered;
2417
+ }
2418
+ /** Get fully-qualified NATS subjects for ordered handlers. */
2419
+ getOrderedSubjects() {
2420
+ return this.getPatternsByKind().ordered.map(
2421
+ (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2422
+ );
2423
+ }
2424
+ /** Check if any registered handler has metadata. */
2425
+ hasMetadata() {
2426
+ return this._hasMetadata;
2427
+ }
2428
+ /**
2429
+ * Get handler metadata entries for KV publishing.
2430
+ *
2431
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
2432
+ * Key format: `{serviceName}.{kind}.{pattern}`.
2433
+ */
2434
+ getMetadataEntries() {
2435
+ const entries = /* @__PURE__ */ new Map();
2436
+ for (const entry of this.registry.values()) {
2437
+ if (!entry.meta) continue;
2438
+ const kind = this.resolveStreamKind(entry);
2439
+ const key = metadataKey(this.options.name, kind, entry.pattern);
2440
+ entries.set(key, entry.meta);
2441
+ }
2442
+ return entries;
2443
+ }
2444
+ /** Get patterns grouped by kind (cached after registration). */
2445
+ getPatternsByKind() {
2446
+ const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
2447
+ return {
2448
+ events: [...patterns.events],
2449
+ commands: [...patterns.commands],
2450
+ broadcasts: [...patterns.broadcasts],
2451
+ ordered: [...patterns.ordered]
2452
+ };
2453
+ }
2454
+ /** Normalize a full NATS subject back to the user-facing pattern. */
2455
+ normalizeSubject(subject) {
2456
+ const name = internalName(this.options.name);
2457
+ const prefixes = [
2458
+ `${name}.${"cmd" /* Command */}.`,
2459
+ `${name}.${"ev" /* Event */}.`,
2460
+ `${name}.${"ordered" /* Ordered */}.`,
2461
+ `${"broadcast" /* Broadcast */}.`
2462
+ ];
2463
+ for (const prefix of prefixes) {
2464
+ if (subject.startsWith(prefix)) {
2465
+ return subject.slice(prefix.length);
2466
+ }
2467
+ }
2468
+ return subject;
2469
+ }
2470
+ buildPatternsByKind() {
2471
+ const events = [];
2472
+ const commands = [];
2473
+ const broadcasts = [];
2474
+ const ordered = [];
2475
+ for (const entry of this.registry.values()) {
2476
+ if (entry.isBroadcast) broadcasts.push(entry.pattern);
2477
+ else if (entry.isOrdered) ordered.push(entry.pattern);
2478
+ else if (entry.isEvent) events.push(entry.pattern);
2479
+ else commands.push(entry.pattern);
2480
+ }
2481
+ return { events, commands, broadcasts, ordered };
2482
+ }
2483
+ resolveStreamKind(entry) {
2484
+ if (entry.isBroadcast) return "broadcast" /* Broadcast */;
2485
+ if (entry.isOrdered) return "ordered" /* Ordered */;
2486
+ if (entry.isEvent) return "ev" /* Event */;
2487
+ return "cmd" /* Command */;
2488
+ }
2489
+ logSummary() {
2490
+ const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
2491
+ const parts = [
2492
+ `${commands.length} RPC`,
2493
+ `${events.length} events`,
2494
+ `${broadcasts.length} broadcasts`
2495
+ ];
2496
+ if (ordered.length > 0) {
2497
+ parts.push(`${ordered.length} ordered`);
2498
+ }
2499
+ this.logger.log(`Registered handlers: ${parts.join(", ")}`);
2500
+ }
2501
+ };
2502
+
2503
+ // src/metrics/metrics.constants.ts
2504
+ var JETSTREAM_METRICS_CONFIG = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_CONFIG");
2505
+ var JETSTREAM_METRICS_REGISTRY = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_REGISTRY");
2506
+ var JETSTREAM_METRICS_PROM_CLIENT = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_PROM_CLIENT");
2507
+ var DEFAULT_METRICS_PREFIX = "jetstream_";
2508
+ var DEFAULT_POLL_INTERVAL_MS = 15e3;
2509
+ var DEFAULT_HISTOGRAM_BUCKETS = {
2510
+ handlerDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
2511
+ publishDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
2512
+ rpcDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
2513
+ };
2514
+ var ERROR_CONTEXT_PREFIXES = [
2515
+ ["connection", "connection"],
2516
+ ["codec", "codec"],
2517
+ ["client-rpc", "publish"],
2518
+ ["jetstream-rpc-publish", "publish"],
2519
+ ["publish", "publish"],
2520
+ ["message-provider", "consume"],
2521
+ ["consume", "consume"],
2522
+ ["core-rpc-handler", "handler"],
2523
+ ["rpc-handler", "handler"],
2524
+ // EventRouter formats contexts as `${StreamKind.*}-handler:...` — the enum
2525
+ // uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
2526
+ ["ev-handler", "handler"],
2527
+ ["event-handler", "handler"],
2528
+ ["broadcast-handler", "handler"],
2529
+ ["ordered-handler", "handler"],
2530
+ ["handler", "handler"],
2531
+ ["shutdown", "shutdown"]
2532
+ ];
2533
+ var UNMATCHED_SUBJECT_LABEL = "<unmatched>";
2534
+ var STREAM_KIND_LABEL = {
2535
+ ["ev" /* Event */]: "event",
2536
+ ["cmd" /* Command */]: "command",
2537
+ ["broadcast" /* Broadcast */]: "broadcast",
2538
+ ["ordered" /* Ordered */]: "ordered"
2539
+ };
2540
+
2541
+ // src/metrics/metrics.service.ts
2542
+ var import_common10 = require("@nestjs/common");
2543
+
2544
+ // src/metrics/error-context-mapper.ts
2545
+ var mapErrorContext = (context7) => {
2546
+ if (!context7) return "other";
2547
+ for (const [prefix, mapped] of ERROR_CONTEXT_PREFIXES) {
2548
+ if (context7 === prefix || context7.startsWith(`${prefix}:`)) {
2549
+ return mapped;
2550
+ }
2551
+ }
2552
+ return "other";
2553
+ };
2554
+
2555
+ // src/metrics/metrics.factory.ts
2556
+ var createMetrics = (opts) => {
2557
+ const { register, promClient } = opts;
2558
+ const prefix = opts.prefix ?? DEFAULT_METRICS_PREFIX;
2559
+ const buckets = {
2560
+ handlerDuration: opts.buckets?.handlerDuration ?? DEFAULT_HISTOGRAM_BUCKETS.handlerDuration,
2561
+ publishDuration: opts.buckets?.publishDuration ?? DEFAULT_HISTOGRAM_BUCKETS.publishDuration,
2562
+ rpcDuration: opts.buckets?.rpcDuration ?? DEFAULT_HISTOGRAM_BUCKETS.rpcDuration
2563
+ };
2564
+ if (opts.defaultLabels && Object.keys(opts.defaultLabels).length > 0) {
2565
+ register.setDefaultLabels(opts.defaultLabels);
2566
+ }
2567
+ const counter = (name, help, labelNames) => new promClient.Counter({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
2568
+ const histogram = (name, help, labelNames, bucketArr) => new promClient.Histogram({
2569
+ name: `${prefix}${name}`,
2570
+ help,
2571
+ labelNames,
2572
+ buckets: bucketArr,
2573
+ registers: [register]
2574
+ });
2575
+ const gauge = (name, help, labelNames) => new promClient.Gauge({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
2576
+ return {
2577
+ messagesReceivedTotal: counter(
2578
+ "messages_received_total",
2579
+ "Total messages routed to a handler.",
2580
+ ["stream", "subject", "kind"]
2581
+ ),
2582
+ messagesProcessedTotal: counter(
2583
+ "messages_processed_total",
2584
+ "Total messages whose handler completed.",
2585
+ ["stream", "subject", "kind", "status"]
2586
+ ),
2587
+ messagesUnhandledTotal: counter(
2588
+ "messages_unhandled_total",
2589
+ "Messages received but not matching any registered handler.",
2590
+ ["subject"]
2591
+ ),
2592
+ messagesDeadLetterTotal: counter(
2593
+ "messages_dead_letter_total",
2594
+ "Messages routed to dead-letter after exhausting redelivery attempts.",
2595
+ ["stream", "subject"]
2596
+ ),
2597
+ publishTotal: counter(
2598
+ "publish_total",
2599
+ "Total publish/send operations performed by the client.",
2600
+ ["subject", "kind", "status"]
2601
+ ),
2602
+ rpcTimeoutTotal: counter("rpc_timeout_total", "RPC calls that exceeded the timeout deadline.", [
2603
+ "subject"
2604
+ ]),
2605
+ consumerRecoveredTotal: counter(
2606
+ "consumer_recovered_total",
2607
+ "Self-healing recoveries after consume-loop failures.",
2608
+ ["kind"]
2609
+ ),
2610
+ errorsTotal: counter("errors_total", "Transport-level errors emitted on the EventBus.", [
2611
+ "context"
2612
+ ]),
2613
+ handlerDurationSeconds: histogram(
2614
+ "handler_duration_seconds",
2615
+ "Wall-clock duration of handler execution.",
2616
+ ["stream", "subject", "kind", "status"],
2617
+ buckets.handlerDuration
2618
+ ),
2619
+ publishDurationSeconds: histogram(
2620
+ "publish_duration_seconds",
2621
+ "Wall-clock duration of client publish/send operations.",
2622
+ ["subject", "kind", "status"],
2623
+ buckets.publishDuration
2624
+ ),
2625
+ rpcDurationSeconds: histogram(
2626
+ "rpc_duration_seconds",
2627
+ "Wall-clock duration of RPC round-trips from client perspective.",
2628
+ ["subject", "status"],
2629
+ buckets.rpcDuration
2630
+ ),
2631
+ consumerNumPending: gauge(
2632
+ "consumer_num_pending",
2633
+ "Messages not yet delivered to this consumer.",
2634
+ ["stream", "consumer", "kind"]
2635
+ ),
2636
+ consumerNumAckPending: gauge(
2637
+ "consumer_num_ack_pending",
2638
+ "Messages delivered but not yet acked.",
2639
+ ["stream", "consumer", "kind"]
2640
+ ),
2641
+ consumerNumRedelivered: gauge(
2642
+ "consumer_num_redelivered",
2643
+ "Messages currently in redelivery state.",
2644
+ ["stream", "consumer", "kind"]
2645
+ ),
2646
+ consumerNumWaiting: gauge(
2647
+ "consumer_num_waiting",
2648
+ "Pull-request waiting count for this consumer.",
2649
+ ["stream", "consumer", "kind"]
2650
+ ),
2651
+ streamMessages: gauge("stream_messages", "Total messages stored in this stream.", ["stream"]),
2652
+ streamBytes: gauge("stream_bytes", "Total bytes stored in this stream.", ["stream"]),
2653
+ connectionUp: gauge("connection_up", "NATS connection state (1 connected, 0 disconnected).", [
2654
+ "server"
2655
+ ]),
2656
+ metricsPollErrorsTotal: counter(
2657
+ "metrics_poll_errors_total",
2658
+ "Errors encountered while polling JetStreamManager for gauge data.",
2659
+ ["target"]
2660
+ )
2661
+ };
2662
+ };
2663
+
2664
+ // src/metrics/poll-runner.ts
2665
+ var import_common9 = require("@nestjs/common");
2666
+ var PollRunner = class {
2667
+ constructor(opts) {
2668
+ this.opts = opts;
2669
+ }
2670
+ logger = new import_common9.Logger("Jetstream:Metrics:Poll");
2671
+ timer = null;
2672
+ inFlight = null;
2673
+ start() {
2674
+ if (this.timer !== null) return;
2675
+ if (this.opts.intervalMs <= 0) return;
2676
+ if (this.opts.targets.length === 0) return;
2677
+ this.timer = setInterval(() => {
2678
+ if (this.inFlight !== null) {
2679
+ this.logger.warn("Skipping poll tick \u2014 previous cycle still in flight");
2680
+ return;
2681
+ }
2682
+ this.inFlight = this.tick().finally(() => {
2683
+ this.inFlight = null;
2684
+ });
2685
+ }, this.opts.intervalMs);
2686
+ }
2687
+ async stop() {
2688
+ if (this.timer !== null) {
2689
+ clearInterval(this.timer);
2690
+ this.timer = null;
2691
+ }
2692
+ if (this.inFlight !== null) await this.inFlight;
2693
+ }
2694
+ /** @internal Visible for tests. Runs one poll cycle. */
2695
+ async tick() {
2696
+ let jsm;
2697
+ try {
2698
+ jsm = await this.opts.jsmFactory();
2699
+ } catch {
2700
+ this.recordPollError("jsm.connect");
2701
+ return;
2702
+ }
2703
+ await Promise.all([this.pollConsumers(jsm), this.pollStreams(jsm)]);
2704
+ }
2705
+ async pollConsumers(jsm) {
2706
+ for (const target of this.opts.targets) {
2707
+ try {
2708
+ const info = await jsm.consumers.info(target.stream, target.consumer);
2709
+ const labels = {
2710
+ stream: target.stream,
2711
+ consumer: target.consumer,
2712
+ kind: STREAM_KIND_LABEL[target.kind]
2713
+ };
2714
+ this.opts.metrics.consumerNumPending.labels(labels).set(info.num_pending);
2715
+ this.opts.metrics.consumerNumAckPending.labels(labels).set(info.num_ack_pending);
2716
+ this.opts.metrics.consumerNumRedelivered.labels(labels).set(info.num_redelivered);
2717
+ this.opts.metrics.consumerNumWaiting.labels(labels).set(info.num_waiting);
2718
+ } catch {
2719
+ this.recordPollError("consumer.info");
2720
+ }
2721
+ }
2722
+ }
2723
+ async pollStreams(jsm) {
2724
+ const uniqueStreams = new Set(this.opts.targets.map((t) => t.stream));
2725
+ for (const stream of uniqueStreams) {
2726
+ try {
2727
+ const info = await jsm.streams.info(stream);
2728
+ this.opts.metrics.streamMessages.labels({ stream }).set(info.state.messages);
2729
+ this.opts.metrics.streamBytes.labels({ stream }).set(info.state.bytes);
2730
+ } catch {
2731
+ this.recordPollError("stream.info");
2732
+ }
2733
+ }
2734
+ }
2735
+ recordPollError(target) {
2736
+ this.opts.metrics.metricsPollErrorsTotal.labels({ target }).inc();
2737
+ }
2738
+ };
2739
+
2740
+ // src/metrics/metrics.service.ts
2741
+ var JetstreamMetricsService = class {
2742
+ constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
2743
+ this.eventBus = eventBus;
2744
+ this.config = config;
2745
+ this.promClient = promClient;
2746
+ this.options = options;
2747
+ this.patternRegistry = patternRegistry;
2748
+ this.connection = connection;
2749
+ }
2750
+ logger = new import_common10.Logger("Jetstream:Metrics");
2751
+ metrics = null;
2752
+ pollRunner = null;
2753
+ activeServers = /* @__PURE__ */ new Set();
2754
+ async onApplicationBootstrap() {
2755
+ if (this.metrics !== null) return;
2756
+ if (!this.options.metrics || !this.config || !this.promClient) return;
2757
+ if (!this.config.register) {
2758
+ throw new Error(
2759
+ "JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
2760
+ );
2761
+ }
2762
+ this.metrics = createMetrics({
2763
+ register: this.config.register,
2764
+ promClient: this.promClient,
2765
+ prefix: this.config.prefix,
2766
+ defaultLabels: this.config.defaultLabels,
2767
+ buckets: this.config.buckets
2768
+ });
2769
+ this.subscribeToEvents();
2770
+ this.syncInitialConnectionState();
2771
+ this.startPolling();
2772
+ this.logger.log(
2773
+ `Metrics enabled (prefix=${this.config.prefix ?? DEFAULT_METRICS_PREFIX}, poll=${this.getEffectivePollInterval()}ms)`
2774
+ );
2775
+ }
2776
+ async onModuleDestroy() {
2777
+ await this.pollRunner?.stop();
2778
+ this.pollRunner = null;
2779
+ }
2780
+ /** @internal Visible for tests. `0` disables polling. */
2781
+ getEffectivePollInterval() {
2782
+ return this.config?.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
2783
+ }
2784
+ /**
2785
+ * NATS connects during early bootstrap, before this service subscribes to
2786
+ * the EventBus — the initial `Connect` emission misses us. Mirror the
2787
+ * current state here so `connection_up` reflects reality the moment metrics
2788
+ * come online; later disconnects/reconnects update it normally.
2789
+ */
2790
+ syncInitialConnectionState() {
2791
+ const nc = this.connection?.unwrap;
2792
+ if (!nc) return;
2793
+ const server = nc.getServer();
2794
+ this.activeServers.add(server);
2795
+ this.metrics?.connectionUp.labels({ server }).set(1);
2796
+ }
2797
+ /** Skips polling for publisher-only deployments and when no kinds are active. */
2798
+ startPolling() {
2799
+ const interval = this.getEffectivePollInterval();
2800
+ const connection = this.connection;
2801
+ if (interval <= 0 || !this.patternRegistry || !connection || !this.metrics) return;
2802
+ const targets = this.buildPollTargets();
2803
+ if (targets.length === 0) return;
2804
+ this.pollRunner = new PollRunner({
2805
+ intervalMs: interval,
2806
+ jsmFactory: async () => connection.getJetStreamManager(),
2807
+ metrics: this.metrics,
2808
+ targets
2809
+ });
2810
+ this.pollRunner.start();
2811
+ }
2812
+ buildPollTargets() {
2813
+ const registry = this.patternRegistry;
2814
+ if (!registry) return [];
2815
+ const targets = [];
2816
+ if (registry.hasEventHandlers()) {
2817
+ targets.push({
2818
+ kind: "ev" /* Event */,
2819
+ stream: streamName(this.options.name, "ev" /* Event */),
2820
+ consumer: consumerName(this.options.name, "ev" /* Event */)
2821
+ });
2822
+ }
2823
+ if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
2824
+ targets.push({
2825
+ kind: "cmd" /* Command */,
2826
+ stream: streamName(this.options.name, "cmd" /* Command */),
2827
+ consumer: consumerName(this.options.name, "cmd" /* Command */)
2828
+ });
2829
+ }
2830
+ if (registry.hasBroadcastHandlers()) {
2831
+ targets.push({
2832
+ kind: "broadcast" /* Broadcast */,
2833
+ stream: streamName(this.options.name, "broadcast" /* Broadcast */),
2834
+ consumer: consumerName(this.options.name, "broadcast" /* Broadcast */)
2835
+ });
2836
+ }
2837
+ return targets;
2838
+ }
2839
+ subscribeToEvents() {
2840
+ this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
2841
+ this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
2842
+ this.eventBus.subscribe("reconnect" /* Reconnect */, this.onReconnect);
2843
+ this.eventBus.subscribe("error" /* Error */, this.onError);
2844
+ this.eventBus.subscribe("rpcTimeout" /* RpcTimeout */, this.onRpcTimeout);
2845
+ this.eventBus.subscribe("messageRouted" /* MessageRouted */, this.onMessageRouted);
2846
+ this.eventBus.subscribe("deadLetter" /* DeadLetter */, this.onDeadLetter);
2847
+ this.eventBus.subscribe("consumerRecovered" /* ConsumerRecovered */, this.onConsumerRecovered);
2848
+ this.eventBus.subscribe("handlerCompleted" /* HandlerCompleted */, this.onHandlerCompleted);
2849
+ this.eventBus.subscribe("published" /* Published */, this.onPublished);
2850
+ this.eventBus.subscribe("rpcCompleted" /* RpcCompleted */, this.onRpcCompleted);
2851
+ }
2852
+ onConnect = (server) => {
2853
+ this.activeServers.add(server);
2854
+ this.metrics?.connectionUp.labels({ server }).set(1);
2855
+ };
2856
+ onReconnect = (server) => {
2857
+ this.activeServers.add(server);
2858
+ this.metrics?.connectionUp.labels({ server }).set(1);
2859
+ };
2860
+ onDisconnect = () => {
2861
+ for (const server of this.activeServers) {
2862
+ this.metrics?.connectionUp.labels({ server }).set(0);
2863
+ }
2864
+ };
2865
+ onError = (_err, context7) => {
2866
+ this.metrics?.errorsTotal.labels({ context: mapErrorContext(context7) }).inc();
2867
+ };
2868
+ onRpcTimeout = (subject, _correlationId) => {
2869
+ const declared = this.resolveDeclared(subject);
2870
+ const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2871
+ this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
2872
+ };
2873
+ // `_kind` collapses broadcast/ordered into MessageKind.Event — we use
2874
+ // declared.kind from PatternRegistry for the precise label instead.
2875
+ onMessageRouted = (subject, _kind) => {
2876
+ if (!this.metrics) return;
2877
+ const declared = this.resolveDeclared(subject);
2878
+ if (!declared) {
2879
+ this.metrics.messagesUnhandledTotal.labels({ subject: UNMATCHED_SUBJECT_LABEL }).inc();
2880
+ return;
2881
+ }
2882
+ this.metrics.messagesReceivedTotal.labels({
2883
+ stream: streamName(this.options.name, declared.kind),
2884
+ subject: declared.pattern,
2885
+ kind: STREAM_KIND_LABEL[declared.kind]
2886
+ }).inc();
2887
+ };
2888
+ onDeadLetter = (info) => {
2889
+ const declared = this.resolveDeclared(info.subject);
2890
+ const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2891
+ this.metrics?.messagesDeadLetterTotal.labels({ stream: info.stream, subject: subjectLabel }).inc();
2892
+ };
2893
+ onConsumerRecovered = (label, _attempts) => {
2894
+ const kindLabel = STREAM_KIND_LABEL[label] ?? String(label);
2895
+ this.metrics?.consumerRecoveredTotal.labels({ kind: kindLabel }).inc();
2896
+ };
2897
+ onHandlerCompleted = (pattern, kind, durationMs, status) => {
2898
+ if (!this.metrics) return;
2899
+ const stream = streamName(this.options.name, kind);
2900
+ const kindLabel = STREAM_KIND_LABEL[kind];
2901
+ const labels = { stream, subject: pattern, kind: kindLabel, status };
2902
+ this.metrics.messagesProcessedTotal.labels(labels).inc();
2903
+ this.metrics.handlerDurationSeconds.labels(labels).observe(durationMs / 1e3);
2904
+ };
2905
+ onPublished = (pattern, kind, durationMs, status) => {
2906
+ if (!this.metrics) return;
2907
+ const labels = { subject: pattern, kind: STREAM_KIND_LABEL[kind], status };
2908
+ this.metrics.publishTotal.labels(labels).inc();
2909
+ this.metrics.publishDurationSeconds.labels(labels).observe(durationMs / 1e3);
2910
+ };
2911
+ onRpcCompleted = (pattern, durationMs, status) => {
2912
+ this.metrics?.rpcDurationSeconds.labels({ subject: pattern, status }).observe(durationMs / 1e3);
2913
+ };
2914
+ resolveDeclared(subject) {
2915
+ return this.patternRegistry?.resolveDeclared(subject) ?? null;
2225
2916
  }
2226
2917
  };
2227
- JetstreamHealthIndicator = __decorateClass([
2228
- (0, import_common7.Injectable)()
2229
- ], JetstreamHealthIndicator);
2918
+ JetstreamMetricsService = __decorateClass([
2919
+ (0, import_common10.Injectable)(),
2920
+ __decorateParam(1, (0, import_common10.Inject)(JETSTREAM_METRICS_CONFIG)),
2921
+ __decorateParam(2, (0, import_common10.Inject)(JETSTREAM_METRICS_PROM_CLIENT)),
2922
+ __decorateParam(3, (0, import_common10.Inject)(JETSTREAM_OPTIONS)),
2923
+ __decorateParam(4, (0, import_common10.Optional)()),
2924
+ __decorateParam(5, (0, import_common10.Optional)()),
2925
+ __decorateParam(5, (0, import_common10.Inject)(JETSTREAM_CONNECTION))
2926
+ ], JetstreamMetricsService);
2927
+
2928
+ // src/metrics/metrics.module.ts
2929
+ var PROM_CLIENT_INSTALL_MESSAGE = "prom-client is required when JetstreamModule.forRoot({ metrics: ... }) is enabled. Install it with: pnpm add prom-client";
2930
+ var resolvePromClient = async () => {
2931
+ try {
2932
+ return await import("prom-client");
2933
+ } catch {
2934
+ throw new Error(PROM_CLIENT_INSTALL_MESSAGE);
2935
+ }
2936
+ };
2937
+ var normalizeMetricsConfig = (option, promClient) => {
2938
+ const user = option && option !== true ? option : {};
2939
+ return {
2940
+ register: user.register ?? promClient.register,
2941
+ prefix: user.prefix ?? DEFAULT_METRICS_PREFIX,
2942
+ defaultLabels: user.defaultLabels,
2943
+ pollInterval: user.pollInterval ?? DEFAULT_POLL_INTERVAL_MS,
2944
+ buckets: user.buckets
2945
+ };
2946
+ };
2947
+ var JetstreamMetricsModule = class {
2948
+ static forFeature() {
2949
+ const promClientProvider = {
2950
+ provide: JETSTREAM_METRICS_PROM_CLIENT,
2951
+ inject: [JETSTREAM_OPTIONS],
2952
+ useFactory: async (opts) => {
2953
+ if (!opts.metrics) return null;
2954
+ const mod = await resolvePromClient();
2955
+ return { Counter: mod.Counter, Histogram: mod.Histogram, Gauge: mod.Gauge };
2956
+ }
2957
+ };
2958
+ const configProvider = {
2959
+ provide: JETSTREAM_METRICS_CONFIG,
2960
+ inject: [JETSTREAM_OPTIONS],
2961
+ useFactory: async (opts) => {
2962
+ if (!opts.metrics) return null;
2963
+ const mod = await resolvePromClient();
2964
+ return normalizeMetricsConfig(opts.metrics, mod);
2965
+ }
2966
+ };
2967
+ const registryProvider = {
2968
+ provide: JETSTREAM_METRICS_REGISTRY,
2969
+ inject: [JETSTREAM_METRICS_CONFIG],
2970
+ useFactory: (cfg) => cfg?.register ?? null
2971
+ };
2972
+ const serviceProvider = {
2973
+ provide: JetstreamMetricsService,
2974
+ inject: [
2975
+ JETSTREAM_EVENT_BUS,
2976
+ JETSTREAM_METRICS_CONFIG,
2977
+ JETSTREAM_METRICS_PROM_CLIENT,
2978
+ JETSTREAM_OPTIONS,
2979
+ { token: PatternRegistry, optional: true },
2980
+ { token: JETSTREAM_CONNECTION, optional: true }
2981
+ ],
2982
+ useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(eventBus, cfg, runtime, opts, patternRegistry, connection)
2983
+ };
2984
+ return {
2985
+ module: JetstreamMetricsModule,
2986
+ providers: [promClientProvider, configProvider, registryProvider, serviceProvider],
2987
+ exports: [
2988
+ JetstreamMetricsService,
2989
+ JETSTREAM_METRICS_CONFIG,
2990
+ JETSTREAM_METRICS_REGISTRY,
2991
+ JETSTREAM_METRICS_PROM_CLIENT
2992
+ ]
2993
+ };
2994
+ }
2995
+ };
2996
+ JetstreamMetricsModule = __decorateClass([
2997
+ (0, import_common11.Module)({})
2998
+ ], JetstreamMetricsModule);
2230
2999
 
2231
3000
  // src/server/strategy.ts
2232
3001
  var import_microservices2 = require("@nestjs/microservices");
@@ -2302,6 +3071,26 @@ var JetstreamStrategy = class extends import_microservices2.Server {
2302
3071
  this.messageProvider.destroy();
2303
3072
  this.started = false;
2304
3073
  }
3074
+ /**
3075
+ * Override NestJS `Server.addHandler` to fail-fast on duplicate pattern registration.
3076
+ *
3077
+ * The base class silently overwrites duplicate RPC handlers (last wins) and appends
3078
+ * duplicate event handlers to a linked list. Both behaviors are hazardous in a
3079
+ * JetStream context: silent overwrite drops a handler the user wrote, and double
3080
+ * event dispatch double-acks/double-processes the same JetStream message.
3081
+ *
3082
+ * We treat any pattern collision as a fatal misconfiguration so it surfaces at
3083
+ * bootstrap instead of in production traffic.
3084
+ */
3085
+ addHandler(pattern, callback, isEventHandler = false, extras = {}) {
3086
+ const normalizedPattern = this.normalizePattern(pattern);
3087
+ if (this.messageHandlers.has(normalizedPattern)) {
3088
+ throw new Error(
3089
+ `Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice \u2014 find and remove the second declaration.`
3090
+ );
3091
+ }
3092
+ super.addHandler(pattern, callback, isEventHandler, extras);
3093
+ }
2305
3094
  /**
2306
3095
  * Register event listener (required by Server base class).
2307
3096
  *
@@ -2374,7 +3163,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
2374
3163
  };
2375
3164
 
2376
3165
  // src/server/core-rpc.server.ts
2377
- var import_common8 = require("@nestjs/common");
3166
+ var import_common12 = require("@nestjs/common");
2378
3167
  var import_transport_node3 = require("@nats-io/transport-node");
2379
3168
 
2380
3169
  // src/context/rpc.context.ts
@@ -2643,7 +3432,7 @@ var CoreRpcServer = class {
2643
3432
  this.serviceName = derived.serviceName;
2644
3433
  this.serverEndpoint = derived.serverEndpoint;
2645
3434
  }
2646
- logger = new import_common8.Logger("Jetstream:CoreRpc");
3435
+ logger = new import_common12.Logger("Jetstream:CoreRpc");
2647
3436
  subscription = null;
2648
3437
  otel;
2649
3438
  serviceName;
@@ -2696,6 +3485,7 @@ var CoreRpcServer = class {
2696
3485
  return;
2697
3486
  }
2698
3487
  const ctx = new RpcContext([msg]);
3488
+ const startedAt = performance.now();
2699
3489
  try {
2700
3490
  const raw = await withConsumeSpan(
2701
3491
  {
@@ -2714,6 +3504,7 @@ var CoreRpcServer = class {
2714
3504
  }
2715
3505
  );
2716
3506
  msg.respond(this.codec.encode(raw));
3507
+ this.reportHandlerCompleted(msg, startedAt, "success");
2717
3508
  } catch (err) {
2718
3509
  this.eventBus.emit(
2719
3510
  "error" /* Error */,
@@ -2721,7 +3512,23 @@ var CoreRpcServer = class {
2721
3512
  `core-rpc-handler:${msg.subject}`
2722
3513
  );
2723
3514
  this.respondWithError(msg, err);
2724
- }
3515
+ this.reportHandlerCompleted(msg, startedAt, "error");
3516
+ }
3517
+ }
3518
+ // See EventRouter.reportHandlerCompleted for the rationale on declared
3519
+ // pattern + per-emit hasHook check.
3520
+ reportHandlerCompleted(msg, startedAt, status) {
3521
+ if (!this.eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
3522
+ const declared = this.patternRegistry.resolveDeclared(msg.subject);
3523
+ const pattern = declared?.pattern ?? msg.subject;
3524
+ const kind = declared?.kind ?? "cmd" /* Command */;
3525
+ this.eventBus.emit(
3526
+ "handlerCompleted" /* HandlerCompleted */,
3527
+ pattern,
3528
+ kind,
3529
+ performance.now() - startedAt,
3530
+ status
3531
+ );
2725
3532
  }
2726
3533
  /** Send an error response back to the caller with x-error header. */
2727
3534
  respondWithError(msg, error) {
@@ -2736,8 +3543,8 @@ var CoreRpcServer = class {
2736
3543
  };
2737
3544
 
2738
3545
  // src/server/infrastructure/stream.provider.ts
2739
- var import_common10 = require("@nestjs/common");
2740
- var import_jetstream14 = require("@nats-io/jetstream");
3546
+ var import_common14 = require("@nestjs/common");
3547
+ var import_jetstream17 = require("@nats-io/jetstream");
2741
3548
 
2742
3549
  // src/server/infrastructure/nats-error-codes.ts
2743
3550
  var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
@@ -2803,8 +3610,8 @@ var isEqual = (a, b) => {
2803
3610
  };
2804
3611
 
2805
3612
  // src/server/infrastructure/stream-migration.ts
2806
- var import_common9 = require("@nestjs/common");
2807
- var import_jetstream13 = require("@nats-io/jetstream");
3613
+ var import_common13 = require("@nestjs/common");
3614
+ var import_jetstream16 = require("@nats-io/jetstream");
2808
3615
  var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
2809
3616
  var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
2810
3617
  var SOURCING_POLL_INTERVAL_MS = 100;
@@ -2812,7 +3619,7 @@ var StreamMigration = class {
2812
3619
  constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
2813
3620
  this.sourcingTimeoutMs = sourcingTimeoutMs;
2814
3621
  }
2815
- logger = new import_common9.Logger("Jetstream:Stream");
3622
+ logger = new import_common13.Logger("Jetstream:Stream");
2816
3623
  async migrate(jsm, streamName2, newConfig) {
2817
3624
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
2818
3625
  const startTime = Date.now();
@@ -2881,7 +3688,7 @@ var StreamMigration = class {
2881
3688
  this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
2882
3689
  await jsm.streams.delete(backupName);
2883
3690
  } catch (err) {
2884
- if (err instanceof import_jetstream13.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3691
+ if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
2885
3692
  return;
2886
3693
  }
2887
3694
  throw err;
@@ -2899,7 +3706,7 @@ var StreamProvider = class {
2899
3706
  this.otelServiceName = derived.serviceName;
2900
3707
  this.otelEndpoint = derived.serverEndpoint;
2901
3708
  }
2902
- logger = new import_common10.Logger("Jetstream:Stream");
3709
+ logger = new import_common14.Logger("Jetstream:Stream");
2903
3710
  migration = new StreamMigration();
2904
3711
  otel;
2905
3712
  otelServiceName;
@@ -2964,7 +3771,7 @@ var StreamProvider = class {
2964
3771
  const currentInfo = await jsm.streams.info(config.name);
2965
3772
  return await this.handleExistingStream(jsm, currentInfo, config);
2966
3773
  } catch (err) {
2967
- if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3774
+ if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
2968
3775
  this.logger.log(`Creating stream: ${config.name}`);
2969
3776
  return await jsm.streams.add(config);
2970
3777
  }
@@ -2991,7 +3798,7 @@ var StreamProvider = class {
2991
3798
  const currentInfo = await jsm.streams.info(config.name);
2992
3799
  return await this.handleExistingStream(jsm, currentInfo, config);
2993
3800
  } catch (err) {
2994
- if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3801
+ if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
2995
3802
  this.logger.log(`Creating DLQ stream: ${config.name}`);
2996
3803
  return await jsm.streams.add(config);
2997
3804
  }
@@ -3154,8 +3961,8 @@ var StreamProvider = class {
3154
3961
  };
3155
3962
 
3156
3963
  // src/server/infrastructure/consumer.provider.ts
3157
- var import_common11 = require("@nestjs/common");
3158
- var import_jetstream16 = require("@nats-io/jetstream");
3964
+ var import_common15 = require("@nestjs/common");
3965
+ var import_jetstream19 = require("@nats-io/jetstream");
3159
3966
  var ConsumerProvider = class {
3160
3967
  constructor(options, connection, streamProvider, patternRegistry) {
3161
3968
  this.options = options;
@@ -3167,7 +3974,7 @@ var ConsumerProvider = class {
3167
3974
  this.otelServiceName = derived.serviceName;
3168
3975
  this.otelEndpoint = derived.serverEndpoint;
3169
3976
  }
3170
- logger = new import_common11.Logger("Jetstream:Consumer");
3977
+ logger = new import_common15.Logger("Jetstream:Consumer");
3171
3978
  otel;
3172
3979
  otelServiceName;
3173
3980
  otelEndpoint;
@@ -3216,7 +4023,7 @@ var ConsumerProvider = class {
3216
4023
  this.logger.debug(`Consumer exists, updating: ${name}`);
3217
4024
  return await jsm.consumers.update(stream, name, config);
3218
4025
  } catch (err) {
3219
- if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4026
+ if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
3220
4027
  throw err;
3221
4028
  }
3222
4029
  return await this.createConsumer(jsm, stream, name, config);
@@ -3256,7 +4063,7 @@ var ConsumerProvider = class {
3256
4063
  try {
3257
4064
  return await jsm.consumers.info(stream, name);
3258
4065
  } catch (err) {
3259
- if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4066
+ if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
3260
4067
  throw err;
3261
4068
  }
3262
4069
  return await this.createConsumer(jsm, stream, name, config);
@@ -3277,7 +4084,7 @@ var ConsumerProvider = class {
3277
4084
  `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
3278
4085
  );
3279
4086
  } catch (err) {
3280
- if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4087
+ if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3281
4088
  return;
3282
4089
  }
3283
4090
  throw err;
@@ -3291,7 +4098,7 @@ var ConsumerProvider = class {
3291
4098
  try {
3292
4099
  return await jsm.consumers.add(stream, config);
3293
4100
  } catch (addErr) {
3294
- if (addErr instanceof import_jetstream16.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
4101
+ if (addErr instanceof import_jetstream19.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
3295
4102
  this.logger.debug(`Consumer ${name} created by another pod, using existing`);
3296
4103
  return await jsm.consumers.info(stream, name);
3297
4104
  }
@@ -3378,8 +4185,8 @@ var ConsumerProvider = class {
3378
4185
  };
3379
4186
 
3380
4187
  // src/server/infrastructure/message.provider.ts
3381
- var import_common12 = require("@nestjs/common");
3382
- var import_jetstream18 = require("@nats-io/jetstream");
4188
+ var import_common16 = require("@nestjs/common");
4189
+ var import_jetstream21 = require("@nats-io/jetstream");
3383
4190
  var import_rxjs3 = require("rxjs");
3384
4191
  var MessageProvider = class {
3385
4192
  constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
@@ -3388,7 +4195,7 @@ var MessageProvider = class {
3388
4195
  this.consumeOptionsMap = consumeOptionsMap;
3389
4196
  this.consumerRecoveryFn = consumerRecoveryFn;
3390
4197
  }
3391
- logger = new import_common12.Logger("Jetstream:Message");
4198
+ logger = new import_common16.Logger("Jetstream:Message");
3392
4199
  activeIterators = /* @__PURE__ */ new Set();
3393
4200
  orderedReadyResolve = null;
3394
4201
  orderedReadyReject = null;
@@ -3440,7 +4247,7 @@ var MessageProvider = class {
3440
4247
  */
3441
4248
  async startOrdered(streamName2, filterSubjects, orderedConfig) {
3442
4249
  const consumerOpts = { filter_subjects: filterSubjects };
3443
- if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream18.DeliverPolicy.All) {
4250
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream21.DeliverPolicy.All) {
3444
4251
  consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
3445
4252
  }
3446
4253
  if (orderedConfig?.optStartSeq !== void 0) {
@@ -3486,10 +4293,13 @@ var MessageProvider = class {
3486
4293
  /** Create a self-healing consumer flow for a specific kind. */
3487
4294
  createFlow(kind, info) {
3488
4295
  const target$ = this.getTargetSubject(kind);
3489
- return this.createSelfHealingFlow(() => this.consumeOnce(kind, info, target$), info.name);
4296
+ return this.createSelfHealingFlow(
4297
+ (onConnected) => this.consumeOnce(kind, info, target$, onConnected),
4298
+ info.name
4299
+ );
3490
4300
  }
3491
4301
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
3492
- async consumeOnce(kind, info, target$) {
4302
+ async consumeOnce(kind, info, target$, onConnected) {
3493
4303
  const js = this.connection.getJetStreamClient();
3494
4304
  let consumer;
3495
4305
  let consumerName2 = info.name;
@@ -3517,6 +4327,7 @@ var MessageProvider = class {
3517
4327
  });
3518
4328
  this.activeIterators.add(messages);
3519
4329
  this.monitorConsumerHealth(messages, consumerName2);
4330
+ onConnected();
3520
4331
  try {
3521
4332
  await messages.closed();
3522
4333
  } finally {
@@ -3571,7 +4382,7 @@ var MessageProvider = class {
3571
4382
  /** Create a self-healing ordered consumer flow. */
3572
4383
  createOrderedFlow(streamName2, consumerOpts) {
3573
4384
  return this.createSelfHealingFlow(
3574
- () => this.consumeOrderedOnce(streamName2, consumerOpts),
4385
+ (onConnected) => this.consumeOrderedOnce(streamName2, consumerOpts, onConnected),
3575
4386
  "ordered" /* Ordered */,
3576
4387
  (err) => {
3577
4388
  if (this.orderedReadyReject) {
@@ -3585,10 +4396,15 @@ var MessageProvider = class {
3585
4396
  /** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
3586
4397
  createSelfHealingFlow(source, label, onFirstError) {
3587
4398
  let consecutiveFailures = 0;
3588
- return (0, import_rxjs3.defer)(source).pipe(
3589
- (0, import_rxjs3.tap)(() => {
3590
- consecutiveFailures = 0;
3591
- }),
4399
+ const onConnected = () => {
4400
+ if (consecutiveFailures > 0) {
4401
+ const attempts = consecutiveFailures;
4402
+ this.logger.log(`Consumer ${label} recovered after ${attempts} failed attempt(s)`);
4403
+ this.eventBus.emit("consumerRecovered" /* ConsumerRecovered */, label, attempts);
4404
+ }
4405
+ consecutiveFailures = 0;
4406
+ };
4407
+ return (0, import_rxjs3.defer)(() => source(onConnected)).pipe(
3592
4408
  (0, import_rxjs3.catchError)((err) => {
3593
4409
  consecutiveFailures++;
3594
4410
  this.logger.error(`Consumer ${label} error, will restart:`, err);
@@ -3611,7 +4427,7 @@ var MessageProvider = class {
3611
4427
  );
3612
4428
  }
3613
4429
  /** Single iteration: create ordered consumer -> push messages into the subject. */
3614
- async consumeOrderedOnce(streamName2, consumerOpts) {
4430
+ async consumeOrderedOnce(streamName2, consumerOpts, onConnected) {
3615
4431
  const js = this.connection.getJetStreamClient();
3616
4432
  const consumer = await js.consumers.get(streamName2, consumerOpts);
3617
4433
  const orderedMessages$ = this.orderedMessages$;
@@ -3626,6 +4442,7 @@ var MessageProvider = class {
3626
4442
  this.orderedReadyReject = null;
3627
4443
  }
3628
4444
  this.activeIterators.add(messages);
4445
+ onConnected();
3629
4446
  try {
3630
4447
  await messages.closed();
3631
4448
  } finally {
@@ -3635,7 +4452,7 @@ var MessageProvider = class {
3635
4452
  };
3636
4453
 
3637
4454
  // src/server/infrastructure/metadata.provider.ts
3638
- var import_common13 = require("@nestjs/common");
4455
+ var import_common17 = require("@nestjs/common");
3639
4456
  var import_kv = require("@nats-io/kv");
3640
4457
  var MetadataProvider = class {
3641
4458
  constructor(options, connection) {
@@ -3644,7 +4461,7 @@ var MetadataProvider = class {
3644
4461
  this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
3645
4462
  this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
3646
4463
  }
3647
- logger = new import_common13.Logger("Jetstream:Metadata");
4464
+ logger = new import_common17.Logger("Jetstream:Metadata");
3648
4465
  bucketName;
3649
4466
  replicas;
3650
4467
  ttl;
@@ -3736,176 +4553,8 @@ var MetadataProvider = class {
3736
4553
  }
3737
4554
  };
3738
4555
 
3739
- // src/server/routing/pattern-registry.ts
3740
- var import_common14 = require("@nestjs/common");
3741
- var HANDLER_LABELS = {
3742
- ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
3743
- ["ordered" /* Ordered */]: "ordered" /* Ordered */,
3744
- ["ev" /* Event */]: "event" /* Event */,
3745
- ["cmd" /* Command */]: "rpc" /* Rpc */
3746
- };
3747
- var PatternRegistry = class {
3748
- constructor(options) {
3749
- this.options = options;
3750
- }
3751
- logger = new import_common14.Logger("Jetstream:PatternRegistry");
3752
- registry = /* @__PURE__ */ new Map();
3753
- // Cached after registerHandlers() — the registry is immutable from that point
3754
- cachedPatterns = null;
3755
- _hasEvents = false;
3756
- _hasCommands = false;
3757
- _hasBroadcasts = false;
3758
- _hasOrdered = false;
3759
- _hasMetadata = false;
3760
- /**
3761
- * Register all handlers from the NestJS strategy.
3762
- *
3763
- * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
3764
- */
3765
- registerHandlers(handlers) {
3766
- const serviceName = this.options.name;
3767
- for (const [pattern, handler] of handlers) {
3768
- const extras = handler.extras;
3769
- const isEvent = handler.isEventHandler ?? false;
3770
- const isBroadcast = !!extras?.broadcast;
3771
- const isOrdered = !!extras?.ordered;
3772
- const meta = extras?.meta;
3773
- if (isBroadcast && isOrdered) {
3774
- throw new Error(
3775
- `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
3776
- );
3777
- }
3778
- let kind;
3779
- if (isBroadcast) kind = "broadcast" /* Broadcast */;
3780
- else if (isOrdered) kind = "ordered" /* Ordered */;
3781
- else if (isEvent) kind = "ev" /* Event */;
3782
- else kind = "cmd" /* Command */;
3783
- const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
3784
- this.registry.set(fullSubject, {
3785
- handler,
3786
- pattern,
3787
- isEvent: isEvent && !isOrdered,
3788
- isBroadcast,
3789
- isOrdered,
3790
- meta
3791
- });
3792
- this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
3793
- }
3794
- this.cachedPatterns = this.buildPatternsByKind();
3795
- this._hasEvents = this.cachedPatterns.events.length > 0;
3796
- this._hasCommands = this.cachedPatterns.commands.length > 0;
3797
- this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
3798
- this._hasOrdered = this.cachedPatterns.ordered.length > 0;
3799
- this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
3800
- this.logSummary();
3801
- }
3802
- /** Find handler for a full NATS subject. */
3803
- getHandler(subject) {
3804
- return this.registry.get(subject)?.handler ?? null;
3805
- }
3806
- /** Get all registered broadcast patterns (for consumer filter_subject setup). */
3807
- getBroadcastPatterns() {
3808
- return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
3809
- }
3810
- hasBroadcastHandlers() {
3811
- return this._hasBroadcasts;
3812
- }
3813
- hasRpcHandlers() {
3814
- return this._hasCommands;
3815
- }
3816
- hasEventHandlers() {
3817
- return this._hasEvents;
3818
- }
3819
- hasOrderedHandlers() {
3820
- return this._hasOrdered;
3821
- }
3822
- /** Get fully-qualified NATS subjects for ordered handlers. */
3823
- getOrderedSubjects() {
3824
- return this.getPatternsByKind().ordered.map(
3825
- (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
3826
- );
3827
- }
3828
- /** Check if any registered handler has metadata. */
3829
- hasMetadata() {
3830
- return this._hasMetadata;
3831
- }
3832
- /**
3833
- * Get handler metadata entries for KV publishing.
3834
- *
3835
- * Returns a map of KV key -> metadata object for all handlers that have `meta`.
3836
- * Key format: `{serviceName}.{kind}.{pattern}`.
3837
- */
3838
- getMetadataEntries() {
3839
- const entries = /* @__PURE__ */ new Map();
3840
- for (const entry of this.registry.values()) {
3841
- if (!entry.meta) continue;
3842
- const kind = this.resolveStreamKind(entry);
3843
- const key = metadataKey(this.options.name, kind, entry.pattern);
3844
- entries.set(key, entry.meta);
3845
- }
3846
- return entries;
3847
- }
3848
- /** Get patterns grouped by kind (cached after registration). */
3849
- getPatternsByKind() {
3850
- const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
3851
- return {
3852
- events: [...patterns.events],
3853
- commands: [...patterns.commands],
3854
- broadcasts: [...patterns.broadcasts],
3855
- ordered: [...patterns.ordered]
3856
- };
3857
- }
3858
- /** Normalize a full NATS subject back to the user-facing pattern. */
3859
- normalizeSubject(subject) {
3860
- const name = internalName(this.options.name);
3861
- const prefixes = [
3862
- `${name}.${"cmd" /* Command */}.`,
3863
- `${name}.${"ev" /* Event */}.`,
3864
- `${name}.${"ordered" /* Ordered */}.`,
3865
- `${"broadcast" /* Broadcast */}.`
3866
- ];
3867
- for (const prefix of prefixes) {
3868
- if (subject.startsWith(prefix)) {
3869
- return subject.slice(prefix.length);
3870
- }
3871
- }
3872
- return subject;
3873
- }
3874
- buildPatternsByKind() {
3875
- const events = [];
3876
- const commands = [];
3877
- const broadcasts = [];
3878
- const ordered = [];
3879
- for (const entry of this.registry.values()) {
3880
- if (entry.isBroadcast) broadcasts.push(entry.pattern);
3881
- else if (entry.isOrdered) ordered.push(entry.pattern);
3882
- else if (entry.isEvent) events.push(entry.pattern);
3883
- else commands.push(entry.pattern);
3884
- }
3885
- return { events, commands, broadcasts, ordered };
3886
- }
3887
- resolveStreamKind(entry) {
3888
- if (entry.isBroadcast) return "broadcast" /* Broadcast */;
3889
- if (entry.isOrdered) return "ordered" /* Ordered */;
3890
- if (entry.isEvent) return "ev" /* Event */;
3891
- return "cmd" /* Command */;
3892
- }
3893
- logSummary() {
3894
- const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
3895
- const parts = [
3896
- `${commands.length} RPC`,
3897
- `${events.length} events`,
3898
- `${broadcasts.length} broadcasts`
3899
- ];
3900
- if (ordered.length > 0) {
3901
- parts.push(`${ordered.length} ordered`);
3902
- }
3903
- this.logger.log(`Registered handlers: ${parts.join(", ")}`);
3904
- }
3905
- };
3906
-
3907
4556
  // src/server/routing/event.router.ts
3908
- var import_common15 = require("@nestjs/common");
4557
+ var import_common18 = require("@nestjs/common");
3909
4558
  var import_transport_node4 = require("@nats-io/transport-node");
3910
4559
  var eventConsumeKindFor = (kind) => {
3911
4560
  if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
@@ -3934,7 +4583,7 @@ var EventRouter = class {
3934
4583
  this.serverEndpoint = null;
3935
4584
  }
3936
4585
  }
3937
- logger = new import_common15.Logger("Jetstream:EventRouter");
4586
+ logger = new import_common18.Logger("Jetstream:EventRouter");
3938
4587
  subscriptions = [];
3939
4588
  otel;
3940
4589
  serviceName;
@@ -3978,7 +4627,14 @@ var EventRouter = class {
3978
4627
  const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
3979
4628
  const concurrency = this.getConcurrency(kind);
3980
4629
  const hasDlqCheck = deadLetterConfig !== void 0;
3981
- const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
4630
+ const reportHandlerCompleted = (msg, startedAt, status) => {
4631
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
4632
+ const declared = patternRegistry.resolveDeclared(msg.subject);
4633
+ const pattern = declared?.pattern ?? msg.subject;
4634
+ const declaredKind = declared?.kind ?? kind;
4635
+ const durationMs = performance.now() - startedAt;
4636
+ eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
4637
+ };
3982
4638
  const isDeadLetter = (msg) => {
3983
4639
  if (!hasDlqCheck) return false;
3984
4640
  const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
@@ -4015,7 +4671,7 @@ var EventRouter = class {
4015
4671
  logger5.error(`Decode error for ${subject}:`, err);
4016
4672
  return null;
4017
4673
  }
4018
- if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
4674
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
4019
4675
  return { handler, data };
4020
4676
  } catch (err) {
4021
4677
  logger5.error(`Unexpected error in ${kind} event router`, err);
@@ -4027,12 +4683,18 @@ var EventRouter = class {
4027
4683
  return null;
4028
4684
  }
4029
4685
  };
4686
+ const statusForContext = (ctx) => {
4687
+ if (ctx.shouldTerminate) return "terminated";
4688
+ if (ctx.shouldRetry) return "retried";
4689
+ return "success";
4690
+ };
4030
4691
  const handleSafe = (msg) => {
4031
4692
  const resolved = resolveEvent(msg);
4032
4693
  if (resolved === null) return void 0;
4033
4694
  const { handler, data } = resolved;
4034
4695
  const ctx = new RpcContext([msg]);
4035
4696
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
4697
+ const startedAt = performance.now();
4036
4698
  let pending;
4037
4699
  try {
4038
4700
  pending = withConsumeSpan(
@@ -4055,18 +4717,21 @@ var EventRouter = class {
4055
4717
  err instanceof Error ? err : new Error(String(err)),
4056
4718
  `${kind}-handler:${msg.subject}`
4057
4719
  );
4720
+ reportHandlerCompleted(msg, startedAt, "error");
4058
4721
  return settleFailure(msg, data, err).finally(() => {
4059
4722
  if (stopAckExtension !== null) stopAckExtension();
4060
4723
  });
4061
4724
  }
4062
4725
  if (!isPromiseLike2(pending)) {
4063
4726
  settleSuccess(msg, ctx);
4727
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4064
4728
  if (stopAckExtension !== null) stopAckExtension();
4065
4729
  return void 0;
4066
4730
  }
4067
4731
  return pending.then(
4068
4732
  () => {
4069
4733
  settleSuccess(msg, ctx);
4734
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4070
4735
  if (stopAckExtension !== null) stopAckExtension();
4071
4736
  },
4072
4737
  async (err) => {
@@ -4075,6 +4740,7 @@ var EventRouter = class {
4075
4740
  err instanceof Error ? err : new Error(String(err)),
4076
4741
  `${kind}-handler:${msg.subject}`
4077
4742
  );
4743
+ reportHandlerCompleted(msg, startedAt, "error");
4078
4744
  try {
4079
4745
  await settleFailure(msg, data, err);
4080
4746
  } finally {
@@ -4099,7 +4765,7 @@ var EventRouter = class {
4099
4765
  logger5.error(`Decode error for ${subject}:`, err);
4100
4766
  return void 0;
4101
4767
  }
4102
- if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
4768
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
4103
4769
  } catch (err) {
4104
4770
  logger5.error(`Ordered handler error (${subject}):`, err);
4105
4771
  return void 0;
@@ -4112,6 +4778,7 @@ var EventRouter = class {
4112
4778
  );
4113
4779
  }
4114
4780
  };
4781
+ const startedAt = performance.now();
4115
4782
  let pending;
4116
4783
  try {
4117
4784
  pending = withConsumeSpan(
@@ -4130,15 +4797,24 @@ var EventRouter = class {
4130
4797
  );
4131
4798
  } catch (err) {
4132
4799
  logger5.error(`Ordered handler error (${subject}):`, err);
4800
+ reportHandlerCompleted(msg, startedAt, "error");
4133
4801
  return void 0;
4134
4802
  }
4135
4803
  if (!isPromiseLike2(pending)) {
4136
4804
  warnIfSettlementAttempted();
4805
+ reportHandlerCompleted(msg, startedAt, "success");
4137
4806
  return void 0;
4138
4807
  }
4139
- return pending.then(warnIfSettlementAttempted, (err) => {
4140
- logger5.error(`Ordered handler error (${subject}):`, err);
4141
- });
4808
+ return pending.then(
4809
+ () => {
4810
+ warnIfSettlementAttempted();
4811
+ reportHandlerCompleted(msg, startedAt, "success");
4812
+ },
4813
+ (err) => {
4814
+ logger5.error(`Ordered handler error (${subject}):`, err);
4815
+ reportHandlerCompleted(msg, startedAt, "error");
4816
+ }
4817
+ );
4142
4818
  };
4143
4819
  const route = isOrdered ? handleOrderedSafe : handleSafe;
4144
4820
  const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
@@ -4324,7 +5000,7 @@ var EventRouter = class {
4324
5000
  };
4325
5001
 
4326
5002
  // src/server/routing/rpc.router.ts
4327
- var import_common16 = require("@nestjs/common");
5003
+ var import_common19 = require("@nestjs/common");
4328
5004
  var import_transport_node5 = require("@nats-io/transport-node");
4329
5005
  var RpcRouter = class {
4330
5006
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
@@ -4348,7 +5024,7 @@ var RpcRouter = class {
4348
5024
  this.serverEndpoint = null;
4349
5025
  }
4350
5026
  }
4351
- logger = new import_common16.Logger("Jetstream:RpcRouter");
5027
+ logger = new import_common19.Logger("Jetstream:RpcRouter");
4352
5028
  timeout;
4353
5029
  concurrency;
4354
5030
  resolvedAckExtensionInterval;
@@ -4384,6 +5060,19 @@ var RpcRouter = class {
4384
5060
  const emitRpcTimeout = (subject, correlationId) => {
4385
5061
  eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
4386
5062
  };
5063
+ const reportHandlerCompleted = (msg, startedAt, status) => {
5064
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
5065
+ const declared = patternRegistry.resolveDeclared(msg.subject);
5066
+ const pattern = declared?.pattern ?? msg.subject;
5067
+ const declaredKind = declared?.kind ?? "cmd" /* Command */;
5068
+ eventBus.emit(
5069
+ "handlerCompleted" /* HandlerCompleted */,
5070
+ pattern,
5071
+ declaredKind,
5072
+ performance.now() - startedAt,
5073
+ status
5074
+ );
5075
+ };
4387
5076
  const publishReply = (replyTo, correlationId, payload) => {
4388
5077
  try {
4389
5078
  const hdrs = (0, import_transport_node5.headers)();
@@ -4447,6 +5136,7 @@ var RpcRouter = class {
4447
5136
  const subject = msg.subject;
4448
5137
  const ctx = new RpcContext([msg]);
4449
5138
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
5139
+ const startedAt = performance.now();
4450
5140
  const reportHandlerError = (err) => {
4451
5141
  eventBus.emit(
4452
5142
  "error" /* Error */,
@@ -4477,12 +5167,14 @@ var RpcRouter = class {
4477
5167
  } catch (err) {
4478
5168
  if (stopAckExtension !== null) stopAckExtension();
4479
5169
  reportHandlerError(err);
5170
+ reportHandlerCompleted(msg, startedAt, "error");
4480
5171
  return void 0;
4481
5172
  }
4482
5173
  if (!isPromiseLike2(pending)) {
4483
5174
  if (stopAckExtension !== null) stopAckExtension();
4484
5175
  msg.ack();
4485
5176
  publishReply(replyTo, correlationId, pending);
5177
+ reportHandlerCompleted(msg, startedAt, "success");
4486
5178
  return void 0;
4487
5179
  }
4488
5180
  let settled = false;
@@ -4493,6 +5185,7 @@ var RpcRouter = class {
4493
5185
  abortController.abort();
4494
5186
  emitRpcTimeout(subject, correlationId);
4495
5187
  msg.term("Handler timeout");
5188
+ reportHandlerCompleted(msg, startedAt, "terminated");
4496
5189
  }, timeout);
4497
5190
  return pending.then(
4498
5191
  (result) => {
@@ -4502,6 +5195,7 @@ var RpcRouter = class {
4502
5195
  if (stopAckExtension !== null) stopAckExtension();
4503
5196
  msg.ack();
4504
5197
  publishReply(replyTo, correlationId, result);
5198
+ reportHandlerCompleted(msg, startedAt, "success");
4505
5199
  },
4506
5200
  (err) => {
4507
5201
  if (settled) return;
@@ -4509,6 +5203,7 @@ var RpcRouter = class {
4509
5203
  clearTimeout(timeoutId);
4510
5204
  if (stopAckExtension !== null) stopAckExtension();
4511
5205
  reportHandlerError(err);
5206
+ reportHandlerCompleted(msg, startedAt, "error");
4512
5207
  }
4513
5208
  );
4514
5209
  };
@@ -4568,14 +5263,14 @@ var RpcRouter = class {
4568
5263
  };
4569
5264
 
4570
5265
  // src/shutdown/shutdown.manager.ts
4571
- var import_common17 = require("@nestjs/common");
5266
+ var import_common20 = require("@nestjs/common");
4572
5267
  var ShutdownManager = class {
4573
5268
  constructor(connection, eventBus, timeout) {
4574
5269
  this.connection = connection;
4575
5270
  this.eventBus = eventBus;
4576
5271
  this.timeout = timeout;
4577
5272
  }
4578
- logger = new import_common17.Logger("Jetstream:Shutdown");
5273
+ logger = new import_common20.Logger("Jetstream:Shutdown");
4579
5274
  shutdownPromise;
4580
5275
  /**
4581
5276
  * Execute the full shutdown sequence.
@@ -4629,12 +5324,14 @@ var JetstreamModule = class {
4629
5324
  return {
4630
5325
  module: JetstreamModule,
4631
5326
  global: true,
5327
+ imports: [JetstreamMetricsModule.forFeature()],
4632
5328
  providers,
4633
5329
  exports: [
4634
5330
  JETSTREAM_CONNECTION,
4635
5331
  JETSTREAM_CODEC,
4636
5332
  JETSTREAM_EVENT_BUS,
4637
5333
  JETSTREAM_OPTIONS,
5334
+ PatternRegistry,
4638
5335
  ShutdownManager,
4639
5336
  JetstreamStrategy,
4640
5337
  JetstreamHealthIndicator
@@ -4656,13 +5353,14 @@ var JetstreamModule = class {
4656
5353
  return {
4657
5354
  module: JetstreamModule,
4658
5355
  global: true,
4659
- imports: asyncOptions.imports ?? [],
5356
+ imports: [...asyncOptions.imports ?? [], JetstreamMetricsModule.forFeature()],
4660
5357
  providers: [...asyncProviders, ...coreProviders],
4661
5358
  exports: [
4662
5359
  JETSTREAM_CONNECTION,
4663
5360
  JETSTREAM_CODEC,
4664
5361
  JETSTREAM_EVENT_BUS,
4665
5362
  JETSTREAM_OPTIONS,
5363
+ PatternRegistry,
4666
5364
  ShutdownManager,
4667
5365
  JetstreamStrategy,
4668
5366
  JetstreamHealthIndicator
@@ -4711,7 +5409,7 @@ var JetstreamModule = class {
4711
5409
  provide: JETSTREAM_EVENT_BUS,
4712
5410
  inject: [JETSTREAM_OPTIONS],
4713
5411
  useFactory: (options) => {
4714
- const logger5 = new import_common18.Logger("Jetstream:Module");
5412
+ const logger5 = new import_common21.Logger("Jetstream:Module");
4715
5413
  return new EventBus(logger5, options.hooks);
4716
5414
  }
4717
5415
  },
@@ -5005,12 +5703,12 @@ var JetstreamModule = class {
5005
5703
  }
5006
5704
  };
5007
5705
  JetstreamModule = __decorateClass([
5008
- (0, import_common18.Global)(),
5009
- (0, import_common18.Module)({}),
5010
- __decorateParam(0, (0, import_common18.Optional)()),
5011
- __decorateParam(0, (0, import_common18.Inject)(ShutdownManager)),
5012
- __decorateParam(1, (0, import_common18.Optional)()),
5013
- __decorateParam(1, (0, import_common18.Inject)(JetstreamStrategy))
5706
+ (0, import_common21.Global)(),
5707
+ (0, import_common21.Module)({}),
5708
+ __decorateParam(0, (0, import_common21.Optional)()),
5709
+ __decorateParam(0, (0, import_common21.Inject)(ShutdownManager)),
5710
+ __decorateParam(1, (0, import_common21.Optional)()),
5711
+ __decorateParam(1, (0, import_common21.Inject)(JetstreamStrategy))
5014
5712
  ], JetstreamModule);
5015
5713
  // Annotate the CommonJS export names for ESM import in node:
5016
5714
  0 && (module.exports = {