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