@horizon-republic/nestjs-jetstream 2.12.1 → 2.13.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
@@ -71,6 +71,7 @@ __export(index_exports, {
71
71
  JetstreamTrace: () => JetstreamTrace,
72
72
  JsonCodec: () => JsonCodec,
73
73
  MIN_METADATA_TTL: () => MIN_METADATA_TTL,
74
+ ManagementMode: () => ManagementMode,
74
75
  MessageKind: () => MessageKind,
75
76
  MsgpackCodec: () => MsgpackCodec,
76
77
  NatsErrorCode: () => NatsErrorCode,
@@ -96,14 +97,14 @@ __export(index_exports, {
96
97
  module.exports = __toCommonJS(index_exports);
97
98
 
98
99
  // src/jetstream.module.ts
99
- var import_common21 = require("@nestjs/common");
100
+ var import_common24 = require("@nestjs/common");
100
101
 
101
102
  // src/client/jetstream.client.ts
102
- var import_common5 = require("@nestjs/common");
103
+ var import_common6 = require("@nestjs/common");
103
104
  var import_microservices = require("@nestjs/microservices");
104
105
  var import_api8 = require("@opentelemetry/api");
105
106
  var import_nuid = require("@nats-io/nuid");
106
- var import_transport_node = require("@nats-io/transport-node");
107
+ var import_transport_node2 = require("@nats-io/transport-node");
107
108
 
108
109
  // src/interfaces/hooks.interface.ts
109
110
  var MessageKind = /* @__PURE__ */ ((MessageKind2) => {
@@ -128,6 +129,13 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
128
129
  return TransportEvent2;
129
130
  })(TransportEvent || {});
130
131
 
132
+ // src/interfaces/options.interface.ts
133
+ var ManagementMode = /* @__PURE__ */ ((ManagementMode2) => {
134
+ ManagementMode2["Auto"] = "auto";
135
+ ManagementMode2["Manual"] = "manual";
136
+ return ManagementMode2;
137
+ })(ManagementMode || {});
138
+
131
139
  // src/interfaces/stream.interface.ts
132
140
  var StreamKind = /* @__PURE__ */ ((StreamKind2) => {
133
141
  StreamKind2["Event"] = "ev";
@@ -277,8 +285,27 @@ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
277
285
  ]);
278
286
  var NATS_CONTROL_HEADER_PREFIX = "nats-";
279
287
  var internalName = (name) => `${name}__microservice`;
280
- var buildSubject = (serviceName, kind, pattern) => `${internalName(serviceName)}.${kind}.${pattern}`;
281
- var buildBroadcastSubject = (pattern) => `broadcast.${pattern}`;
288
+ var subjectPrefix = (serviceName, kind) => `${internalName(serviceName)}.${kind}.`;
289
+ var buildSubject = (serviceName, kind, pattern) => `${subjectPrefix(serviceName, kind)}${pattern}`;
290
+ var BROADCAST_SUBJECT_PREFIX = "broadcast.";
291
+ var SCHEDULE_SEGMENT = "_sch.";
292
+ var buildBroadcastSubject = (pattern) => `${BROADCAST_SUBJECT_PREFIX}${pattern}`;
293
+ var conventionScheduleSubjectBase = (targetName, eventSubject) => {
294
+ if (eventSubject.startsWith(BROADCAST_SUBJECT_PREFIX)) {
295
+ const bare = eventSubject.slice(BROADCAST_SUBJECT_PREFIX.length);
296
+ return `${BROADCAST_SUBJECT_PREFIX}${SCHEDULE_SEGMENT}${bare}`;
297
+ }
298
+ const targetPrefix = `${internalName(targetName)}.`;
299
+ if (!eventSubject.startsWith(targetPrefix)) {
300
+ throw new Error(`Unexpected event subject format: ${eventSubject}`);
301
+ }
302
+ const withoutPrefix = eventSubject.slice(targetPrefix.length);
303
+ const dotIndex = withoutPrefix.indexOf(".");
304
+ if (dotIndex === -1) {
305
+ throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
306
+ }
307
+ return `${targetPrefix}${SCHEDULE_SEGMENT}${withoutPrefix.slice(dotIndex + 1)}`;
308
+ };
282
309
  var streamName = (serviceName, kind) => {
283
310
  if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
284
311
  return `${internalName(serviceName)}_${kind}-stream`;
@@ -298,6 +325,118 @@ var PatternPrefix = /* @__PURE__ */ ((PatternPrefix2) => {
298
325
  var isJetStreamRpcMode = (rpc) => rpc?.mode === "jetstream";
299
326
  var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
300
327
 
328
+ // src/server/infrastructure/management.ts
329
+ var kindOptionsBlock = (options, kind) => {
330
+ switch (kind) {
331
+ case "ev" /* Event */:
332
+ return options.events;
333
+ case "broadcast" /* Broadcast */:
334
+ return options.broadcast;
335
+ case "ordered" /* Ordered */:
336
+ return options.ordered;
337
+ case "cmd" /* Command */:
338
+ return isJetStreamRpcMode(options.rpc) ? options.rpc : void 0;
339
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
340
+ default: {
341
+ const _exhaustive = kind;
342
+ throw new Error(`Unhandled StreamKind: ${String(_exhaustive)}`);
343
+ }
344
+ }
345
+ };
346
+ var entityManagementFor = (options, kind) => {
347
+ if (kind === "dlq") return options.dlq?.management;
348
+ return kindOptionsBlock(options, kind)?.management;
349
+ };
350
+ var resolveManagementMode = (options, kind, entity) => entityManagementFor(options, kind)?.[entity] ?? options.provisioning?.management ?? "auto" /* Auto */;
351
+
352
+ // src/server/infrastructure/name-resolver.ts
353
+ var NameResolver = class {
354
+ constructor(options) {
355
+ this.options = options;
356
+ this.dlq = options.dlq?.stream?.name ?? dlqStreamName(options.name);
357
+ this.kinds = this.buildKindMap();
358
+ }
359
+ kinds;
360
+ dlq;
361
+ streamName(kind) {
362
+ return this.get(kind).stream;
363
+ }
364
+ consumerName(kind) {
365
+ return this.get(kind).consumer;
366
+ }
367
+ dlqStreamName() {
368
+ return this.dlq;
369
+ }
370
+ subject(kind, pattern) {
371
+ return `${this.get(kind).prefix}${pattern}`;
372
+ }
373
+ filterSubject(kind) {
374
+ return `${this.get(kind).prefix}>`;
375
+ }
376
+ schedulePrefix(kind) {
377
+ return this.get(kind).schedulePrefix;
378
+ }
379
+ hasCustomPrefix(kind) {
380
+ return this.get(kind).custom;
381
+ }
382
+ /**
383
+ * Map a resolved event subject back to its schedule-holder base subject
384
+ * (the `_sch` namespace twin, without the per-message unique suffix).
385
+ */
386
+ scheduleSubjectBase(eventSubject) {
387
+ for (const kind of ["broadcast" /* Broadcast */, "ev" /* Event */, "ordered" /* Ordered */]) {
388
+ const { prefix, schedulePrefix } = this.get(kind);
389
+ if (eventSubject.startsWith(prefix)) {
390
+ return `${schedulePrefix}${eventSubject.slice(prefix.length)}`;
391
+ }
392
+ }
393
+ throw new Error(`Unexpected event subject format: ${eventSubject}`);
394
+ }
395
+ get(kind) {
396
+ const entry = this.kinds.get(kind);
397
+ if (!entry) throw new Error(`Unknown StreamKind: ${String(kind)}`);
398
+ return entry;
399
+ }
400
+ buildKindMap() {
401
+ const map = /* @__PURE__ */ new Map();
402
+ const { name } = this.options;
403
+ for (const kind of Object.values(StreamKind)) {
404
+ const block = kindOptionsBlock(this.options, kind);
405
+ const customPrefix = block?.subjectPrefix;
406
+ const custom = customPrefix !== void 0;
407
+ const prefix = custom ? this.normalizePrefix(customPrefix) : this.conventionPrefix(name, kind);
408
+ map.set(kind, {
409
+ stream: block?.stream?.name ?? streamName(name, kind),
410
+ consumer: block?.consumer?.durable_name ?? consumerName(name, kind),
411
+ prefix,
412
+ schedulePrefix: custom ? `${prefix}${SCHEDULE_SEGMENT}` : this.conventionSchedulePrefix(name, kind),
413
+ custom
414
+ });
415
+ }
416
+ return map;
417
+ }
418
+ normalizePrefix(raw) {
419
+ let end = raw.length;
420
+ while (end > 0 && raw[end - 1] === ".") end -= 1;
421
+ const trimmed = raw.slice(0, end);
422
+ const tokens = trimmed.split(".");
423
+ if (trimmed.length === 0 || tokens.some((t) => t.length === 0 || t === ">")) {
424
+ throw new Error(
425
+ `Invalid subjectPrefix "${raw}": expected non-empty dot-separated tokens without ">".`
426
+ );
427
+ }
428
+ return `${trimmed}.`;
429
+ }
430
+ conventionPrefix(name, kind) {
431
+ if (kind === "broadcast" /* Broadcast */) return BROADCAST_SUBJECT_PREFIX;
432
+ return subjectPrefix(name, kind);
433
+ }
434
+ conventionSchedulePrefix(name, kind) {
435
+ if (kind === "broadcast" /* Broadcast */) return `${BROADCAST_SUBJECT_PREFIX}${SCHEDULE_SEGMENT}`;
436
+ return `${internalName(name)}.${SCHEDULE_SEGMENT}`;
437
+ }
438
+ };
439
+
301
440
  // src/otel/constants.ts
302
441
  var TRACER_NAME = "@horizon-republic/nestjs-jetstream";
303
442
 
@@ -598,7 +737,7 @@ var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ct
598
737
 
599
738
  // src/otel/tracer.ts
600
739
  var import_api2 = require("@opentelemetry/api");
601
- var PACKAGE_VERSION = true ? "2.12.1" : "0.0.0";
740
+ var PACKAGE_VERSION = true ? "2.13.0" : "0.0.0";
602
741
  var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
603
742
 
604
743
  // src/otel/carrier.ts
@@ -1391,17 +1530,135 @@ var nanosToGoDuration = (nanos) => {
1391
1530
  return `${nanos}ns`;
1392
1531
  };
1393
1532
 
1533
+ // src/client/rpc-reply-inbox.ts
1534
+ var import_common5 = require("@nestjs/common");
1535
+ var import_transport_node = require("@nats-io/transport-node");
1536
+ var RpcReplyInbox = class {
1537
+ constructor(codec, inboxPrefix) {
1538
+ this.codec = codec;
1539
+ this.inboxPrefix = inboxPrefix;
1540
+ }
1541
+ logger = new import_common5.Logger("Jetstream:RpcInbox");
1542
+ inbox = null;
1543
+ subscription = null;
1544
+ pending = /* @__PURE__ */ new Map();
1545
+ timeouts = /* @__PURE__ */ new Map();
1546
+ /** Reply-to subject for outgoing requests; null until {@link setup} ran. */
1547
+ get address() {
1548
+ return this.inbox;
1549
+ }
1550
+ /** True while the inbox subscription is live. */
1551
+ get active() {
1552
+ return this.subscription !== null;
1553
+ }
1554
+ /** Create the inbox subject and subscribe reply routing on it. */
1555
+ setup(nc) {
1556
+ this.inbox = (0, import_transport_node.createInbox)(this.inboxPrefix);
1557
+ this.subscription = nc.subscribe(this.inbox, {
1558
+ callback: (err, msg) => {
1559
+ if (err) {
1560
+ this.logger.error("Inbox subscription error:", err);
1561
+ return;
1562
+ }
1563
+ this.route(msg);
1564
+ }
1565
+ });
1566
+ this.logger.debug(`Inbox subscription: ${this.inbox}`);
1567
+ }
1568
+ /** Register the callback that settles the round-trip for `correlationId`. */
1569
+ register(correlationId, callback) {
1570
+ this.pending.set(correlationId, callback);
1571
+ }
1572
+ /**
1573
+ * Arm the request deadline. When it fires while the request is still
1574
+ * pending, both registry entries are removed before `onExpired` runs.
1575
+ */
1576
+ armTimeout(correlationId, ms, onExpired) {
1577
+ const existing = this.timeouts.get(correlationId);
1578
+ if (existing !== void 0) clearTimeout(existing);
1579
+ const timeoutId = setTimeout(() => {
1580
+ if (!this.pending.has(correlationId)) return;
1581
+ this.timeouts.delete(correlationId);
1582
+ this.pending.delete(correlationId);
1583
+ onExpired();
1584
+ }, ms);
1585
+ this.timeouts.set(correlationId, timeoutId);
1586
+ }
1587
+ /** True while the request has not been settled or discarded. */
1588
+ has(correlationId) {
1589
+ return this.pending.has(correlationId);
1590
+ }
1591
+ /**
1592
+ * Drop a request without invoking its callback: clears the deadline and
1593
+ * returns whether the request was still pending.
1594
+ */
1595
+ discard(correlationId) {
1596
+ const timeoutId = this.timeouts.get(correlationId);
1597
+ if (timeoutId !== void 0) {
1598
+ clearTimeout(timeoutId);
1599
+ this.timeouts.delete(correlationId);
1600
+ }
1601
+ return this.pending.delete(correlationId);
1602
+ }
1603
+ /** Fail every pending request with `error` and tear the inbox down. */
1604
+ rejectAll(error) {
1605
+ for (const callback of this.pending.values()) {
1606
+ callback({ err: error, response: null, isDisposed: true });
1607
+ }
1608
+ for (const timeoutId of this.timeouts.values()) {
1609
+ clearTimeout(timeoutId);
1610
+ }
1611
+ this.pending.clear();
1612
+ this.timeouts.clear();
1613
+ this.subscription?.unsubscribe();
1614
+ this.subscription = null;
1615
+ this.inbox = null;
1616
+ }
1617
+ /** Route an inbox reply to the matching pending callback. */
1618
+ route(msg) {
1619
+ const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
1620
+ if (!correlationId) {
1621
+ this.logger.warn("Inbox reply without correlation-id, ignoring");
1622
+ return;
1623
+ }
1624
+ const callback = this.pending.get(correlationId);
1625
+ if (!callback) {
1626
+ this.logger.warn(`No pending handler for correlation-id: ${correlationId}`);
1627
+ return;
1628
+ }
1629
+ const timeoutId = this.timeouts.get(correlationId);
1630
+ if (timeoutId) {
1631
+ clearTimeout(timeoutId);
1632
+ this.timeouts.delete(correlationId);
1633
+ }
1634
+ try {
1635
+ const decoded = this.codec.decode(msg.data);
1636
+ if (msg.headers?.get("x-error" /* Error */)) {
1637
+ callback({ err: decoded, response: null, isDisposed: true });
1638
+ } else {
1639
+ callback({ err: null, response: decoded, isDisposed: true });
1640
+ }
1641
+ } catch (err) {
1642
+ callback({
1643
+ err: err instanceof Error ? err : new Error("Decode error"),
1644
+ response: null,
1645
+ isDisposed: true
1646
+ });
1647
+ } finally {
1648
+ this.pending.delete(correlationId);
1649
+ }
1650
+ }
1651
+ };
1652
+
1394
1653
  // src/client/jetstream.client.ts
1395
- var BROADCAST_SUBJECT_PREFIX = "broadcast.";
1396
1654
  var detectEventKind = (pattern) => {
1397
1655
  if (pattern.startsWith("broadcast:" /* Broadcast */)) return "broadcast" /* Broadcast */;
1398
1656
  if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
1399
1657
  return "event" /* Event */;
1400
1658
  };
1401
- var declaredEventPattern = (pattern) => {
1402
- if (pattern.startsWith("broadcast:" /* Broadcast */))
1403
- return pattern.slice("broadcast:" /* Broadcast */.length);
1404
- if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
1659
+ var declaredEventPattern = (pattern, kind) => {
1660
+ if (kind === "broadcast" /* Broadcast */) return pattern.slice("broadcast:" /* Broadcast */.length);
1661
+ if (kind === "ordered" /* Ordered */) return pattern.slice("ordered:" /* Ordered */.length);
1405
1662
  return pattern;
1406
1663
  };
1407
1664
  var eventStreamKind = (kind) => {
@@ -1410,7 +1667,7 @@ var eventStreamKind = (kind) => {
1410
1667
  return "ev" /* Event */;
1411
1668
  };
1412
1669
  var JetstreamClient = class extends import_microservices.ClientProxy {
1413
- constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
1670
+ constructor(rootOptions, targetServiceName, connection, codec, eventBus, names) {
1414
1671
  super();
1415
1672
  this.rootOptions = rootOptions;
1416
1673
  this.connection = connection;
@@ -1418,29 +1675,34 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1418
1675
  this.eventBus = eventBus;
1419
1676
  this.targetName = targetServiceName;
1420
1677
  this.callerName = internalName(this.rootOptions.name);
1678
+ const isSelf = targetServiceName === rootOptions.name;
1679
+ const ownNames = names ?? (isSelf ? new NameResolver(rootOptions) : null);
1680
+ this.selfNames = isSelf ? ownNames : null;
1681
+ this.broadcastPrefix = ownNames ? ownNames.subject("broadcast" /* Broadcast */, "") : BROADCAST_SUBJECT_PREFIX;
1421
1682
  const targetInternal = internalName(targetServiceName);
1422
1683
  this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
1423
1684
  this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
1424
1685
  this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
1686
+ this.rpcInbox = new RpcReplyInbox(codec, this.callerName);
1425
1687
  this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
1426
- this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
1688
+ this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
1427
1689
  const derived = deriveOtelAttrs(this.rootOptions);
1428
1690
  this.otel = derived.otel;
1429
1691
  this.serverEndpoint = derived.serverEndpoint;
1430
1692
  }
1431
- logger = new import_common5.Logger("Jetstream:Client");
1693
+ logger = new import_common6.Logger("Jetstream:Client");
1432
1694
  /** Target service name this client sends messages to. */
1433
1695
  targetName;
1434
1696
  /** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
1435
1697
  callerName;
1436
- /**
1437
- * Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
1438
- * per stream kind this client may publish to. Built once in the constructor
1439
- * so producing a full subject is a single string concat with the user pattern.
1440
- */
1698
+ /** Convention subject prefixes for foreign targets; one per stream kind. */
1441
1699
  eventSubjectPrefix;
1442
1700
  commandSubjectPrefix;
1443
1701
  orderedSubjectPrefix;
1702
+ /** Broadcast subject prefix derived from the own resolver; never null. */
1703
+ broadcastPrefix;
1704
+ /** Resolver for self-target subjects; null when targeting a foreign service. */
1705
+ selfNames;
1444
1706
  /**
1445
1707
  * RPC configuration snapshots. The values are derived from rootOptions at
1446
1708
  * construction time so the publish hot path never has to re-run
@@ -1452,13 +1714,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1452
1714
  otel;
1453
1715
  /** Server endpoint parts used for `server.address` / `server.port` span attributes. */
1454
1716
  serverEndpoint;
1455
- /** Shared inbox for JetStream-mode RPC responses. */
1456
- inbox = null;
1457
- inboxSubscription = null;
1458
- /** Pending JetStream-mode RPC callbacks, keyed by correlation ID. */
1459
- pendingMessages = /* @__PURE__ */ new Map();
1460
- /** Pending JetStream-mode RPC timeouts, keyed by correlation ID. */
1461
- pendingTimeouts = /* @__PURE__ */ new Map();
1717
+ /** Reply inbox and pending-request registry for JetStream-mode RPC. */
1718
+ rpcInbox;
1462
1719
  /** Subscription to connection status events for disconnect handling. */
1463
1720
  statusSubscription = null;
1464
1721
  /**
@@ -1477,8 +1734,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1477
1734
  */
1478
1735
  async connect() {
1479
1736
  const nc = await this.connection.getConnection();
1480
- if (!this.isCoreMode && !this.inboxSubscription) {
1481
- this.setupInbox(nc);
1737
+ if (!this.isCoreMode && !this.rpcInbox.active) {
1738
+ this.rpcInbox.setup(nc);
1482
1739
  }
1483
1740
  this.statusSubscription ??= this.connection.status$.subscribe((status) => {
1484
1741
  if (status.type === "disconnect") {
@@ -1493,7 +1750,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1493
1750
  this.statusSubscription?.unsubscribe();
1494
1751
  this.statusSubscription = null;
1495
1752
  this.readyForPublish = false;
1496
- this.rejectPendingRpcs(new Error("Client closed"));
1753
+ this.rpcInbox.rejectAll(new Error("Client closed"));
1497
1754
  }
1498
1755
  /**
1499
1756
  * Direct access to the raw NATS connection.
@@ -1503,7 +1760,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1503
1760
  unwrap() {
1504
1761
  const nc = this.connection.unwrap;
1505
1762
  if (!nc) {
1506
- throw new Error("Not connected \u2014 call connect() before unwrap()");
1763
+ throw new Error("Not connected; call connect() before unwrap()");
1507
1764
  }
1508
1765
  return nc;
1509
1766
  }
@@ -1530,7 +1787,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1530
1787
  const encoded = this.codec.encode(data);
1531
1788
  const effectiveMsgId = messageId ?? import_nuid.nuid.next();
1532
1789
  const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
1533
- const declaredPattern = declaredEventPattern(packet.pattern);
1790
+ const declaredPattern = declaredEventPattern(packet.pattern, publishKind);
1534
1791
  const streamKind = eventStreamKind(publishKind);
1535
1792
  const startedAt = performance.now();
1536
1793
  try {
@@ -1550,13 +1807,6 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1550
1807
  },
1551
1808
  this.otel,
1552
1809
  async () => {
1553
- const warnIfDuplicate = (kindLabel, ack2) => {
1554
- if (ack2.duplicate) {
1555
- this.logger.warn(
1556
- `Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
1557
- );
1558
- }
1559
- };
1560
1810
  if (schedule) {
1561
1811
  const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1562
1812
  headers: msgHeaders,
@@ -1567,7 +1817,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1567
1817
  ttl
1568
1818
  }
1569
1819
  });
1570
- warnIfDuplicate("scheduled", ack2);
1820
+ this.warnIfDuplicate("scheduled", publishSubject, ack2);
1571
1821
  return;
1572
1822
  }
1573
1823
  const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
@@ -1575,7 +1825,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1575
1825
  msgID: effectiveMsgId,
1576
1826
  ttl
1577
1827
  });
1578
- warnIfDuplicate("event", ack);
1828
+ this.warnIfDuplicate("event", publishSubject, ack);
1579
1829
  }
1580
1830
  );
1581
1831
  this.reportPublished(declaredPattern, streamKind, startedAt, "success");
@@ -1592,7 +1842,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1592
1842
  * JetStream mode: publishes to stream + waits for inbox response.
1593
1843
  */
1594
1844
  publish(packet, callback) {
1595
- const subject = this.commandSubjectPrefix + packet.pattern;
1845
+ const subject = this.selfNames ? this.selfNames.subject("cmd" /* Command */, packet.pattern) : this.commandSubjectPrefix + packet.pattern;
1596
1846
  const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
1597
1847
  if (schedule) {
1598
1848
  this.logger.warn(
@@ -1625,12 +1875,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1625
1875
  }
1626
1876
  return () => {
1627
1877
  if (jetStreamCorrelationId) {
1628
- const timeoutId = this.pendingTimeouts.get(jetStreamCorrelationId);
1629
- if (timeoutId) {
1630
- clearTimeout(timeoutId);
1631
- this.pendingTimeouts.delete(jetStreamCorrelationId);
1632
- }
1633
- this.pendingMessages.delete(jetStreamCorrelationId);
1878
+ this.rpcInbox.discard(jetStreamCorrelationId);
1634
1879
  }
1635
1880
  };
1636
1881
  }
@@ -1674,7 +1919,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1674
1919
  }
1675
1920
  } catch (err) {
1676
1921
  const error = err instanceof Error ? err : new Error("Unknown error");
1677
- if (error instanceof import_transport_node.TimeoutError) {
1922
+ if (error instanceof import_transport_node2.TimeoutError) {
1678
1923
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1679
1924
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, "");
1680
1925
  this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
@@ -1695,7 +1940,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1695
1940
  const hdrs = this.buildHeaders(customHeaders, {
1696
1941
  subject,
1697
1942
  correlationId,
1698
- replyTo: this.inbox ?? ""
1943
+ replyTo: this.rpcInbox.address ?? ""
1699
1944
  });
1700
1945
  const encoded = this.codec.encode(data);
1701
1946
  const spanHandle = beginRpcClientSpan(
@@ -1712,7 +1957,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1712
1957
  this.otel
1713
1958
  );
1714
1959
  const startedAt = performance.now();
1715
- this.pendingMessages.set(correlationId, (packet) => {
1960
+ const settleRoundTrip = (packet) => {
1716
1961
  if (packet.err) {
1717
1962
  if (packet.err instanceof Error) {
1718
1963
  spanHandle.finish({ kind: "error" /* Error */, error: packet.err });
@@ -1725,30 +1970,25 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1725
1970
  this.reportRpcCompleted(declaredPattern, startedAt, "success");
1726
1971
  }
1727
1972
  callback(packet);
1728
- });
1729
- const timeoutId = setTimeout(() => {
1730
- if (!this.pendingMessages.has(correlationId)) return;
1731
- this.pendingTimeouts.delete(correlationId);
1732
- this.pendingMessages.delete(correlationId);
1973
+ };
1974
+ this.rpcInbox.register(correlationId, settleRoundTrip);
1975
+ this.rpcInbox.armTimeout(correlationId, effectiveTimeout, () => {
1733
1976
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1734
1977
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
1735
1978
  this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1736
1979
  callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
1737
- }, effectiveTimeout);
1738
- this.pendingTimeouts.set(correlationId, timeoutId);
1980
+ });
1739
1981
  try {
1740
1982
  if (!this.readyForPublish) await this.connect();
1741
- if (!this.pendingMessages.has(correlationId)) return;
1742
- if (!this.inbox) {
1743
- clearTimeout(timeoutId);
1744
- this.pendingTimeouts.delete(correlationId);
1745
- this.pendingMessages.delete(correlationId);
1983
+ if (!this.rpcInbox.has(correlationId)) return;
1984
+ if (!this.rpcInbox.address) {
1985
+ this.rpcInbox.discard(correlationId);
1746
1986
  const inboxError = new Error("Inbox not initialized");
1747
1987
  spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
1748
1988
  this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1749
1989
  this.reportRpcCompleted(declaredPattern, startedAt, "error");
1750
1990
  callback({
1751
- err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
1991
+ err: new Error("Inbox not initialized; JetStream RPC mode requires a connected inbox"),
1752
1992
  response: null,
1753
1993
  isDisposed: true
1754
1994
  });
@@ -1768,13 +2008,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1768
2008
  }
1769
2009
  this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1770
2010
  } catch (err) {
1771
- const existingTimeout = this.pendingTimeouts.get(correlationId);
1772
- if (existingTimeout) {
1773
- clearTimeout(existingTimeout);
1774
- this.pendingTimeouts.delete(correlationId);
1775
- }
1776
- if (!this.pendingMessages.has(correlationId)) return;
1777
- this.pendingMessages.delete(correlationId);
2011
+ if (!this.rpcInbox.discard(correlationId)) return;
1778
2012
  const error = err instanceof Error ? err : new Error("Unknown error");
1779
2013
  spanHandle.finish({ kind: "error" /* Error */, error });
1780
2014
  this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
@@ -1783,6 +2017,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1783
2017
  callback({ err: error, response: null, isDisposed: true });
1784
2018
  }
1785
2019
  }
2020
+ warnIfDuplicate(kindLabel, subject, ack) {
2021
+ if (ack.duplicate) {
2022
+ this.logger.warn(`Duplicate ${kindLabel} publish detected: ${subject} (seq: ${ack.seq})`);
2023
+ }
2024
+ }
1786
2025
  // hasHook is per-emit so late subscribers (JetstreamMetricsService during
1787
2026
  // OnApplicationBootstrap) still receive events.
1788
2027
  reportPublished(declaredPattern, kind, startedAt, status) {
@@ -1806,92 +2045,28 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1806
2045
  }
1807
2046
  /** Fail-fast all pending JetStream RPC callbacks on connection loss. */
1808
2047
  handleDisconnect() {
1809
- this.rejectPendingRpcs(new Error("Connection lost"));
1810
- this.inbox = null;
2048
+ this.rpcInbox.rejectAll(new Error("Connection lost"));
1811
2049
  this.readyForPublish = false;
1812
2050
  }
1813
- /** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
1814
- rejectPendingRpcs(error) {
1815
- for (const callback of this.pendingMessages.values()) {
1816
- callback({ err: error, response: null, isDisposed: true });
1817
- }
1818
- for (const timeoutId of this.pendingTimeouts.values()) {
1819
- clearTimeout(timeoutId);
1820
- }
1821
- this.pendingMessages.clear();
1822
- this.pendingTimeouts.clear();
1823
- this.inboxSubscription?.unsubscribe();
1824
- this.inboxSubscription = null;
1825
- this.inbox = null;
1826
- }
1827
- /** Setup shared inbox subscription for JetStream RPC responses. */
1828
- setupInbox(nc) {
1829
- this.inbox = (0, import_transport_node.createInbox)(internalName(this.rootOptions.name));
1830
- this.inboxSubscription = nc.subscribe(this.inbox, {
1831
- callback: (err, msg) => {
1832
- if (err) {
1833
- this.logger.error("Inbox subscription error:", err);
1834
- return;
1835
- }
1836
- this.routeInboxReply(msg);
1837
- }
1838
- });
1839
- this.logger.debug(`Inbox subscription: ${this.inbox}`);
1840
- }
1841
- /** Route an inbox reply to the matching pending callback. */
1842
- routeInboxReply(msg) {
1843
- const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
1844
- if (!correlationId) {
1845
- this.logger.warn("Inbox reply without correlation-id, ignoring");
1846
- return;
1847
- }
1848
- const callback = this.pendingMessages.get(correlationId);
1849
- if (!callback) {
1850
- this.logger.warn(`No pending handler for correlation-id: ${correlationId}`);
1851
- return;
1852
- }
1853
- const timeoutId = this.pendingTimeouts.get(correlationId);
1854
- if (timeoutId) {
1855
- clearTimeout(timeoutId);
1856
- this.pendingTimeouts.delete(correlationId);
1857
- }
1858
- try {
1859
- const decoded = this.codec.decode(msg.data);
1860
- if (msg.headers?.get("x-error" /* Error */)) {
1861
- callback({ err: decoded, response: null, isDisposed: true });
1862
- } else {
1863
- callback({ err: null, response: decoded, isDisposed: true });
1864
- }
1865
- } catch (err) {
1866
- callback({
1867
- err: err instanceof Error ? err : new Error("Decode error"),
1868
- response: null,
1869
- isDisposed: true
1870
- });
1871
- } finally {
1872
- this.pendingMessages.delete(correlationId);
1873
- }
1874
- }
1875
2051
  /**
1876
- * Resolve a user pattern to a fully-qualified NATS subject, dispatching
1877
- * between the event, broadcast, and ordered prefixes.
1878
- *
1879
- * The leading-char check short-circuits the `startsWith` comparisons for
1880
- * patterns that cannot possibly carry a broadcast/ordered marker, which is
1881
- * the overwhelmingly common case.
2052
+ * Resolve a user pattern to a fully-qualified NATS subject. Self-targets go
2053
+ * through the resolver so custom subjectPrefix options are honoured.
1882
2054
  */
1883
2055
  buildEventSubject(pattern) {
1884
- if (pattern.charCodeAt(0) === 98 && pattern.startsWith("broadcast:" /* Broadcast */)) {
1885
- return BROADCAST_SUBJECT_PREFIX + pattern.slice("broadcast:" /* Broadcast */.length);
2056
+ if (pattern.startsWith("broadcast:" /* Broadcast */)) {
2057
+ return this.broadcastPrefix + pattern.slice("broadcast:" /* Broadcast */.length);
1886
2058
  }
1887
- if (pattern.charCodeAt(0) === 111 && pattern.startsWith("ordered:" /* Ordered */)) {
1888
- return this.orderedSubjectPrefix + pattern.slice("ordered:" /* Ordered */.length);
2059
+ if (pattern.startsWith("ordered:" /* Ordered */)) {
2060
+ const bare = pattern.slice("ordered:" /* Ordered */.length);
2061
+ if (this.selfNames) return this.selfNames.subject("ordered" /* Ordered */, bare);
2062
+ return this.orderedSubjectPrefix + bare;
1889
2063
  }
2064
+ if (this.selfNames) return this.selfNames.subject("ev" /* Event */, pattern);
1890
2065
  return this.eventSubjectPrefix + pattern;
1891
2066
  }
1892
2067
  /** Build NATS headers merging custom headers with transport headers. */
1893
2068
  buildHeaders(customHeaders, transport) {
1894
- const hdrs = (0, import_transport_node.headers)();
2069
+ const hdrs = (0, import_transport_node2.headers)();
1895
2070
  hdrs.set("x-subject" /* Subject */, transport.subject);
1896
2071
  hdrs.set("x-caller-name" /* CallerName */, this.callerName);
1897
2072
  if (transport.correlationId) {
@@ -1931,33 +2106,16 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1931
2106
  /**
1932
2107
  * Build a schedule-holder subject for NATS message scheduling.
1933
2108
  *
1934
- * The schedule-holder subject resides in the same stream as the target but
1935
- * uses a separate `_sch` namespace that is NOT matched by any consumer filter.
1936
- * NATS holds the message and publishes it to the target subject after the delay.
1937
- *
1938
- * A unique per-message suffix is appended because the server stores schedules
1939
- * as rollup messages one active schedule per subject (ADR-51). Without it,
1940
- * concurrent schedules of the same pattern would silently replace each other.
1941
- *
1942
- * Examples:
1943
- * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder.<nuid>`
1944
- * - `broadcast.config.updated` → `broadcast._sch.config.updated.<nuid>`
2109
+ * The holder lives in the same stream as the target but under the `_sch`
2110
+ * namespace no consumer filter matches; the server publishes it to the
2111
+ * target subject when the schedule fires. The unique per-message suffix
2112
+ * exists because the server stores schedules as rollup messages, one
2113
+ * active schedule per subject (ADR-51): without it, concurrent schedules
2114
+ * of the same pattern would silently replace each other.
1945
2115
  */
1946
2116
  buildScheduleSubject(eventSubject) {
1947
- if (eventSubject.startsWith("broadcast.")) {
1948
- return `${eventSubject.replace("broadcast.", "broadcast._sch.")}.${import_nuid.nuid.next()}`;
1949
- }
1950
- const targetPrefix = `${internalName(this.targetName)}.`;
1951
- if (!eventSubject.startsWith(targetPrefix)) {
1952
- throw new Error(`Unexpected event subject format: ${eventSubject}`);
1953
- }
1954
- const withoutPrefix = eventSubject.slice(targetPrefix.length);
1955
- const dotIndex = withoutPrefix.indexOf(".");
1956
- if (dotIndex === -1) {
1957
- throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
1958
- }
1959
- const pattern = withoutPrefix.slice(dotIndex + 1);
1960
- return `${targetPrefix}_sch.${pattern}.${import_nuid.nuid.next()}`;
2117
+ const base = this.selfNames ? this.selfNames.scheduleSubjectBase(eventSubject) : conventionScheduleSubjectBase(this.targetName, eventSubject);
2118
+ return `${base}.${import_nuid.nuid.next()}`;
1961
2119
  }
1962
2120
  };
1963
2121
 
@@ -1989,9 +2147,9 @@ var MsgpackCodec = class {
1989
2147
  };
1990
2148
 
1991
2149
  // src/connection/connection.provider.ts
1992
- var import_common6 = require("@nestjs/common");
1993
- var import_transport_node2 = require("@nats-io/transport-node");
1994
- var import_jetstream8 = require("@nats-io/jetstream");
2150
+ var import_common7 = require("@nestjs/common");
2151
+ var import_transport_node3 = require("@nats-io/transport-node");
2152
+ var import_jetstream11 = require("@nats-io/jetstream");
1995
2153
  var import_rxjs = require("rxjs");
1996
2154
  var DEFAULT_OPTIONS = {
1997
2155
  maxReconnectAttempts: -1,
@@ -2017,7 +2175,7 @@ var ConnectionProvider = class {
2017
2175
  nc$;
2018
2176
  /** Live stream of connection status events (no replay). */
2019
2177
  status$;
2020
- logger = new import_common6.Logger("Jetstream:Connection");
2178
+ logger = new import_common7.Logger("Jetstream:Connection");
2021
2179
  connection = null;
2022
2180
  connectionPromise = null;
2023
2181
  jsClient = null;
@@ -2028,7 +2186,7 @@ var ConnectionProvider = class {
2028
2186
  otelEndpoint;
2029
2187
  lifecycleSpan = null;
2030
2188
  /**
2031
- * Establish NATS connection. Idempotent returns cached connection on subsequent calls.
2189
+ * Establish NATS connection. Idempotent: returns cached connection on subsequent calls.
2032
2190
  *
2033
2191
  * @throws Error if connection is refused (fail fast).
2034
2192
  */
@@ -2067,9 +2225,9 @@ var ConnectionProvider = class {
2067
2225
  */
2068
2226
  getJetStreamClient() {
2069
2227
  if (!this.connection || this.connection.isClosed()) {
2070
- throw new Error("Not connected \u2014 call getConnection() before getJetStreamClient()");
2228
+ throw new Error("Not connected; call getConnection() before getJetStreamClient()");
2071
2229
  }
2072
- this.jsClient ??= (0, import_jetstream8.jetstream)(this.connection);
2230
+ this.jsClient ??= (0, import_jetstream11.jetstream)(this.connection);
2073
2231
  return this.jsClient;
2074
2232
  }
2075
2233
  /** Direct access to the raw NATS connection, or `null` if not yet connected. */
@@ -2079,7 +2237,7 @@ var ConnectionProvider = class {
2079
2237
  /**
2080
2238
  * Gracefully shut down the connection.
2081
2239
  *
2082
- * Sequence: drain wait for close. Falls back to force-close on error.
2240
+ * Sequence: drain -> wait for close. Falls back to force-close on error.
2083
2241
  */
2084
2242
  async shutdown() {
2085
2243
  if (this.connectionPromise) {
@@ -2118,7 +2276,7 @@ var ConnectionProvider = class {
2118
2276
  async initJetStreamManager() {
2119
2277
  try {
2120
2278
  const nc = await this.getConnection();
2121
- this.jsmInstance = await (0, import_jetstream8.jetstreamManager)(nc);
2279
+ this.jsmInstance = await (0, import_jetstream11.jetstreamManager)(nc);
2122
2280
  this.logger.log("JetStream manager initialized");
2123
2281
  return this.jsmInstance;
2124
2282
  } finally {
@@ -2128,7 +2286,7 @@ var ConnectionProvider = class {
2128
2286
  /** Internal: establish the physical connection with reconnect monitoring. */
2129
2287
  async establish() {
2130
2288
  try {
2131
- const nc = await (0, import_transport_node2.connect)({
2289
+ const nc = await (0, import_transport_node3.connect)({
2132
2290
  ...DEFAULT_OPTIONS,
2133
2291
  // Default the NATS connection name to the OTel-derived service name so
2134
2292
  // `nats server info` lines up with span attributes, but let user-supplied
@@ -2282,12 +2440,12 @@ var EventBus = class {
2282
2440
  };
2283
2441
 
2284
2442
  // src/health/jetstream.health-indicator.ts
2285
- var import_common7 = require("@nestjs/common");
2443
+ var import_common8 = require("@nestjs/common");
2286
2444
  var JetstreamHealthIndicator = class {
2287
2445
  constructor(connection) {
2288
2446
  this.connection = connection;
2289
2447
  }
2290
- logger = new import_common7.Logger("Jetstream:Health");
2448
+ logger = new import_common8.Logger("Jetstream:Health");
2291
2449
  /**
2292
2450
  * Plain health status check.
2293
2451
  *
@@ -2317,7 +2475,7 @@ var JetstreamHealthIndicator = class {
2317
2475
  * Returns `{ [key]: { status: 'up', ... } }` on success.
2318
2476
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
2319
2477
  *
2320
- * The thrown error sets `isHealthCheckError: true` and `causes` the
2478
+ * The thrown error sets `isHealthCheckError: true` and `causes`, the
2321
2479
  * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
2322
2480
  * health failures from unexpected exceptions. Works with both Terminus v10
2323
2481
  * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
@@ -2344,14 +2502,14 @@ var JetstreamHealthIndicator = class {
2344
2502
  }
2345
2503
  };
2346
2504
  JetstreamHealthIndicator = __decorateClass([
2347
- (0, import_common7.Injectable)()
2505
+ (0, import_common8.Injectable)()
2348
2506
  ], JetstreamHealthIndicator);
2349
2507
 
2350
2508
  // src/metrics/metrics.module.ts
2351
- var import_common11 = require("@nestjs/common");
2509
+ var import_common12 = require("@nestjs/common");
2352
2510
 
2353
2511
  // src/server/routing/pattern-registry.ts
2354
- var import_common8 = require("@nestjs/common");
2512
+ var import_common9 = require("@nestjs/common");
2355
2513
  var HANDLER_LABELS = {
2356
2514
  ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
2357
2515
  ["ordered" /* Ordered */]: "ordered" /* Ordered */,
@@ -2359,12 +2517,13 @@ var HANDLER_LABELS = {
2359
2517
  ["cmd" /* Command */]: "rpc" /* Rpc */
2360
2518
  };
2361
2519
  var PatternRegistry = class {
2362
- constructor(options) {
2520
+ constructor(options, names) {
2363
2521
  this.options = options;
2522
+ this.names = names;
2364
2523
  }
2365
- logger = new import_common8.Logger("Jetstream:PatternRegistry");
2524
+ logger = new import_common9.Logger("Jetstream:PatternRegistry");
2366
2525
  registry = /* @__PURE__ */ new Map();
2367
- // Cached after registerHandlers() the registry is immutable from that point
2526
+ // Cached after registerHandlers(); the registry is immutable from that point
2368
2527
  cachedPatterns = null;
2369
2528
  _hasEvents = false;
2370
2529
  _hasCommands = false;
@@ -2377,7 +2536,6 @@ var PatternRegistry = class {
2377
2536
  * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
2378
2537
  */
2379
2538
  registerHandlers(handlers) {
2380
- const serviceName = this.options.name;
2381
2539
  for (const [pattern, handler] of handlers) {
2382
2540
  const extras = handler.extras;
2383
2541
  const isEvent = handler.isEventHandler ?? false;
@@ -2394,7 +2552,7 @@ var PatternRegistry = class {
2394
2552
  else if (isOrdered) kind = "ordered" /* Ordered */;
2395
2553
  else if (isEvent) kind = "ev" /* Event */;
2396
2554
  else kind = "cmd" /* Command */;
2397
- const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
2555
+ const fullSubject = this.names.subject(kind, pattern);
2398
2556
  this.registry.set(fullSubject, {
2399
2557
  handler,
2400
2558
  pattern,
@@ -2421,7 +2579,7 @@ var PatternRegistry = class {
2421
2579
  * Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
2422
2580
  *
2423
2581
  * Returns `null` when the subject is not registered. The declared pattern is
2424
- * the value the user passed to `@EventPattern`/`@MessagePattern` stable and
2582
+ * the value the user passed to `@EventPattern`/`@MessagePattern`: stable and
2425
2583
  * bounded, suitable for use as a Prometheus label without cardinality risk.
2426
2584
  */
2427
2585
  resolveDeclared(subject) {
@@ -2431,7 +2589,17 @@ var PatternRegistry = class {
2431
2589
  }
2432
2590
  /** Get all registered broadcast patterns (for consumer filter_subject setup). */
2433
2591
  getBroadcastPatterns() {
2434
- return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
2592
+ return this.getPatternsByKind().broadcasts.map(
2593
+ (p) => this.names.subject("broadcast" /* Broadcast */, p)
2594
+ );
2595
+ }
2596
+ /** Get registered event patterns as raw user-declared patterns. */
2597
+ getEventPatterns() {
2598
+ return this.getPatternsByKind().events;
2599
+ }
2600
+ /** Get registered command patterns as raw user-declared patterns. */
2601
+ getCommandPatterns() {
2602
+ return this.getPatternsByKind().commands;
2435
2603
  }
2436
2604
  hasBroadcastHandlers() {
2437
2605
  return this._hasBroadcasts;
@@ -2447,9 +2615,7 @@ var PatternRegistry = class {
2447
2615
  }
2448
2616
  /** Get fully-qualified NATS subjects for ordered handlers. */
2449
2617
  getOrderedSubjects() {
2450
- return this.getPatternsByKind().ordered.map(
2451
- (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2452
- );
2618
+ return this.getPatternsByKind().ordered.map((p) => this.names.subject("ordered" /* Ordered */, p));
2453
2619
  }
2454
2620
  /** Check if any registered handler has metadata. */
2455
2621
  hasMetadata() {
@@ -2481,22 +2647,6 @@ var PatternRegistry = class {
2481
2647
  ordered: [...patterns.ordered]
2482
2648
  };
2483
2649
  }
2484
- /** Normalize a full NATS subject back to the user-facing pattern. */
2485
- normalizeSubject(subject) {
2486
- const name = internalName(this.options.name);
2487
- const prefixes = [
2488
- `${name}.${"cmd" /* Command */}.`,
2489
- `${name}.${"ev" /* Event */}.`,
2490
- `${name}.${"ordered" /* Ordered */}.`,
2491
- `${"broadcast" /* Broadcast */}.`
2492
- ];
2493
- for (const prefix of prefixes) {
2494
- if (subject.startsWith(prefix)) {
2495
- return subject.slice(prefix.length);
2496
- }
2497
- }
2498
- return subject;
2499
- }
2500
2650
  buildPatternsByKind() {
2501
2651
  const events = [];
2502
2652
  const commands = [];
@@ -2551,7 +2701,7 @@ var ERROR_CONTEXT_PREFIXES = [
2551
2701
  ["consume", "consume"],
2552
2702
  ["core-rpc-handler", "handler"],
2553
2703
  ["rpc-handler", "handler"],
2554
- // EventRouter formats contexts as `${StreamKind.*}-handler:...` the enum
2704
+ // EventRouter formats contexts as `${StreamKind.*}-handler:...`; the enum
2555
2705
  // uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
2556
2706
  ["ev-handler", "handler"],
2557
2707
  ["event-handler", "handler"],
@@ -2569,7 +2719,7 @@ var STREAM_KIND_LABEL = {
2569
2719
  };
2570
2720
 
2571
2721
  // src/metrics/metrics.service.ts
2572
- var import_common10 = require("@nestjs/common");
2722
+ var import_common11 = require("@nestjs/common");
2573
2723
 
2574
2724
  // src/metrics/error-context-mapper.ts
2575
2725
  var mapErrorContext = (context7) => {
@@ -2692,12 +2842,12 @@ var createMetrics = (opts) => {
2692
2842
  };
2693
2843
 
2694
2844
  // src/metrics/poll-runner.ts
2695
- var import_common9 = require("@nestjs/common");
2845
+ var import_common10 = require("@nestjs/common");
2696
2846
  var PollRunner = class {
2697
2847
  constructor(opts) {
2698
2848
  this.opts = opts;
2699
2849
  }
2700
- logger = new import_common9.Logger("Jetstream:Metrics:Poll");
2850
+ logger = new import_common10.Logger("Jetstream:Metrics:Poll");
2701
2851
  timer = null;
2702
2852
  inFlight = null;
2703
2853
  start() {
@@ -2706,7 +2856,7 @@ var PollRunner = class {
2706
2856
  if (this.opts.targets.length === 0) return;
2707
2857
  this.timer = setInterval(() => {
2708
2858
  if (this.inFlight !== null) {
2709
- this.logger.warn("Skipping poll tick \u2014 previous cycle still in flight");
2859
+ this.logger.warn("Skipping poll tick; previous cycle still in flight");
2710
2860
  return;
2711
2861
  }
2712
2862
  this.inFlight = this.tick().finally(() => {
@@ -2769,15 +2919,16 @@ var PollRunner = class {
2769
2919
 
2770
2920
  // src/metrics/metrics.service.ts
2771
2921
  var JetstreamMetricsService = class {
2772
- constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
2922
+ constructor(eventBus, config, promClient, options, patternRegistry, connection = null, names = null) {
2773
2923
  this.eventBus = eventBus;
2774
2924
  this.config = config;
2775
2925
  this.promClient = promClient;
2776
2926
  this.options = options;
2777
2927
  this.patternRegistry = patternRegistry;
2778
2928
  this.connection = connection;
2929
+ this.names = names;
2779
2930
  }
2780
- logger = new import_common10.Logger("Jetstream:Metrics");
2931
+ logger = new import_common11.Logger("Jetstream:Metrics");
2781
2932
  metrics = null;
2782
2933
  pollRunner = null;
2783
2934
  activeServers = /* @__PURE__ */ new Set();
@@ -2786,7 +2937,7 @@ var JetstreamMetricsService = class {
2786
2937
  if (!this.options.metrics || !this.config || !this.promClient) return;
2787
2938
  if (!this.config.register) {
2788
2939
  throw new Error(
2789
- "JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
2940
+ "JetstreamMetricsService requires a prom-client Registry; none was resolved by JetstreamMetricsModule."
2790
2941
  );
2791
2942
  }
2792
2943
  this.metrics = createMetrics({
@@ -2813,7 +2964,7 @@ var JetstreamMetricsService = class {
2813
2964
  }
2814
2965
  /**
2815
2966
  * NATS connects during early bootstrap, before this service subscribes to
2816
- * the EventBus the initial `Connect` emission misses us. Mirror the
2967
+ * the EventBus, so the initial `Connect` emission misses us. Mirror the
2817
2968
  * current state here so `connection_up` reflects reality the moment metrics
2818
2969
  * come online; later disconnects/reconnects update it normally.
2819
2970
  */
@@ -2846,26 +2997,32 @@ var JetstreamMetricsService = class {
2846
2997
  if (registry.hasEventHandlers()) {
2847
2998
  targets.push({
2848
2999
  kind: "ev" /* Event */,
2849
- stream: streamName(this.options.name, "ev" /* Event */),
2850
- consumer: consumerName(this.options.name, "ev" /* Event */)
3000
+ stream: this.resolveStreamName("ev" /* Event */),
3001
+ consumer: this.resolveConsumerName("ev" /* Event */)
2851
3002
  });
2852
3003
  }
2853
3004
  if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
2854
3005
  targets.push({
2855
3006
  kind: "cmd" /* Command */,
2856
- stream: streamName(this.options.name, "cmd" /* Command */),
2857
- consumer: consumerName(this.options.name, "cmd" /* Command */)
3007
+ stream: this.resolveStreamName("cmd" /* Command */),
3008
+ consumer: this.resolveConsumerName("cmd" /* Command */)
2858
3009
  });
2859
3010
  }
2860
3011
  if (registry.hasBroadcastHandlers()) {
2861
3012
  targets.push({
2862
3013
  kind: "broadcast" /* Broadcast */,
2863
- stream: streamName(this.options.name, "broadcast" /* Broadcast */),
2864
- consumer: consumerName(this.options.name, "broadcast" /* Broadcast */)
3014
+ stream: this.resolveStreamName("broadcast" /* Broadcast */),
3015
+ consumer: this.resolveConsumerName("broadcast" /* Broadcast */)
2865
3016
  });
2866
3017
  }
2867
3018
  return targets;
2868
3019
  }
3020
+ resolveStreamName(kind) {
3021
+ return this.names ? this.names.streamName(kind) : streamName(this.options.name, kind);
3022
+ }
3023
+ resolveConsumerName(kind) {
3024
+ return this.names ? this.names.consumerName(kind) : consumerName(this.options.name, kind);
3025
+ }
2869
3026
  subscribeToEvents() {
2870
3027
  this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
2871
3028
  this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
@@ -2900,7 +3057,7 @@ var JetstreamMetricsService = class {
2900
3057
  const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2901
3058
  this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
2902
3059
  };
2903
- // `_kind` collapses broadcast/ordered into MessageKind.Event we use
3060
+ // `_kind` collapses broadcast/ordered into MessageKind.Event; we use
2904
3061
  // declared.kind from PatternRegistry for the precise label instead.
2905
3062
  onMessageRouted = (subject, _kind) => {
2906
3063
  if (!this.metrics) return;
@@ -2910,7 +3067,7 @@ var JetstreamMetricsService = class {
2910
3067
  return;
2911
3068
  }
2912
3069
  this.metrics.messagesReceivedTotal.labels({
2913
- stream: streamName(this.options.name, declared.kind),
3070
+ stream: this.resolveStreamName(declared.kind),
2914
3071
  subject: declared.pattern,
2915
3072
  kind: STREAM_KIND_LABEL[declared.kind]
2916
3073
  }).inc();
@@ -2926,7 +3083,7 @@ var JetstreamMetricsService = class {
2926
3083
  };
2927
3084
  onHandlerCompleted = (pattern, kind, durationMs, status) => {
2928
3085
  if (!this.metrics) return;
2929
- const stream = streamName(this.options.name, kind);
3086
+ const stream = this.resolveStreamName(kind);
2930
3087
  const kindLabel = STREAM_KIND_LABEL[kind];
2931
3088
  const labels = { stream, subject: pattern, kind: kindLabel, status };
2932
3089
  this.metrics.messagesProcessedTotal.labels(labels).inc();
@@ -2946,13 +3103,14 @@ var JetstreamMetricsService = class {
2946
3103
  }
2947
3104
  };
2948
3105
  JetstreamMetricsService = __decorateClass([
2949
- (0, import_common10.Injectable)(),
2950
- __decorateParam(1, (0, import_common10.Inject)(JETSTREAM_METRICS_CONFIG)),
2951
- __decorateParam(2, (0, import_common10.Inject)(JETSTREAM_METRICS_PROM_CLIENT)),
2952
- __decorateParam(3, (0, import_common10.Inject)(JETSTREAM_OPTIONS)),
2953
- __decorateParam(4, (0, import_common10.Optional)()),
2954
- __decorateParam(5, (0, import_common10.Optional)()),
2955
- __decorateParam(5, (0, import_common10.Inject)(JETSTREAM_CONNECTION))
3106
+ (0, import_common11.Injectable)(),
3107
+ __decorateParam(1, (0, import_common11.Inject)(JETSTREAM_METRICS_CONFIG)),
3108
+ __decorateParam(2, (0, import_common11.Inject)(JETSTREAM_METRICS_PROM_CLIENT)),
3109
+ __decorateParam(3, (0, import_common11.Inject)(JETSTREAM_OPTIONS)),
3110
+ __decorateParam(4, (0, import_common11.Optional)()),
3111
+ __decorateParam(5, (0, import_common11.Optional)()),
3112
+ __decorateParam(5, (0, import_common11.Inject)(JETSTREAM_CONNECTION)),
3113
+ __decorateParam(6, (0, import_common11.Optional)())
2956
3114
  ], JetstreamMetricsService);
2957
3115
 
2958
3116
  // src/metrics/metrics.module.ts
@@ -3007,9 +3165,18 @@ var JetstreamMetricsModule = class {
3007
3165
  JETSTREAM_METRICS_PROM_CLIENT,
3008
3166
  JETSTREAM_OPTIONS,
3009
3167
  { token: PatternRegistry, optional: true },
3010
- { token: JETSTREAM_CONNECTION, optional: true }
3168
+ { token: JETSTREAM_CONNECTION, optional: true },
3169
+ { token: NameResolver, optional: true }
3011
3170
  ],
3012
- useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(eventBus, cfg, runtime, opts, patternRegistry, connection)
3171
+ useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection, names) => new JetstreamMetricsService(
3172
+ eventBus,
3173
+ cfg,
3174
+ runtime,
3175
+ opts,
3176
+ patternRegistry,
3177
+ connection,
3178
+ names
3179
+ )
3013
3180
  };
3014
3181
  return {
3015
3182
  module: JetstreamMetricsModule,
@@ -3024,7 +3191,7 @@ var JetstreamMetricsModule = class {
3024
3191
  }
3025
3192
  };
3026
3193
  JetstreamMetricsModule = __decorateClass([
3027
- (0, import_common11.Module)({})
3194
+ (0, import_common12.Module)({})
3028
3195
  ], JetstreamMetricsModule);
3029
3196
 
3030
3197
  // src/server/strategy.ts
@@ -3070,31 +3237,22 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3070
3237
  this.started = false;
3071
3238
  }
3072
3239
  /**
3073
- * Override NestJS `Server.addHandler` to fail-fast on duplicate pattern registration.
3074
- *
3075
- * The base class silently overwrites duplicate RPC handlers (last wins) and appends
3076
- * duplicate event handlers to a linked list. Both behaviors are hazardous in a
3077
- * JetStream context: silent overwrite drops a handler the user wrote, and double
3078
- * event dispatch double-acks/double-processes the same JetStream message.
3240
+ * Override NestJS `Server.addHandler` to fail fast on duplicate pattern registration.
3079
3241
  *
3080
- * We treat any pattern collision as a fatal misconfiguration so it surfaces at
3081
- * bootstrap instead of in production traffic.
3242
+ * The base class silently overwrites duplicate RPC handlers and chains duplicate event
3243
+ * handlers, which would double-ack the same JetStream message. Any collision is treated
3244
+ * as a fatal misconfiguration so it surfaces at bootstrap, not in production traffic.
3082
3245
  */
3083
3246
  addHandler(pattern, callback, isEventHandler = false, extras = {}) {
3084
3247
  const normalizedPattern = this.normalizePattern(pattern);
3085
3248
  if (this.messageHandlers.has(normalizedPattern)) {
3086
3249
  throw new Error(
3087
- `Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice \u2014 find and remove the second declaration.`
3250
+ `Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice; find and remove the second declaration.`
3088
3251
  );
3089
3252
  }
3090
3253
  super.addHandler(pattern, callback, isEventHandler, extras);
3091
3254
  }
3092
- /**
3093
- * Register event listener (required by Server base class).
3094
- *
3095
- * Stores callbacks for client use. Primary lifecycle events
3096
- * are routed through EventBus.
3097
- */
3255
+ /** Register event listener (required by Server base class); lifecycle events use EventBus. */
3098
3256
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
3099
3257
  on(event, callback) {
3100
3258
  const existing = this.listeners.get(event) ?? [];
@@ -3109,7 +3267,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3109
3267
  unwrap() {
3110
3268
  const nc = this.connection.unwrap;
3111
3269
  if (!nc) {
3112
- throw new Error("Not connected \u2014 transport has not started");
3270
+ throw new Error("Not connected; transport has not started");
3113
3271
  }
3114
3272
  return nc;
3115
3273
  }
@@ -3119,7 +3277,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3119
3277
  }
3120
3278
  async doListen(callback) {
3121
3279
  if (this.started) {
3122
- this.logger.warn("listen() called more than once \u2014 ignoring");
3280
+ this.logger.warn("listen() called more than once; ignoring");
3123
3281
  return;
3124
3282
  }
3125
3283
  this.started = true;
@@ -3210,8 +3368,8 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3210
3368
  };
3211
3369
 
3212
3370
  // src/server/core-rpc.server.ts
3213
- var import_common12 = require("@nestjs/common");
3214
- var import_transport_node3 = require("@nats-io/transport-node");
3371
+ var import_common13 = require("@nestjs/common");
3372
+ var import_transport_node4 = require("@nats-io/transport-node");
3215
3373
 
3216
3374
  // src/context/rpc.context.ts
3217
3375
  var import_microservices3 = require("@nestjs/microservices");
@@ -3287,7 +3445,7 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
3287
3445
  retry(opts) {
3288
3446
  this.assertJetStream("retry");
3289
3447
  if (this._shouldTerminate) {
3290
- throw new Error("Cannot retry \u2014 terminate() was already called");
3448
+ throw new Error("Cannot retry; terminate() was already called");
3291
3449
  }
3292
3450
  this._shouldRetry = true;
3293
3451
  this._retryDelay = opts?.delayMs;
@@ -3304,7 +3462,7 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
3304
3462
  terminate(reason) {
3305
3463
  this.assertJetStream("terminate");
3306
3464
  if (this._shouldRetry) {
3307
- throw new Error("Cannot terminate \u2014 retry() was already called");
3465
+ throw new Error("Cannot terminate; retry() was already called");
3308
3466
  }
3309
3467
  this._shouldTerminate = true;
3310
3468
  this._terminateReason = reason;
@@ -3313,7 +3471,7 @@ var RpcContext = class extends import_microservices3.BaseRpcContext {
3313
3471
  asJetStream() {
3314
3472
  return this.isJetStream() ? this.args[0] : null;
3315
3473
  }
3316
- /** Ensure the message is JetStream settlement actions are not available for Core NATS. */
3474
+ /** Ensure the message is JetStream; settlement actions are not available for Core NATS. */
3317
3475
  assertJetStream(method) {
3318
3476
  if (!this.isJetStream()) {
3319
3477
  throw new Error(`${method}() is only available for JetStream messages`);
@@ -3478,17 +3636,18 @@ var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
3478
3636
 
3479
3637
  // src/server/core-rpc.server.ts
3480
3638
  var CoreRpcServer = class {
3481
- constructor(options, connection, patternRegistry, codec, eventBus) {
3639
+ constructor(options, connection, patternRegistry, codec, eventBus, names) {
3482
3640
  this.connection = connection;
3483
3641
  this.patternRegistry = patternRegistry;
3484
3642
  this.codec = codec;
3485
3643
  this.eventBus = eventBus;
3644
+ this.names = names;
3486
3645
  const derived = deriveOtelAttrs(options);
3487
3646
  this.otel = derived.otel;
3488
3647
  this.serviceName = derived.serviceName;
3489
3648
  this.serverEndpoint = derived.serverEndpoint;
3490
3649
  }
3491
- logger = new import_common12.Logger("Jetstream:CoreRpc");
3650
+ logger = new import_common13.Logger("Jetstream:CoreRpc");
3492
3651
  subscription = null;
3493
3652
  otel;
3494
3653
  serviceName;
@@ -3496,7 +3655,7 @@ var CoreRpcServer = class {
3496
3655
  /** Start listening for RPC requests on the command subject. */
3497
3656
  async start() {
3498
3657
  const nc = await this.connection.getConnection();
3499
- const subject = `${this.serviceName}.cmd.>`;
3658
+ const subject = this.names ? this.names.filterSubject("cmd" /* Command */) : `${this.serviceName}.cmd.>`;
3500
3659
  const queue = `${this.serviceName}_cmd_queue`;
3501
3660
  this.subscription = nc.subscribe(subject, {
3502
3661
  queue,
@@ -3589,7 +3748,7 @@ var CoreRpcServer = class {
3589
3748
  /** Send an error response back to the caller with x-error header. */
3590
3749
  respondWithError(msg, error) {
3591
3750
  try {
3592
- const hdrs = (0, import_transport_node3.headers)();
3751
+ const hdrs = (0, import_transport_node4.headers)();
3593
3752
  hdrs.set("x-error" /* Error */, "true");
3594
3753
  msg.respond(this.codec.encode(serializeError(error)), { headers: hdrs });
3595
3754
  } catch {
@@ -3599,8 +3758,8 @@ var CoreRpcServer = class {
3599
3758
  };
3600
3759
 
3601
3760
  // src/server/infrastructure/stream.provider.ts
3602
- var import_common14 = require("@nestjs/common");
3603
- var import_jetstream19 = require("@nats-io/jetstream");
3761
+ var import_common15 = require("@nestjs/common");
3762
+ var import_jetstream22 = require("@nats-io/jetstream");
3604
3763
 
3605
3764
  // src/server/infrastructure/nats-error-codes.ts
3606
3765
  var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
@@ -3613,7 +3772,7 @@ var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
3613
3772
  })(NatsErrorCode || {});
3614
3773
 
3615
3774
  // src/server/infrastructure/provisioning-budget.ts
3616
- var import_jetstream16 = require("@nats-io/jetstream");
3775
+ var import_jetstream19 = require("@nats-io/jetstream");
3617
3776
  var GIB = 1024 ** 3;
3618
3777
  var fmt = (bytes) => `${(bytes / GIB).toFixed(2)} GiB`;
3619
3778
  var resolveTierBudget = (info, replicas) => {
@@ -3628,7 +3787,7 @@ var resolveTierBudget = (info, replicas) => {
3628
3787
  var groupByReplicas = (reservations) => {
3629
3788
  const groups = /* @__PURE__ */ new Map();
3630
3789
  for (const r of reservations) {
3631
- if (r.storage !== import_jetstream16.StorageType.File) continue;
3790
+ if (r.storage !== import_jetstream19.StorageType.File) continue;
3632
3791
  const prev = groups.get(r.numReplicas) ?? 0;
3633
3792
  groups.set(r.numReplicas, prev + r.maxBytes * r.numReplicas);
3634
3793
  }
@@ -3669,7 +3828,7 @@ var assertStorageBudget = async (jsm, serviceName, reservations, logger5) => {
3669
3828
  );
3670
3829
  }
3671
3830
  } catch (err) {
3672
- logger5.debug(`Storage preflight skipped \u2014 account info unavailable: ${String(err)}`);
3831
+ logger5.debug(`Storage preflight skipped; account info unavailable: ${String(err)}`);
3673
3832
  }
3674
3833
  };
3675
3834
 
@@ -3690,7 +3849,7 @@ var JetstreamProvisioningError = class _JetstreamProvisioningError extends Error
3690
3849
  numReplicas;
3691
3850
  reservation;
3692
3851
  constructor(fields) {
3693
- const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B \xD7 replicas=${fields.numReplicas}).` : "";
3852
+ const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B x replicas=${fields.numReplicas}).` : "";
3694
3853
  super(
3695
3854
  `JetStream ${fields.entity} provisioning failed for "${fields.target}" (kind=${fields.kind}): ${fields.errDescription} [err_code=${fields.errCode}].${reservationNote} ${fields.remediation}`,
3696
3855
  { cause: fields.cause }
@@ -3727,7 +3886,7 @@ var mapProvisioningError = (err, ctx) => {
3727
3886
  };
3728
3887
 
3729
3888
  // src/server/infrastructure/provisioning-summary.ts
3730
- var import_jetstream17 = require("@nats-io/jetstream");
3889
+ var import_jetstream20 = require("@nats-io/jetstream");
3731
3890
  var GIB2 = 1024 ** 3;
3732
3891
  var NANOS_PER_SECOND = 1e9;
3733
3892
  var NANOS_PER_HOUR = 3600 * NANOS_PER_SECOND;
@@ -3742,18 +3901,22 @@ var formatAge = (nanos) => {
3742
3901
  if (nanos >= NANOS_PER_HOUR) return `${(nanos / NANOS_PER_HOUR).toFixed(1)}h`;
3743
3902
  return `${(nanos / NANOS_PER_SECOND).toFixed(0)}s`;
3744
3903
  };
3745
- var formatProvisioningSummary = (serviceName, reservations) => {
3746
- const lines = [`Provisioning ${reservations.length} stream(s) for "${serviceName}":`];
3904
+ var formatProvisioningSummary = (serviceName, reservations, external = []) => {
3905
+ const totalStreams = reservations.length + external.length;
3906
+ const lines = [`Provisioning ${totalStreams} stream(s) for "${serviceName}":`];
3747
3907
  let totalFileMaxBytes = 0;
3748
3908
  for (const r of reservations) {
3749
- if (r.storage === import_jetstream17.StorageType.File) totalFileMaxBytes += r.maxBytes;
3909
+ if (r.storage === import_jetstream20.StorageType.File) totalFileMaxBytes += r.maxBytes;
3750
3910
  const clusterReservation = r.maxBytes * r.numReplicas;
3751
3911
  lines.push(
3752
- ` \u2022 ${r.name} [${r.kind}] storage=${r.storage} replicas=${r.numReplicas} max_bytes=${formatBytes(r.maxBytes)} max_age=${formatAge(r.maxAge)} retention=${r.retention} \u2192 cluster reservation ${formatBytes(clusterReservation)}`
3912
+ ` - ${r.name} [${r.kind}] storage=${r.storage} replicas=${r.numReplicas} max_bytes=${formatBytes(r.maxBytes)} max_age=${formatAge(r.maxAge)} retention=${r.retention} -> cluster reservation ${formatBytes(clusterReservation)}`
3753
3913
  );
3754
3914
  }
3915
+ for (const e of external) {
3916
+ lines.push(` - ${e.name} [${e.kind}] external (bound)`);
3917
+ }
3755
3918
  lines.push(
3756
- ` \u03A3 per-node file-backed footprint \u2248 ${formatBytes(totalFileMaxBytes)} (sum of max_bytes; worst case replicas = nodes). Ensure the NATS server max_file_store accommodates the sum across ALL services.`
3919
+ ` Total per-node file-backed footprint ~ ${formatBytes(totalFileMaxBytes)} (sum of max_bytes; worst case replicas = nodes). Ensure the NATS server max_file_store accommodates the sum across ALL services.`
3757
3920
  );
3758
3921
  return lines.join("\n");
3759
3922
  };
@@ -3814,8 +3977,8 @@ var isEqual = (a, b) => {
3814
3977
  };
3815
3978
 
3816
3979
  // src/server/infrastructure/stream-migration.ts
3817
- var import_common13 = require("@nestjs/common");
3818
- var import_jetstream18 = require("@nats-io/jetstream");
3980
+ var import_common14 = require("@nestjs/common");
3981
+ var import_jetstream21 = require("@nats-io/jetstream");
3819
3982
  var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
3820
3983
  var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
3821
3984
  var SOURCING_POLL_INTERVAL_MS = 100;
@@ -3827,7 +3990,7 @@ var StreamMigration = class {
3827
3990
  this.sourcingTimeoutMs = sourcingTimeoutMs;
3828
3991
  this.peerWaitMs = peerWaitMs;
3829
3992
  }
3830
- logger = new import_common13.Logger("Jetstream:Stream");
3993
+ logger = new import_common14.Logger("Jetstream:Stream");
3831
3994
  async migrate(jsm, streamName2, newConfig) {
3832
3995
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
3833
3996
  const startTime = Date.now();
@@ -3846,7 +4009,7 @@ var StreamMigration = class {
3846
4009
  await jsm.streams.update(streamName2, { ...currentInfo.config, subjects: [] });
3847
4010
  drainedCount = (await jsm.streams.info(streamName2)).state.messages;
3848
4011
  if (drainedCount > 0) {
3849
- this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages \u2192 ${backupName}`);
4012
+ this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages -> ${backupName}`);
3850
4013
  await jsm.streams.add({
3851
4014
  ...currentInfo.config,
3852
4015
  name: backupName,
@@ -3867,7 +4030,7 @@ var StreamMigration = class {
3867
4030
  } catch (err) {
3868
4031
  if (originalDeleted) {
3869
4032
  this.logger.error(
3870
- `Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved \u2014 restoration resumes on the next startup.`
4033
+ `Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved; restoration resumes on the next startup.`
3871
4034
  );
3872
4035
  } else {
3873
4036
  await this.rollbackBeforeDelete(jsm, streamName2, currentInfo, backupName);
@@ -3935,7 +4098,7 @@ var StreamMigration = class {
3935
4098
  await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
3936
4099
  }
3937
4100
  /**
3938
- * Lag-based drain check live publishes cannot fake completion. A fresh
4101
+ * Lag-based drain check: live publishes cannot fake completion. A fresh
3939
4102
  * source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
3940
4103
  * hence the active guard.
3941
4104
  */
@@ -3954,13 +4117,13 @@ var StreamMigration = class {
3954
4117
  );
3955
4118
  }
3956
4119
  /**
3957
- * A backup present at migrate() start is a live peer migration wait it
4120
+ * A backup present at migrate() start is a live peer migration; wait it
3958
4121
  * out. Stale leftovers were already handled by recoverInterrupted().
3959
4122
  */
3960
4123
  async waitOutPeerMigration(jsm, backupName) {
3961
4124
  if (await this.tryInfo(jsm, backupName) === null) return false;
3962
4125
  this.logger.warn(
3963
- `Migration backup ${backupName} exists \u2014 another instance appears to be migrating; waiting`
4126
+ `Migration backup ${backupName} exists; another instance appears to be migrating; waiting`
3964
4127
  );
3965
4128
  const deadline = Date.now() + this.peerWaitMs;
3966
4129
  while (Date.now() < deadline) {
@@ -3981,7 +4144,7 @@ var StreamMigration = class {
3981
4144
  }
3982
4145
  } catch (rollbackErr) {
3983
4146
  this.logger.error(
3984
- `Rollback of ${streamName2} after a failed migration also failed \u2014 the stream may be left quiesced:`,
4147
+ `Rollback of ${streamName2} after a failed migration also failed; the stream may be left quiesced:`,
3985
4148
  rollbackErr
3986
4149
  );
3987
4150
  }
@@ -3997,7 +4160,7 @@ var StreamMigration = class {
3997
4160
  try {
3998
4161
  return await jsm.streams.info(name);
3999
4162
  } catch (err) {
4000
- if (err instanceof import_jetstream18.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4163
+ if (err instanceof import_jetstream21.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4001
4164
  return null;
4002
4165
  }
4003
4166
  throw err;
@@ -4005,7 +4168,7 @@ var StreamMigration = class {
4005
4168
  }
4006
4169
  };
4007
4170
 
4008
- // src/server/infrastructure/stream.provider.ts
4171
+ // src/server/infrastructure/subject-utils.ts
4009
4172
  var subjectCovers = (broad, narrow) => {
4010
4173
  if (broad === narrow) return false;
4011
4174
  const broadTokens = broad.split(".");
@@ -4017,16 +4180,21 @@ var subjectCovers = (broad, narrow) => {
4017
4180
  }
4018
4181
  return broadTokens.length === narrowTokens.length;
4019
4182
  };
4183
+ var coversOrEquals = (broad, subject) => broad === subject || subjectCovers(broad, subject);
4184
+
4185
+ // src/server/infrastructure/stream.provider.ts
4020
4186
  var StreamProvider = class {
4021
- constructor(options, connection) {
4187
+ constructor(options, connection, names, binder) {
4022
4188
  this.options = options;
4023
4189
  this.connection = connection;
4190
+ this.names = names;
4191
+ this.binder = binder;
4024
4192
  const derived = deriveOtelAttrs(options);
4025
4193
  this.otel = derived.otel;
4026
4194
  this.otelServiceName = derived.serviceName;
4027
4195
  this.otelEndpoint = derived.serverEndpoint;
4028
4196
  }
4029
- logger = new import_common14.Logger("Jetstream:Stream");
4197
+ logger = new import_common15.Logger("Jetstream:Stream");
4030
4198
  migration = new StreamMigration();
4031
4199
  otel;
4032
4200
  otelServiceName;
@@ -4040,42 +4208,48 @@ var StreamProvider = class {
4040
4208
  */
4041
4209
  async ensureStreams(kinds) {
4042
4210
  const jsm = await this.connection.getJetStreamManager();
4043
- const reservations = kinds.map((kind) => this.buildReservation(kind, this.buildConfig(kind)));
4211
+ const { autoKinds, externalKinds } = this.partitionByManagement(kinds);
4212
+ const reservations = autoKinds.map(
4213
+ (kind) => this.buildReservation(kind, this.buildConfig(kind))
4214
+ );
4215
+ const external = externalKinds.map((kind) => ({
4216
+ kind,
4217
+ name: this.names.streamName(kind)
4218
+ }));
4219
+ const dlqIsManual = !!this.options.dlq && resolveManagementMode(this.options, "dlq", "stream") === "manual" /* Manual */;
4044
4220
  if (this.options.dlq) {
4045
- reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
4221
+ if (dlqIsManual) {
4222
+ external.push({ kind: "dlq", name: this.names.dlqStreamName() });
4223
+ } else {
4224
+ reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
4225
+ }
4046
4226
  }
4047
4227
  this.logger.log(`
4048
- ${formatProvisioningSummary(this.options.name, reservations)}`);
4049
- if (this.options.provisioning?.preflightStorageCheck) {
4228
+ ${formatProvisioningSummary(this.options.name, reservations, external)}`);
4229
+ if (this.options.provisioning?.preflightStorageCheck && reservations.length > 0) {
4050
4230
  await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
4051
4231
  }
4052
- await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
4232
+ await Promise.all([
4233
+ ...autoKinds.map((kind) => this.ensureStream(jsm, kind)),
4234
+ ...externalKinds.map((kind) => this.bindStream(jsm, kind))
4235
+ ]);
4053
4236
  if (this.options.dlq) {
4054
- await this.ensureDlqStream(jsm);
4237
+ if (dlqIsManual) {
4238
+ await this.bindDlqStream(jsm);
4239
+ } else {
4240
+ await this.ensureDlqStream(jsm);
4241
+ }
4055
4242
  }
4056
4243
  }
4057
4244
  /** Get the stream name for a given kind. */
4058
4245
  getStreamName(kind) {
4059
- return streamName(this.options.name, kind);
4246
+ return this.names.streamName(kind);
4060
4247
  }
4061
4248
  /** Get the subjects pattern for a given kind. */
4062
4249
  getSubjects(kind) {
4063
- const name = internalName(this.options.name);
4064
- switch (kind) {
4065
- case "ev" /* Event */: {
4066
- const subjects = [`${name}.${"ev" /* Event */}.>`];
4067
- if (this.isSchedulingEnabled(kind)) {
4068
- subjects.push(`${name}._sch.>`);
4069
- }
4070
- return subjects;
4071
- }
4072
- case "cmd" /* Command */:
4073
- return [`${name}.${"cmd" /* Command */}.>`];
4074
- case "broadcast" /* Broadcast */:
4075
- return ["broadcast.>"];
4076
- case "ordered" /* Ordered */:
4077
- return [`${name}.${"ordered" /* Ordered */}.>`];
4078
- }
4250
+ const filter = this.names.filterSubject(kind);
4251
+ const dedicatedSchedule = kind === "ev" /* Event */ && this.isSchedulingEnabled(kind) && !this.names.hasCustomPrefix(kind);
4252
+ return dedicatedSchedule ? [filter, `${this.names.schedulePrefix(kind)}>`] : [filter];
4079
4253
  }
4080
4254
  /** Ensure a single stream exists, creating or updating as needed. */
4081
4255
  async ensureStream(jsm, kind) {
@@ -4100,7 +4274,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4100
4274
  const currentInfo = await jsm.streams.info(config.name);
4101
4275
  return await this.handleExistingStream(jsm, currentInfo, config, ctx);
4102
4276
  } catch (err) {
4103
- if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4277
+ if (err instanceof import_jetstream22.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4104
4278
  this.logger.log(`Creating stream: ${config.name}`);
4105
4279
  return await this.runStreamOp(ctx, () => jsm.streams.add(config));
4106
4280
  }
@@ -4131,7 +4305,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4131
4305
  const currentInfo = await jsm.streams.info(config.name);
4132
4306
  return await this.handleExistingStream(jsm, currentInfo, config, ctx);
4133
4307
  } catch (err) {
4134
- if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4308
+ if (err instanceof import_jetstream22.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4135
4309
  this.logger.log(`Creating DLQ stream: ${config.name}`);
4136
4310
  return await this.runStreamOp(ctx, () => jsm.streams.add(config));
4137
4311
  }
@@ -4152,7 +4326,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4152
4326
  }
4153
4327
  this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
4154
4328
  if (diff.hasTransportControlledConflicts) {
4155
- const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
4329
+ const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`).join(", ");
4156
4330
  throw new Error(
4157
4331
  `Stream ${config.name} has transport-controlled config conflicts that cannot be migrated: ${conflicts}. The retention policy is managed by the transport and must match the stream kind.`
4158
4332
  );
@@ -4202,13 +4376,13 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4202
4376
  }
4203
4377
  logChanges(streamName2, diff, migrationEnabled) {
4204
4378
  for (const c of diff.changes) {
4205
- const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
4379
+ const detail = `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`;
4206
4380
  if (c.mutability === "transport-controlled") {
4207
4381
  this.logger.error(
4208
- `Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
4382
+ `Stream ${streamName2}: ${detail}; transport-controlled, cannot be changed`
4209
4383
  );
4210
4384
  } else if (c.mutability === "immutable" && !migrationEnabled) {
4211
- this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
4385
+ this.logger.warn(`Stream ${streamName2}: ${detail}; requires allowDestructiveMigration`);
4212
4386
  } else {
4213
4387
  this.logger.log(`Stream ${streamName2}: ${detail}`);
4214
4388
  }
@@ -4219,12 +4393,12 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4219
4393
  return {
4220
4394
  kind,
4221
4395
  name: config.name,
4222
- storage: config.storage ?? import_jetstream19.StorageType.File,
4396
+ storage: config.storage ?? import_jetstream22.StorageType.File,
4223
4397
  numReplicas: config.num_replicas ?? 1,
4224
4398
  maxBytes: mb !== void 0 && mb >= 0 ? mb : 0,
4225
4399
  // NATS uses -1 for unlimited
4226
4400
  maxAge: config.max_age ?? 0,
4227
- retention: config.retention ?? import_jetstream19.RetentionPolicy.Limits
4401
+ retention: config.retention ?? import_jetstream22.RetentionPolicy.Limits
4228
4402
  };
4229
4403
  }
4230
4404
  errorContext(kind, config) {
@@ -4240,39 +4414,77 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4240
4414
  try {
4241
4415
  return await op();
4242
4416
  } catch (err) {
4243
- if (err instanceof import_jetstream19.JetStreamApiError) {
4417
+ if (err instanceof import_jetstream22.JetStreamApiError) {
4244
4418
  throw mapProvisioningError(err, ctx);
4245
4419
  }
4246
4420
  throw err;
4247
4421
  }
4248
4422
  }
4249
- /** The broadcast stream is global — every service in the cluster shares it. */
4250
- isSharedStream(name) {
4251
- return name === this.getStreamName("broadcast" /* Broadcast */);
4252
- }
4253
- /** Build the full stream config by merging defaults with user overrides. */
4254
- buildConfig(kind) {
4255
- const name = this.getStreamName(kind);
4256
- const subjects = this.getSubjects(kind);
4257
- const description = kind === "broadcast" /* Broadcast */ ? "JetStream broadcast stream (shared across services)" : `JetStream ${kind} stream for ${this.options.name}`;
4258
- const defaults = this.getDefaults(kind);
4259
- const overrides = this.getOverrides(kind);
4260
- return {
4261
- ...defaults,
4262
- ...overrides,
4263
- name,
4264
- subjects,
4265
- description
4266
- };
4423
+ partitionByManagement(kinds) {
4424
+ const autoKinds = [];
4425
+ const externalKinds = [];
4426
+ for (const kind of kinds) {
4427
+ if (resolveManagementMode(this.options, kind, "stream") === "manual" /* Manual */) {
4428
+ externalKinds.push(kind);
4429
+ } else {
4430
+ autoKinds.push(kind);
4431
+ }
4432
+ }
4433
+ return { autoKinds, externalKinds };
4267
4434
  }
4268
- /**
4269
- * Build the stream configuration for the Dead-Letter Queue (DLQ).
4270
- *
4271
- * Merges the library default DLQ config with user-provided overrides.
4272
- * Ensures transport-controlled settings like retention are safely decoupled.
4435
+ async bindStream(jsm, kind) {
4436
+ const name = this.names.streamName(kind);
4437
+ return withProvisioningSpan(
4438
+ this.otel,
4439
+ {
4440
+ serviceName: this.otelServiceName,
4441
+ endpoint: this.otelEndpoint,
4442
+ entity: "stream",
4443
+ name,
4444
+ action: "bind"
4445
+ },
4446
+ () => this.binder.bindStream(jsm, kind)
4447
+ );
4448
+ }
4449
+ async bindDlqStream(jsm) {
4450
+ const name = this.names.dlqStreamName();
4451
+ return withProvisioningSpan(
4452
+ this.otel,
4453
+ {
4454
+ serviceName: this.otelServiceName,
4455
+ endpoint: this.otelEndpoint,
4456
+ entity: "stream",
4457
+ name,
4458
+ action: "bind"
4459
+ },
4460
+ () => this.binder.bindDlqStream(jsm)
4461
+ );
4462
+ }
4463
+ /** The broadcast stream is global; every service in the cluster shares it. */
4464
+ isSharedStream(name) {
4465
+ return name === this.getStreamName("broadcast" /* Broadcast */);
4466
+ }
4467
+ /** Build the full stream config by merging defaults with user overrides. */
4468
+ buildConfig(kind) {
4469
+ const name = this.getStreamName(kind);
4470
+ const subjects = this.getSubjects(kind);
4471
+ const description = kind === "broadcast" /* Broadcast */ ? "JetStream broadcast stream (shared across services)" : `JetStream ${kind} stream for ${this.options.name}`;
4472
+ const defaults = this.getDefaults(kind);
4473
+ const overrides = this.getOverrides(kind);
4474
+ return {
4475
+ ...defaults,
4476
+ ...overrides,
4477
+ name,
4478
+ subjects,
4479
+ description
4480
+ };
4481
+ }
4482
+ /**
4483
+ * Build the DLQ stream config: library defaults merged with user overrides,
4484
+ * with transport-controlled settings like retention stripped.
4273
4485
  */
4274
4486
  buildDlqConfig() {
4275
- const name = dlqStreamName(this.options.name);
4487
+ const name = this.names.dlqStreamName();
4276
4488
  const subjects = [name];
4277
4489
  const description = `JetStream DLQ stream for ${this.options.name}`;
4278
4490
  const overrides = this.options.dlq?.stream ?? {};
@@ -4305,22 +4517,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4305
4517
  }
4306
4518
  /** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
4307
4519
  getOverrides(kind) {
4308
- let overrides;
4309
- switch (kind) {
4310
- case "ev" /* Event */:
4311
- overrides = this.options.events?.stream ?? {};
4312
- break;
4313
- case "cmd" /* Command */:
4314
- overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
4315
- break;
4316
- case "broadcast" /* Broadcast */:
4317
- overrides = this.options.broadcast?.stream ?? {};
4318
- break;
4319
- case "ordered" /* Ordered */:
4320
- overrides = this.options.ordered?.stream ?? {};
4321
- break;
4322
- }
4323
- return this.stripTransportControlled(overrides);
4520
+ return this.stripTransportControlled(kindOptionsBlock(this.options, kind)?.stream ?? {});
4324
4521
  }
4325
4522
  /**
4326
4523
  * Remove transport-controlled properties from user overrides.
@@ -4330,7 +4527,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4330
4527
  stripTransportControlled(overrides) {
4331
4528
  if (!("retention" in overrides)) return overrides;
4332
4529
  this.logger.debug(
4333
- "Stripping user-provided retention override \u2014 retention is managed by the transport"
4530
+ "Stripping user-provided retention override; retention is managed by the transport"
4334
4531
  );
4335
4532
  const cleaned = { ...overrides };
4336
4533
  delete cleaned.retention;
@@ -4339,20 +4536,22 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4339
4536
  };
4340
4537
 
4341
4538
  // src/server/infrastructure/consumer.provider.ts
4342
- var import_common15 = require("@nestjs/common");
4343
- var import_jetstream21 = require("@nats-io/jetstream");
4539
+ var import_common16 = require("@nestjs/common");
4540
+ var import_jetstream24 = require("@nats-io/jetstream");
4344
4541
  var ConsumerProvider = class {
4345
- constructor(options, connection, streamProvider, patternRegistry) {
4542
+ constructor(options, connection, streamProvider, patternRegistry, names, binder) {
4346
4543
  this.options = options;
4347
4544
  this.connection = connection;
4348
4545
  this.streamProvider = streamProvider;
4349
4546
  this.patternRegistry = patternRegistry;
4547
+ this.names = names;
4548
+ this.binder = binder;
4350
4549
  const derived = deriveOtelAttrs(options);
4351
4550
  this.otel = derived.otel;
4352
4551
  this.otelServiceName = derived.serviceName;
4353
4552
  this.otelEndpoint = derived.serverEndpoint;
4354
4553
  }
4355
- logger = new import_common15.Logger("Jetstream:Consumer");
4554
+ logger = new import_common16.Logger("Jetstream:Consumer");
4356
4555
  otel;
4357
4556
  otelServiceName;
4358
4557
  otelEndpoint;
@@ -4374,24 +4573,33 @@ var ConsumerProvider = class {
4374
4573
  }
4375
4574
  /** Get the consumer name for a given kind. */
4376
4575
  getConsumerName(kind) {
4377
- return consumerName(this.options.name, kind);
4576
+ return this.names.consumerName(kind);
4378
4577
  }
4379
4578
  /**
4380
4579
  * Ensure a single consumer exists with the desired config.
4381
- * Used at **startup** — creates or updates the consumer to match
4382
- * the current pod's configuration.
4580
+ * Startup path: creates or updates the consumer to match the current pod's configuration.
4383
4581
  */
4384
4582
  async ensureConsumer(jsm, kind) {
4385
4583
  const stream = this.streamProvider.getStreamName(kind);
4386
4584
  const config = this.buildConfig(kind);
4387
4585
  const name = config.durable_name;
4586
+ const spanAttrs = {
4587
+ serviceName: this.otelServiceName,
4588
+ endpoint: this.otelEndpoint,
4589
+ entity: "consumer",
4590
+ name
4591
+ };
4592
+ if (resolveManagementMode(this.options, kind, "consumer") === "manual" /* Manual */) {
4593
+ return withProvisioningSpan(
4594
+ this.otel,
4595
+ { ...spanAttrs, action: "bind" },
4596
+ () => this.binder.bindConsumer(jsm, kind)
4597
+ );
4598
+ }
4388
4599
  return withProvisioningSpan(
4389
4600
  this.otel,
4390
4601
  {
4391
- serviceName: this.otelServiceName,
4392
- endpoint: this.otelEndpoint,
4393
- entity: "consumer",
4394
- name,
4602
+ ...spanAttrs,
4395
4603
  action: "ensure"
4396
4604
  },
4397
4605
  async () => {
@@ -4402,7 +4610,7 @@ var ConsumerProvider = class {
4402
4610
  this.logger.debug(`Consumer exists, updating: ${name}`);
4403
4611
  return await this.runConsumerOp(ctx, () => jsm.consumers.update(stream, name, config));
4404
4612
  } catch (err) {
4405
- if (!(err instanceof import_jetstream21.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4613
+ if (!(err instanceof import_jetstream24.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4406
4614
  throw err;
4407
4615
  }
4408
4616
  return await this.createConsumer(jsm, stream, name, kind, config);
@@ -4412,28 +4620,40 @@ var ConsumerProvider = class {
4412
4620
  }
4413
4621
  /**
4414
4622
  * Recover a consumer that disappeared during runtime.
4415
- * Used by **self-healing** — creates if missing, but NEVER updates config.
4416
4623
  *
4417
- * If a migration backup stream exists, another pod is mid-migration we
4418
- * throw so the self-healing retry loop waits with backoff until migration
4419
- * completes and the backup is cleaned up.
4420
- *
4421
- * This prevents old pods from:
4422
- * - Overwriting a newer pod's consumer config during rolling updates
4423
- * - Creating consumers during migration (which would consume and delete
4424
- * workqueue messages while they're being restored)
4624
+ * Self-healing path: creates if missing but never updates config, so an old pod
4625
+ * cannot overwrite a newer pod's config during rolling updates. If a migration
4626
+ * backup stream exists, throws so the retry loop backs off until migration completes;
4627
+ * creating a consumer mid-migration would eat workqueue messages being restored.
4425
4628
  */
4426
4629
  async recoverConsumer(jsm, kind) {
4427
4630
  const stream = this.streamProvider.getStreamName(kind);
4428
4631
  const config = this.buildConfig(kind);
4429
4632
  const name = config.durable_name;
4633
+ const spanAttrs = {
4634
+ serviceName: this.otelServiceName,
4635
+ endpoint: this.otelEndpoint,
4636
+ entity: "consumer",
4637
+ name
4638
+ };
4639
+ if (resolveManagementMode(this.options, kind, "consumer") === "manual" /* Manual */) {
4640
+ return withProvisioningSpan(this.otel, { ...spanAttrs, action: "rebind" }, async () => {
4641
+ try {
4642
+ return await jsm.consumers.info(stream, name);
4643
+ } catch (err) {
4644
+ if (err instanceof import_jetstream24.JetStreamApiError && err.apiError().err_code === 10014 /* ConsumerNotFound */) {
4645
+ throw new Error(
4646
+ `Consumer ${name} on ${stream} is externally managed and currently absent; waiting for it to be restored.`
4647
+ );
4648
+ }
4649
+ throw err;
4650
+ }
4651
+ });
4652
+ }
4430
4653
  return withProvisioningSpan(
4431
4654
  this.otel,
4432
4655
  {
4433
- serviceName: this.otelServiceName,
4434
- endpoint: this.otelEndpoint,
4435
- entity: "consumer",
4436
- name,
4656
+ ...spanAttrs,
4437
4657
  action: "recover"
4438
4658
  },
4439
4659
  async () => {
@@ -4442,7 +4662,7 @@ var ConsumerProvider = class {
4442
4662
  try {
4443
4663
  return await jsm.consumers.info(stream, name);
4444
4664
  } catch (err) {
4445
- if (!(err instanceof import_jetstream21.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4665
+ if (!(err instanceof import_jetstream24.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4446
4666
  throw err;
4447
4667
  }
4448
4668
  return await this.createConsumer(jsm, stream, name, kind, config);
@@ -4463,26 +4683,24 @@ var ConsumerProvider = class {
4463
4683
  `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
4464
4684
  );
4465
4685
  } catch (err) {
4466
- if (err instanceof import_jetstream21.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4686
+ if (err instanceof import_jetstream24.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4467
4687
  return;
4468
4688
  }
4469
4689
  throw err;
4470
4690
  }
4471
4691
  }
4472
- /**
4473
- * Create a consumer, handling the race where another pod creates it first.
4474
- */
4692
+ /** Create a consumer, handling the race where another pod creates it first. */
4475
4693
  async createConsumer(jsm, stream, name, kind, config) {
4476
4694
  this.logger.log(`Creating consumer: ${name}`);
4477
4695
  const ctx = { entity: "consumer", name, kind };
4478
4696
  try {
4479
4697
  return await jsm.consumers.add(stream, config);
4480
4698
  } catch (addErr) {
4481
- if (addErr instanceof import_jetstream21.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
4699
+ if (addErr instanceof import_jetstream24.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
4482
4700
  this.logger.debug(`Consumer ${name} created by another pod, using existing`);
4483
4701
  return await jsm.consumers.info(stream, name);
4484
4702
  }
4485
- if (addErr instanceof import_jetstream21.JetStreamApiError) {
4703
+ if (addErr instanceof import_jetstream24.JetStreamApiError) {
4486
4704
  throw mapProvisioningError(addErr, ctx);
4487
4705
  }
4488
4706
  throw addErr;
@@ -4492,17 +4710,15 @@ var ConsumerProvider = class {
4492
4710
  try {
4493
4711
  return await op();
4494
4712
  } catch (err) {
4495
- if (err instanceof import_jetstream21.JetStreamApiError) {
4713
+ if (err instanceof import_jetstream24.JetStreamApiError) {
4496
4714
  throw mapProvisioningError(err, ctx);
4497
4715
  }
4498
4716
  throw err;
4499
4717
  }
4500
4718
  }
4501
4719
  /** Build consumer config by merging defaults with user overrides. */
4502
- // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
4503
4720
  buildConfig(kind) {
4504
- const name = this.getConsumerName(kind);
4505
- const serviceName = internalName(this.options.name);
4721
+ const durableName = this.getConsumerName(kind);
4506
4722
  const defaults = this.getDefaults(kind);
4507
4723
  const overrides = this.getOverrides(kind);
4508
4724
  if (kind === "broadcast" /* Broadcast */) {
@@ -4514,31 +4730,54 @@ var ConsumerProvider = class {
4514
4730
  return {
4515
4731
  ...defaults,
4516
4732
  ...overrides,
4517
- name,
4518
- durable_name: name,
4733
+ name: durableName,
4734
+ durable_name: durableName,
4519
4735
  filter_subject: broadcastPatterns[0]
4520
4736
  };
4521
4737
  }
4522
4738
  return {
4523
4739
  ...defaults,
4524
4740
  ...overrides,
4525
- name,
4526
- durable_name: name,
4741
+ name: durableName,
4742
+ durable_name: durableName,
4527
4743
  filter_subjects: broadcastPatterns
4528
4744
  };
4529
4745
  }
4530
4746
  if (kind !== "ev" /* Event */ && kind !== "cmd" /* Command */) {
4531
4747
  throw new Error(`Unexpected durable consumer kind: ${kind}`);
4532
4748
  }
4533
- const filter_subject = `${serviceName}.${kind}.>`;
4749
+ if (this.names.hasCustomPrefix(kind)) {
4750
+ return this.buildCustomPrefixConfig(kind, durableName, defaults, overrides);
4751
+ }
4752
+ const filter_subject = this.names.filterSubject(kind);
4534
4753
  return {
4535
4754
  ...defaults,
4536
4755
  ...overrides,
4537
- name,
4538
- durable_name: name,
4756
+ name: durableName,
4757
+ durable_name: durableName,
4539
4758
  filter_subject
4540
4759
  };
4541
4760
  }
4761
+ buildCustomPrefixConfig(kind, durableName, defaults, overrides) {
4762
+ const patterns = kind === "ev" /* Event */ ? this.patternRegistry.getEventPatterns() : this.patternRegistry.getCommandPatterns();
4763
+ const subjects = patterns.map((p) => this.names.subject(kind, p));
4764
+ if (subjects.length === 1) {
4765
+ return {
4766
+ ...defaults,
4767
+ ...overrides,
4768
+ name: durableName,
4769
+ durable_name: durableName,
4770
+ filter_subject: subjects[0]
4771
+ };
4772
+ }
4773
+ return {
4774
+ ...defaults,
4775
+ ...overrides,
4776
+ name: durableName,
4777
+ durable_name: durableName,
4778
+ filter_subjects: subjects
4779
+ };
4780
+ }
4542
4781
  /** Get default config for a consumer kind. */
4543
4782
  getDefaults(kind) {
4544
4783
  switch (kind) {
@@ -4559,27 +4798,13 @@ var ConsumerProvider = class {
4559
4798
  }
4560
4799
  /** Get user-provided overrides for a consumer kind. */
4561
4800
  getOverrides(kind) {
4562
- switch (kind) {
4563
- case "ev" /* Event */:
4564
- return this.options.events?.consumer ?? {};
4565
- case "cmd" /* Command */:
4566
- return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
4567
- case "broadcast" /* Broadcast */:
4568
- return this.options.broadcast?.consumer ?? {};
4569
- case "ordered" /* Ordered */:
4570
- throw new Error("Ordered consumers are ephemeral and should not use durable config");
4571
- /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
4572
- default: {
4573
- const _exhaustive = kind;
4574
- throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
4575
- }
4576
- }
4801
+ return kindOptionsBlock(this.options, kind)?.consumer ?? {};
4577
4802
  }
4578
4803
  };
4579
4804
 
4580
4805
  // src/server/infrastructure/message.provider.ts
4581
- var import_common16 = require("@nestjs/common");
4582
- var import_jetstream23 = require("@nats-io/jetstream");
4806
+ var import_common17 = require("@nestjs/common");
4807
+ var import_jetstream26 = require("@nats-io/jetstream");
4583
4808
  var import_rxjs3 = require("rxjs");
4584
4809
  var MessageProvider = class {
4585
4810
  constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
@@ -4588,7 +4813,7 @@ var MessageProvider = class {
4588
4813
  this.consumeOptionsMap = consumeOptionsMap;
4589
4814
  this.consumerRecoveryFn = consumerRecoveryFn;
4590
4815
  }
4591
- logger = new import_common16.Logger("Jetstream:Message");
4816
+ logger = new import_common17.Logger("Jetstream:Message");
4592
4817
  activeIterators = /* @__PURE__ */ new Set();
4593
4818
  orderedReadyResolve = null;
4594
4819
  orderedReadyReject = null;
@@ -4631,7 +4856,7 @@ var MessageProvider = class {
4631
4856
  /**
4632
4857
  * Start an ordered consumer for strict sequential delivery.
4633
4858
  *
4634
- * Unlike durable consumers, ordered consumers are ephemeral created at
4859
+ * Unlike durable consumers, ordered consumers are ephemeral: created at
4635
4860
  * consumption time, no durable state. nats.js handles auto-recreation.
4636
4861
  *
4637
4862
  * @param streamName - JetStream stream to consume from.
@@ -4640,7 +4865,7 @@ var MessageProvider = class {
4640
4865
  */
4641
4866
  async startOrdered(streamName2, filterSubjects, orderedConfig) {
4642
4867
  const consumerOpts = { filter_subjects: filterSubjects };
4643
- if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream23.DeliverPolicy.All) {
4868
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream26.DeliverPolicy.All) {
4644
4869
  consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
4645
4870
  }
4646
4871
  if (orderedConfig?.optStartSeq !== void 0) {
@@ -4845,7 +5070,7 @@ var MessageProvider = class {
4845
5070
  };
4846
5071
 
4847
5072
  // src/server/infrastructure/metadata.provider.ts
4848
- var import_common17 = require("@nestjs/common");
5073
+ var import_common18 = require("@nestjs/common");
4849
5074
  var import_kv = require("@nats-io/kv");
4850
5075
  var MetadataProvider = class {
4851
5076
  constructor(options, connection) {
@@ -4854,7 +5079,7 @@ var MetadataProvider = class {
4854
5079
  this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
4855
5080
  this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
4856
5081
  }
4857
- logger = new import_common17.Logger("Jetstream:Metadata");
5082
+ logger = new import_common18.Logger("Jetstream:Metadata");
4858
5083
  bucketName;
4859
5084
  replicas;
4860
5085
  ttl;
@@ -4862,16 +5087,12 @@ var MetadataProvider = class {
4862
5087
  heartbeatTimer;
4863
5088
  cachedKv;
4864
5089
  /**
4865
- * Write handler metadata entries to the KV bucket and start heartbeat.
5090
+ * Write handler metadata entries to the KV bucket and start the heartbeat.
4866
5091
  *
4867
- * Creates the bucket if it doesn't exist (idempotent).
4868
- * Skips silently when entries map is empty.
4869
- * Starts a heartbeat interval that refreshes entries every `ttl / 2`
4870
- * to prevent TTL expiry while the pod is alive.
5092
+ * Creates the bucket if missing; skips silently when the map is empty.
5093
+ * Non-critical: errors are logged but do not prevent transport startup.
4871
5094
  *
4872
- * Non-critical errors are logged but do not prevent transport startup.
4873
- *
4874
- * @param entries Map of KV key → metadata object.
5095
+ * @param entries Map of KV key to metadata object.
4875
5096
  */
4876
5097
  async publish(entries) {
4877
5098
  if (entries.size === 0) return;
@@ -4947,396 +5168,626 @@ var MetadataProvider = class {
4947
5168
  };
4948
5169
 
4949
5170
  // src/server/routing/event.router.ts
4950
- var import_common18 = require("@nestjs/common");
4951
- var import_transport_node4 = require("@nats-io/transport-node");
4952
- var DLQ_PUBLISH_ATTEMPTS = 3;
4953
- var eventConsumeKindFor = (kind) => {
4954
- if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
4955
- if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
4956
- return "event" /* Event */;
5171
+ var import_common20 = require("@nestjs/common");
5172
+
5173
+ // src/server/routing/concurrency-gate.ts
5174
+ var BACKLOG_WARN_THRESHOLD = 1e3;
5175
+ var ConcurrencyGate = class {
5176
+ constructor(maxActive, route, parkTimer, logger5, label) {
5177
+ this.maxActive = maxActive;
5178
+ this.route = route;
5179
+ this.parkTimer = parkTimer;
5180
+ this.logger = logger5;
5181
+ this.label = label;
5182
+ }
5183
+ active = 0;
5184
+ backlogWarned = false;
5185
+ backlog = [];
5186
+ /** Entry point for each incoming message. */
5187
+ push(msg) {
5188
+ if (this.active >= this.maxActive) {
5189
+ this.backlog.push({
5190
+ msg,
5191
+ stopAckExtension: this.parkTimer ? this.parkTimer(msg) : null
5192
+ });
5193
+ if (!this.backlogWarned && this.backlog.length >= BACKLOG_WARN_THRESHOLD) {
5194
+ this.backlogWarned = true;
5195
+ this.logger.warn(
5196
+ `${this.label} backlog reached ${this.backlog.length} messages; consumer may be falling behind`
5197
+ );
5198
+ }
5199
+ return;
5200
+ }
5201
+ this.active++;
5202
+ const result = this.routeSafely(msg);
5203
+ if (result !== void 0) {
5204
+ this.trackAsync(result, msg);
5205
+ } else {
5206
+ this.active--;
5207
+ if (this.backlog.length > 0) this.drainBacklog();
5208
+ }
5209
+ }
5210
+ /** Stop parked timers and drop the backlog. */
5211
+ dispose() {
5212
+ for (const queued of this.backlog) {
5213
+ queued.stopAckExtension?.();
5214
+ }
5215
+ this.backlog.length = 0;
5216
+ }
5217
+ onAsyncDone = () => {
5218
+ this.active--;
5219
+ this.drainBacklog();
5220
+ };
5221
+ /** A throw here must not leak the concurrency slot or kill the subscription. */
5222
+ routeSafely(msg) {
5223
+ try {
5224
+ return this.route(msg);
5225
+ } catch (err) {
5226
+ this.logger.error(`Unexpected routing failure for ${msg.subject}:`, err);
5227
+ return void 0;
5228
+ }
5229
+ }
5230
+ trackAsync(result, msg) {
5231
+ void result.catch((err) => {
5232
+ this.logger.error(`Unexpected routing failure for ${msg.subject}:`, err);
5233
+ }).finally(this.onAsyncDone);
5234
+ }
5235
+ drainBacklog() {
5236
+ while (this.active < this.maxActive) {
5237
+ const next = this.backlog.shift();
5238
+ if (next === void 0) break;
5239
+ next.stopAckExtension?.();
5240
+ this.active++;
5241
+ const result = this.routeSafely(next.msg);
5242
+ if (result !== void 0) {
5243
+ this.trackAsync(result, next.msg);
5244
+ } else {
5245
+ this.active--;
5246
+ }
5247
+ }
5248
+ if (this.backlog.length < BACKLOG_WARN_THRESHOLD) this.backlogWarned = false;
5249
+ }
4957
5250
  };
4958
- var EventRouter = class {
4959
- constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
4960
- this.messageProvider = messageProvider;
5251
+
5252
+ // src/server/routing/dead-letter-capture.ts
5253
+ var import_common19 = require("@nestjs/common");
5254
+ var import_transport_node5 = require("@nats-io/transport-node");
5255
+ var DLQ_PUBLISH_ATTEMPTS = 3;
5256
+ var DeadLetterCapture = class {
5257
+ constructor(patternRegistry, eventBus, deadLetterConfig, otel, serviceName, serverEndpoint, connection, options, names) {
4961
5258
  this.patternRegistry = patternRegistry;
4962
- this.codec = codec;
4963
5259
  this.eventBus = eventBus;
4964
5260
  this.deadLetterConfig = deadLetterConfig;
4965
- this.processingConfig = processingConfig;
4966
- this.ackWaitMap = ackWaitMap;
5261
+ this.otel = otel;
5262
+ this.serviceName = serviceName;
5263
+ this.serverEndpoint = serverEndpoint;
4967
5264
  this.connection = connection;
4968
5265
  this.options = options;
4969
- if (options) {
4970
- const derived = deriveOtelAttrs(options);
4971
- this.otel = derived.otel;
4972
- this.serviceName = derived.serviceName;
4973
- this.serverEndpoint = derived.serverEndpoint;
4974
- } else {
4975
- this.otel = resolveOtelOptions({ enabled: false });
4976
- this.serviceName = "";
4977
- this.serverEndpoint = null;
4978
- }
5266
+ this.names = names;
5267
+ }
5268
+ logger = new import_common19.Logger("Jetstream:DeadLetter");
5269
+ /** True when this delivery is the consumer's last attempt for the message. */
5270
+ isFinalDelivery(msg) {
5271
+ const maxDeliver = this.deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
5272
+ if (maxDeliver === void 0 || maxDeliver <= 0) return false;
5273
+ return msg.info.deliveryCount >= maxDeliver;
5274
+ }
5275
+ /** Emit the dead-letter event and route the message to the DLQ or the fallback callback. */
5276
+ async capture(msg, data, error) {
5277
+ const info = {
5278
+ subject: msg.subject,
5279
+ data,
5280
+ headers: msg.headers,
5281
+ error,
5282
+ deliveryCount: msg.info.deliveryCount,
5283
+ stream: msg.info.stream,
5284
+ streamSequence: msg.info.streamSequence,
5285
+ timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
5286
+ };
5287
+ await withDeadLetterSpan(
5288
+ {
5289
+ msg,
5290
+ // Surface the registered pattern so APM can filter dead letters by
5291
+ // handler; falls back to the raw subject when no handler matches.
5292
+ pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
5293
+ finalDeliveryCount: msg.info.deliveryCount,
5294
+ reason: error instanceof Error ? error.message : String(error),
5295
+ serviceName: this.serviceName,
5296
+ endpoint: this.serverEndpoint
5297
+ },
5298
+ this.otel,
5299
+ async () => {
5300
+ this.eventBus.emit("deadLetter" /* DeadLetter */, info);
5301
+ if (!this.options?.dlq) {
5302
+ await this.fallbackToOnDeadLetterCallback(info, msg);
5303
+ } else {
5304
+ await this.publishToDlq(msg, info, error);
5305
+ }
5306
+ }
5307
+ );
4979
5308
  }
4980
- logger = new import_common18.Logger("Jetstream:EventRouter");
4981
- subscriptions = [];
4982
- otel;
4983
- serviceName;
4984
- serverEndpoint;
4985
5309
  /**
4986
- * Update the max_deliver thresholds from actual NATS consumer configs.
4987
- * Called after consumers are ensured so the DLQ map reflects reality.
5310
+ * Publish the dead letter to the DLQ stream with diagnostic headers. On
5311
+ * success the `onDeadLetter` callback is notified and the message termed;
5312
+ * on failure everything falls back to the callback to avoid silent loss.
4988
5313
  */
4989
- updateMaxDeliverMap(consumerMaxDelivers) {
4990
- if (!this.deadLetterConfig) return;
4991
- this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
4992
- }
4993
- /** Start routing event, broadcast, and ordered messages to handlers. */
4994
- start() {
4995
- this.subscribeToStream(this.messageProvider.events$, "ev" /* Event */);
4996
- this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast" /* Broadcast */);
4997
- if (this.patternRegistry.hasOrderedHandlers()) {
4998
- this.subscribeToStream(this.messageProvider.ordered$, "ordered" /* Ordered */);
5314
+ async publishToDlq(msg, info, error) {
5315
+ const serviceName = this.options?.name;
5316
+ if (!this.connection || !serviceName) {
5317
+ this.logger.error(
5318
+ `Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
5319
+ );
5320
+ await this.fallbackToOnDeadLetterCallback(info, msg);
5321
+ return;
5322
+ }
5323
+ const dlqStreamOverride = this.options.dlq?.stream?.name;
5324
+ const destinationSubject = this.names ? this.names.dlqStreamName() : dlqStreamOverride ?? dlqStreamName(serviceName);
5325
+ const hdrs = this.buildDlqHeaders(msg);
5326
+ hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, this.extractErrorReason(error));
5327
+ hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
5328
+ hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
5329
+ hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
5330
+ hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
5331
+ try {
5332
+ await this.publishWithRetry(this.connection, destinationSubject, msg.data, hdrs);
5333
+ this.logger.log(`Message sent to DLQ: ${msg.subject}`);
5334
+ await this.notifyDeadLetterCallback(info, msg);
5335
+ } catch (publishErr) {
5336
+ this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
5337
+ await this.fallbackToOnDeadLetterCallback(info, msg);
4999
5338
  }
5000
5339
  }
5001
- /** Stop routing and unsubscribe from all streams. */
5002
- destroy() {
5003
- for (const sub of this.subscriptions) {
5004
- sub.unsubscribe();
5340
+ /**
5341
+ * Past max_deliver the server never redelivers, so these in-process attempts
5342
+ * are the only second chance a dead letter gets. No artificial delay: an
5343
+ * unreachable broker already spaces attempts via its own request timeout.
5344
+ */
5345
+ async publishWithRetry(connection, subject, data, headers2) {
5346
+ let lastErr;
5347
+ for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
5348
+ try {
5349
+ await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
5350
+ return;
5351
+ } catch (err) {
5352
+ lastErr = err;
5353
+ if (attempt < DLQ_PUBLISH_ATTEMPTS) {
5354
+ this.logger.warn(
5355
+ `DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
5356
+ );
5357
+ }
5358
+ }
5005
5359
  }
5006
- this.subscriptions.length = 0;
5360
+ throw lastErr;
5007
5361
  }
5008
- /** Subscribe to a message stream and route each message to its handler. */
5009
- subscribeToStream(stream$, kind) {
5010
- const isOrdered = kind === "ordered" /* Ordered */;
5011
- const patternRegistry = this.patternRegistry;
5012
- const codec = this.codec;
5013
- const eventBus = this.eventBus;
5014
- const logger5 = this.logger;
5015
- const deadLetterConfig = this.deadLetterConfig;
5016
- const otel = this.otel;
5017
- const serviceName = this.serviceName;
5018
- const serverEndpoint = this.serverEndpoint;
5019
- const spanKind = eventConsumeKindFor(kind);
5020
- const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
5021
- const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
5022
- const concurrency = this.getConcurrency(kind);
5023
- const hasDlqCheck = deadLetterConfig !== void 0;
5024
- const reportHandlerCompleted = (msg, startedAt, status) => {
5025
- if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
5026
- const declared = patternRegistry.resolveDeclared(msg.subject);
5027
- const pattern = declared?.pattern ?? msg.subject;
5028
- const declaredKind = declared?.kind ?? kind;
5029
- const durationMs = performance.now() - startedAt;
5030
- eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
5031
- };
5032
- const isDeadLetter = (msg) => {
5033
- if (!hasDlqCheck) return false;
5034
- const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
5035
- if (maxDeliver === void 0 || maxDeliver <= 0) return false;
5036
- return msg.info.deliveryCount >= maxDeliver;
5037
- };
5038
- const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
5039
- const settleSuccess = (msg, ctx, data) => {
5040
- if (ctx.shouldTerminate) {
5041
- settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
5042
- msg.term(ctx.terminateReason);
5043
- });
5044
- return void 0;
5362
+ /**
5363
+ * Copy headers for the DLQ republish, dropping NATS control headers: a
5364
+ * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
5365
+ */
5366
+ buildDlqHeaders(msg) {
5367
+ const hdrs = (0, import_transport_node5.headers)();
5368
+ if (!msg.headers) return hdrs;
5369
+ for (const [k, v] of msg.headers) {
5370
+ if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
5371
+ for (const val of v) {
5372
+ hdrs.append(k, val);
5045
5373
  }
5046
- if (ctx.shouldRetry) {
5047
- if (handleDeadLetter !== null && isDeadLetter(msg)) {
5048
- return handleDeadLetter(
5049
- msg,
5050
- data,
5051
- new Error("Retry requested on the final delivery attempt")
5052
- );
5053
- }
5054
- settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5055
- msg.nak(ctx.retryDelay);
5056
- });
5057
- return void 0;
5374
+ }
5375
+ return hdrs;
5376
+ }
5377
+ async notifyDeadLetterCallback(info, msg) {
5378
+ if (this.deadLetterConfig.onDeadLetter) {
5379
+ try {
5380
+ await this.deadLetterConfig.onDeadLetter(info);
5381
+ } catch (hookErr) {
5382
+ this.logger.warn(
5383
+ `onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
5384
+ hookErr
5385
+ );
5058
5386
  }
5059
- settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
5060
- msg.ack();
5387
+ }
5388
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5389
+ msg.term("Moved to DLQ stream");
5390
+ });
5391
+ }
5392
+ /**
5393
+ * Last resort: invoke onDeadLetter, then term on success. On failure the
5394
+ * message is nak'd: never redelivered past max_deliver, but preserved.
5395
+ */
5396
+ async fallbackToOnDeadLetterCallback(info, msg) {
5397
+ const onDeadLetter = this.deadLetterConfig.onDeadLetter;
5398
+ if (!onDeadLetter) {
5399
+ this.logger.error(
5400
+ `Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback); leaving the message in the stream`
5401
+ );
5402
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5403
+ msg.nak();
5404
+ });
5405
+ return;
5406
+ }
5407
+ try {
5408
+ await onDeadLetter(info);
5409
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5410
+ msg.term("Dead letter processed via fallback callback");
5411
+ });
5412
+ } catch (hookErr) {
5413
+ this.logger.error(
5414
+ `Fallback onDeadLetter callback failed for ${msg.subject}; the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
5415
+ hookErr
5416
+ );
5417
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5418
+ msg.nak();
5419
+ });
5420
+ }
5421
+ }
5422
+ extractErrorReason(error) {
5423
+ if (error instanceof Error) {
5424
+ return error.message;
5425
+ }
5426
+ if (typeof error === "object" && error !== null && "message" in error) {
5427
+ return String(error.message);
5428
+ }
5429
+ return String(error);
5430
+ }
5431
+ };
5432
+
5433
+ // src/server/routing/settlement.ts
5434
+ var statusForContext = (ctx) => {
5435
+ if (ctx.shouldTerminate) return "terminated";
5436
+ if (ctx.shouldRetry) return "retried";
5437
+ return "success";
5438
+ };
5439
+ var createSettlement = (logger5, capture) => {
5440
+ const settleSuccess = (msg, ctx, data) => {
5441
+ if (ctx.shouldTerminate) {
5442
+ settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
5443
+ msg.term(ctx.terminateReason);
5061
5444
  });
5062
5445
  return void 0;
5063
- };
5064
- const settleFailure = async (msg, data, err) => {
5065
- if (handleDeadLetter !== null && isDeadLetter(msg)) {
5066
- await handleDeadLetter(msg, data, err);
5067
- return;
5446
+ }
5447
+ if (ctx.shouldRetry) {
5448
+ if (capture?.isFinalDelivery(msg)) {
5449
+ return capture.capture(
5450
+ msg,
5451
+ data,
5452
+ new Error("Retry requested on the final delivery attempt")
5453
+ );
5068
5454
  }
5069
5455
  settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5070
- msg.nak();
5456
+ msg.nak(ctx.retryDelay);
5071
5457
  });
5072
- };
5073
- const captureUnroutable = (capture, msg, err) => {
5458
+ return void 0;
5459
+ }
5460
+ settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
5461
+ msg.ack();
5462
+ });
5463
+ return void 0;
5464
+ };
5465
+ const settleFailure = async (msg, data, err) => {
5466
+ if (capture?.isFinalDelivery(msg)) {
5467
+ await capture.capture(msg, data, err);
5468
+ return;
5469
+ }
5470
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5471
+ msg.nak();
5472
+ });
5473
+ };
5474
+ return { settleSuccess, settleFailure };
5475
+ };
5476
+
5477
+ // src/server/routing/event-pipeline.ts
5478
+ var createHandlerReporter = (rctx) => {
5479
+ const { eventBus, patternRegistry, kind } = rctx;
5480
+ return (msg, startedAt, status) => {
5481
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
5482
+ const declared = patternRegistry.resolveDeclared(msg.subject);
5483
+ const pattern = declared?.pattern ?? msg.subject;
5484
+ const declaredKind = declared?.kind ?? kind;
5485
+ const durationMs = performance.now() - startedAt;
5486
+ eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
5487
+ };
5488
+ };
5489
+ var createEventResolver = (rctx) => {
5490
+ const { patternRegistry, codec, eventBus, logger: logger5, capture, kind } = rctx;
5491
+ const captureUnroutable = (activeCapture, msg, err) => {
5492
+ let data;
5493
+ try {
5494
+ data = codec.decode(msg.data);
5495
+ } catch {
5496
+ data = void 0;
5497
+ }
5498
+ return activeCapture.capture(msg, data, err).catch((captureErr) => {
5499
+ logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
5500
+ });
5501
+ };
5502
+ return (msg) => {
5503
+ const subject = msg.subject;
5504
+ try {
5505
+ const handler = patternRegistry.getHandler(subject);
5506
+ if (!handler) {
5507
+ logger5.error(`No handler for subject: ${subject}`);
5508
+ if (capture !== null) {
5509
+ return captureUnroutable(capture, msg, new Error(`No handler for event: ${subject}`));
5510
+ }
5511
+ msg.term(`No handler for event: ${subject}`);
5512
+ return null;
5513
+ }
5074
5514
  let data;
5075
5515
  try {
5076
5516
  data = codec.decode(msg.data);
5077
- } catch {
5078
- data = void 0;
5079
- }
5080
- return capture(msg, data, err).catch((captureErr) => {
5081
- logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
5082
- });
5083
- };
5084
- const resolveEvent = (msg) => {
5085
- const subject = msg.subject;
5086
- try {
5087
- const handler = patternRegistry.getHandler(subject);
5088
- if (!handler) {
5089
- logger5.error(`No handler for subject: ${subject}`);
5090
- if (handleDeadLetter !== null) {
5091
- return captureUnroutable(
5092
- handleDeadLetter,
5093
- msg,
5094
- new Error(`No handler for event: ${subject}`)
5095
- );
5096
- }
5097
- msg.term(`No handler for event: ${subject}`);
5098
- return null;
5099
- }
5100
- let data;
5101
- try {
5102
- data = codec.decode(msg.data);
5103
- } catch (err) {
5104
- logger5.error(`Decode error for ${subject}:`, err);
5105
- if (handleDeadLetter !== null) {
5106
- return captureUnroutable(
5107
- handleDeadLetter,
5108
- msg,
5109
- new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
5110
- );
5111
- }
5112
- msg.term("Decode error");
5113
- return null;
5114
- }
5115
- eventBus.emitMessageRouted(subject, "event" /* Event */);
5116
- return { handler, data };
5117
5517
  } catch (err) {
5118
- logger5.error(`Unexpected error in ${kind} event router`, err);
5119
- try {
5120
- msg.term("Unexpected router error");
5121
- } catch (termErr) {
5122
- logger5.error(`Failed to terminate message ${subject}:`, termErr);
5518
+ logger5.error(`Decode error for ${subject}:`, err);
5519
+ if (capture !== null) {
5520
+ return captureUnroutable(
5521
+ capture,
5522
+ msg,
5523
+ new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
5524
+ );
5123
5525
  }
5526
+ msg.term("Decode error");
5124
5527
  return null;
5125
5528
  }
5126
- };
5127
- const statusForContext = (ctx) => {
5128
- if (ctx.shouldTerminate) return "terminated";
5129
- if (ctx.shouldRetry) return "retried";
5130
- return "success";
5131
- };
5132
- const handleSafe = (msg) => {
5133
- const resolved = resolveEvent(msg);
5134
- if (resolved === null) return void 0;
5135
- if (isPromiseLike2(resolved)) return resolved;
5136
- const { handler, data } = resolved;
5137
- const ctx = new RpcContext([msg]);
5138
- const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
5139
- const startedAt = performance.now();
5140
- let pending;
5529
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
5530
+ return { handler, data };
5531
+ } catch (err) {
5532
+ logger5.error(`Unexpected error in ${kind} event router`, err);
5141
5533
  try {
5142
- pending = withConsumeSpan(
5143
- {
5144
- subject: msg.subject,
5145
- msg,
5146
- info: msg.info,
5147
- kind: spanKind,
5148
- payloadBytes: msg.data.length,
5149
- handlerMetadata: { pattern: msg.subject },
5150
- serviceName,
5151
- endpoint: serverEndpoint
5152
- },
5153
- otel,
5154
- () => unwrapResult(handler(data, ctx))
5155
- );
5156
- } catch (err) {
5534
+ msg.term("Unexpected router error");
5535
+ } catch (termErr) {
5536
+ logger5.error(`Failed to terminate message ${subject}:`, termErr);
5537
+ }
5538
+ return null;
5539
+ }
5540
+ };
5541
+ };
5542
+ var createWorkqueuePipeline = (rctx) => {
5543
+ const { kind, spanKind, logger: logger5, eventBus, otel, serviceName, serverEndpoint } = rctx;
5544
+ const { ackExtensionInterval } = rctx;
5545
+ const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
5546
+ const reportHandlerCompleted = createHandlerReporter(rctx);
5547
+ const resolveEvent = createEventResolver(rctx);
5548
+ const { settleSuccess, settleFailure } = createSettlement(logger5, rctx.capture);
5549
+ return (msg) => {
5550
+ const resolved = resolveEvent(msg);
5551
+ if (resolved === null) return void 0;
5552
+ if (isPromiseLike2(resolved)) return resolved;
5553
+ const { handler, data } = resolved;
5554
+ const ctx = new RpcContext([msg]);
5555
+ const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
5556
+ const startedAt = performance.now();
5557
+ let pending;
5558
+ try {
5559
+ pending = withConsumeSpan(
5560
+ {
5561
+ subject: msg.subject,
5562
+ msg,
5563
+ info: msg.info,
5564
+ kind: spanKind,
5565
+ payloadBytes: msg.data.length,
5566
+ handlerMetadata: { pattern: msg.subject },
5567
+ serviceName,
5568
+ endpoint: serverEndpoint
5569
+ },
5570
+ otel,
5571
+ () => unwrapResult(handler(data, ctx))
5572
+ );
5573
+ } catch (err) {
5574
+ eventBus.emit(
5575
+ "error" /* Error */,
5576
+ err instanceof Error ? err : new Error(String(err)),
5577
+ `${kind}-handler:${msg.subject}`
5578
+ );
5579
+ reportHandlerCompleted(msg, startedAt, "error");
5580
+ return settleFailure(msg, data, err).finally(() => {
5581
+ if (stopAckExtension !== null) stopAckExtension();
5582
+ });
5583
+ }
5584
+ if (!isPromiseLike2(pending)) {
5585
+ const settled = settleSuccess(msg, ctx, data);
5586
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5587
+ if (settled === void 0) {
5588
+ if (stopAckExtension !== null) stopAckExtension();
5589
+ return void 0;
5590
+ }
5591
+ return settled.finally(() => {
5592
+ if (stopAckExtension !== null) stopAckExtension();
5593
+ });
5594
+ }
5595
+ return pending.then(
5596
+ async () => {
5597
+ try {
5598
+ await settleSuccess(msg, ctx, data);
5599
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5600
+ } finally {
5601
+ if (stopAckExtension !== null) stopAckExtension();
5602
+ }
5603
+ },
5604
+ async (err) => {
5157
5605
  eventBus.emit(
5158
5606
  "error" /* Error */,
5159
5607
  err instanceof Error ? err : new Error(String(err)),
5160
5608
  `${kind}-handler:${msg.subject}`
5161
5609
  );
5162
5610
  reportHandlerCompleted(msg, startedAt, "error");
5163
- return settleFailure(msg, data, err).finally(() => {
5164
- if (stopAckExtension !== null) stopAckExtension();
5165
- });
5166
- }
5167
- if (!isPromiseLike2(pending)) {
5168
- const settled = settleSuccess(msg, ctx, data);
5169
- reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5170
- if (settled === void 0) {
5611
+ try {
5612
+ await settleFailure(msg, data, err);
5613
+ } finally {
5171
5614
  if (stopAckExtension !== null) stopAckExtension();
5172
- return void 0;
5173
5615
  }
5174
- return settled.finally(() => {
5175
- if (stopAckExtension !== null) stopAckExtension();
5176
- });
5177
5616
  }
5178
- return pending.then(
5179
- async () => {
5180
- try {
5181
- await settleSuccess(msg, ctx, data);
5182
- reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5183
- } finally {
5184
- if (stopAckExtension !== null) stopAckExtension();
5185
- }
5186
- },
5187
- async (err) => {
5188
- eventBus.emit(
5189
- "error" /* Error */,
5190
- err instanceof Error ? err : new Error(String(err)),
5191
- `${kind}-handler:${msg.subject}`
5192
- );
5193
- reportHandlerCompleted(msg, startedAt, "error");
5194
- try {
5195
- await settleFailure(msg, data, err);
5196
- } finally {
5197
- if (stopAckExtension !== null) stopAckExtension();
5198
- }
5199
- }
5200
- );
5201
- };
5202
- const handleOrderedSafe = (msg) => {
5203
- const subject = msg.subject;
5204
- let handler;
5205
- let data;
5206
- try {
5207
- handler = patternRegistry.getHandler(subject);
5208
- if (!handler) {
5209
- logger5.error(`No handler for subject: ${subject}`);
5210
- return void 0;
5211
- }
5212
- try {
5213
- data = codec.decode(msg.data);
5214
- } catch (err) {
5215
- logger5.error(`Decode error for ${subject}:`, err);
5216
- return void 0;
5217
- }
5218
- eventBus.emitMessageRouted(subject, "event" /* Event */);
5219
- } catch (err) {
5220
- logger5.error(`Ordered handler error (${subject}):`, err);
5617
+ );
5618
+ };
5619
+ };
5620
+ var createOrderedPipeline = (rctx) => {
5621
+ const { spanKind, codec, logger: logger5, eventBus, patternRegistry, otel } = rctx;
5622
+ const { serviceName, serverEndpoint } = rctx;
5623
+ const reportHandlerCompleted = createHandlerReporter(rctx);
5624
+ return (msg) => {
5625
+ const subject = msg.subject;
5626
+ let handler;
5627
+ let data;
5628
+ try {
5629
+ handler = patternRegistry.getHandler(subject);
5630
+ if (!handler) {
5631
+ logger5.error(`No handler for subject: ${subject}`);
5221
5632
  return void 0;
5222
5633
  }
5223
- const ctx = new RpcContext([msg]);
5224
- const warnIfSettlementAttempted = () => {
5225
- if (ctx.shouldRetry || ctx.shouldTerminate) {
5226
- logger5.warn(
5227
- `retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
5228
- );
5229
- }
5230
- };
5231
- const startedAt = performance.now();
5232
- let pending;
5233
5634
  try {
5234
- pending = withConsumeSpan(
5235
- {
5236
- subject: msg.subject,
5237
- msg,
5238
- info: msg.info,
5239
- kind: spanKind,
5240
- payloadBytes: msg.data.length,
5241
- handlerMetadata: { pattern: msg.subject },
5242
- serviceName,
5243
- endpoint: serverEndpoint
5244
- },
5245
- otel,
5246
- () => unwrapResult(handler(data, ctx))
5247
- );
5635
+ data = codec.decode(msg.data);
5248
5636
  } catch (err) {
5249
- logger5.error(`Ordered handler error (${subject}):`, err);
5250
- reportHandlerCompleted(msg, startedAt, "error");
5637
+ logger5.error(`Decode error for ${subject}:`, err);
5251
5638
  return void 0;
5252
5639
  }
5253
- if (!isPromiseLike2(pending)) {
5254
- warnIfSettlementAttempted();
5255
- reportHandlerCompleted(msg, startedAt, "success");
5256
- return void 0;
5640
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
5641
+ } catch (err) {
5642
+ logger5.error(`Ordered handler error (${subject}):`, err);
5643
+ return void 0;
5644
+ }
5645
+ const ctx = new RpcContext([msg]);
5646
+ const warnIfSettlementAttempted = () => {
5647
+ if (ctx.shouldRetry || ctx.shouldTerminate) {
5648
+ logger5.warn(
5649
+ `retry()/terminate() ignored for ordered message ${subject}; ordered consumers auto-acknowledge`
5650
+ );
5257
5651
  }
5258
- return pending.then(
5259
- () => {
5260
- warnIfSettlementAttempted();
5261
- reportHandlerCompleted(msg, startedAt, "success");
5652
+ };
5653
+ const startedAt = performance.now();
5654
+ let pending;
5655
+ try {
5656
+ pending = withConsumeSpan(
5657
+ {
5658
+ subject: msg.subject,
5659
+ msg,
5660
+ info: msg.info,
5661
+ kind: spanKind,
5662
+ payloadBytes: msg.data.length,
5663
+ handlerMetadata: { pattern: msg.subject },
5664
+ serviceName,
5665
+ endpoint: serverEndpoint
5262
5666
  },
5263
- (err) => {
5264
- logger5.error(`Ordered handler error (${subject}):`, err);
5265
- reportHandlerCompleted(msg, startedAt, "error");
5266
- }
5667
+ otel,
5668
+ () => unwrapResult(handler(data, ctx))
5267
5669
  );
5268
- };
5269
- const route = isOrdered ? handleOrderedSafe : handleSafe;
5270
- const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
5271
- const backlogWarnThreshold = 1e3;
5272
- let active = 0;
5273
- let backlogWarned = false;
5274
- const backlog = [];
5275
- const onAsyncDone = () => {
5276
- active--;
5277
- drainBacklog();
5278
- };
5279
- const routeSafely = (msg) => {
5280
- try {
5281
- return route(msg);
5282
- } catch (err) {
5283
- logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5284
- return void 0;
5285
- }
5286
- };
5287
- const trackAsync = (result, msg) => {
5288
- void result.catch((err) => {
5289
- logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5290
- }).finally(onAsyncDone);
5291
- };
5292
- const drainBacklog = () => {
5293
- while (active < maxActive) {
5294
- const next = backlog.shift();
5295
- if (next === void 0) return;
5296
- next.stopAckExtension?.();
5297
- active++;
5298
- const result = routeSafely(next.msg);
5299
- if (result !== void 0) {
5300
- trackAsync(result, next.msg);
5301
- } else {
5302
- active--;
5303
- }
5670
+ } catch (err) {
5671
+ logger5.error(`Ordered handler error (${subject}):`, err);
5672
+ reportHandlerCompleted(msg, startedAt, "error");
5673
+ return void 0;
5674
+ }
5675
+ if (!isPromiseLike2(pending)) {
5676
+ warnIfSettlementAttempted();
5677
+ reportHandlerCompleted(msg, startedAt, "success");
5678
+ return void 0;
5679
+ }
5680
+ return pending.then(
5681
+ () => {
5682
+ warnIfSettlementAttempted();
5683
+ reportHandlerCompleted(msg, startedAt, "success");
5684
+ },
5685
+ (err) => {
5686
+ logger5.error(`Ordered handler error (${subject}):`, err);
5687
+ reportHandlerCompleted(msg, startedAt, "error");
5304
5688
  }
5305
- if (backlog.length < backlogWarnThreshold) backlogWarned = false;
5689
+ );
5690
+ };
5691
+ };
5692
+
5693
+ // src/server/routing/event.router.ts
5694
+ var eventConsumeKindFor = (kind) => {
5695
+ if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
5696
+ if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
5697
+ return "event" /* Event */;
5698
+ };
5699
+ var EventRouter = class {
5700
+ constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options, names) {
5701
+ this.messageProvider = messageProvider;
5702
+ this.patternRegistry = patternRegistry;
5703
+ this.codec = codec;
5704
+ this.eventBus = eventBus;
5705
+ this.deadLetterConfig = deadLetterConfig;
5706
+ this.processingConfig = processingConfig;
5707
+ this.ackWaitMap = ackWaitMap;
5708
+ if (options) {
5709
+ const derived = deriveOtelAttrs(options);
5710
+ this.otel = derived.otel;
5711
+ this.serviceName = derived.serviceName;
5712
+ this.serverEndpoint = derived.serverEndpoint;
5713
+ } else {
5714
+ this.otel = resolveOtelOptions({ enabled: false });
5715
+ this.serviceName = "";
5716
+ this.serverEndpoint = null;
5717
+ }
5718
+ this.capture = deadLetterConfig ? new DeadLetterCapture(
5719
+ patternRegistry,
5720
+ eventBus,
5721
+ deadLetterConfig,
5722
+ this.otel,
5723
+ this.serviceName,
5724
+ this.serverEndpoint,
5725
+ connection,
5726
+ options,
5727
+ names
5728
+ ) : null;
5729
+ }
5730
+ logger = new import_common20.Logger("Jetstream:EventRouter");
5731
+ subscriptions = [];
5732
+ otel;
5733
+ serviceName;
5734
+ serverEndpoint;
5735
+ capture;
5736
+ /**
5737
+ * Update the max_deliver thresholds from actual NATS consumer configs.
5738
+ * Called after consumers are ensured so the DLQ map reflects reality.
5739
+ */
5740
+ updateMaxDeliverMap(consumerMaxDelivers) {
5741
+ if (!this.deadLetterConfig) return;
5742
+ this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
5743
+ }
5744
+ /** Start routing event, broadcast, and ordered messages to handlers. */
5745
+ start() {
5746
+ this.subscribeToStream(this.messageProvider.events$, "ev" /* Event */);
5747
+ this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast" /* Broadcast */);
5748
+ if (this.patternRegistry.hasOrderedHandlers()) {
5749
+ this.subscribeToStream(this.messageProvider.ordered$, "ordered" /* Ordered */);
5750
+ }
5751
+ }
5752
+ /** Stop routing and unsubscribe from all streams. */
5753
+ destroy() {
5754
+ for (const sub of this.subscriptions) {
5755
+ sub.unsubscribe();
5756
+ }
5757
+ this.subscriptions.length = 0;
5758
+ }
5759
+ /** Assemble the pipeline and concurrency gate for one stream and subscribe. */
5760
+ subscribeToStream(stream$, kind) {
5761
+ const isOrdered = kind === "ordered" /* Ordered */;
5762
+ const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
5763
+ const rctx = {
5764
+ kind,
5765
+ spanKind: eventConsumeKindFor(kind),
5766
+ codec: this.codec,
5767
+ logger: this.logger,
5768
+ eventBus: this.eventBus,
5769
+ patternRegistry: this.patternRegistry,
5770
+ otel: this.otel,
5771
+ serviceName: this.serviceName,
5772
+ serverEndpoint: this.serverEndpoint,
5773
+ ackExtensionInterval,
5774
+ capture: this.capture
5306
5775
  };
5776
+ const route = isOrdered ? createOrderedPipeline(rctx) : createWorkqueuePipeline(rctx);
5777
+ const maxActive = isOrdered ? 1 : this.getConcurrency(kind) ?? Number.POSITIVE_INFINITY;
5778
+ const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
5779
+ const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
5780
+ const gate = new ConcurrencyGate(maxActive, route, parkTimer, this.logger, kind);
5307
5781
  const subscription = stream$.subscribe({
5308
5782
  next: (msg) => {
5309
- if (active >= maxActive) {
5310
- backlog.push({
5311
- msg,
5312
- stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5313
- });
5314
- if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
5315
- backlogWarned = true;
5316
- logger5.warn(
5317
- `${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
5318
- );
5319
- }
5320
- return;
5321
- }
5322
- active++;
5323
- const result = routeSafely(msg);
5324
- if (result !== void 0) {
5325
- trackAsync(result, msg);
5326
- } else {
5327
- active--;
5328
- if (backlog.length > 0) drainBacklog();
5329
- }
5783
+ gate.push(msg);
5330
5784
  },
5331
5785
  error: (err) => {
5332
- logger5.error(`Stream error in ${kind} router`, err);
5786
+ this.logger.error(`Stream error in ${kind} router`, err);
5333
5787
  }
5334
5788
  });
5335
5789
  subscription.add(() => {
5336
- for (const queued of backlog) {
5337
- queued.stopAckExtension?.();
5338
- }
5339
- backlog.length = 0;
5790
+ gate.dispose();
5340
5791
  });
5341
5792
  this.subscriptions.push(subscription);
5342
5793
  }
@@ -5350,170 +5801,11 @@ var EventRouter = class {
5350
5801
  if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
5351
5802
  return void 0;
5352
5803
  }
5353
- /**
5354
- * Last resort: invoke onDeadLetter, then term on success. On failure the
5355
- * message is nak'd — never redelivered past max_deliver, but preserved.
5356
- */
5357
- async fallbackToOnDeadLetterCallback(info, msg) {
5358
- const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
5359
- if (!onDeadLetter) {
5360
- this.logger.error(
5361
- `Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
5362
- );
5363
- settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5364
- msg.nak();
5365
- });
5366
- return;
5367
- }
5368
- try {
5369
- await onDeadLetter(info);
5370
- settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5371
- msg.term("Dead letter processed via fallback callback");
5372
- });
5373
- } catch (hookErr) {
5374
- this.logger.error(
5375
- `Fallback onDeadLetter callback failed for ${msg.subject} \u2014 the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
5376
- hookErr
5377
- );
5378
- settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5379
- msg.nak();
5380
- });
5381
- }
5382
- }
5383
- /**
5384
- * Copy headers for the DLQ republish, dropping NATS control headers — a
5385
- * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
5386
- */
5387
- buildDlqHeaders(msg) {
5388
- const hdrs = (0, import_transport_node4.headers)();
5389
- if (!msg.headers) return hdrs;
5390
- for (const [k, v] of msg.headers) {
5391
- if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
5392
- for (const val of v) {
5393
- hdrs.append(k, val);
5394
- }
5395
- }
5396
- return hdrs;
5397
- }
5398
- /**
5399
- * Past max_deliver the server never redelivers, so these in-process attempts
5400
- * are the only second chance a dead letter gets. No artificial delay — an
5401
- * unreachable broker already spaces attempts via its own request timeout.
5402
- */
5403
- async publishToDlqWithRetry(connection, subject, data, headers2) {
5404
- let lastErr;
5405
- for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
5406
- try {
5407
- await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
5408
- return;
5409
- } catch (err) {
5410
- lastErr = err;
5411
- if (attempt < DLQ_PUBLISH_ATTEMPTS) {
5412
- this.logger.warn(
5413
- `DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
5414
- );
5415
- }
5416
- }
5417
- }
5418
- throw lastErr;
5419
- }
5420
- /**
5421
- * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
5422
- *
5423
- * Appends diagnostic metadata headers to the original message and preserves
5424
- * the primary payload. If publishing succeeds, it notifies the standard
5425
- * `onDeadLetter` callback and terminates the message. If it fails, it falls
5426
- * back to the callback entirely to prevent silent data loss.
5427
- */
5428
- async publishToDlq(msg, info, error) {
5429
- const serviceName = this.options?.name;
5430
- if (!this.connection || !serviceName) {
5431
- this.logger.error(
5432
- `Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
5433
- );
5434
- await this.fallbackToOnDeadLetterCallback(info, msg);
5435
- return;
5436
- }
5437
- const destinationSubject = dlqStreamName(serviceName);
5438
- const hdrs = this.buildDlqHeaders(msg);
5439
- let reason = String(error);
5440
- if (error instanceof Error) {
5441
- reason = error.message;
5442
- } else if (typeof error === "object" && error !== null && "message" in error) {
5443
- reason = String(error.message);
5444
- }
5445
- hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
5446
- hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
5447
- hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
5448
- hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
5449
- hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
5450
- try {
5451
- await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
5452
- this.logger.log(`Message sent to DLQ: ${msg.subject}`);
5453
- if (this.deadLetterConfig?.onDeadLetter) {
5454
- try {
5455
- await this.deadLetterConfig.onDeadLetter(info);
5456
- } catch (hookErr) {
5457
- this.logger.warn(
5458
- `onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
5459
- hookErr
5460
- );
5461
- }
5462
- }
5463
- settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5464
- msg.term("Moved to DLQ stream");
5465
- });
5466
- } catch (publishErr) {
5467
- this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
5468
- await this.fallbackToOnDeadLetterCallback(info, msg);
5469
- }
5470
- }
5471
- /**
5472
- * Orchestrates the handling of a message that has exhausted delivery limits.
5473
- *
5474
- * Emits a system event and delegates either to the robust DLQ stream publisher
5475
- * or directly to the fallback callback based on the active module configuration.
5476
- */
5477
- async handleDeadLetter(msg, data, error) {
5478
- const info = {
5479
- subject: msg.subject,
5480
- data,
5481
- headers: msg.headers,
5482
- error,
5483
- deliveryCount: msg.info.deliveryCount,
5484
- stream: msg.info.stream,
5485
- streamSequence: msg.info.streamSequence,
5486
- timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
5487
- };
5488
- await withDeadLetterSpan(
5489
- {
5490
- msg,
5491
- // Pattern resolution mirrors event-routing: when a registered
5492
- // pattern matches, surface it on the DLQ span so APM can filter
5493
- // dead letters by handler without parsing the subject. Falls back
5494
- // to the subject itself when no glob handler is in play.
5495
- pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
5496
- finalDeliveryCount: msg.info.deliveryCount,
5497
- reason: error instanceof Error ? error.message : String(error),
5498
- serviceName: this.serviceName,
5499
- endpoint: this.serverEndpoint
5500
- },
5501
- this.otel,
5502
- async () => {
5503
- this.eventBus.emit("deadLetter" /* DeadLetter */, info);
5504
- if (!this.options?.dlq) {
5505
- await this.fallbackToOnDeadLetterCallback(info, msg);
5506
- } else {
5507
- await this.publishToDlq(msg, info, error);
5508
- }
5509
- }
5510
- );
5511
- }
5512
5804
  };
5513
5805
 
5514
5806
  // src/server/routing/rpc.router.ts
5515
- var import_common19 = require("@nestjs/common");
5516
- var import_transport_node5 = require("@nats-io/transport-node");
5807
+ var import_common21 = require("@nestjs/common");
5808
+ var import_transport_node6 = require("@nats-io/transport-node");
5517
5809
  var RpcRouter = class {
5518
5810
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
5519
5811
  this.messageProvider = messageProvider;
@@ -5536,7 +5828,7 @@ var RpcRouter = class {
5536
5828
  this.serverEndpoint = null;
5537
5829
  }
5538
5830
  }
5539
- logger = new import_common19.Logger("Jetstream:RpcRouter");
5831
+ logger = new import_common21.Logger("Jetstream:RpcRouter");
5540
5832
  timeout;
5541
5833
  concurrency;
5542
5834
  resolvedAckExtensionInterval;
@@ -5587,7 +5879,7 @@ var RpcRouter = class {
5587
5879
  };
5588
5880
  const publishReply = (replyTo, correlationId, payload) => {
5589
5881
  try {
5590
- const hdrs = (0, import_transport_node5.headers)();
5882
+ const hdrs = (0, import_transport_node6.headers)();
5591
5883
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
5592
5884
  nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
5593
5885
  } catch (publishErr) {
@@ -5596,7 +5888,7 @@ var RpcRouter = class {
5596
5888
  };
5597
5889
  const publishErrorReply = (replyTo, correlationId, subject, err) => {
5598
5890
  try {
5599
- const hdrs = (0, import_transport_node5.headers)();
5891
+ const hdrs = (0, import_transport_node6.headers)();
5600
5892
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
5601
5893
  hdrs.set("x-error" /* Error */, "true");
5602
5894
  nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
@@ -5727,75 +6019,18 @@ var RpcRouter = class {
5727
6019
  }
5728
6020
  );
5729
6021
  };
5730
- const backlogWarnThreshold = 1e3;
5731
- let active = 0;
5732
- let backlogWarned = false;
5733
- const backlog = [];
5734
- const onAsyncDone = () => {
5735
- active--;
5736
- drainBacklog();
5737
- };
5738
- const routeSafely = (msg) => {
5739
- try {
5740
- return handleSafe(msg);
5741
- } catch (err) {
5742
- logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5743
- return void 0;
5744
- }
5745
- };
5746
- const trackAsync = (result, msg) => {
5747
- void result.catch((err) => {
5748
- logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5749
- }).finally(onAsyncDone);
5750
- };
5751
- const drainBacklog = () => {
5752
- while (active < maxActive) {
5753
- const next = backlog.shift();
5754
- if (next === void 0) return;
5755
- next.stopAckExtension?.();
5756
- active++;
5757
- const result = routeSafely(next.msg);
5758
- if (result !== void 0) {
5759
- trackAsync(result, next.msg);
5760
- } else {
5761
- active--;
5762
- }
5763
- }
5764
- if (backlog.length < backlogWarnThreshold) backlogWarned = false;
5765
- };
6022
+ const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
6023
+ const gate = new ConcurrencyGate(maxActive, handleSafe, parkTimer, logger5, "RPC");
5766
6024
  this.subscription = this.messageProvider.commands$.subscribe({
5767
6025
  next: (msg) => {
5768
- if (active >= maxActive) {
5769
- backlog.push({
5770
- msg,
5771
- stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5772
- });
5773
- if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
5774
- backlogWarned = true;
5775
- logger5.warn(
5776
- `RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
5777
- );
5778
- }
5779
- return;
5780
- }
5781
- active++;
5782
- const result = routeSafely(msg);
5783
- if (result !== void 0) {
5784
- trackAsync(result, msg);
5785
- } else {
5786
- active--;
5787
- if (backlog.length > 0) drainBacklog();
5788
- }
6026
+ gate.push(msg);
5789
6027
  },
5790
6028
  error: (err) => {
5791
6029
  logger5.error("Stream error in RPC router", err);
5792
6030
  }
5793
6031
  });
5794
6032
  this.subscription.add(() => {
5795
- for (const queued of backlog) {
5796
- queued.stopAckExtension?.();
5797
- }
5798
- backlog.length = 0;
6033
+ gate.dispose();
5799
6034
  });
5800
6035
  }
5801
6036
  /** Stop routing and unsubscribe. */
@@ -5805,20 +6040,220 @@ var RpcRouter = class {
5805
6040
  }
5806
6041
  };
5807
6042
 
6043
+ // src/server/infrastructure/infrastructure-binder.ts
6044
+ var import_common22 = require("@nestjs/common");
6045
+ var import_jetstream30 = require("@nats-io/jetstream");
6046
+ var WORKQUEUE_KINDS = /* @__PURE__ */ new Set(["ev" /* Event */, "cmd" /* Command */]);
6047
+ var manualRemediation = (entity) => `Management mode is Manual; the ${entity} must be provisioned externally before boot.`;
6048
+ var isSchedulingEnabled = (options, kind) => kindOptionsBlock(options, kind)?.stream?.allow_msg_schedules === true;
6049
+ var resolveAckExtension = (options, kind) => kindOptionsBlock(options, kind)?.ackExtension;
6050
+ var filterCoversSubject = (filter_subject, filter_subjects, subject) => {
6051
+ if (filter_subject !== void 0) {
6052
+ return coversOrEquals(filter_subject, subject);
6053
+ }
6054
+ if (filter_subjects !== void 0) {
6055
+ return filter_subjects.some((f) => coversOrEquals(f, subject));
6056
+ }
6057
+ return true;
6058
+ };
6059
+ var InfrastructureBinder = class {
6060
+ constructor(options, names, registry) {
6061
+ this.options = options;
6062
+ this.names = names;
6063
+ this.registry = registry;
6064
+ }
6065
+ logger = new import_common22.Logger("Jetstream:Binder");
6066
+ async bindStream(jsm, kind) {
6067
+ const name = this.names.streamName(kind);
6068
+ const info = await this.fetchStream(jsm, name, kind);
6069
+ await this.warnOnOrphanedMigrationBackup(jsm, name);
6070
+ if (isSchedulingEnabled(this.options, kind)) {
6071
+ this.assertScheduleCoverage(info, kind);
6072
+ this.warnOnSchedulesDisabled(info, kind);
6073
+ }
6074
+ if (WORKQUEUE_KINDS.has(kind)) {
6075
+ this.warnOnRetention(info, kind);
6076
+ }
6077
+ return info;
6078
+ }
6079
+ async bindDlqStream(jsm) {
6080
+ const dlqName = this.names.dlqStreamName();
6081
+ const info = await this.fetchStream(jsm, dlqName, "dlq");
6082
+ await this.warnOnOrphanedMigrationBackup(jsm, dlqName);
6083
+ this.assertDlqSubjectCoverage(info);
6084
+ return info;
6085
+ }
6086
+ async bindConsumer(jsm, kind) {
6087
+ const info = await this.fetchConsumer(jsm, kind);
6088
+ this.assertHandlersCovered(info, kind);
6089
+ this.assertScheduleHoldersNotConsumed(info, kind);
6090
+ this.warnOnUnlimitedDelivery(info, kind);
6091
+ this.warnOnShortAckWait(info, kind);
6092
+ return info;
6093
+ }
6094
+ async fetchStream(jsm, name, kind) {
6095
+ try {
6096
+ return await jsm.streams.info(name);
6097
+ } catch (err) {
6098
+ if (err instanceof import_jetstream30.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
6099
+ const api = err.apiError();
6100
+ throw new JetstreamProvisioningError({
6101
+ entity: "stream",
6102
+ target: name,
6103
+ kind: String(kind),
6104
+ errCode: api.err_code,
6105
+ errDescription: api.description,
6106
+ remediation: manualRemediation("stream"),
6107
+ cause: err
6108
+ });
6109
+ }
6110
+ throw err;
6111
+ }
6112
+ }
6113
+ async fetchConsumer(jsm, kind) {
6114
+ const stream = this.names.streamName(kind);
6115
+ const consumer = this.names.consumerName(kind);
6116
+ try {
6117
+ return await jsm.consumers.info(stream, consumer);
6118
+ } catch (err) {
6119
+ if (err instanceof import_jetstream30.JetStreamApiError && err.apiError().err_code === 10014 /* ConsumerNotFound */) {
6120
+ const api = err.apiError();
6121
+ throw new JetstreamProvisioningError({
6122
+ entity: "consumer",
6123
+ target: `${consumer} on stream "${stream}"`,
6124
+ kind: String(kind),
6125
+ errCode: api.err_code,
6126
+ errDescription: api.description,
6127
+ remediation: manualRemediation("consumer"),
6128
+ cause: err
6129
+ });
6130
+ }
6131
+ throw err;
6132
+ }
6133
+ }
6134
+ assertHandlersCovered(info, kind) {
6135
+ const subjects = this.resolveHandlerSubjects(kind);
6136
+ if (subjects.length === 0) return;
6137
+ const { filter_subject, filter_subjects } = info.config;
6138
+ const uncovered = subjects.filter(
6139
+ (s) => !filterCoversSubject(filter_subject, filter_subjects, s)
6140
+ );
6141
+ if (uncovered.length > 0) {
6142
+ throw new Error(
6143
+ `Consumer "${this.names.consumerName(kind)}" (kind=${String(kind)}) does not cover the following registered handler subjects: ${uncovered.join(", ")}. Update the consumer's filter_subject / filter_subjects to include them.`
6144
+ );
6145
+ }
6146
+ }
6147
+ assertDlqSubjectCoverage(info) {
6148
+ const dlqSubject = this.names.dlqStreamName();
6149
+ const covered = info.config.subjects.some((s) => coversOrEquals(s, dlqSubject));
6150
+ if (!covered) {
6151
+ throw new Error(
6152
+ `DLQ stream "${dlqSubject}" subjects do not cover "${dlqSubject}" (dead letters publish to a subject equal to the stream name). Add it to the stream's subjects list.`
6153
+ );
6154
+ }
6155
+ }
6156
+ assertScheduleCoverage(info, kind) {
6157
+ const scheduleWildcard = `${this.names.schedulePrefix(kind)}>`;
6158
+ const covered = info.config.subjects.some((s) => coversOrEquals(s, scheduleWildcard));
6159
+ if (!covered) {
6160
+ throw new Error(
6161
+ `Stream "${this.names.streamName(kind)}" (kind=${String(kind)}) has scheduling enabled (allow_msg_schedules=true) but its subjects do not cover the schedule prefix "${this.names.schedulePrefix(kind)}". Add "${scheduleWildcard}" to the stream's subjects.`
6162
+ );
6163
+ }
6164
+ }
6165
+ assertScheduleHoldersNotConsumed(info, kind) {
6166
+ if (!isSchedulingEnabled(this.options, kind)) return;
6167
+ const scheduleWildcard = `${this.names.schedulePrefix(kind)}>`;
6168
+ const { filter_subject, filter_subjects } = info.config;
6169
+ const filters = filter_subjects ?? (filter_subject !== void 0 ? [filter_subject] : []);
6170
+ const swallowing = filters.length === 0 ? ["<no filter, consumes the whole stream>"] : filters.filter((f) => coversOrEquals(f, scheduleWildcard));
6171
+ if (swallowing.length > 0) {
6172
+ throw new Error(
6173
+ `Consumer "${this.names.consumerName(kind)}" (kind=${String(kind)}) filter ${swallowing.join(", ")} also matches the schedule namespace "${this.names.schedulePrefix(kind)}". Consuming schedule holders removes pending schedules from the stream. Use exact filter_subjects for the registered handler subjects instead.`
6174
+ );
6175
+ }
6176
+ }
6177
+ async warnOnOrphanedMigrationBackup(jsm, streamName2) {
6178
+ const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
6179
+ try {
6180
+ await jsm.streams.info(backupName);
6181
+ } catch {
6182
+ return;
6183
+ }
6184
+ this.logger.warn(
6185
+ `Found migration backup "${backupName}" for the externally managed stream "${streamName2}". A previous Auto-managed migration was interrupted and undelivered messages may still reside in the backup. Recover them by sourcing the backup back, or re-enable Auto management for one boot to let the library finish the recovery.`
6186
+ );
6187
+ }
6188
+ warnOnSchedulesDisabled(info, kind) {
6189
+ if (info.config.allow_msg_schedules === true) return;
6190
+ this.logger.warn(
6191
+ `Stream "${this.names.streamName(kind)}" (kind=${String(kind)}) does not report allow_msg_schedules=true, but scheduling is enabled in the application options. Scheduled publishes will be rejected by the server until the stream allows message schedules.`
6192
+ );
6193
+ }
6194
+ warnOnRetention(info, kind) {
6195
+ if (info.config.retention !== import_jetstream30.RetentionPolicy.Workqueue) {
6196
+ this.logger.warn(
6197
+ `Stream "${this.names.streamName(kind)}" (kind=${String(kind)}) retention is "${String(info.config.retention)}"; expected "workqueue" for reliable at-least-once delivery.`
6198
+ );
6199
+ }
6200
+ }
6201
+ warnOnUnlimitedDelivery(info, kind) {
6202
+ if (!this.options.dlq) return;
6203
+ const maxDeliver = info.config.max_deliver;
6204
+ if (maxDeliver === void 0 || maxDeliver <= 0) {
6205
+ this.logger.warn(
6206
+ `Consumer "${this.names.consumerName(kind)}" (kind=${String(kind)}) has unlimited max_deliver but options.dlq is enabled; messages will never be dead-lettered. Set max_deliver > 0 on the consumer.`
6207
+ );
6208
+ }
6209
+ }
6210
+ warnOnShortAckWait(info, kind) {
6211
+ const ackExtConfig = resolveAckExtension(this.options, kind);
6212
+ if (ackExtConfig === void 0 || ackExtConfig === false) return;
6213
+ const ackWaitNanos = info.config.ack_wait;
6214
+ const intervalMs = resolveAckExtensionInterval(ackExtConfig, ackWaitNanos);
6215
+ if (intervalMs === null) return;
6216
+ const ackWaitMs = ackWaitNanos !== void 0 ? ackWaitNanos / 1e6 : void 0;
6217
+ if (ackWaitMs !== void 0 && ackWaitMs < intervalMs) {
6218
+ this.logger.warn(
6219
+ `Consumer "${this.names.consumerName(kind)}" (kind=${String(kind)}) ack_wait (${ackWaitMs}ms) is shorter than the ackExtension interval (${intervalMs}ms). Messages may redeliver before the handler finishes. Increase ack_wait.`
6220
+ );
6221
+ }
6222
+ }
6223
+ resolveHandlerSubjects(kind) {
6224
+ const patterns = this.registry.getPatternsByKind();
6225
+ switch (kind) {
6226
+ case "ev" /* Event */:
6227
+ return patterns.events.map((p) => this.names.subject("ev" /* Event */, p));
6228
+ case "cmd" /* Command */:
6229
+ return patterns.commands.map((p) => this.names.subject("cmd" /* Command */, p));
6230
+ case "broadcast" /* Broadcast */:
6231
+ return this.registry.getBroadcastPatterns();
6232
+ case "ordered" /* Ordered */:
6233
+ return this.registry.getOrderedSubjects();
6234
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
6235
+ default: {
6236
+ const _exhaustive = kind;
6237
+ throw new Error(`Unhandled StreamKind: ${String(_exhaustive)}`);
6238
+ }
6239
+ }
6240
+ }
6241
+ };
6242
+
5808
6243
  // src/shutdown/shutdown.manager.ts
5809
- var import_common20 = require("@nestjs/common");
6244
+ var import_common23 = require("@nestjs/common");
5810
6245
  var ShutdownManager = class {
5811
6246
  constructor(connection, eventBus, timeout) {
5812
6247
  this.connection = connection;
5813
6248
  this.eventBus = eventBus;
5814
6249
  this.timeout = timeout;
5815
6250
  }
5816
- logger = new import_common20.Logger("Jetstream:Shutdown");
6251
+ logger = new import_common23.Logger("Jetstream:Shutdown");
5817
6252
  shutdownPromise;
5818
6253
  /**
5819
6254
  * Execute the full shutdown sequence.
5820
6255
  *
5821
- * Idempotent concurrent or repeated calls return the same promise.
6256
+ * Idempotent: concurrent or repeated calls return the same promise.
5822
6257
  *
5823
6258
  * @param strategy Optional stoppable to close (stops consumers and subscriptions).
5824
6259
  */
@@ -5848,6 +6283,12 @@ var ShutdownManager = class {
5848
6283
 
5849
6284
  // src/jetstream.module.ts
5850
6285
  var JETSTREAM_ACK_WAIT_MAP = /* @__PURE__ */ Symbol("JETSTREAM_ACK_WAIT_MAP");
6286
+ var DESTRUCTIVE_MIGRATION_MANUAL_WARNING = "allowDestructiveMigration has no effect under provisioning.management: Manual; the library never migrates externally managed streams.";
6287
+ var warnIfManualWithDestructive = (options, logger5) => {
6288
+ if (options.allowDestructiveMigration && options.provisioning?.management === "manual" /* Manual */) {
6289
+ logger5.warn(DESTRUCTIVE_MIGRATION_MANUAL_WARNING);
6290
+ }
6291
+ };
5851
6292
  var JetstreamModule = class {
5852
6293
  constructor(shutdownManager, strategy) {
5853
6294
  this.shutdownManager = shutdownManager;
@@ -5877,7 +6318,8 @@ var JetstreamModule = class {
5877
6318
  PatternRegistry,
5878
6319
  ShutdownManager,
5879
6320
  JetstreamStrategy,
5880
- JetstreamHealthIndicator
6321
+ JetstreamHealthIndicator,
6322
+ NameResolver
5881
6323
  ]
5882
6324
  };
5883
6325
  }
@@ -5906,7 +6348,8 @@ var JetstreamModule = class {
5906
6348
  PatternRegistry,
5907
6349
  ShutdownManager,
5908
6350
  JetstreamStrategy,
5909
- JetstreamHealthIndicator
6351
+ JetstreamHealthIndicator,
6352
+ NameResolver
5910
6353
  ]
5911
6354
  };
5912
6355
  }
@@ -5923,10 +6366,23 @@ var JetstreamModule = class {
5923
6366
  const clientToken = getClientToken(options.name);
5924
6367
  const clientProvider = {
5925
6368
  provide: clientToken,
5926
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_CODEC, JETSTREAM_EVENT_BUS],
5927
- useFactory: (rootOptions, connection, rootCodec, eventBus) => {
6369
+ inject: [
6370
+ JETSTREAM_OPTIONS,
6371
+ JETSTREAM_CONNECTION,
6372
+ JETSTREAM_CODEC,
6373
+ JETSTREAM_EVENT_BUS,
6374
+ { token: NameResolver, optional: true }
6375
+ ],
6376
+ useFactory: (rootOptions, connection, rootCodec, eventBus, names) => {
5928
6377
  const codec = options.codec ?? rootCodec;
5929
- return new JetstreamClient(rootOptions, options.name, connection, codec, eventBus);
6378
+ return new JetstreamClient(
6379
+ rootOptions,
6380
+ options.name,
6381
+ connection,
6382
+ codec,
6383
+ eventBus,
6384
+ names ?? void 0
6385
+ );
5930
6386
  }
5931
6387
  };
5932
6388
  return {
@@ -5947,16 +6403,14 @@ var JetstreamModule = class {
5947
6403
  /** Create providers that depend on JETSTREAM_OPTIONS (shared by sync and async). */
5948
6404
  static createCoreDependentProviders() {
5949
6405
  return [
5950
- // EventBus — hook system with Logger fallback
5951
6406
  {
5952
6407
  provide: JETSTREAM_EVENT_BUS,
5953
6408
  inject: [JETSTREAM_OPTIONS],
5954
6409
  useFactory: (options) => {
5955
- const logger5 = new import_common21.Logger("Jetstream:Module");
6410
+ const logger5 = new import_common24.Logger("Jetstream:Module");
5956
6411
  return new EventBus(logger5, options.hooks);
5957
6412
  }
5958
6413
  },
5959
- // Codec — global encode/decode
5960
6414
  {
5961
6415
  provide: JETSTREAM_CODEC,
5962
6416
  inject: [JETSTREAM_OPTIONS],
@@ -5964,7 +6418,6 @@ var JetstreamModule = class {
5964
6418
  return options.codec ?? new JsonCodec();
5965
6419
  }
5966
6420
  },
5967
- // ConnectionProvider — single NATS connection
5968
6421
  {
5969
6422
  provide: JETSTREAM_CONNECTION,
5970
6423
  inject: [JETSTREAM_OPTIONS, JETSTREAM_EVENT_BUS],
@@ -5972,7 +6425,6 @@ var JetstreamModule = class {
5972
6425
  return new ConnectionProvider(options, eventBus);
5973
6426
  }
5974
6427
  },
5975
- // JetstreamHealthIndicator — health check for NATS connection
5976
6428
  {
5977
6429
  provide: JetstreamHealthIndicator,
5978
6430
  inject: [JETSTREAM_CONNECTION],
@@ -5980,7 +6432,6 @@ var JetstreamModule = class {
5980
6432
  return new JetstreamHealthIndicator(connection);
5981
6433
  }
5982
6434
  },
5983
- // ShutdownManager — graceful shutdown orchestration
5984
6435
  {
5985
6436
  provide: ShutdownManager,
5986
6437
  inject: [JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, JETSTREAM_OPTIONS],
@@ -5992,41 +6443,69 @@ var JetstreamModule = class {
5992
6443
  );
5993
6444
  }
5994
6445
  },
5995
- // Consumer infrastructure only created when consumer !== false.
5996
- // Providers return null when consumer is disabled (publisher-only mode).
5997
- // PatternRegistry — subject-to-handler mapping
6446
+ // Consumer infrastructure providers below return null when consumer === false
6447
+ // (publisher-only mode). NameResolver is the exception: clients need it too.
5998
6448
  {
5999
- provide: PatternRegistry,
6449
+ provide: NameResolver,
6000
6450
  inject: [JETSTREAM_OPTIONS],
6001
6451
  useFactory: (options) => {
6452
+ const logger5 = new import_common24.Logger("Jetstream:Module");
6453
+ warnIfManualWithDestructive(options, logger5);
6454
+ return new NameResolver(options);
6455
+ }
6456
+ },
6457
+ {
6458
+ provide: PatternRegistry,
6459
+ inject: [JETSTREAM_OPTIONS, NameResolver],
6460
+ useFactory: (options, names) => {
6461
+ if (options.consumer === false) return null;
6462
+ return new PatternRegistry(options, names);
6463
+ }
6464
+ },
6465
+ {
6466
+ provide: InfrastructureBinder,
6467
+ inject: [JETSTREAM_OPTIONS, NameResolver, PatternRegistry],
6468
+ useFactory: (options, names, registry) => {
6002
6469
  if (options.consumer === false) return null;
6003
- return new PatternRegistry(options);
6470
+ return new InfrastructureBinder(options, names, registry);
6004
6471
  }
6005
6472
  },
6006
- // StreamProvider — JetStream stream lifecycle
6007
6473
  {
6008
6474
  provide: StreamProvider,
6009
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
6010
- useFactory: (options, connection) => {
6475
+ inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, NameResolver, InfrastructureBinder],
6476
+ useFactory: (options, connection, names, binder) => {
6011
6477
  if (options.consumer === false) return null;
6012
- return new StreamProvider(options, connection);
6478
+ return new StreamProvider(options, connection, names, binder);
6013
6479
  }
6014
6480
  },
6015
- // ConsumerProvider JetStream consumer lifecycle (receives PatternRegistry for broadcast filtering)
6481
+ // ConsumerProvider needs PatternRegistry for broadcast filtering.
6016
6482
  {
6017
6483
  provide: ConsumerProvider,
6018
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, StreamProvider, PatternRegistry],
6019
- useFactory: (options, connection, streamProvider, patternRegistry) => {
6484
+ inject: [
6485
+ JETSTREAM_OPTIONS,
6486
+ JETSTREAM_CONNECTION,
6487
+ StreamProvider,
6488
+ PatternRegistry,
6489
+ NameResolver,
6490
+ InfrastructureBinder
6491
+ ],
6492
+ useFactory: (options, connection, streamProvider, patternRegistry, names, binder) => {
6020
6493
  if (options.consumer === false) return null;
6021
- return new ConsumerProvider(options, connection, streamProvider, patternRegistry);
6494
+ return new ConsumerProvider(
6495
+ options,
6496
+ connection,
6497
+ streamProvider,
6498
+ patternRegistry,
6499
+ names,
6500
+ binder
6501
+ );
6022
6502
  }
6023
6503
  },
6024
- // Shared ack_wait map populated by strategy after ensureConsumers()
6504
+ // Shared ack_wait map, populated by the strategy after ensureConsumers().
6025
6505
  {
6026
6506
  provide: JETSTREAM_ACK_WAIT_MAP,
6027
6507
  useFactory: () => /* @__PURE__ */ new Map()
6028
6508
  },
6029
- // MessageProvider — pull-based message consumption
6030
6509
  {
6031
6510
  provide: MessageProvider,
6032
6511
  inject: [
@@ -6065,7 +6544,6 @@ var JetstreamModule = class {
6065
6544
  return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
6066
6545
  }
6067
6546
  },
6068
- // EventRouter — routes event and broadcast messages to handlers
6069
6547
  {
6070
6548
  provide: EventRouter,
6071
6549
  inject: [
@@ -6075,9 +6553,10 @@ var JetstreamModule = class {
6075
6553
  JETSTREAM_CODEC,
6076
6554
  JETSTREAM_EVENT_BUS,
6077
6555
  JETSTREAM_ACK_WAIT_MAP,
6078
- JETSTREAM_CONNECTION
6556
+ JETSTREAM_CONNECTION,
6557
+ NameResolver
6079
6558
  ],
6080
- useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
6559
+ useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection, names) => {
6081
6560
  if (options.consumer === false) return null;
6082
6561
  const deadLetterConfig = options.onDeadLetter || options.dlq ? {
6083
6562
  maxDeliverByStream: /* @__PURE__ */ new Map(),
@@ -6102,11 +6581,11 @@ var JetstreamModule = class {
6102
6581
  processingConfig,
6103
6582
  ackWaitMap,
6104
6583
  connection,
6105
- options
6584
+ options,
6585
+ names
6106
6586
  );
6107
6587
  }
6108
6588
  },
6109
- // RpcRouter — routes RPC command messages in JetStream mode
6110
6589
  {
6111
6590
  provide: RpcRouter,
6112
6591
  inject: [
@@ -6137,7 +6616,6 @@ var JetstreamModule = class {
6137
6616
  );
6138
6617
  }
6139
6618
  },
6140
- // CoreRpcServer — RPC via NATS Core request/reply
6141
6619
  {
6142
6620
  provide: CoreRpcServer,
6143
6621
  inject: [
@@ -6145,14 +6623,14 @@ var JetstreamModule = class {
6145
6623
  JETSTREAM_CONNECTION,
6146
6624
  PatternRegistry,
6147
6625
  JETSTREAM_CODEC,
6148
- JETSTREAM_EVENT_BUS
6626
+ JETSTREAM_EVENT_BUS,
6627
+ NameResolver
6149
6628
  ],
6150
- useFactory: (options, connection, patternRegistry, codec, eventBus) => {
6629
+ useFactory: (options, connection, patternRegistry, codec, eventBus, names) => {
6151
6630
  if (options.consumer === false) return null;
6152
- return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
6631
+ return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus, names);
6153
6632
  }
6154
6633
  },
6155
- // MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
6156
6634
  {
6157
6635
  provide: MetadataProvider,
6158
6636
  inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
@@ -6161,7 +6639,6 @@ var JetstreamModule = class {
6161
6639
  return new MetadataProvider(options, connection);
6162
6640
  }
6163
6641
  },
6164
- // JetstreamStrategy — server-side transport (only when consumer enabled)
6165
6642
  {
6166
6643
  provide: JetstreamStrategy,
6167
6644
  inject: [
@@ -6246,12 +6723,12 @@ var JetstreamModule = class {
6246
6723
  }
6247
6724
  };
6248
6725
  JetstreamModule = __decorateClass([
6249
- (0, import_common21.Global)(),
6250
- (0, import_common21.Module)({}),
6251
- __decorateParam(0, (0, import_common21.Optional)()),
6252
- __decorateParam(0, (0, import_common21.Inject)(ShutdownManager)),
6253
- __decorateParam(1, (0, import_common21.Optional)()),
6254
- __decorateParam(1, (0, import_common21.Inject)(JetstreamStrategy))
6726
+ (0, import_common24.Global)(),
6727
+ (0, import_common24.Module)({}),
6728
+ __decorateParam(0, (0, import_common24.Optional)()),
6729
+ __decorateParam(0, (0, import_common24.Inject)(ShutdownManager)),
6730
+ __decorateParam(1, (0, import_common24.Optional)()),
6731
+ __decorateParam(1, (0, import_common24.Inject)(JetstreamStrategy))
6255
6732
  ], JetstreamModule);
6256
6733
  // Annotate the CommonJS export names for ESM import in node:
6257
6734
  0 && (module.exports = {
@@ -6287,6 +6764,7 @@ JetstreamModule = __decorateClass([
6287
6764
  JetstreamTrace,
6288
6765
  JsonCodec,
6289
6766
  MIN_METADATA_TTL,
6767
+ ManagementMode,
6290
6768
  MessageKind,
6291
6769
  MsgpackCodec,
6292
6770
  NatsErrorCode,