@horizon-republic/nestjs-jetstream 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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
 
@@ -577,7 +591,7 @@ var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ct
577
591
 
578
592
  // src/otel/tracer.ts
579
593
  var import_api2 = require("@opentelemetry/api");
580
- var PACKAGE_VERSION = true ? "2.10.0" : "0.0.0";
594
+ var PACKAGE_VERSION = true ? "2.11.0" : "0.0.0";
581
595
  var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
582
596
 
583
597
  // src/otel/carrier.ts
@@ -1368,6 +1382,17 @@ var detectEventKind = (pattern) => {
1368
1382
  if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
1369
1383
  return "event" /* Event */;
1370
1384
  };
1385
+ var declaredEventPattern = (pattern) => {
1386
+ if (pattern.startsWith("broadcast:" /* Broadcast */))
1387
+ return pattern.slice("broadcast:" /* Broadcast */.length);
1388
+ if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
1389
+ return pattern;
1390
+ };
1391
+ var eventStreamKind = (kind) => {
1392
+ if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
1393
+ if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
1394
+ return "ev" /* Event */;
1395
+ };
1371
1396
  var JetstreamClient = class extends import_microservices.ClientProxy {
1372
1397
  constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
1373
1398
  super();
@@ -1483,50 +1508,60 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1483
1508
  const encoded = this.codec.encode(data);
1484
1509
  const effectiveMsgId = messageId ?? import_nuid.nuid.next();
1485
1510
  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
- );
1511
+ const publishKind = detectEventKind(packet.pattern);
1512
+ const declaredPattern = declaredEventPattern(packet.pattern);
1513
+ const streamKind = eventStreamKind(publishKind);
1514
+ const startedAt = performance.now();
1515
+ try {
1516
+ await withPublishSpan(
1517
+ {
1518
+ subject: publishSubject,
1519
+ pattern: packet.pattern,
1520
+ record,
1521
+ kind: publishKind,
1522
+ payloadBytes: encoded.length,
1523
+ payload: encoded,
1524
+ messageId: effectiveMsgId,
1525
+ headers: msgHeaders,
1526
+ serviceName: this.callerName,
1527
+ endpoint: this.serverEndpoint,
1528
+ scheduleTarget: schedule ? eventSubject : void 0
1529
+ },
1530
+ this.otel,
1531
+ async () => {
1532
+ const warnIfDuplicate = (kindLabel, ack2) => {
1533
+ if (ack2.duplicate) {
1534
+ this.logger.warn(
1535
+ `Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
1536
+ );
1537
+ }
1538
+ };
1539
+ if (schedule) {
1540
+ const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1541
+ headers: msgHeaders,
1542
+ msgID: effectiveMsgId,
1543
+ ttl,
1544
+ schedule: {
1545
+ specification: schedule.at,
1546
+ target: eventSubject
1547
+ }
1548
+ });
1549
+ warnIfDuplicate("scheduled", ack2);
1550
+ return;
1507
1551
  }
1508
- };
1509
- if (schedule) {
1510
- const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1552
+ const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1511
1553
  headers: msgHeaders,
1512
1554
  msgID: effectiveMsgId,
1513
- ttl,
1514
- schedule: {
1515
- specification: schedule.at,
1516
- target: eventSubject
1517
- }
1555
+ ttl
1518
1556
  });
1519
- warnIfDuplicate("scheduled", ack2);
1520
- return;
1557
+ warnIfDuplicate("event", ack);
1521
1558
  }
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
- );
1559
+ );
1560
+ this.reportPublished(declaredPattern, streamKind, startedAt, "success");
1561
+ } catch (err) {
1562
+ this.reportPublished(declaredPattern, streamKind, startedAt, "error");
1563
+ throw err;
1564
+ }
1530
1565
  return void 0;
1531
1566
  }
1532
1567
  /**
@@ -1554,14 +1589,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1554
1589
  };
1555
1590
  let jetStreamCorrelationId = null;
1556
1591
  if (this.isCoreMode) {
1557
- this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
1592
+ this.publishCoreRpc(subject, data, hdrs, timeout, callback, packet.pattern).catch(
1593
+ onUnhandled
1594
+ );
1558
1595
  } else {
1559
1596
  jetStreamCorrelationId = import_nuid.nuid.next();
1560
1597
  this.publishJetStreamRpc(subject, data, callback, {
1561
1598
  headers: hdrs,
1562
1599
  timeout,
1563
1600
  correlationId: jetStreamCorrelationId,
1564
- messageId
1601
+ messageId,
1602
+ declaredPattern: packet.pattern
1565
1603
  }).catch(onUnhandled);
1566
1604
  }
1567
1605
  return () => {
@@ -1576,7 +1614,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1576
1614
  };
1577
1615
  }
1578
1616
  /** Core mode: nc.request() with timeout. */
1579
- async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
1617
+ async publishCoreRpc(subject, data, customHeaders, timeout, callback, declaredPattern) {
1580
1618
  const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
1581
1619
  const hdrs = this.buildHeaders(customHeaders, { subject });
1582
1620
  const encoded = this.codec.encode(data);
@@ -1591,6 +1629,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1591
1629
  },
1592
1630
  this.otel
1593
1631
  );
1632
+ const startedAt = performance.now();
1594
1633
  try {
1595
1634
  const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
1596
1635
  const response = await import_api8.context.with(
@@ -1603,9 +1642,13 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1603
1642
  const decoded = this.codec.decode(response.data);
1604
1643
  if (response.headers?.get("x-error" /* Error */)) {
1605
1644
  spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: decoded });
1645
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1646
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1606
1647
  callback({ err: decoded, response: null, isDisposed: true });
1607
1648
  } else {
1608
1649
  spanHandle.finish({ kind: "ok" /* Ok */, reply: decoded });
1650
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1651
+ this.reportRpcCompleted(declaredPattern, startedAt, "success");
1609
1652
  callback({ err: null, response: decoded, isDisposed: true });
1610
1653
  }
1611
1654
  } catch (err) {
@@ -1613,16 +1656,20 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1613
1656
  if (error instanceof import_transport_node.TimeoutError) {
1614
1657
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1615
1658
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, "");
1659
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1660
+ this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1616
1661
  } else {
1617
1662
  spanHandle.finish({ kind: "error" /* Error */, error });
1618
1663
  this.eventBus.emit("error" /* Error */, error, "client-rpc");
1664
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1665
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1619
1666
  }
1620
1667
  callback({ err: error, response: null, isDisposed: true });
1621
1668
  }
1622
1669
  }
1623
1670
  /** JetStream mode: publish to stream + wait for inbox response. */
1624
1671
  async publishJetStreamRpc(subject, data, callback, options) {
1625
- const { headers: customHeaders, correlationId, messageId } = options;
1672
+ const { headers: customHeaders, correlationId, messageId, declaredPattern } = options;
1626
1673
  const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
1627
1674
  const hdrs = this.buildHeaders(customHeaders, {
1628
1675
  subject,
@@ -1643,6 +1690,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1643
1690
  },
1644
1691
  this.otel
1645
1692
  );
1693
+ const startedAt = performance.now();
1646
1694
  this.pendingMessages.set(correlationId, (packet) => {
1647
1695
  if (packet.err) {
1648
1696
  if (packet.err instanceof Error) {
@@ -1650,8 +1698,10 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1650
1698
  } else {
1651
1699
  spanHandle.finish({ kind: "reply-error" /* ReplyError */, replyPayload: packet.err });
1652
1700
  }
1701
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1653
1702
  } else {
1654
1703
  spanHandle.finish({ kind: "ok" /* Ok */, reply: packet.response });
1704
+ this.reportRpcCompleted(declaredPattern, startedAt, "success");
1655
1705
  }
1656
1706
  callback(packet);
1657
1707
  });
@@ -1661,6 +1711,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1661
1711
  this.pendingMessages.delete(correlationId);
1662
1712
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1663
1713
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
1714
+ this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1664
1715
  callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
1665
1716
  }, effectiveTimeout);
1666
1717
  this.pendingTimeouts.set(correlationId, timeoutId);
@@ -1673,6 +1724,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1673
1724
  this.pendingMessages.delete(correlationId);
1674
1725
  const inboxError = new Error("Inbox not initialized");
1675
1726
  spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
1727
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1728
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1676
1729
  callback({
1677
1730
  err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
1678
1731
  response: null,
@@ -1687,6 +1740,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1687
1740
  msgID: messageId ?? import_nuid.nuid.next()
1688
1741
  })
1689
1742
  );
1743
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1690
1744
  } catch (err) {
1691
1745
  const existingTimeout = this.pendingTimeouts.get(correlationId);
1692
1746
  if (existingTimeout) {
@@ -1698,9 +1752,32 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1698
1752
  const error = err instanceof Error ? err : new Error("Unknown error");
1699
1753
  spanHandle.finish({ kind: "error" /* Error */, error });
1700
1754
  this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
1755
+ this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1756
+ this.reportRpcCompleted(declaredPattern, startedAt, "error");
1701
1757
  callback({ err: error, response: null, isDisposed: true });
1702
1758
  }
1703
1759
  }
1760
+ // hasHook is per-emit so late subscribers (JetstreamMetricsService during
1761
+ // OnApplicationBootstrap) still receive events.
1762
+ reportPublished(declaredPattern, kind, startedAt, status) {
1763
+ if (!this.eventBus.hasHook("published" /* Published */)) return;
1764
+ this.eventBus.emit(
1765
+ "published" /* Published */,
1766
+ declaredPattern,
1767
+ kind,
1768
+ performance.now() - startedAt,
1769
+ status
1770
+ );
1771
+ }
1772
+ reportRpcCompleted(declaredPattern, startedAt, status) {
1773
+ if (!this.eventBus.hasHook("rpcCompleted" /* RpcCompleted */)) return;
1774
+ this.eventBus.emit(
1775
+ "rpcCompleted" /* RpcCompleted */,
1776
+ declaredPattern,
1777
+ performance.now() - startedAt,
1778
+ status
1779
+ );
1780
+ }
1704
1781
  /** Fail-fast all pending JetStream RPC callbacks on connection loss. */
1705
1782
  handleDisconnect() {
1706
1783
  this.rejectPendingRpcs(new Error("Connection lost"));
@@ -2106,43 +2183,53 @@ var ConnectionProvider = class {
2106
2183
  var EventBus = class {
2107
2184
  hooks;
2108
2185
  logger;
2186
+ subscribers = /* @__PURE__ */ new Map();
2109
2187
  constructor(logger5, hooks) {
2110
2188
  this.logger = logger5;
2111
2189
  this.hooks = hooks ?? {};
2112
2190
  }
2113
2191
  /**
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.
2192
+ * Subscribe to a transport event. Used by built-in observers (e.g. metrics).
2193
+ * Multiple subscribers per event are supported; each is called independently.
2194
+ */
2195
+ subscribe(event, handler) {
2196
+ const list = this.subscribers.get(event) ?? [];
2197
+ list.push(handler);
2198
+ this.subscribers.set(event, list);
2199
+ }
2200
+ /**
2201
+ * Emit a lifecycle event. Dispatches to all internal subscribers and the
2202
+ * registered user hook (if any).
2118
2203
  */
2119
2204
  emit(event, ...args) {
2120
- const hook = this.hooks[event];
2121
- if (!hook) return;
2122
- this.callHook(event, hook, ...args);
2205
+ this.dispatch(event, args);
2123
2206
  }
2124
2207
  /**
2125
2208
  * Hot-path optimized emit for MessageRouted events.
2126
2209
  * Avoids rest/spread overhead of the generic `emit()`.
2127
2210
  */
2128
2211
  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
- );
2212
+ this.dispatch("messageRouted" /* MessageRouted */, [subject, kind]);
2137
2213
  }
2138
2214
  /**
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.
2215
+ * Check whether any listener (user hook or internal subscriber) is registered
2216
+ * for the given event. Used by routing hot path to elide the emit call when
2217
+ * no one is listening.
2143
2218
  */
2144
2219
  hasHook(event) {
2145
- return this.hooks[event] !== void 0;
2220
+ return this.hooks[event] !== void 0 || (this.subscribers.get(event)?.length ?? 0) > 0;
2221
+ }
2222
+ dispatch(event, args) {
2223
+ const subs = this.subscribers.get(event);
2224
+ if (subs?.length) {
2225
+ for (const sub of [...subs]) {
2226
+ this.callHook(event, sub, ...args);
2227
+ }
2228
+ }
2229
+ const hook = this.hooks[event];
2230
+ if (hook) {
2231
+ this.callHook(event, hook, ...args);
2232
+ }
2146
2233
  }
2147
2234
  callHook(event, hook, ...args) {
2148
2235
  try {
@@ -2221,12 +2308,690 @@ var JetstreamHealthIndicator = class {
2221
2308
  isHealthCheckError: true
2222
2309
  });
2223
2310
  }
2224
- return { [key]: details };
2311
+ return { [key]: details };
2312
+ }
2313
+ };
2314
+ JetstreamHealthIndicator = __decorateClass([
2315
+ (0, import_common7.Injectable)()
2316
+ ], JetstreamHealthIndicator);
2317
+
2318
+ // src/metrics/metrics.module.ts
2319
+ var import_common11 = require("@nestjs/common");
2320
+
2321
+ // src/server/routing/pattern-registry.ts
2322
+ var import_common8 = require("@nestjs/common");
2323
+ var HANDLER_LABELS = {
2324
+ ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
2325
+ ["ordered" /* Ordered */]: "ordered" /* Ordered */,
2326
+ ["ev" /* Event */]: "event" /* Event */,
2327
+ ["cmd" /* Command */]: "rpc" /* Rpc */
2328
+ };
2329
+ var PatternRegistry = class {
2330
+ constructor(options) {
2331
+ this.options = options;
2332
+ }
2333
+ logger = new import_common8.Logger("Jetstream:PatternRegistry");
2334
+ registry = /* @__PURE__ */ new Map();
2335
+ // Cached after registerHandlers() — the registry is immutable from that point
2336
+ cachedPatterns = null;
2337
+ _hasEvents = false;
2338
+ _hasCommands = false;
2339
+ _hasBroadcasts = false;
2340
+ _hasOrdered = false;
2341
+ _hasMetadata = false;
2342
+ /**
2343
+ * Register all handlers from the NestJS strategy.
2344
+ *
2345
+ * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
2346
+ */
2347
+ registerHandlers(handlers) {
2348
+ const serviceName = this.options.name;
2349
+ for (const [pattern, handler] of handlers) {
2350
+ const extras = handler.extras;
2351
+ const isEvent = handler.isEventHandler ?? false;
2352
+ const isBroadcast = !!extras?.broadcast;
2353
+ const isOrdered = !!extras?.ordered;
2354
+ const meta = extras?.meta;
2355
+ if (isBroadcast && isOrdered) {
2356
+ throw new Error(
2357
+ `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
2358
+ );
2359
+ }
2360
+ let kind;
2361
+ if (isBroadcast) kind = "broadcast" /* Broadcast */;
2362
+ else if (isOrdered) kind = "ordered" /* Ordered */;
2363
+ else if (isEvent) kind = "ev" /* Event */;
2364
+ else kind = "cmd" /* Command */;
2365
+ const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
2366
+ this.registry.set(fullSubject, {
2367
+ handler,
2368
+ pattern,
2369
+ isEvent: isEvent && !isOrdered,
2370
+ isBroadcast,
2371
+ isOrdered,
2372
+ meta
2373
+ });
2374
+ this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
2375
+ }
2376
+ this.cachedPatterns = this.buildPatternsByKind();
2377
+ this._hasEvents = this.cachedPatterns.events.length > 0;
2378
+ this._hasCommands = this.cachedPatterns.commands.length > 0;
2379
+ this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
2380
+ this._hasOrdered = this.cachedPatterns.ordered.length > 0;
2381
+ this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
2382
+ this.logSummary();
2383
+ }
2384
+ /** Find handler for a full NATS subject. */
2385
+ getHandler(subject) {
2386
+ return this.registry.get(subject)?.handler ?? null;
2387
+ }
2388
+ /**
2389
+ * Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
2390
+ *
2391
+ * Returns `null` when the subject is not registered. The declared pattern is
2392
+ * the value the user passed to `@EventPattern`/`@MessagePattern` — stable and
2393
+ * bounded, suitable for use as a Prometheus label without cardinality risk.
2394
+ */
2395
+ resolveDeclared(subject) {
2396
+ const entry = this.registry.get(subject);
2397
+ if (!entry) return null;
2398
+ return { pattern: entry.pattern, kind: this.resolveStreamKind(entry) };
2399
+ }
2400
+ /** Get all registered broadcast patterns (for consumer filter_subject setup). */
2401
+ getBroadcastPatterns() {
2402
+ return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
2403
+ }
2404
+ hasBroadcastHandlers() {
2405
+ return this._hasBroadcasts;
2406
+ }
2407
+ hasRpcHandlers() {
2408
+ return this._hasCommands;
2409
+ }
2410
+ hasEventHandlers() {
2411
+ return this._hasEvents;
2412
+ }
2413
+ hasOrderedHandlers() {
2414
+ return this._hasOrdered;
2415
+ }
2416
+ /** Get fully-qualified NATS subjects for ordered handlers. */
2417
+ getOrderedSubjects() {
2418
+ return this.getPatternsByKind().ordered.map(
2419
+ (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2420
+ );
2421
+ }
2422
+ /** Check if any registered handler has metadata. */
2423
+ hasMetadata() {
2424
+ return this._hasMetadata;
2425
+ }
2426
+ /**
2427
+ * Get handler metadata entries for KV publishing.
2428
+ *
2429
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
2430
+ * Key format: `{serviceName}.{kind}.{pattern}`.
2431
+ */
2432
+ getMetadataEntries() {
2433
+ const entries = /* @__PURE__ */ new Map();
2434
+ for (const entry of this.registry.values()) {
2435
+ if (!entry.meta) continue;
2436
+ const kind = this.resolveStreamKind(entry);
2437
+ const key = metadataKey(this.options.name, kind, entry.pattern);
2438
+ entries.set(key, entry.meta);
2439
+ }
2440
+ return entries;
2441
+ }
2442
+ /** Get patterns grouped by kind (cached after registration). */
2443
+ getPatternsByKind() {
2444
+ const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
2445
+ return {
2446
+ events: [...patterns.events],
2447
+ commands: [...patterns.commands],
2448
+ broadcasts: [...patterns.broadcasts],
2449
+ ordered: [...patterns.ordered]
2450
+ };
2451
+ }
2452
+ /** Normalize a full NATS subject back to the user-facing pattern. */
2453
+ normalizeSubject(subject) {
2454
+ const name = internalName(this.options.name);
2455
+ const prefixes = [
2456
+ `${name}.${"cmd" /* Command */}.`,
2457
+ `${name}.${"ev" /* Event */}.`,
2458
+ `${name}.${"ordered" /* Ordered */}.`,
2459
+ `${"broadcast" /* Broadcast */}.`
2460
+ ];
2461
+ for (const prefix of prefixes) {
2462
+ if (subject.startsWith(prefix)) {
2463
+ return subject.slice(prefix.length);
2464
+ }
2465
+ }
2466
+ return subject;
2467
+ }
2468
+ buildPatternsByKind() {
2469
+ const events = [];
2470
+ const commands = [];
2471
+ const broadcasts = [];
2472
+ const ordered = [];
2473
+ for (const entry of this.registry.values()) {
2474
+ if (entry.isBroadcast) broadcasts.push(entry.pattern);
2475
+ else if (entry.isOrdered) ordered.push(entry.pattern);
2476
+ else if (entry.isEvent) events.push(entry.pattern);
2477
+ else commands.push(entry.pattern);
2478
+ }
2479
+ return { events, commands, broadcasts, ordered };
2480
+ }
2481
+ resolveStreamKind(entry) {
2482
+ if (entry.isBroadcast) return "broadcast" /* Broadcast */;
2483
+ if (entry.isOrdered) return "ordered" /* Ordered */;
2484
+ if (entry.isEvent) return "ev" /* Event */;
2485
+ return "cmd" /* Command */;
2486
+ }
2487
+ logSummary() {
2488
+ const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
2489
+ const parts = [
2490
+ `${commands.length} RPC`,
2491
+ `${events.length} events`,
2492
+ `${broadcasts.length} broadcasts`
2493
+ ];
2494
+ if (ordered.length > 0) {
2495
+ parts.push(`${ordered.length} ordered`);
2496
+ }
2497
+ this.logger.log(`Registered handlers: ${parts.join(", ")}`);
2498
+ }
2499
+ };
2500
+
2501
+ // src/metrics/metrics.constants.ts
2502
+ var JETSTREAM_METRICS_CONFIG = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_CONFIG");
2503
+ var JETSTREAM_METRICS_REGISTRY = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_REGISTRY");
2504
+ var JETSTREAM_METRICS_PROM_CLIENT = /* @__PURE__ */ Symbol("JETSTREAM_METRICS_PROM_CLIENT");
2505
+ var DEFAULT_METRICS_PREFIX = "jetstream_";
2506
+ var DEFAULT_POLL_INTERVAL_MS = 15e3;
2507
+ var DEFAULT_HISTOGRAM_BUCKETS = {
2508
+ handlerDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
2509
+ publishDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
2510
+ rpcDuration: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
2511
+ };
2512
+ var ERROR_CONTEXT_PREFIXES = [
2513
+ ["connection", "connection"],
2514
+ ["codec", "codec"],
2515
+ ["client-rpc", "publish"],
2516
+ ["jetstream-rpc-publish", "publish"],
2517
+ ["publish", "publish"],
2518
+ ["message-provider", "consume"],
2519
+ ["consume", "consume"],
2520
+ ["core-rpc-handler", "handler"],
2521
+ ["rpc-handler", "handler"],
2522
+ // EventRouter formats contexts as `${StreamKind.*}-handler:...` — the enum
2523
+ // uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
2524
+ ["ev-handler", "handler"],
2525
+ ["event-handler", "handler"],
2526
+ ["broadcast-handler", "handler"],
2527
+ ["ordered-handler", "handler"],
2528
+ ["handler", "handler"],
2529
+ ["shutdown", "shutdown"]
2530
+ ];
2531
+ var UNMATCHED_SUBJECT_LABEL = "<unmatched>";
2532
+ var STREAM_KIND_LABEL = {
2533
+ ["ev" /* Event */]: "event",
2534
+ ["cmd" /* Command */]: "command",
2535
+ ["broadcast" /* Broadcast */]: "broadcast",
2536
+ ["ordered" /* Ordered */]: "ordered"
2537
+ };
2538
+
2539
+ // src/metrics/metrics.service.ts
2540
+ var import_common10 = require("@nestjs/common");
2541
+
2542
+ // src/metrics/error-context-mapper.ts
2543
+ var mapErrorContext = (context7) => {
2544
+ if (!context7) return "other";
2545
+ for (const [prefix, mapped] of ERROR_CONTEXT_PREFIXES) {
2546
+ if (context7 === prefix || context7.startsWith(`${prefix}:`)) {
2547
+ return mapped;
2548
+ }
2549
+ }
2550
+ return "other";
2551
+ };
2552
+
2553
+ // src/metrics/metrics.factory.ts
2554
+ var createMetrics = (opts) => {
2555
+ const { register, promClient } = opts;
2556
+ const prefix = opts.prefix ?? DEFAULT_METRICS_PREFIX;
2557
+ const buckets = {
2558
+ handlerDuration: opts.buckets?.handlerDuration ?? DEFAULT_HISTOGRAM_BUCKETS.handlerDuration,
2559
+ publishDuration: opts.buckets?.publishDuration ?? DEFAULT_HISTOGRAM_BUCKETS.publishDuration,
2560
+ rpcDuration: opts.buckets?.rpcDuration ?? DEFAULT_HISTOGRAM_BUCKETS.rpcDuration
2561
+ };
2562
+ if (opts.defaultLabels && Object.keys(opts.defaultLabels).length > 0) {
2563
+ register.setDefaultLabels(opts.defaultLabels);
2564
+ }
2565
+ const counter = (name, help, labelNames) => new promClient.Counter({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
2566
+ const histogram = (name, help, labelNames, bucketArr) => new promClient.Histogram({
2567
+ name: `${prefix}${name}`,
2568
+ help,
2569
+ labelNames,
2570
+ buckets: bucketArr,
2571
+ registers: [register]
2572
+ });
2573
+ const gauge = (name, help, labelNames) => new promClient.Gauge({ name: `${prefix}${name}`, help, labelNames, registers: [register] });
2574
+ return {
2575
+ messagesReceivedTotal: counter(
2576
+ "messages_received_total",
2577
+ "Total messages routed to a handler.",
2578
+ ["stream", "subject", "kind"]
2579
+ ),
2580
+ messagesProcessedTotal: counter(
2581
+ "messages_processed_total",
2582
+ "Total messages whose handler completed.",
2583
+ ["stream", "subject", "kind", "status"]
2584
+ ),
2585
+ messagesUnhandledTotal: counter(
2586
+ "messages_unhandled_total",
2587
+ "Messages received but not matching any registered handler.",
2588
+ ["subject"]
2589
+ ),
2590
+ messagesDeadLetterTotal: counter(
2591
+ "messages_dead_letter_total",
2592
+ "Messages routed to dead-letter after exhausting redelivery attempts.",
2593
+ ["stream", "subject"]
2594
+ ),
2595
+ publishTotal: counter(
2596
+ "publish_total",
2597
+ "Total publish/send operations performed by the client.",
2598
+ ["subject", "kind", "status"]
2599
+ ),
2600
+ rpcTimeoutTotal: counter("rpc_timeout_total", "RPC calls that exceeded the timeout deadline.", [
2601
+ "subject"
2602
+ ]),
2603
+ consumerRecoveredTotal: counter(
2604
+ "consumer_recovered_total",
2605
+ "Self-healing recoveries after consume-loop failures.",
2606
+ ["kind"]
2607
+ ),
2608
+ errorsTotal: counter("errors_total", "Transport-level errors emitted on the EventBus.", [
2609
+ "context"
2610
+ ]),
2611
+ handlerDurationSeconds: histogram(
2612
+ "handler_duration_seconds",
2613
+ "Wall-clock duration of handler execution.",
2614
+ ["stream", "subject", "kind", "status"],
2615
+ buckets.handlerDuration
2616
+ ),
2617
+ publishDurationSeconds: histogram(
2618
+ "publish_duration_seconds",
2619
+ "Wall-clock duration of client publish/send operations.",
2620
+ ["subject", "kind", "status"],
2621
+ buckets.publishDuration
2622
+ ),
2623
+ rpcDurationSeconds: histogram(
2624
+ "rpc_duration_seconds",
2625
+ "Wall-clock duration of RPC round-trips from client perspective.",
2626
+ ["subject", "status"],
2627
+ buckets.rpcDuration
2628
+ ),
2629
+ consumerNumPending: gauge(
2630
+ "consumer_num_pending",
2631
+ "Messages not yet delivered to this consumer.",
2632
+ ["stream", "consumer", "kind"]
2633
+ ),
2634
+ consumerNumAckPending: gauge(
2635
+ "consumer_num_ack_pending",
2636
+ "Messages delivered but not yet acked.",
2637
+ ["stream", "consumer", "kind"]
2638
+ ),
2639
+ consumerNumRedelivered: gauge(
2640
+ "consumer_num_redelivered",
2641
+ "Messages currently in redelivery state.",
2642
+ ["stream", "consumer", "kind"]
2643
+ ),
2644
+ consumerNumWaiting: gauge(
2645
+ "consumer_num_waiting",
2646
+ "Pull-request waiting count for this consumer.",
2647
+ ["stream", "consumer", "kind"]
2648
+ ),
2649
+ streamMessages: gauge("stream_messages", "Total messages stored in this stream.", ["stream"]),
2650
+ streamBytes: gauge("stream_bytes", "Total bytes stored in this stream.", ["stream"]),
2651
+ connectionUp: gauge("connection_up", "NATS connection state (1 connected, 0 disconnected).", [
2652
+ "server"
2653
+ ]),
2654
+ metricsPollErrorsTotal: counter(
2655
+ "metrics_poll_errors_total",
2656
+ "Errors encountered while polling JetStreamManager for gauge data.",
2657
+ ["target"]
2658
+ )
2659
+ };
2660
+ };
2661
+
2662
+ // src/metrics/poll-runner.ts
2663
+ var import_common9 = require("@nestjs/common");
2664
+ var PollRunner = class {
2665
+ constructor(opts) {
2666
+ this.opts = opts;
2667
+ }
2668
+ logger = new import_common9.Logger("Jetstream:Metrics:Poll");
2669
+ timer = null;
2670
+ inFlight = null;
2671
+ start() {
2672
+ if (this.timer !== null) return;
2673
+ if (this.opts.intervalMs <= 0) return;
2674
+ if (this.opts.targets.length === 0) return;
2675
+ this.timer = setInterval(() => {
2676
+ if (this.inFlight !== null) {
2677
+ this.logger.warn("Skipping poll tick \u2014 previous cycle still in flight");
2678
+ return;
2679
+ }
2680
+ this.inFlight = this.tick().finally(() => {
2681
+ this.inFlight = null;
2682
+ });
2683
+ }, this.opts.intervalMs);
2684
+ }
2685
+ async stop() {
2686
+ if (this.timer !== null) {
2687
+ clearInterval(this.timer);
2688
+ this.timer = null;
2689
+ }
2690
+ if (this.inFlight !== null) await this.inFlight;
2691
+ }
2692
+ /** @internal Visible for tests. Runs one poll cycle. */
2693
+ async tick() {
2694
+ let jsm;
2695
+ try {
2696
+ jsm = await this.opts.jsmFactory();
2697
+ } catch {
2698
+ this.recordPollError("jsm.connect");
2699
+ return;
2700
+ }
2701
+ await Promise.all([this.pollConsumers(jsm), this.pollStreams(jsm)]);
2702
+ }
2703
+ async pollConsumers(jsm) {
2704
+ for (const target of this.opts.targets) {
2705
+ try {
2706
+ const info = await jsm.consumers.info(target.stream, target.consumer);
2707
+ const labels = {
2708
+ stream: target.stream,
2709
+ consumer: target.consumer,
2710
+ kind: STREAM_KIND_LABEL[target.kind]
2711
+ };
2712
+ this.opts.metrics.consumerNumPending.labels(labels).set(info.num_pending);
2713
+ this.opts.metrics.consumerNumAckPending.labels(labels).set(info.num_ack_pending);
2714
+ this.opts.metrics.consumerNumRedelivered.labels(labels).set(info.num_redelivered);
2715
+ this.opts.metrics.consumerNumWaiting.labels(labels).set(info.num_waiting);
2716
+ } catch {
2717
+ this.recordPollError("consumer.info");
2718
+ }
2719
+ }
2720
+ }
2721
+ async pollStreams(jsm) {
2722
+ const uniqueStreams = new Set(this.opts.targets.map((t) => t.stream));
2723
+ for (const stream of uniqueStreams) {
2724
+ try {
2725
+ const info = await jsm.streams.info(stream);
2726
+ this.opts.metrics.streamMessages.labels({ stream }).set(info.state.messages);
2727
+ this.opts.metrics.streamBytes.labels({ stream }).set(info.state.bytes);
2728
+ } catch {
2729
+ this.recordPollError("stream.info");
2730
+ }
2731
+ }
2732
+ }
2733
+ recordPollError(target) {
2734
+ this.opts.metrics.metricsPollErrorsTotal.labels({ target }).inc();
2735
+ }
2736
+ };
2737
+
2738
+ // src/metrics/metrics.service.ts
2739
+ var JetstreamMetricsService = class {
2740
+ constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
2741
+ this.eventBus = eventBus;
2742
+ this.config = config;
2743
+ this.promClient = promClient;
2744
+ this.options = options;
2745
+ this.patternRegistry = patternRegistry;
2746
+ this.connection = connection;
2747
+ }
2748
+ logger = new import_common10.Logger("Jetstream:Metrics");
2749
+ metrics = null;
2750
+ pollRunner = null;
2751
+ activeServers = /* @__PURE__ */ new Set();
2752
+ async onApplicationBootstrap() {
2753
+ if (this.metrics !== null) return;
2754
+ if (!this.config.register) {
2755
+ throw new Error(
2756
+ "JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
2757
+ );
2758
+ }
2759
+ this.metrics = createMetrics({
2760
+ register: this.config.register,
2761
+ promClient: this.promClient,
2762
+ prefix: this.config.prefix,
2763
+ defaultLabels: this.config.defaultLabels,
2764
+ buckets: this.config.buckets
2765
+ });
2766
+ this.subscribeToEvents();
2767
+ this.syncInitialConnectionState();
2768
+ this.startPolling();
2769
+ this.logger.log(
2770
+ `Metrics enabled (prefix=${this.config.prefix ?? DEFAULT_METRICS_PREFIX}, poll=${this.getEffectivePollInterval()}ms)`
2771
+ );
2772
+ }
2773
+ async onModuleDestroy() {
2774
+ await this.pollRunner?.stop();
2775
+ this.pollRunner = null;
2776
+ }
2777
+ /** @internal Visible for tests. `0` disables polling. */
2778
+ getEffectivePollInterval() {
2779
+ return this.config.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
2780
+ }
2781
+ /**
2782
+ * NATS connects during early bootstrap, before this service subscribes to
2783
+ * the EventBus — the initial `Connect` emission misses us. Mirror the
2784
+ * current state here so `connection_up` reflects reality the moment metrics
2785
+ * come online; later disconnects/reconnects update it normally.
2786
+ */
2787
+ syncInitialConnectionState() {
2788
+ const nc = this.connection?.unwrap;
2789
+ if (!nc) return;
2790
+ const server = nc.getServer();
2791
+ this.activeServers.add(server);
2792
+ this.metrics?.connectionUp.labels({ server }).set(1);
2793
+ }
2794
+ /** Skips polling for publisher-only deployments and when no kinds are active. */
2795
+ startPolling() {
2796
+ const interval = this.getEffectivePollInterval();
2797
+ const connection = this.connection;
2798
+ if (interval <= 0 || !this.patternRegistry || !connection || !this.metrics) return;
2799
+ const targets = this.buildPollTargets();
2800
+ if (targets.length === 0) return;
2801
+ this.pollRunner = new PollRunner({
2802
+ intervalMs: interval,
2803
+ jsmFactory: async () => connection.getJetStreamManager(),
2804
+ metrics: this.metrics,
2805
+ targets
2806
+ });
2807
+ this.pollRunner.start();
2808
+ }
2809
+ buildPollTargets() {
2810
+ const registry = this.patternRegistry;
2811
+ if (!registry) return [];
2812
+ const targets = [];
2813
+ if (registry.hasEventHandlers()) {
2814
+ targets.push({
2815
+ kind: "ev" /* Event */,
2816
+ stream: streamName(this.options.name, "ev" /* Event */),
2817
+ consumer: consumerName(this.options.name, "ev" /* Event */)
2818
+ });
2819
+ }
2820
+ if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
2821
+ targets.push({
2822
+ kind: "cmd" /* Command */,
2823
+ stream: streamName(this.options.name, "cmd" /* Command */),
2824
+ consumer: consumerName(this.options.name, "cmd" /* Command */)
2825
+ });
2826
+ }
2827
+ if (registry.hasBroadcastHandlers()) {
2828
+ targets.push({
2829
+ kind: "broadcast" /* Broadcast */,
2830
+ stream: streamName(this.options.name, "broadcast" /* Broadcast */),
2831
+ consumer: consumerName(this.options.name, "broadcast" /* Broadcast */)
2832
+ });
2833
+ }
2834
+ return targets;
2835
+ }
2836
+ subscribeToEvents() {
2837
+ this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
2838
+ this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
2839
+ this.eventBus.subscribe("reconnect" /* Reconnect */, this.onReconnect);
2840
+ this.eventBus.subscribe("error" /* Error */, this.onError);
2841
+ this.eventBus.subscribe("rpcTimeout" /* RpcTimeout */, this.onRpcTimeout);
2842
+ this.eventBus.subscribe("messageRouted" /* MessageRouted */, this.onMessageRouted);
2843
+ this.eventBus.subscribe("deadLetter" /* DeadLetter */, this.onDeadLetter);
2844
+ this.eventBus.subscribe("consumerRecovered" /* ConsumerRecovered */, this.onConsumerRecovered);
2845
+ this.eventBus.subscribe("handlerCompleted" /* HandlerCompleted */, this.onHandlerCompleted);
2846
+ this.eventBus.subscribe("published" /* Published */, this.onPublished);
2847
+ this.eventBus.subscribe("rpcCompleted" /* RpcCompleted */, this.onRpcCompleted);
2848
+ }
2849
+ onConnect = (server) => {
2850
+ this.activeServers.add(server);
2851
+ this.metrics?.connectionUp.labels({ server }).set(1);
2852
+ };
2853
+ onReconnect = (server) => {
2854
+ this.activeServers.add(server);
2855
+ this.metrics?.connectionUp.labels({ server }).set(1);
2856
+ };
2857
+ onDisconnect = () => {
2858
+ for (const server of this.activeServers) {
2859
+ this.metrics?.connectionUp.labels({ server }).set(0);
2860
+ }
2861
+ };
2862
+ onError = (_err, context7) => {
2863
+ this.metrics?.errorsTotal.labels({ context: mapErrorContext(context7) }).inc();
2864
+ };
2865
+ onRpcTimeout = (subject, _correlationId) => {
2866
+ const declared = this.resolveDeclared(subject);
2867
+ const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2868
+ this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
2869
+ };
2870
+ // `_kind` collapses broadcast/ordered into MessageKind.Event — we use
2871
+ // declared.kind from PatternRegistry for the precise label instead.
2872
+ onMessageRouted = (subject, _kind) => {
2873
+ if (!this.metrics) return;
2874
+ const declared = this.resolveDeclared(subject);
2875
+ if (!declared) {
2876
+ this.metrics.messagesUnhandledTotal.labels({ subject: UNMATCHED_SUBJECT_LABEL }).inc();
2877
+ return;
2878
+ }
2879
+ this.metrics.messagesReceivedTotal.labels({
2880
+ stream: streamName(this.options.name, declared.kind),
2881
+ subject: declared.pattern,
2882
+ kind: STREAM_KIND_LABEL[declared.kind]
2883
+ }).inc();
2884
+ };
2885
+ onDeadLetter = (info) => {
2886
+ const declared = this.resolveDeclared(info.subject);
2887
+ const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2888
+ this.metrics?.messagesDeadLetterTotal.labels({ stream: info.stream, subject: subjectLabel }).inc();
2889
+ };
2890
+ onConsumerRecovered = (label, _attempts) => {
2891
+ const kindLabel = STREAM_KIND_LABEL[label] ?? String(label);
2892
+ this.metrics?.consumerRecoveredTotal.labels({ kind: kindLabel }).inc();
2893
+ };
2894
+ onHandlerCompleted = (pattern, kind, durationMs, status) => {
2895
+ if (!this.metrics) return;
2896
+ const stream = streamName(this.options.name, kind);
2897
+ const kindLabel = STREAM_KIND_LABEL[kind];
2898
+ const labels = { stream, subject: pattern, kind: kindLabel, status };
2899
+ this.metrics.messagesProcessedTotal.labels(labels).inc();
2900
+ this.metrics.handlerDurationSeconds.labels(labels).observe(durationMs / 1e3);
2901
+ };
2902
+ onPublished = (pattern, kind, durationMs, status) => {
2903
+ if (!this.metrics) return;
2904
+ const labels = { subject: pattern, kind: STREAM_KIND_LABEL[kind], status };
2905
+ this.metrics.publishTotal.labels(labels).inc();
2906
+ this.metrics.publishDurationSeconds.labels(labels).observe(durationMs / 1e3);
2907
+ };
2908
+ onRpcCompleted = (pattern, durationMs, status) => {
2909
+ this.metrics?.rpcDurationSeconds.labels({ subject: pattern, status }).observe(durationMs / 1e3);
2910
+ };
2911
+ resolveDeclared(subject) {
2912
+ return this.patternRegistry?.resolveDeclared(subject) ?? null;
2225
2913
  }
2226
2914
  };
2227
- JetstreamHealthIndicator = __decorateClass([
2228
- (0, import_common7.Injectable)()
2229
- ], JetstreamHealthIndicator);
2915
+ JetstreamMetricsService = __decorateClass([
2916
+ (0, import_common10.Injectable)(),
2917
+ __decorateParam(1, (0, import_common10.Inject)(JETSTREAM_METRICS_CONFIG)),
2918
+ __decorateParam(2, (0, import_common10.Inject)(JETSTREAM_METRICS_PROM_CLIENT)),
2919
+ __decorateParam(3, (0, import_common10.Inject)(JETSTREAM_OPTIONS)),
2920
+ __decorateParam(4, (0, import_common10.Optional)()),
2921
+ __decorateParam(5, (0, import_common10.Optional)()),
2922
+ __decorateParam(5, (0, import_common10.Inject)(JETSTREAM_CONNECTION))
2923
+ ], JetstreamMetricsService);
2924
+
2925
+ // src/metrics/metrics.module.ts
2926
+ var PROM_CLIENT_INSTALL_MESSAGE = "prom-client is required when JetstreamModule.forRoot({ metrics: ... }) is enabled. Install it with: pnpm add prom-client";
2927
+ var resolvePromClient = async () => {
2928
+ try {
2929
+ return await import("prom-client");
2930
+ } catch {
2931
+ throw new Error(PROM_CLIENT_INSTALL_MESSAGE);
2932
+ }
2933
+ };
2934
+ var normalizeMetricsConfig = (option, promClient) => {
2935
+ const user = option && option !== true ? option : {};
2936
+ return {
2937
+ register: user.register ?? promClient.register,
2938
+ prefix: user.prefix ?? DEFAULT_METRICS_PREFIX,
2939
+ defaultLabels: user.defaultLabels,
2940
+ pollInterval: user.pollInterval ?? DEFAULT_POLL_INTERVAL_MS,
2941
+ buckets: user.buckets
2942
+ };
2943
+ };
2944
+ var JetstreamMetricsModule = class {
2945
+ static forFeature(metricsOption) {
2946
+ if (!metricsOption) {
2947
+ return { module: JetstreamMetricsModule, providers: [], exports: [] };
2948
+ }
2949
+ const promClientProvider = {
2950
+ provide: JETSTREAM_METRICS_PROM_CLIENT,
2951
+ useFactory: async () => {
2952
+ const mod = await resolvePromClient();
2953
+ return { Counter: mod.Counter, Histogram: mod.Histogram, Gauge: mod.Gauge };
2954
+ }
2955
+ };
2956
+ const configProvider = {
2957
+ provide: JETSTREAM_METRICS_CONFIG,
2958
+ useFactory: async () => {
2959
+ const mod = await resolvePromClient();
2960
+ return normalizeMetricsConfig(metricsOption, mod);
2961
+ }
2962
+ };
2963
+ const registryProvider = {
2964
+ provide: JETSTREAM_METRICS_REGISTRY,
2965
+ inject: [JETSTREAM_METRICS_CONFIG],
2966
+ useFactory: (cfg) => cfg.register
2967
+ };
2968
+ const serviceProvider = {
2969
+ provide: JetstreamMetricsService,
2970
+ inject: [
2971
+ JETSTREAM_EVENT_BUS,
2972
+ JETSTREAM_METRICS_CONFIG,
2973
+ JETSTREAM_METRICS_PROM_CLIENT,
2974
+ JETSTREAM_OPTIONS,
2975
+ { token: PatternRegistry, optional: true },
2976
+ { token: JETSTREAM_CONNECTION, optional: true }
2977
+ ],
2978
+ useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(eventBus, cfg, runtime, opts, patternRegistry, connection)
2979
+ };
2980
+ return {
2981
+ module: JetstreamMetricsModule,
2982
+ providers: [promClientProvider, configProvider, registryProvider, serviceProvider],
2983
+ exports: [
2984
+ JetstreamMetricsService,
2985
+ JETSTREAM_METRICS_CONFIG,
2986
+ JETSTREAM_METRICS_REGISTRY,
2987
+ JETSTREAM_METRICS_PROM_CLIENT
2988
+ ]
2989
+ };
2990
+ }
2991
+ };
2992
+ JetstreamMetricsModule = __decorateClass([
2993
+ (0, import_common11.Module)({})
2994
+ ], JetstreamMetricsModule);
2230
2995
 
2231
2996
  // src/server/strategy.ts
2232
2997
  var import_microservices2 = require("@nestjs/microservices");
@@ -2302,6 +3067,26 @@ var JetstreamStrategy = class extends import_microservices2.Server {
2302
3067
  this.messageProvider.destroy();
2303
3068
  this.started = false;
2304
3069
  }
3070
+ /**
3071
+ * Override NestJS `Server.addHandler` to fail-fast on duplicate pattern registration.
3072
+ *
3073
+ * The base class silently overwrites duplicate RPC handlers (last wins) and appends
3074
+ * duplicate event handlers to a linked list. Both behaviors are hazardous in a
3075
+ * JetStream context: silent overwrite drops a handler the user wrote, and double
3076
+ * event dispatch double-acks/double-processes the same JetStream message.
3077
+ *
3078
+ * We treat any pattern collision as a fatal misconfiguration so it surfaces at
3079
+ * bootstrap instead of in production traffic.
3080
+ */
3081
+ addHandler(pattern, callback, isEventHandler = false, extras = {}) {
3082
+ const normalizedPattern = this.normalizePattern(pattern);
3083
+ if (this.messageHandlers.has(normalizedPattern)) {
3084
+ throw new Error(
3085
+ `Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice \u2014 find and remove the second declaration.`
3086
+ );
3087
+ }
3088
+ super.addHandler(pattern, callback, isEventHandler, extras);
3089
+ }
2305
3090
  /**
2306
3091
  * Register event listener (required by Server base class).
2307
3092
  *
@@ -2374,7 +3159,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
2374
3159
  };
2375
3160
 
2376
3161
  // src/server/core-rpc.server.ts
2377
- var import_common8 = require("@nestjs/common");
3162
+ var import_common12 = require("@nestjs/common");
2378
3163
  var import_transport_node3 = require("@nats-io/transport-node");
2379
3164
 
2380
3165
  // src/context/rpc.context.ts
@@ -2643,7 +3428,7 @@ var CoreRpcServer = class {
2643
3428
  this.serviceName = derived.serviceName;
2644
3429
  this.serverEndpoint = derived.serverEndpoint;
2645
3430
  }
2646
- logger = new import_common8.Logger("Jetstream:CoreRpc");
3431
+ logger = new import_common12.Logger("Jetstream:CoreRpc");
2647
3432
  subscription = null;
2648
3433
  otel;
2649
3434
  serviceName;
@@ -2696,6 +3481,7 @@ var CoreRpcServer = class {
2696
3481
  return;
2697
3482
  }
2698
3483
  const ctx = new RpcContext([msg]);
3484
+ const startedAt = performance.now();
2699
3485
  try {
2700
3486
  const raw = await withConsumeSpan(
2701
3487
  {
@@ -2714,6 +3500,7 @@ var CoreRpcServer = class {
2714
3500
  }
2715
3501
  );
2716
3502
  msg.respond(this.codec.encode(raw));
3503
+ this.reportHandlerCompleted(msg, startedAt, "success");
2717
3504
  } catch (err) {
2718
3505
  this.eventBus.emit(
2719
3506
  "error" /* Error */,
@@ -2721,7 +3508,23 @@ var CoreRpcServer = class {
2721
3508
  `core-rpc-handler:${msg.subject}`
2722
3509
  );
2723
3510
  this.respondWithError(msg, err);
2724
- }
3511
+ this.reportHandlerCompleted(msg, startedAt, "error");
3512
+ }
3513
+ }
3514
+ // See EventRouter.reportHandlerCompleted for the rationale on declared
3515
+ // pattern + per-emit hasHook check.
3516
+ reportHandlerCompleted(msg, startedAt, status) {
3517
+ if (!this.eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
3518
+ const declared = this.patternRegistry.resolveDeclared(msg.subject);
3519
+ const pattern = declared?.pattern ?? msg.subject;
3520
+ const kind = declared?.kind ?? "cmd" /* Command */;
3521
+ this.eventBus.emit(
3522
+ "handlerCompleted" /* HandlerCompleted */,
3523
+ pattern,
3524
+ kind,
3525
+ performance.now() - startedAt,
3526
+ status
3527
+ );
2725
3528
  }
2726
3529
  /** Send an error response back to the caller with x-error header. */
2727
3530
  respondWithError(msg, error) {
@@ -2736,8 +3539,8 @@ var CoreRpcServer = class {
2736
3539
  };
2737
3540
 
2738
3541
  // src/server/infrastructure/stream.provider.ts
2739
- var import_common10 = require("@nestjs/common");
2740
- var import_jetstream14 = require("@nats-io/jetstream");
3542
+ var import_common14 = require("@nestjs/common");
3543
+ var import_jetstream17 = require("@nats-io/jetstream");
2741
3544
 
2742
3545
  // src/server/infrastructure/nats-error-codes.ts
2743
3546
  var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
@@ -2803,8 +3606,8 @@ var isEqual = (a, b) => {
2803
3606
  };
2804
3607
 
2805
3608
  // src/server/infrastructure/stream-migration.ts
2806
- var import_common9 = require("@nestjs/common");
2807
- var import_jetstream13 = require("@nats-io/jetstream");
3609
+ var import_common13 = require("@nestjs/common");
3610
+ var import_jetstream16 = require("@nats-io/jetstream");
2808
3611
  var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
2809
3612
  var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
2810
3613
  var SOURCING_POLL_INTERVAL_MS = 100;
@@ -2812,7 +3615,7 @@ var StreamMigration = class {
2812
3615
  constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
2813
3616
  this.sourcingTimeoutMs = sourcingTimeoutMs;
2814
3617
  }
2815
- logger = new import_common9.Logger("Jetstream:Stream");
3618
+ logger = new import_common13.Logger("Jetstream:Stream");
2816
3619
  async migrate(jsm, streamName2, newConfig) {
2817
3620
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
2818
3621
  const startTime = Date.now();
@@ -2881,7 +3684,7 @@ var StreamMigration = class {
2881
3684
  this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
2882
3685
  await jsm.streams.delete(backupName);
2883
3686
  } catch (err) {
2884
- if (err instanceof import_jetstream13.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3687
+ if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
2885
3688
  return;
2886
3689
  }
2887
3690
  throw err;
@@ -2899,7 +3702,7 @@ var StreamProvider = class {
2899
3702
  this.otelServiceName = derived.serviceName;
2900
3703
  this.otelEndpoint = derived.serverEndpoint;
2901
3704
  }
2902
- logger = new import_common10.Logger("Jetstream:Stream");
3705
+ logger = new import_common14.Logger("Jetstream:Stream");
2903
3706
  migration = new StreamMigration();
2904
3707
  otel;
2905
3708
  otelServiceName;
@@ -2964,7 +3767,7 @@ var StreamProvider = class {
2964
3767
  const currentInfo = await jsm.streams.info(config.name);
2965
3768
  return await this.handleExistingStream(jsm, currentInfo, config);
2966
3769
  } catch (err) {
2967
- if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3770
+ if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
2968
3771
  this.logger.log(`Creating stream: ${config.name}`);
2969
3772
  return await jsm.streams.add(config);
2970
3773
  }
@@ -2991,7 +3794,7 @@ var StreamProvider = class {
2991
3794
  const currentInfo = await jsm.streams.info(config.name);
2992
3795
  return await this.handleExistingStream(jsm, currentInfo, config);
2993
3796
  } catch (err) {
2994
- if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3797
+ if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
2995
3798
  this.logger.log(`Creating DLQ stream: ${config.name}`);
2996
3799
  return await jsm.streams.add(config);
2997
3800
  }
@@ -3154,8 +3957,8 @@ var StreamProvider = class {
3154
3957
  };
3155
3958
 
3156
3959
  // src/server/infrastructure/consumer.provider.ts
3157
- var import_common11 = require("@nestjs/common");
3158
- var import_jetstream16 = require("@nats-io/jetstream");
3960
+ var import_common15 = require("@nestjs/common");
3961
+ var import_jetstream19 = require("@nats-io/jetstream");
3159
3962
  var ConsumerProvider = class {
3160
3963
  constructor(options, connection, streamProvider, patternRegistry) {
3161
3964
  this.options = options;
@@ -3167,7 +3970,7 @@ var ConsumerProvider = class {
3167
3970
  this.otelServiceName = derived.serviceName;
3168
3971
  this.otelEndpoint = derived.serverEndpoint;
3169
3972
  }
3170
- logger = new import_common11.Logger("Jetstream:Consumer");
3973
+ logger = new import_common15.Logger("Jetstream:Consumer");
3171
3974
  otel;
3172
3975
  otelServiceName;
3173
3976
  otelEndpoint;
@@ -3216,7 +4019,7 @@ var ConsumerProvider = class {
3216
4019
  this.logger.debug(`Consumer exists, updating: ${name}`);
3217
4020
  return await jsm.consumers.update(stream, name, config);
3218
4021
  } catch (err) {
3219
- if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4022
+ if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
3220
4023
  throw err;
3221
4024
  }
3222
4025
  return await this.createConsumer(jsm, stream, name, config);
@@ -3256,7 +4059,7 @@ var ConsumerProvider = class {
3256
4059
  try {
3257
4060
  return await jsm.consumers.info(stream, name);
3258
4061
  } catch (err) {
3259
- if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4062
+ if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
3260
4063
  throw err;
3261
4064
  }
3262
4065
  return await this.createConsumer(jsm, stream, name, config);
@@ -3277,7 +4080,7 @@ var ConsumerProvider = class {
3277
4080
  `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
3278
4081
  );
3279
4082
  } catch (err) {
3280
- if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4083
+ if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3281
4084
  return;
3282
4085
  }
3283
4086
  throw err;
@@ -3291,7 +4094,7 @@ var ConsumerProvider = class {
3291
4094
  try {
3292
4095
  return await jsm.consumers.add(stream, config);
3293
4096
  } catch (addErr) {
3294
- if (addErr instanceof import_jetstream16.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
4097
+ if (addErr instanceof import_jetstream19.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
3295
4098
  this.logger.debug(`Consumer ${name} created by another pod, using existing`);
3296
4099
  return await jsm.consumers.info(stream, name);
3297
4100
  }
@@ -3378,8 +4181,8 @@ var ConsumerProvider = class {
3378
4181
  };
3379
4182
 
3380
4183
  // src/server/infrastructure/message.provider.ts
3381
- var import_common12 = require("@nestjs/common");
3382
- var import_jetstream18 = require("@nats-io/jetstream");
4184
+ var import_common16 = require("@nestjs/common");
4185
+ var import_jetstream21 = require("@nats-io/jetstream");
3383
4186
  var import_rxjs3 = require("rxjs");
3384
4187
  var MessageProvider = class {
3385
4188
  constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
@@ -3388,7 +4191,7 @@ var MessageProvider = class {
3388
4191
  this.consumeOptionsMap = consumeOptionsMap;
3389
4192
  this.consumerRecoveryFn = consumerRecoveryFn;
3390
4193
  }
3391
- logger = new import_common12.Logger("Jetstream:Message");
4194
+ logger = new import_common16.Logger("Jetstream:Message");
3392
4195
  activeIterators = /* @__PURE__ */ new Set();
3393
4196
  orderedReadyResolve = null;
3394
4197
  orderedReadyReject = null;
@@ -3440,7 +4243,7 @@ var MessageProvider = class {
3440
4243
  */
3441
4244
  async startOrdered(streamName2, filterSubjects, orderedConfig) {
3442
4245
  const consumerOpts = { filter_subjects: filterSubjects };
3443
- if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream18.DeliverPolicy.All) {
4246
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream21.DeliverPolicy.All) {
3444
4247
  consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
3445
4248
  }
3446
4249
  if (orderedConfig?.optStartSeq !== void 0) {
@@ -3486,10 +4289,13 @@ var MessageProvider = class {
3486
4289
  /** Create a self-healing consumer flow for a specific kind. */
3487
4290
  createFlow(kind, info) {
3488
4291
  const target$ = this.getTargetSubject(kind);
3489
- return this.createSelfHealingFlow(() => this.consumeOnce(kind, info, target$), info.name);
4292
+ return this.createSelfHealingFlow(
4293
+ (onConnected) => this.consumeOnce(kind, info, target$, onConnected),
4294
+ info.name
4295
+ );
3490
4296
  }
3491
4297
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
3492
- async consumeOnce(kind, info, target$) {
4298
+ async consumeOnce(kind, info, target$, onConnected) {
3493
4299
  const js = this.connection.getJetStreamClient();
3494
4300
  let consumer;
3495
4301
  let consumerName2 = info.name;
@@ -3517,6 +4323,7 @@ var MessageProvider = class {
3517
4323
  });
3518
4324
  this.activeIterators.add(messages);
3519
4325
  this.monitorConsumerHealth(messages, consumerName2);
4326
+ onConnected();
3520
4327
  try {
3521
4328
  await messages.closed();
3522
4329
  } finally {
@@ -3571,7 +4378,7 @@ var MessageProvider = class {
3571
4378
  /** Create a self-healing ordered consumer flow. */
3572
4379
  createOrderedFlow(streamName2, consumerOpts) {
3573
4380
  return this.createSelfHealingFlow(
3574
- () => this.consumeOrderedOnce(streamName2, consumerOpts),
4381
+ (onConnected) => this.consumeOrderedOnce(streamName2, consumerOpts, onConnected),
3575
4382
  "ordered" /* Ordered */,
3576
4383
  (err) => {
3577
4384
  if (this.orderedReadyReject) {
@@ -3585,10 +4392,15 @@ var MessageProvider = class {
3585
4392
  /** Shared self-healing flow: defer -> retry with exponential backoff on error/completion. */
3586
4393
  createSelfHealingFlow(source, label, onFirstError) {
3587
4394
  let consecutiveFailures = 0;
3588
- return (0, import_rxjs3.defer)(source).pipe(
3589
- (0, import_rxjs3.tap)(() => {
3590
- consecutiveFailures = 0;
3591
- }),
4395
+ const onConnected = () => {
4396
+ if (consecutiveFailures > 0) {
4397
+ const attempts = consecutiveFailures;
4398
+ this.logger.log(`Consumer ${label} recovered after ${attempts} failed attempt(s)`);
4399
+ this.eventBus.emit("consumerRecovered" /* ConsumerRecovered */, label, attempts);
4400
+ }
4401
+ consecutiveFailures = 0;
4402
+ };
4403
+ return (0, import_rxjs3.defer)(() => source(onConnected)).pipe(
3592
4404
  (0, import_rxjs3.catchError)((err) => {
3593
4405
  consecutiveFailures++;
3594
4406
  this.logger.error(`Consumer ${label} error, will restart:`, err);
@@ -3611,7 +4423,7 @@ var MessageProvider = class {
3611
4423
  );
3612
4424
  }
3613
4425
  /** Single iteration: create ordered consumer -> push messages into the subject. */
3614
- async consumeOrderedOnce(streamName2, consumerOpts) {
4426
+ async consumeOrderedOnce(streamName2, consumerOpts, onConnected) {
3615
4427
  const js = this.connection.getJetStreamClient();
3616
4428
  const consumer = await js.consumers.get(streamName2, consumerOpts);
3617
4429
  const orderedMessages$ = this.orderedMessages$;
@@ -3626,6 +4438,7 @@ var MessageProvider = class {
3626
4438
  this.orderedReadyReject = null;
3627
4439
  }
3628
4440
  this.activeIterators.add(messages);
4441
+ onConnected();
3629
4442
  try {
3630
4443
  await messages.closed();
3631
4444
  } finally {
@@ -3635,7 +4448,7 @@ var MessageProvider = class {
3635
4448
  };
3636
4449
 
3637
4450
  // src/server/infrastructure/metadata.provider.ts
3638
- var import_common13 = require("@nestjs/common");
4451
+ var import_common17 = require("@nestjs/common");
3639
4452
  var import_kv = require("@nats-io/kv");
3640
4453
  var MetadataProvider = class {
3641
4454
  constructor(options, connection) {
@@ -3644,7 +4457,7 @@ var MetadataProvider = class {
3644
4457
  this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
3645
4458
  this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
3646
4459
  }
3647
- logger = new import_common13.Logger("Jetstream:Metadata");
4460
+ logger = new import_common17.Logger("Jetstream:Metadata");
3648
4461
  bucketName;
3649
4462
  replicas;
3650
4463
  ttl;
@@ -3736,176 +4549,8 @@ var MetadataProvider = class {
3736
4549
  }
3737
4550
  };
3738
4551
 
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
4552
  // src/server/routing/event.router.ts
3908
- var import_common15 = require("@nestjs/common");
4553
+ var import_common18 = require("@nestjs/common");
3909
4554
  var import_transport_node4 = require("@nats-io/transport-node");
3910
4555
  var eventConsumeKindFor = (kind) => {
3911
4556
  if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
@@ -3934,7 +4579,7 @@ var EventRouter = class {
3934
4579
  this.serverEndpoint = null;
3935
4580
  }
3936
4581
  }
3937
- logger = new import_common15.Logger("Jetstream:EventRouter");
4582
+ logger = new import_common18.Logger("Jetstream:EventRouter");
3938
4583
  subscriptions = [];
3939
4584
  otel;
3940
4585
  serviceName;
@@ -3978,7 +4623,14 @@ var EventRouter = class {
3978
4623
  const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
3979
4624
  const concurrency = this.getConcurrency(kind);
3980
4625
  const hasDlqCheck = deadLetterConfig !== void 0;
3981
- const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
4626
+ const reportHandlerCompleted = (msg, startedAt, status) => {
4627
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
4628
+ const declared = patternRegistry.resolveDeclared(msg.subject);
4629
+ const pattern = declared?.pattern ?? msg.subject;
4630
+ const declaredKind = declared?.kind ?? kind;
4631
+ const durationMs = performance.now() - startedAt;
4632
+ eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
4633
+ };
3982
4634
  const isDeadLetter = (msg) => {
3983
4635
  if (!hasDlqCheck) return false;
3984
4636
  const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
@@ -4015,7 +4667,7 @@ var EventRouter = class {
4015
4667
  logger5.error(`Decode error for ${subject}:`, err);
4016
4668
  return null;
4017
4669
  }
4018
- if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
4670
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
4019
4671
  return { handler, data };
4020
4672
  } catch (err) {
4021
4673
  logger5.error(`Unexpected error in ${kind} event router`, err);
@@ -4027,12 +4679,18 @@ var EventRouter = class {
4027
4679
  return null;
4028
4680
  }
4029
4681
  };
4682
+ const statusForContext = (ctx) => {
4683
+ if (ctx.shouldTerminate) return "terminated";
4684
+ if (ctx.shouldRetry) return "retried";
4685
+ return "success";
4686
+ };
4030
4687
  const handleSafe = (msg) => {
4031
4688
  const resolved = resolveEvent(msg);
4032
4689
  if (resolved === null) return void 0;
4033
4690
  const { handler, data } = resolved;
4034
4691
  const ctx = new RpcContext([msg]);
4035
4692
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
4693
+ const startedAt = performance.now();
4036
4694
  let pending;
4037
4695
  try {
4038
4696
  pending = withConsumeSpan(
@@ -4055,18 +4713,21 @@ var EventRouter = class {
4055
4713
  err instanceof Error ? err : new Error(String(err)),
4056
4714
  `${kind}-handler:${msg.subject}`
4057
4715
  );
4716
+ reportHandlerCompleted(msg, startedAt, "error");
4058
4717
  return settleFailure(msg, data, err).finally(() => {
4059
4718
  if (stopAckExtension !== null) stopAckExtension();
4060
4719
  });
4061
4720
  }
4062
4721
  if (!isPromiseLike2(pending)) {
4063
4722
  settleSuccess(msg, ctx);
4723
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4064
4724
  if (stopAckExtension !== null) stopAckExtension();
4065
4725
  return void 0;
4066
4726
  }
4067
4727
  return pending.then(
4068
4728
  () => {
4069
4729
  settleSuccess(msg, ctx);
4730
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4070
4731
  if (stopAckExtension !== null) stopAckExtension();
4071
4732
  },
4072
4733
  async (err) => {
@@ -4075,6 +4736,7 @@ var EventRouter = class {
4075
4736
  err instanceof Error ? err : new Error(String(err)),
4076
4737
  `${kind}-handler:${msg.subject}`
4077
4738
  );
4739
+ reportHandlerCompleted(msg, startedAt, "error");
4078
4740
  try {
4079
4741
  await settleFailure(msg, data, err);
4080
4742
  } finally {
@@ -4099,7 +4761,7 @@ var EventRouter = class {
4099
4761
  logger5.error(`Decode error for ${subject}:`, err);
4100
4762
  return void 0;
4101
4763
  }
4102
- if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
4764
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
4103
4765
  } catch (err) {
4104
4766
  logger5.error(`Ordered handler error (${subject}):`, err);
4105
4767
  return void 0;
@@ -4112,6 +4774,7 @@ var EventRouter = class {
4112
4774
  );
4113
4775
  }
4114
4776
  };
4777
+ const startedAt = performance.now();
4115
4778
  let pending;
4116
4779
  try {
4117
4780
  pending = withConsumeSpan(
@@ -4130,15 +4793,24 @@ var EventRouter = class {
4130
4793
  );
4131
4794
  } catch (err) {
4132
4795
  logger5.error(`Ordered handler error (${subject}):`, err);
4796
+ reportHandlerCompleted(msg, startedAt, "error");
4133
4797
  return void 0;
4134
4798
  }
4135
4799
  if (!isPromiseLike2(pending)) {
4136
4800
  warnIfSettlementAttempted();
4801
+ reportHandlerCompleted(msg, startedAt, "success");
4137
4802
  return void 0;
4138
4803
  }
4139
- return pending.then(warnIfSettlementAttempted, (err) => {
4140
- logger5.error(`Ordered handler error (${subject}):`, err);
4141
- });
4804
+ return pending.then(
4805
+ () => {
4806
+ warnIfSettlementAttempted();
4807
+ reportHandlerCompleted(msg, startedAt, "success");
4808
+ },
4809
+ (err) => {
4810
+ logger5.error(`Ordered handler error (${subject}):`, err);
4811
+ reportHandlerCompleted(msg, startedAt, "error");
4812
+ }
4813
+ );
4142
4814
  };
4143
4815
  const route = isOrdered ? handleOrderedSafe : handleSafe;
4144
4816
  const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
@@ -4324,7 +4996,7 @@ var EventRouter = class {
4324
4996
  };
4325
4997
 
4326
4998
  // src/server/routing/rpc.router.ts
4327
- var import_common16 = require("@nestjs/common");
4999
+ var import_common19 = require("@nestjs/common");
4328
5000
  var import_transport_node5 = require("@nats-io/transport-node");
4329
5001
  var RpcRouter = class {
4330
5002
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
@@ -4348,7 +5020,7 @@ var RpcRouter = class {
4348
5020
  this.serverEndpoint = null;
4349
5021
  }
4350
5022
  }
4351
- logger = new import_common16.Logger("Jetstream:RpcRouter");
5023
+ logger = new import_common19.Logger("Jetstream:RpcRouter");
4352
5024
  timeout;
4353
5025
  concurrency;
4354
5026
  resolvedAckExtensionInterval;
@@ -4384,6 +5056,19 @@ var RpcRouter = class {
4384
5056
  const emitRpcTimeout = (subject, correlationId) => {
4385
5057
  eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
4386
5058
  };
5059
+ const reportHandlerCompleted = (msg, startedAt, status) => {
5060
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
5061
+ const declared = patternRegistry.resolveDeclared(msg.subject);
5062
+ const pattern = declared?.pattern ?? msg.subject;
5063
+ const declaredKind = declared?.kind ?? "cmd" /* Command */;
5064
+ eventBus.emit(
5065
+ "handlerCompleted" /* HandlerCompleted */,
5066
+ pattern,
5067
+ declaredKind,
5068
+ performance.now() - startedAt,
5069
+ status
5070
+ );
5071
+ };
4387
5072
  const publishReply = (replyTo, correlationId, payload) => {
4388
5073
  try {
4389
5074
  const hdrs = (0, import_transport_node5.headers)();
@@ -4447,6 +5132,7 @@ var RpcRouter = class {
4447
5132
  const subject = msg.subject;
4448
5133
  const ctx = new RpcContext([msg]);
4449
5134
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
5135
+ const startedAt = performance.now();
4450
5136
  const reportHandlerError = (err) => {
4451
5137
  eventBus.emit(
4452
5138
  "error" /* Error */,
@@ -4477,12 +5163,14 @@ var RpcRouter = class {
4477
5163
  } catch (err) {
4478
5164
  if (stopAckExtension !== null) stopAckExtension();
4479
5165
  reportHandlerError(err);
5166
+ reportHandlerCompleted(msg, startedAt, "error");
4480
5167
  return void 0;
4481
5168
  }
4482
5169
  if (!isPromiseLike2(pending)) {
4483
5170
  if (stopAckExtension !== null) stopAckExtension();
4484
5171
  msg.ack();
4485
5172
  publishReply(replyTo, correlationId, pending);
5173
+ reportHandlerCompleted(msg, startedAt, "success");
4486
5174
  return void 0;
4487
5175
  }
4488
5176
  let settled = false;
@@ -4493,6 +5181,7 @@ var RpcRouter = class {
4493
5181
  abortController.abort();
4494
5182
  emitRpcTimeout(subject, correlationId);
4495
5183
  msg.term("Handler timeout");
5184
+ reportHandlerCompleted(msg, startedAt, "terminated");
4496
5185
  }, timeout);
4497
5186
  return pending.then(
4498
5187
  (result) => {
@@ -4502,6 +5191,7 @@ var RpcRouter = class {
4502
5191
  if (stopAckExtension !== null) stopAckExtension();
4503
5192
  msg.ack();
4504
5193
  publishReply(replyTo, correlationId, result);
5194
+ reportHandlerCompleted(msg, startedAt, "success");
4505
5195
  },
4506
5196
  (err) => {
4507
5197
  if (settled) return;
@@ -4509,6 +5199,7 @@ var RpcRouter = class {
4509
5199
  clearTimeout(timeoutId);
4510
5200
  if (stopAckExtension !== null) stopAckExtension();
4511
5201
  reportHandlerError(err);
5202
+ reportHandlerCompleted(msg, startedAt, "error");
4512
5203
  }
4513
5204
  );
4514
5205
  };
@@ -4568,14 +5259,14 @@ var RpcRouter = class {
4568
5259
  };
4569
5260
 
4570
5261
  // src/shutdown/shutdown.manager.ts
4571
- var import_common17 = require("@nestjs/common");
5262
+ var import_common20 = require("@nestjs/common");
4572
5263
  var ShutdownManager = class {
4573
5264
  constructor(connection, eventBus, timeout) {
4574
5265
  this.connection = connection;
4575
5266
  this.eventBus = eventBus;
4576
5267
  this.timeout = timeout;
4577
5268
  }
4578
- logger = new import_common17.Logger("Jetstream:Shutdown");
5269
+ logger = new import_common20.Logger("Jetstream:Shutdown");
4579
5270
  shutdownPromise;
4580
5271
  /**
4581
5272
  * Execute the full shutdown sequence.
@@ -4629,12 +5320,14 @@ var JetstreamModule = class {
4629
5320
  return {
4630
5321
  module: JetstreamModule,
4631
5322
  global: true,
5323
+ imports: options.metrics ? [JetstreamMetricsModule.forFeature(options.metrics)] : [],
4632
5324
  providers,
4633
5325
  exports: [
4634
5326
  JETSTREAM_CONNECTION,
4635
5327
  JETSTREAM_CODEC,
4636
5328
  JETSTREAM_EVENT_BUS,
4637
5329
  JETSTREAM_OPTIONS,
5330
+ PatternRegistry,
4638
5331
  ShutdownManager,
4639
5332
  JetstreamStrategy,
4640
5333
  JetstreamHealthIndicator
@@ -4653,16 +5346,18 @@ var JetstreamModule = class {
4653
5346
  static forRootAsync(asyncOptions) {
4654
5347
  const asyncProviders = this.createAsyncOptionsProvider(asyncOptions);
4655
5348
  const coreProviders = this.createCoreDependentProviders();
5349
+ const metricsImports = asyncOptions.metrics ? [JetstreamMetricsModule.forFeature(asyncOptions.metrics)] : [];
4656
5350
  return {
4657
5351
  module: JetstreamModule,
4658
5352
  global: true,
4659
- imports: asyncOptions.imports ?? [],
5353
+ imports: [...asyncOptions.imports ?? [], ...metricsImports],
4660
5354
  providers: [...asyncProviders, ...coreProviders],
4661
5355
  exports: [
4662
5356
  JETSTREAM_CONNECTION,
4663
5357
  JETSTREAM_CODEC,
4664
5358
  JETSTREAM_EVENT_BUS,
4665
5359
  JETSTREAM_OPTIONS,
5360
+ PatternRegistry,
4666
5361
  ShutdownManager,
4667
5362
  JetstreamStrategy,
4668
5363
  JetstreamHealthIndicator
@@ -4711,7 +5406,7 @@ var JetstreamModule = class {
4711
5406
  provide: JETSTREAM_EVENT_BUS,
4712
5407
  inject: [JETSTREAM_OPTIONS],
4713
5408
  useFactory: (options) => {
4714
- const logger5 = new import_common18.Logger("Jetstream:Module");
5409
+ const logger5 = new import_common21.Logger("Jetstream:Module");
4715
5410
  return new EventBus(logger5, options.hooks);
4716
5411
  }
4717
5412
  },
@@ -5005,12 +5700,12 @@ var JetstreamModule = class {
5005
5700
  }
5006
5701
  };
5007
5702
  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))
5703
+ (0, import_common21.Global)(),
5704
+ (0, import_common21.Module)({}),
5705
+ __decorateParam(0, (0, import_common21.Optional)()),
5706
+ __decorateParam(0, (0, import_common21.Inject)(ShutdownManager)),
5707
+ __decorateParam(1, (0, import_common21.Optional)()),
5708
+ __decorateParam(1, (0, import_common21.Inject)(JetstreamStrategy))
5014
5709
  ], JetstreamModule);
5015
5710
  // Annotate the CommonJS export names for ESM import in node:
5016
5711
  0 && (module.exports = {