@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.js CHANGED
@@ -14,18 +14,17 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
14
14
  import {
15
15
  Global,
16
16
  Inject as Inject2,
17
- Logger as Logger20,
17
+ Logger as Logger23,
18
18
  Module as Module2,
19
19
  Optional as Optional2
20
20
  } from "@nestjs/common";
21
21
 
22
22
  // src/client/jetstream.client.ts
23
- import { Logger as Logger5 } from "@nestjs/common";
23
+ import { Logger as Logger6 } from "@nestjs/common";
24
24
  import { ClientProxy } from "@nestjs/microservices";
25
25
  import { context as context6 } from "@opentelemetry/api";
26
26
  import { nuid } from "@nats-io/nuid";
27
27
  import {
28
- createInbox,
29
28
  headers as natsHeaders,
30
29
  TimeoutError
31
30
  } from "@nats-io/transport-node";
@@ -53,6 +52,13 @@ var TransportEvent = /* @__PURE__ */ ((TransportEvent2) => {
53
52
  return TransportEvent2;
54
53
  })(TransportEvent || {});
55
54
 
55
+ // src/interfaces/options.interface.ts
56
+ var ManagementMode = /* @__PURE__ */ ((ManagementMode2) => {
57
+ ManagementMode2["Auto"] = "auto";
58
+ ManagementMode2["Manual"] = "manual";
59
+ return ManagementMode2;
60
+ })(ManagementMode || {});
61
+
56
62
  // src/interfaces/stream.interface.ts
57
63
  var StreamKind = /* @__PURE__ */ ((StreamKind2) => {
58
64
  StreamKind2["Event"] = "ev";
@@ -210,8 +216,27 @@ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
210
216
  ]);
211
217
  var NATS_CONTROL_HEADER_PREFIX = "nats-";
212
218
  var internalName = (name) => `${name}__microservice`;
213
- var buildSubject = (serviceName, kind, pattern) => `${internalName(serviceName)}.${kind}.${pattern}`;
214
- var buildBroadcastSubject = (pattern) => `broadcast.${pattern}`;
219
+ var subjectPrefix = (serviceName, kind) => `${internalName(serviceName)}.${kind}.`;
220
+ var buildSubject = (serviceName, kind, pattern) => `${subjectPrefix(serviceName, kind)}${pattern}`;
221
+ var BROADCAST_SUBJECT_PREFIX = "broadcast.";
222
+ var SCHEDULE_SEGMENT = "_sch.";
223
+ var buildBroadcastSubject = (pattern) => `${BROADCAST_SUBJECT_PREFIX}${pattern}`;
224
+ var conventionScheduleSubjectBase = (targetName, eventSubject) => {
225
+ if (eventSubject.startsWith(BROADCAST_SUBJECT_PREFIX)) {
226
+ const bare = eventSubject.slice(BROADCAST_SUBJECT_PREFIX.length);
227
+ return `${BROADCAST_SUBJECT_PREFIX}${SCHEDULE_SEGMENT}${bare}`;
228
+ }
229
+ const targetPrefix = `${internalName(targetName)}.`;
230
+ if (!eventSubject.startsWith(targetPrefix)) {
231
+ throw new Error(`Unexpected event subject format: ${eventSubject}`);
232
+ }
233
+ const withoutPrefix = eventSubject.slice(targetPrefix.length);
234
+ const dotIndex = withoutPrefix.indexOf(".");
235
+ if (dotIndex === -1) {
236
+ throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
237
+ }
238
+ return `${targetPrefix}${SCHEDULE_SEGMENT}${withoutPrefix.slice(dotIndex + 1)}`;
239
+ };
215
240
  var streamName = (serviceName, kind) => {
216
241
  if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
217
242
  return `${internalName(serviceName)}_${kind}-stream`;
@@ -231,6 +256,118 @@ var PatternPrefix = /* @__PURE__ */ ((PatternPrefix2) => {
231
256
  var isJetStreamRpcMode = (rpc) => rpc?.mode === "jetstream";
232
257
  var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
233
258
 
259
+ // src/server/infrastructure/management.ts
260
+ var kindOptionsBlock = (options, kind) => {
261
+ switch (kind) {
262
+ case "ev" /* Event */:
263
+ return options.events;
264
+ case "broadcast" /* Broadcast */:
265
+ return options.broadcast;
266
+ case "ordered" /* Ordered */:
267
+ return options.ordered;
268
+ case "cmd" /* Command */:
269
+ return isJetStreamRpcMode(options.rpc) ? options.rpc : void 0;
270
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
271
+ default: {
272
+ const _exhaustive = kind;
273
+ throw new Error(`Unhandled StreamKind: ${String(_exhaustive)}`);
274
+ }
275
+ }
276
+ };
277
+ var entityManagementFor = (options, kind) => {
278
+ if (kind === "dlq") return options.dlq?.management;
279
+ return kindOptionsBlock(options, kind)?.management;
280
+ };
281
+ var resolveManagementMode = (options, kind, entity) => entityManagementFor(options, kind)?.[entity] ?? options.provisioning?.management ?? "auto" /* Auto */;
282
+
283
+ // src/server/infrastructure/name-resolver.ts
284
+ var NameResolver = class {
285
+ constructor(options) {
286
+ this.options = options;
287
+ this.dlq = options.dlq?.stream?.name ?? dlqStreamName(options.name);
288
+ this.kinds = this.buildKindMap();
289
+ }
290
+ kinds;
291
+ dlq;
292
+ streamName(kind) {
293
+ return this.get(kind).stream;
294
+ }
295
+ consumerName(kind) {
296
+ return this.get(kind).consumer;
297
+ }
298
+ dlqStreamName() {
299
+ return this.dlq;
300
+ }
301
+ subject(kind, pattern) {
302
+ return `${this.get(kind).prefix}${pattern}`;
303
+ }
304
+ filterSubject(kind) {
305
+ return `${this.get(kind).prefix}>`;
306
+ }
307
+ schedulePrefix(kind) {
308
+ return this.get(kind).schedulePrefix;
309
+ }
310
+ hasCustomPrefix(kind) {
311
+ return this.get(kind).custom;
312
+ }
313
+ /**
314
+ * Map a resolved event subject back to its schedule-holder base subject
315
+ * (the `_sch` namespace twin, without the per-message unique suffix).
316
+ */
317
+ scheduleSubjectBase(eventSubject) {
318
+ for (const kind of ["broadcast" /* Broadcast */, "ev" /* Event */, "ordered" /* Ordered */]) {
319
+ const { prefix, schedulePrefix } = this.get(kind);
320
+ if (eventSubject.startsWith(prefix)) {
321
+ return `${schedulePrefix}${eventSubject.slice(prefix.length)}`;
322
+ }
323
+ }
324
+ throw new Error(`Unexpected event subject format: ${eventSubject}`);
325
+ }
326
+ get(kind) {
327
+ const entry = this.kinds.get(kind);
328
+ if (!entry) throw new Error(`Unknown StreamKind: ${String(kind)}`);
329
+ return entry;
330
+ }
331
+ buildKindMap() {
332
+ const map = /* @__PURE__ */ new Map();
333
+ const { name } = this.options;
334
+ for (const kind of Object.values(StreamKind)) {
335
+ const block = kindOptionsBlock(this.options, kind);
336
+ const customPrefix = block?.subjectPrefix;
337
+ const custom = customPrefix !== void 0;
338
+ const prefix = custom ? this.normalizePrefix(customPrefix) : this.conventionPrefix(name, kind);
339
+ map.set(kind, {
340
+ stream: block?.stream?.name ?? streamName(name, kind),
341
+ consumer: block?.consumer?.durable_name ?? consumerName(name, kind),
342
+ prefix,
343
+ schedulePrefix: custom ? `${prefix}${SCHEDULE_SEGMENT}` : this.conventionSchedulePrefix(name, kind),
344
+ custom
345
+ });
346
+ }
347
+ return map;
348
+ }
349
+ normalizePrefix(raw) {
350
+ let end = raw.length;
351
+ while (end > 0 && raw[end - 1] === ".") end -= 1;
352
+ const trimmed = raw.slice(0, end);
353
+ const tokens = trimmed.split(".");
354
+ if (trimmed.length === 0 || tokens.some((t) => t.length === 0 || t === ">")) {
355
+ throw new Error(
356
+ `Invalid subjectPrefix "${raw}": expected non-empty dot-separated tokens without ">".`
357
+ );
358
+ }
359
+ return `${trimmed}.`;
360
+ }
361
+ conventionPrefix(name, kind) {
362
+ if (kind === "broadcast" /* Broadcast */) return BROADCAST_SUBJECT_PREFIX;
363
+ return subjectPrefix(name, kind);
364
+ }
365
+ conventionSchedulePrefix(name, kind) {
366
+ if (kind === "broadcast" /* Broadcast */) return `${BROADCAST_SUBJECT_PREFIX}${SCHEDULE_SEGMENT}`;
367
+ return `${internalName(name)}.${SCHEDULE_SEGMENT}`;
368
+ }
369
+ };
370
+
234
371
  // src/otel/constants.ts
235
372
  var TRACER_NAME = "@horizon-republic/nestjs-jetstream";
236
373
 
@@ -533,7 +670,7 @@ var extractContext = (ctx, carrier, getter) => propagation.extract(ctx, carrier,
533
670
 
534
671
  // src/otel/tracer.ts
535
672
  import { trace } from "@opentelemetry/api";
536
- var PACKAGE_VERSION = true ? "2.12.1" : "0.0.0";
673
+ var PACKAGE_VERSION = true ? "2.13.0" : "0.0.0";
537
674
  var getTracer = () => trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
538
675
 
539
676
  // src/otel/carrier.ts
@@ -1337,17 +1474,137 @@ var nanosToGoDuration = (nanos) => {
1337
1474
  return `${nanos}ns`;
1338
1475
  };
1339
1476
 
1477
+ // src/client/rpc-reply-inbox.ts
1478
+ import { Logger as Logger5 } from "@nestjs/common";
1479
+ import {
1480
+ createInbox
1481
+ } from "@nats-io/transport-node";
1482
+ var RpcReplyInbox = class {
1483
+ constructor(codec, inboxPrefix) {
1484
+ this.codec = codec;
1485
+ this.inboxPrefix = inboxPrefix;
1486
+ }
1487
+ logger = new Logger5("Jetstream:RpcInbox");
1488
+ inbox = null;
1489
+ subscription = null;
1490
+ pending = /* @__PURE__ */ new Map();
1491
+ timeouts = /* @__PURE__ */ new Map();
1492
+ /** Reply-to subject for outgoing requests; null until {@link setup} ran. */
1493
+ get address() {
1494
+ return this.inbox;
1495
+ }
1496
+ /** True while the inbox subscription is live. */
1497
+ get active() {
1498
+ return this.subscription !== null;
1499
+ }
1500
+ /** Create the inbox subject and subscribe reply routing on it. */
1501
+ setup(nc) {
1502
+ this.inbox = createInbox(this.inboxPrefix);
1503
+ this.subscription = nc.subscribe(this.inbox, {
1504
+ callback: (err, msg) => {
1505
+ if (err) {
1506
+ this.logger.error("Inbox subscription error:", err);
1507
+ return;
1508
+ }
1509
+ this.route(msg);
1510
+ }
1511
+ });
1512
+ this.logger.debug(`Inbox subscription: ${this.inbox}`);
1513
+ }
1514
+ /** Register the callback that settles the round-trip for `correlationId`. */
1515
+ register(correlationId, callback) {
1516
+ this.pending.set(correlationId, callback);
1517
+ }
1518
+ /**
1519
+ * Arm the request deadline. When it fires while the request is still
1520
+ * pending, both registry entries are removed before `onExpired` runs.
1521
+ */
1522
+ armTimeout(correlationId, ms, onExpired) {
1523
+ const existing = this.timeouts.get(correlationId);
1524
+ if (existing !== void 0) clearTimeout(existing);
1525
+ const timeoutId = setTimeout(() => {
1526
+ if (!this.pending.has(correlationId)) return;
1527
+ this.timeouts.delete(correlationId);
1528
+ this.pending.delete(correlationId);
1529
+ onExpired();
1530
+ }, ms);
1531
+ this.timeouts.set(correlationId, timeoutId);
1532
+ }
1533
+ /** True while the request has not been settled or discarded. */
1534
+ has(correlationId) {
1535
+ return this.pending.has(correlationId);
1536
+ }
1537
+ /**
1538
+ * Drop a request without invoking its callback: clears the deadline and
1539
+ * returns whether the request was still pending.
1540
+ */
1541
+ discard(correlationId) {
1542
+ const timeoutId = this.timeouts.get(correlationId);
1543
+ if (timeoutId !== void 0) {
1544
+ clearTimeout(timeoutId);
1545
+ this.timeouts.delete(correlationId);
1546
+ }
1547
+ return this.pending.delete(correlationId);
1548
+ }
1549
+ /** Fail every pending request with `error` and tear the inbox down. */
1550
+ rejectAll(error) {
1551
+ for (const callback of this.pending.values()) {
1552
+ callback({ err: error, response: null, isDisposed: true });
1553
+ }
1554
+ for (const timeoutId of this.timeouts.values()) {
1555
+ clearTimeout(timeoutId);
1556
+ }
1557
+ this.pending.clear();
1558
+ this.timeouts.clear();
1559
+ this.subscription?.unsubscribe();
1560
+ this.subscription = null;
1561
+ this.inbox = null;
1562
+ }
1563
+ /** Route an inbox reply to the matching pending callback. */
1564
+ route(msg) {
1565
+ const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
1566
+ if (!correlationId) {
1567
+ this.logger.warn("Inbox reply without correlation-id, ignoring");
1568
+ return;
1569
+ }
1570
+ const callback = this.pending.get(correlationId);
1571
+ if (!callback) {
1572
+ this.logger.warn(`No pending handler for correlation-id: ${correlationId}`);
1573
+ return;
1574
+ }
1575
+ const timeoutId = this.timeouts.get(correlationId);
1576
+ if (timeoutId) {
1577
+ clearTimeout(timeoutId);
1578
+ this.timeouts.delete(correlationId);
1579
+ }
1580
+ try {
1581
+ const decoded = this.codec.decode(msg.data);
1582
+ if (msg.headers?.get("x-error" /* Error */)) {
1583
+ callback({ err: decoded, response: null, isDisposed: true });
1584
+ } else {
1585
+ callback({ err: null, response: decoded, isDisposed: true });
1586
+ }
1587
+ } catch (err) {
1588
+ callback({
1589
+ err: err instanceof Error ? err : new Error("Decode error"),
1590
+ response: null,
1591
+ isDisposed: true
1592
+ });
1593
+ } finally {
1594
+ this.pending.delete(correlationId);
1595
+ }
1596
+ }
1597
+ };
1598
+
1340
1599
  // src/client/jetstream.client.ts
1341
- var BROADCAST_SUBJECT_PREFIX = "broadcast.";
1342
1600
  var detectEventKind = (pattern) => {
1343
1601
  if (pattern.startsWith("broadcast:" /* Broadcast */)) return "broadcast" /* Broadcast */;
1344
1602
  if (pattern.startsWith("ordered:" /* Ordered */)) return "ordered" /* Ordered */;
1345
1603
  return "event" /* Event */;
1346
1604
  };
1347
- var declaredEventPattern = (pattern) => {
1348
- if (pattern.startsWith("broadcast:" /* Broadcast */))
1349
- return pattern.slice("broadcast:" /* Broadcast */.length);
1350
- if (pattern.startsWith("ordered:" /* Ordered */)) return pattern.slice("ordered:" /* Ordered */.length);
1605
+ var declaredEventPattern = (pattern, kind) => {
1606
+ if (kind === "broadcast" /* Broadcast */) return pattern.slice("broadcast:" /* Broadcast */.length);
1607
+ if (kind === "ordered" /* Ordered */) return pattern.slice("ordered:" /* Ordered */.length);
1351
1608
  return pattern;
1352
1609
  };
1353
1610
  var eventStreamKind = (kind) => {
@@ -1356,7 +1613,7 @@ var eventStreamKind = (kind) => {
1356
1613
  return "ev" /* Event */;
1357
1614
  };
1358
1615
  var JetstreamClient = class extends ClientProxy {
1359
- constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
1616
+ constructor(rootOptions, targetServiceName, connection, codec, eventBus, names) {
1360
1617
  super();
1361
1618
  this.rootOptions = rootOptions;
1362
1619
  this.connection = connection;
@@ -1364,29 +1621,34 @@ var JetstreamClient = class extends ClientProxy {
1364
1621
  this.eventBus = eventBus;
1365
1622
  this.targetName = targetServiceName;
1366
1623
  this.callerName = internalName(this.rootOptions.name);
1624
+ const isSelf = targetServiceName === rootOptions.name;
1625
+ const ownNames = names ?? (isSelf ? new NameResolver(rootOptions) : null);
1626
+ this.selfNames = isSelf ? ownNames : null;
1627
+ this.broadcastPrefix = ownNames ? ownNames.subject("broadcast" /* Broadcast */, "") : BROADCAST_SUBJECT_PREFIX;
1367
1628
  const targetInternal = internalName(targetServiceName);
1368
1629
  this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
1369
1630
  this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
1370
1631
  this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
1632
+ this.rpcInbox = new RpcReplyInbox(codec, this.callerName);
1371
1633
  this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
1372
- this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
1634
+ this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
1373
1635
  const derived = deriveOtelAttrs(this.rootOptions);
1374
1636
  this.otel = derived.otel;
1375
1637
  this.serverEndpoint = derived.serverEndpoint;
1376
1638
  }
1377
- logger = new Logger5("Jetstream:Client");
1639
+ logger = new Logger6("Jetstream:Client");
1378
1640
  /** Target service name this client sends messages to. */
1379
1641
  targetName;
1380
1642
  /** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
1381
1643
  callerName;
1382
- /**
1383
- * Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
1384
- * per stream kind this client may publish to. Built once in the constructor
1385
- * so producing a full subject is a single string concat with the user pattern.
1386
- */
1644
+ /** Convention subject prefixes for foreign targets; one per stream kind. */
1387
1645
  eventSubjectPrefix;
1388
1646
  commandSubjectPrefix;
1389
1647
  orderedSubjectPrefix;
1648
+ /** Broadcast subject prefix derived from the own resolver; never null. */
1649
+ broadcastPrefix;
1650
+ /** Resolver for self-target subjects; null when targeting a foreign service. */
1651
+ selfNames;
1390
1652
  /**
1391
1653
  * RPC configuration snapshots. The values are derived from rootOptions at
1392
1654
  * construction time so the publish hot path never has to re-run
@@ -1398,13 +1660,8 @@ var JetstreamClient = class extends ClientProxy {
1398
1660
  otel;
1399
1661
  /** Server endpoint parts used for `server.address` / `server.port` span attributes. */
1400
1662
  serverEndpoint;
1401
- /** Shared inbox for JetStream-mode RPC responses. */
1402
- inbox = null;
1403
- inboxSubscription = null;
1404
- /** Pending JetStream-mode RPC callbacks, keyed by correlation ID. */
1405
- pendingMessages = /* @__PURE__ */ new Map();
1406
- /** Pending JetStream-mode RPC timeouts, keyed by correlation ID. */
1407
- pendingTimeouts = /* @__PURE__ */ new Map();
1663
+ /** Reply inbox and pending-request registry for JetStream-mode RPC. */
1664
+ rpcInbox;
1408
1665
  /** Subscription to connection status events for disconnect handling. */
1409
1666
  statusSubscription = null;
1410
1667
  /**
@@ -1423,8 +1680,8 @@ var JetstreamClient = class extends ClientProxy {
1423
1680
  */
1424
1681
  async connect() {
1425
1682
  const nc = await this.connection.getConnection();
1426
- if (!this.isCoreMode && !this.inboxSubscription) {
1427
- this.setupInbox(nc);
1683
+ if (!this.isCoreMode && !this.rpcInbox.active) {
1684
+ this.rpcInbox.setup(nc);
1428
1685
  }
1429
1686
  this.statusSubscription ??= this.connection.status$.subscribe((status) => {
1430
1687
  if (status.type === "disconnect") {
@@ -1439,7 +1696,7 @@ var JetstreamClient = class extends ClientProxy {
1439
1696
  this.statusSubscription?.unsubscribe();
1440
1697
  this.statusSubscription = null;
1441
1698
  this.readyForPublish = false;
1442
- this.rejectPendingRpcs(new Error("Client closed"));
1699
+ this.rpcInbox.rejectAll(new Error("Client closed"));
1443
1700
  }
1444
1701
  /**
1445
1702
  * Direct access to the raw NATS connection.
@@ -1449,7 +1706,7 @@ var JetstreamClient = class extends ClientProxy {
1449
1706
  unwrap() {
1450
1707
  const nc = this.connection.unwrap;
1451
1708
  if (!nc) {
1452
- throw new Error("Not connected \u2014 call connect() before unwrap()");
1709
+ throw new Error("Not connected; call connect() before unwrap()");
1453
1710
  }
1454
1711
  return nc;
1455
1712
  }
@@ -1476,7 +1733,7 @@ var JetstreamClient = class extends ClientProxy {
1476
1733
  const encoded = this.codec.encode(data);
1477
1734
  const effectiveMsgId = messageId ?? nuid.next();
1478
1735
  const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
1479
- const declaredPattern = declaredEventPattern(packet.pattern);
1736
+ const declaredPattern = declaredEventPattern(packet.pattern, publishKind);
1480
1737
  const streamKind = eventStreamKind(publishKind);
1481
1738
  const startedAt = performance.now();
1482
1739
  try {
@@ -1496,13 +1753,6 @@ var JetstreamClient = class extends ClientProxy {
1496
1753
  },
1497
1754
  this.otel,
1498
1755
  async () => {
1499
- const warnIfDuplicate = (kindLabel, ack2) => {
1500
- if (ack2.duplicate) {
1501
- this.logger.warn(
1502
- `Duplicate ${kindLabel} publish detected: ${publishSubject} (seq: ${ack2.seq})`
1503
- );
1504
- }
1505
- };
1506
1756
  if (schedule) {
1507
1757
  const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1508
1758
  headers: msgHeaders,
@@ -1513,7 +1763,7 @@ var JetstreamClient = class extends ClientProxy {
1513
1763
  ttl
1514
1764
  }
1515
1765
  });
1516
- warnIfDuplicate("scheduled", ack2);
1766
+ this.warnIfDuplicate("scheduled", publishSubject, ack2);
1517
1767
  return;
1518
1768
  }
1519
1769
  const ack = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
@@ -1521,7 +1771,7 @@ var JetstreamClient = class extends ClientProxy {
1521
1771
  msgID: effectiveMsgId,
1522
1772
  ttl
1523
1773
  });
1524
- warnIfDuplicate("event", ack);
1774
+ this.warnIfDuplicate("event", publishSubject, ack);
1525
1775
  }
1526
1776
  );
1527
1777
  this.reportPublished(declaredPattern, streamKind, startedAt, "success");
@@ -1538,7 +1788,7 @@ var JetstreamClient = class extends ClientProxy {
1538
1788
  * JetStream mode: publishes to stream + waits for inbox response.
1539
1789
  */
1540
1790
  publish(packet, callback) {
1541
- const subject = this.commandSubjectPrefix + packet.pattern;
1791
+ const subject = this.selfNames ? this.selfNames.subject("cmd" /* Command */, packet.pattern) : this.commandSubjectPrefix + packet.pattern;
1542
1792
  const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
1543
1793
  if (schedule) {
1544
1794
  this.logger.warn(
@@ -1571,12 +1821,7 @@ var JetstreamClient = class extends ClientProxy {
1571
1821
  }
1572
1822
  return () => {
1573
1823
  if (jetStreamCorrelationId) {
1574
- const timeoutId = this.pendingTimeouts.get(jetStreamCorrelationId);
1575
- if (timeoutId) {
1576
- clearTimeout(timeoutId);
1577
- this.pendingTimeouts.delete(jetStreamCorrelationId);
1578
- }
1579
- this.pendingMessages.delete(jetStreamCorrelationId);
1824
+ this.rpcInbox.discard(jetStreamCorrelationId);
1580
1825
  }
1581
1826
  };
1582
1827
  }
@@ -1641,7 +1886,7 @@ var JetstreamClient = class extends ClientProxy {
1641
1886
  const hdrs = this.buildHeaders(customHeaders, {
1642
1887
  subject,
1643
1888
  correlationId,
1644
- replyTo: this.inbox ?? ""
1889
+ replyTo: this.rpcInbox.address ?? ""
1645
1890
  });
1646
1891
  const encoded = this.codec.encode(data);
1647
1892
  const spanHandle = beginRpcClientSpan(
@@ -1658,7 +1903,7 @@ var JetstreamClient = class extends ClientProxy {
1658
1903
  this.otel
1659
1904
  );
1660
1905
  const startedAt = performance.now();
1661
- this.pendingMessages.set(correlationId, (packet) => {
1906
+ const settleRoundTrip = (packet) => {
1662
1907
  if (packet.err) {
1663
1908
  if (packet.err instanceof Error) {
1664
1909
  spanHandle.finish({ kind: "error" /* Error */, error: packet.err });
@@ -1671,30 +1916,25 @@ var JetstreamClient = class extends ClientProxy {
1671
1916
  this.reportRpcCompleted(declaredPattern, startedAt, "success");
1672
1917
  }
1673
1918
  callback(packet);
1674
- });
1675
- const timeoutId = setTimeout(() => {
1676
- if (!this.pendingMessages.has(correlationId)) return;
1677
- this.pendingTimeouts.delete(correlationId);
1678
- this.pendingMessages.delete(correlationId);
1919
+ };
1920
+ this.rpcInbox.register(correlationId, settleRoundTrip);
1921
+ this.rpcInbox.armTimeout(correlationId, effectiveTimeout, () => {
1679
1922
  spanHandle.finish({ kind: "timeout" /* Timeout */ });
1680
1923
  this.eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
1681
1924
  this.reportRpcCompleted(declaredPattern, startedAt, "timeout");
1682
1925
  callback({ err: new Error(RPC_TIMEOUT_MESSAGE), response: null, isDisposed: true });
1683
- }, effectiveTimeout);
1684
- this.pendingTimeouts.set(correlationId, timeoutId);
1926
+ });
1685
1927
  try {
1686
1928
  if (!this.readyForPublish) await this.connect();
1687
- if (!this.pendingMessages.has(correlationId)) return;
1688
- if (!this.inbox) {
1689
- clearTimeout(timeoutId);
1690
- this.pendingTimeouts.delete(correlationId);
1691
- this.pendingMessages.delete(correlationId);
1929
+ if (!this.rpcInbox.has(correlationId)) return;
1930
+ if (!this.rpcInbox.address) {
1931
+ this.rpcInbox.discard(correlationId);
1692
1932
  const inboxError = new Error("Inbox not initialized");
1693
1933
  spanHandle.finish({ kind: "error" /* Error */, error: inboxError });
1694
1934
  this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "error");
1695
1935
  this.reportRpcCompleted(declaredPattern, startedAt, "error");
1696
1936
  callback({
1697
- err: new Error("Inbox not initialized \u2014 JetStream RPC mode requires a connected inbox"),
1937
+ err: new Error("Inbox not initialized; JetStream RPC mode requires a connected inbox"),
1698
1938
  response: null,
1699
1939
  isDisposed: true
1700
1940
  });
@@ -1714,13 +1954,7 @@ var JetstreamClient = class extends ClientProxy {
1714
1954
  }
1715
1955
  this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1716
1956
  } catch (err) {
1717
- const existingTimeout = this.pendingTimeouts.get(correlationId);
1718
- if (existingTimeout) {
1719
- clearTimeout(existingTimeout);
1720
- this.pendingTimeouts.delete(correlationId);
1721
- }
1722
- if (!this.pendingMessages.has(correlationId)) return;
1723
- this.pendingMessages.delete(correlationId);
1957
+ if (!this.rpcInbox.discard(correlationId)) return;
1724
1958
  const error = err instanceof Error ? err : new Error("Unknown error");
1725
1959
  spanHandle.finish({ kind: "error" /* Error */, error });
1726
1960
  this.eventBus.emit("error" /* Error */, error, `jetstream-rpc-publish:${subject}`);
@@ -1729,6 +1963,11 @@ var JetstreamClient = class extends ClientProxy {
1729
1963
  callback({ err: error, response: null, isDisposed: true });
1730
1964
  }
1731
1965
  }
1966
+ warnIfDuplicate(kindLabel, subject, ack) {
1967
+ if (ack.duplicate) {
1968
+ this.logger.warn(`Duplicate ${kindLabel} publish detected: ${subject} (seq: ${ack.seq})`);
1969
+ }
1970
+ }
1732
1971
  // hasHook is per-emit so late subscribers (JetstreamMetricsService during
1733
1972
  // OnApplicationBootstrap) still receive events.
1734
1973
  reportPublished(declaredPattern, kind, startedAt, status) {
@@ -1752,87 +1991,23 @@ var JetstreamClient = class extends ClientProxy {
1752
1991
  }
1753
1992
  /** Fail-fast all pending JetStream RPC callbacks on connection loss. */
1754
1993
  handleDisconnect() {
1755
- this.rejectPendingRpcs(new Error("Connection lost"));
1756
- this.inbox = null;
1994
+ this.rpcInbox.rejectAll(new Error("Connection lost"));
1757
1995
  this.readyForPublish = false;
1758
1996
  }
1759
- /** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
1760
- rejectPendingRpcs(error) {
1761
- for (const callback of this.pendingMessages.values()) {
1762
- callback({ err: error, response: null, isDisposed: true });
1763
- }
1764
- for (const timeoutId of this.pendingTimeouts.values()) {
1765
- clearTimeout(timeoutId);
1766
- }
1767
- this.pendingMessages.clear();
1768
- this.pendingTimeouts.clear();
1769
- this.inboxSubscription?.unsubscribe();
1770
- this.inboxSubscription = null;
1771
- this.inbox = null;
1772
- }
1773
- /** Setup shared inbox subscription for JetStream RPC responses. */
1774
- setupInbox(nc) {
1775
- this.inbox = createInbox(internalName(this.rootOptions.name));
1776
- this.inboxSubscription = nc.subscribe(this.inbox, {
1777
- callback: (err, msg) => {
1778
- if (err) {
1779
- this.logger.error("Inbox subscription error:", err);
1780
- return;
1781
- }
1782
- this.routeInboxReply(msg);
1783
- }
1784
- });
1785
- this.logger.debug(`Inbox subscription: ${this.inbox}`);
1786
- }
1787
- /** Route an inbox reply to the matching pending callback. */
1788
- routeInboxReply(msg) {
1789
- const correlationId = msg.headers?.get("x-correlation-id" /* CorrelationId */);
1790
- if (!correlationId) {
1791
- this.logger.warn("Inbox reply without correlation-id, ignoring");
1792
- return;
1793
- }
1794
- const callback = this.pendingMessages.get(correlationId);
1795
- if (!callback) {
1796
- this.logger.warn(`No pending handler for correlation-id: ${correlationId}`);
1797
- return;
1798
- }
1799
- const timeoutId = this.pendingTimeouts.get(correlationId);
1800
- if (timeoutId) {
1801
- clearTimeout(timeoutId);
1802
- this.pendingTimeouts.delete(correlationId);
1803
- }
1804
- try {
1805
- const decoded = this.codec.decode(msg.data);
1806
- if (msg.headers?.get("x-error" /* Error */)) {
1807
- callback({ err: decoded, response: null, isDisposed: true });
1808
- } else {
1809
- callback({ err: null, response: decoded, isDisposed: true });
1810
- }
1811
- } catch (err) {
1812
- callback({
1813
- err: err instanceof Error ? err : new Error("Decode error"),
1814
- response: null,
1815
- isDisposed: true
1816
- });
1817
- } finally {
1818
- this.pendingMessages.delete(correlationId);
1819
- }
1820
- }
1821
1997
  /**
1822
- * Resolve a user pattern to a fully-qualified NATS subject, dispatching
1823
- * between the event, broadcast, and ordered prefixes.
1824
- *
1825
- * The leading-char check short-circuits the `startsWith` comparisons for
1826
- * patterns that cannot possibly carry a broadcast/ordered marker, which is
1827
- * the overwhelmingly common case.
1998
+ * Resolve a user pattern to a fully-qualified NATS subject. Self-targets go
1999
+ * through the resolver so custom subjectPrefix options are honoured.
1828
2000
  */
1829
2001
  buildEventSubject(pattern) {
1830
- if (pattern.charCodeAt(0) === 98 && pattern.startsWith("broadcast:" /* Broadcast */)) {
1831
- return BROADCAST_SUBJECT_PREFIX + pattern.slice("broadcast:" /* Broadcast */.length);
2002
+ if (pattern.startsWith("broadcast:" /* Broadcast */)) {
2003
+ return this.broadcastPrefix + pattern.slice("broadcast:" /* Broadcast */.length);
1832
2004
  }
1833
- if (pattern.charCodeAt(0) === 111 && pattern.startsWith("ordered:" /* Ordered */)) {
1834
- return this.orderedSubjectPrefix + pattern.slice("ordered:" /* Ordered */.length);
2005
+ if (pattern.startsWith("ordered:" /* Ordered */)) {
2006
+ const bare = pattern.slice("ordered:" /* Ordered */.length);
2007
+ if (this.selfNames) return this.selfNames.subject("ordered" /* Ordered */, bare);
2008
+ return this.orderedSubjectPrefix + bare;
1835
2009
  }
2010
+ if (this.selfNames) return this.selfNames.subject("ev" /* Event */, pattern);
1836
2011
  return this.eventSubjectPrefix + pattern;
1837
2012
  }
1838
2013
  /** Build NATS headers merging custom headers with transport headers. */
@@ -1877,33 +2052,16 @@ var JetstreamClient = class extends ClientProxy {
1877
2052
  /**
1878
2053
  * Build a schedule-holder subject for NATS message scheduling.
1879
2054
  *
1880
- * The schedule-holder subject resides in the same stream as the target but
1881
- * uses a separate `_sch` namespace that is NOT matched by any consumer filter.
1882
- * NATS holds the message and publishes it to the target subject after the delay.
1883
- *
1884
- * A unique per-message suffix is appended because the server stores schedules
1885
- * as rollup messages one active schedule per subject (ADR-51). Without it,
1886
- * concurrent schedules of the same pattern would silently replace each other.
1887
- *
1888
- * Examples:
1889
- * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder.<nuid>`
1890
- * - `broadcast.config.updated` → `broadcast._sch.config.updated.<nuid>`
2055
+ * The holder lives in the same stream as the target but under the `_sch`
2056
+ * namespace no consumer filter matches; the server publishes it to the
2057
+ * target subject when the schedule fires. The unique per-message suffix
2058
+ * exists because the server stores schedules as rollup messages, one
2059
+ * active schedule per subject (ADR-51): without it, concurrent schedules
2060
+ * of the same pattern would silently replace each other.
1891
2061
  */
1892
2062
  buildScheduleSubject(eventSubject) {
1893
- if (eventSubject.startsWith("broadcast.")) {
1894
- return `${eventSubject.replace("broadcast.", "broadcast._sch.")}.${nuid.next()}`;
1895
- }
1896
- const targetPrefix = `${internalName(this.targetName)}.`;
1897
- if (!eventSubject.startsWith(targetPrefix)) {
1898
- throw new Error(`Unexpected event subject format: ${eventSubject}`);
1899
- }
1900
- const withoutPrefix = eventSubject.slice(targetPrefix.length);
1901
- const dotIndex = withoutPrefix.indexOf(".");
1902
- if (dotIndex === -1) {
1903
- throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
1904
- }
1905
- const pattern = withoutPrefix.slice(dotIndex + 1);
1906
- return `${targetPrefix}_sch.${pattern}.${nuid.next()}`;
2063
+ const base = this.selfNames ? this.selfNames.scheduleSubjectBase(eventSubject) : conventionScheduleSubjectBase(this.targetName, eventSubject);
2064
+ return `${base}.${nuid.next()}`;
1907
2065
  }
1908
2066
  };
1909
2067
 
@@ -1935,7 +2093,7 @@ var MsgpackCodec = class {
1935
2093
  };
1936
2094
 
1937
2095
  // src/connection/connection.provider.ts
1938
- import { Logger as Logger6 } from "@nestjs/common";
2096
+ import { Logger as Logger7 } from "@nestjs/common";
1939
2097
  import {
1940
2098
  connect
1941
2099
  } from "@nats-io/transport-node";
@@ -1968,7 +2126,7 @@ var ConnectionProvider = class {
1968
2126
  nc$;
1969
2127
  /** Live stream of connection status events (no replay). */
1970
2128
  status$;
1971
- logger = new Logger6("Jetstream:Connection");
2129
+ logger = new Logger7("Jetstream:Connection");
1972
2130
  connection = null;
1973
2131
  connectionPromise = null;
1974
2132
  jsClient = null;
@@ -1979,7 +2137,7 @@ var ConnectionProvider = class {
1979
2137
  otelEndpoint;
1980
2138
  lifecycleSpan = null;
1981
2139
  /**
1982
- * Establish NATS connection. Idempotent returns cached connection on subsequent calls.
2140
+ * Establish NATS connection. Idempotent: returns cached connection on subsequent calls.
1983
2141
  *
1984
2142
  * @throws Error if connection is refused (fail fast).
1985
2143
  */
@@ -2018,7 +2176,7 @@ var ConnectionProvider = class {
2018
2176
  */
2019
2177
  getJetStreamClient() {
2020
2178
  if (!this.connection || this.connection.isClosed()) {
2021
- throw new Error("Not connected \u2014 call getConnection() before getJetStreamClient()");
2179
+ throw new Error("Not connected; call getConnection() before getJetStreamClient()");
2022
2180
  }
2023
2181
  this.jsClient ??= jetstream(this.connection);
2024
2182
  return this.jsClient;
@@ -2030,7 +2188,7 @@ var ConnectionProvider = class {
2030
2188
  /**
2031
2189
  * Gracefully shut down the connection.
2032
2190
  *
2033
- * Sequence: drain wait for close. Falls back to force-close on error.
2191
+ * Sequence: drain -> wait for close. Falls back to force-close on error.
2034
2192
  */
2035
2193
  async shutdown() {
2036
2194
  if (this.connectionPromise) {
@@ -2233,12 +2391,12 @@ var EventBus = class {
2233
2391
  };
2234
2392
 
2235
2393
  // src/health/jetstream.health-indicator.ts
2236
- import { Injectable, Logger as Logger7 } from "@nestjs/common";
2394
+ import { Injectable, Logger as Logger8 } from "@nestjs/common";
2237
2395
  var JetstreamHealthIndicator = class {
2238
2396
  constructor(connection) {
2239
2397
  this.connection = connection;
2240
2398
  }
2241
- logger = new Logger7("Jetstream:Health");
2399
+ logger = new Logger8("Jetstream:Health");
2242
2400
  /**
2243
2401
  * Plain health status check.
2244
2402
  *
@@ -2268,7 +2426,7 @@ var JetstreamHealthIndicator = class {
2268
2426
  * Returns `{ [key]: { status: 'up', ... } }` on success.
2269
2427
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
2270
2428
  *
2271
- * The thrown error sets `isHealthCheckError: true` and `causes` the
2429
+ * The thrown error sets `isHealthCheckError: true` and `causes`, the
2272
2430
  * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
2273
2431
  * health failures from unexpected exceptions. Works with both Terminus v10
2274
2432
  * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
@@ -2302,7 +2460,7 @@ JetstreamHealthIndicator = __decorateClass([
2302
2460
  import { Module } from "@nestjs/common";
2303
2461
 
2304
2462
  // src/server/routing/pattern-registry.ts
2305
- import { Logger as Logger8 } from "@nestjs/common";
2463
+ import { Logger as Logger9 } from "@nestjs/common";
2306
2464
  var HANDLER_LABELS = {
2307
2465
  ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
2308
2466
  ["ordered" /* Ordered */]: "ordered" /* Ordered */,
@@ -2310,12 +2468,13 @@ var HANDLER_LABELS = {
2310
2468
  ["cmd" /* Command */]: "rpc" /* Rpc */
2311
2469
  };
2312
2470
  var PatternRegistry = class {
2313
- constructor(options) {
2471
+ constructor(options, names) {
2314
2472
  this.options = options;
2473
+ this.names = names;
2315
2474
  }
2316
- logger = new Logger8("Jetstream:PatternRegistry");
2475
+ logger = new Logger9("Jetstream:PatternRegistry");
2317
2476
  registry = /* @__PURE__ */ new Map();
2318
- // Cached after registerHandlers() the registry is immutable from that point
2477
+ // Cached after registerHandlers(); the registry is immutable from that point
2319
2478
  cachedPatterns = null;
2320
2479
  _hasEvents = false;
2321
2480
  _hasCommands = false;
@@ -2328,7 +2487,6 @@ var PatternRegistry = class {
2328
2487
  * @param handlers Map of pattern -> MessageHandler from `Server.getHandlers()`.
2329
2488
  */
2330
2489
  registerHandlers(handlers) {
2331
- const serviceName = this.options.name;
2332
2490
  for (const [pattern, handler] of handlers) {
2333
2491
  const extras = handler.extras;
2334
2492
  const isEvent = handler.isEventHandler ?? false;
@@ -2345,7 +2503,7 @@ var PatternRegistry = class {
2345
2503
  else if (isOrdered) kind = "ordered" /* Ordered */;
2346
2504
  else if (isEvent) kind = "ev" /* Event */;
2347
2505
  else kind = "cmd" /* Command */;
2348
- const fullSubject = kind === "broadcast" /* Broadcast */ ? buildBroadcastSubject(pattern) : buildSubject(serviceName, kind, pattern);
2506
+ const fullSubject = this.names.subject(kind, pattern);
2349
2507
  this.registry.set(fullSubject, {
2350
2508
  handler,
2351
2509
  pattern,
@@ -2372,7 +2530,7 @@ var PatternRegistry = class {
2372
2530
  * Resolve the declared pattern and {@link StreamKind} for a full NATS subject.
2373
2531
  *
2374
2532
  * Returns `null` when the subject is not registered. The declared pattern is
2375
- * the value the user passed to `@EventPattern`/`@MessagePattern` stable and
2533
+ * the value the user passed to `@EventPattern`/`@MessagePattern`: stable and
2376
2534
  * bounded, suitable for use as a Prometheus label without cardinality risk.
2377
2535
  */
2378
2536
  resolveDeclared(subject) {
@@ -2382,7 +2540,17 @@ var PatternRegistry = class {
2382
2540
  }
2383
2541
  /** Get all registered broadcast patterns (for consumer filter_subject setup). */
2384
2542
  getBroadcastPatterns() {
2385
- return this.getPatternsByKind().broadcasts.map((p) => buildBroadcastSubject(p));
2543
+ return this.getPatternsByKind().broadcasts.map(
2544
+ (p) => this.names.subject("broadcast" /* Broadcast */, p)
2545
+ );
2546
+ }
2547
+ /** Get registered event patterns as raw user-declared patterns. */
2548
+ getEventPatterns() {
2549
+ return this.getPatternsByKind().events;
2550
+ }
2551
+ /** Get registered command patterns as raw user-declared patterns. */
2552
+ getCommandPatterns() {
2553
+ return this.getPatternsByKind().commands;
2386
2554
  }
2387
2555
  hasBroadcastHandlers() {
2388
2556
  return this._hasBroadcasts;
@@ -2398,9 +2566,7 @@ var PatternRegistry = class {
2398
2566
  }
2399
2567
  /** Get fully-qualified NATS subjects for ordered handlers. */
2400
2568
  getOrderedSubjects() {
2401
- return this.getPatternsByKind().ordered.map(
2402
- (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2403
- );
2569
+ return this.getPatternsByKind().ordered.map((p) => this.names.subject("ordered" /* Ordered */, p));
2404
2570
  }
2405
2571
  /** Check if any registered handler has metadata. */
2406
2572
  hasMetadata() {
@@ -2432,22 +2598,6 @@ var PatternRegistry = class {
2432
2598
  ordered: [...patterns.ordered]
2433
2599
  };
2434
2600
  }
2435
- /** Normalize a full NATS subject back to the user-facing pattern. */
2436
- normalizeSubject(subject) {
2437
- const name = internalName(this.options.name);
2438
- const prefixes = [
2439
- `${name}.${"cmd" /* Command */}.`,
2440
- `${name}.${"ev" /* Event */}.`,
2441
- `${name}.${"ordered" /* Ordered */}.`,
2442
- `${"broadcast" /* Broadcast */}.`
2443
- ];
2444
- for (const prefix of prefixes) {
2445
- if (subject.startsWith(prefix)) {
2446
- return subject.slice(prefix.length);
2447
- }
2448
- }
2449
- return subject;
2450
- }
2451
2601
  buildPatternsByKind() {
2452
2602
  const events = [];
2453
2603
  const commands = [];
@@ -2502,7 +2652,7 @@ var ERROR_CONTEXT_PREFIXES = [
2502
2652
  ["consume", "consume"],
2503
2653
  ["core-rpc-handler", "handler"],
2504
2654
  ["rpc-handler", "handler"],
2505
- // EventRouter formats contexts as `${StreamKind.*}-handler:...` the enum
2655
+ // EventRouter formats contexts as `${StreamKind.*}-handler:...`; the enum
2506
2656
  // uses short forms (`ev`, `ordered`, `broadcast`) so both surface in the wild.
2507
2657
  ["ev-handler", "handler"],
2508
2658
  ["event-handler", "handler"],
@@ -2523,7 +2673,7 @@ var STREAM_KIND_LABEL = {
2523
2673
  import {
2524
2674
  Inject,
2525
2675
  Injectable as Injectable2,
2526
- Logger as Logger10,
2676
+ Logger as Logger11,
2527
2677
  Optional
2528
2678
  } from "@nestjs/common";
2529
2679
 
@@ -2648,12 +2798,12 @@ var createMetrics = (opts) => {
2648
2798
  };
2649
2799
 
2650
2800
  // src/metrics/poll-runner.ts
2651
- import { Logger as Logger9 } from "@nestjs/common";
2801
+ import { Logger as Logger10 } from "@nestjs/common";
2652
2802
  var PollRunner = class {
2653
2803
  constructor(opts) {
2654
2804
  this.opts = opts;
2655
2805
  }
2656
- logger = new Logger9("Jetstream:Metrics:Poll");
2806
+ logger = new Logger10("Jetstream:Metrics:Poll");
2657
2807
  timer = null;
2658
2808
  inFlight = null;
2659
2809
  start() {
@@ -2662,7 +2812,7 @@ var PollRunner = class {
2662
2812
  if (this.opts.targets.length === 0) return;
2663
2813
  this.timer = setInterval(() => {
2664
2814
  if (this.inFlight !== null) {
2665
- this.logger.warn("Skipping poll tick \u2014 previous cycle still in flight");
2815
+ this.logger.warn("Skipping poll tick; previous cycle still in flight");
2666
2816
  return;
2667
2817
  }
2668
2818
  this.inFlight = this.tick().finally(() => {
@@ -2725,15 +2875,16 @@ var PollRunner = class {
2725
2875
 
2726
2876
  // src/metrics/metrics.service.ts
2727
2877
  var JetstreamMetricsService = class {
2728
- constructor(eventBus, config, promClient, options, patternRegistry, connection = null) {
2878
+ constructor(eventBus, config, promClient, options, patternRegistry, connection = null, names = null) {
2729
2879
  this.eventBus = eventBus;
2730
2880
  this.config = config;
2731
2881
  this.promClient = promClient;
2732
2882
  this.options = options;
2733
2883
  this.patternRegistry = patternRegistry;
2734
2884
  this.connection = connection;
2885
+ this.names = names;
2735
2886
  }
2736
- logger = new Logger10("Jetstream:Metrics");
2887
+ logger = new Logger11("Jetstream:Metrics");
2737
2888
  metrics = null;
2738
2889
  pollRunner = null;
2739
2890
  activeServers = /* @__PURE__ */ new Set();
@@ -2742,7 +2893,7 @@ var JetstreamMetricsService = class {
2742
2893
  if (!this.options.metrics || !this.config || !this.promClient) return;
2743
2894
  if (!this.config.register) {
2744
2895
  throw new Error(
2745
- "JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
2896
+ "JetstreamMetricsService requires a prom-client Registry; none was resolved by JetstreamMetricsModule."
2746
2897
  );
2747
2898
  }
2748
2899
  this.metrics = createMetrics({
@@ -2769,7 +2920,7 @@ var JetstreamMetricsService = class {
2769
2920
  }
2770
2921
  /**
2771
2922
  * NATS connects during early bootstrap, before this service subscribes to
2772
- * the EventBus the initial `Connect` emission misses us. Mirror the
2923
+ * the EventBus, so the initial `Connect` emission misses us. Mirror the
2773
2924
  * current state here so `connection_up` reflects reality the moment metrics
2774
2925
  * come online; later disconnects/reconnects update it normally.
2775
2926
  */
@@ -2802,26 +2953,32 @@ var JetstreamMetricsService = class {
2802
2953
  if (registry.hasEventHandlers()) {
2803
2954
  targets.push({
2804
2955
  kind: "ev" /* Event */,
2805
- stream: streamName(this.options.name, "ev" /* Event */),
2806
- consumer: consumerName(this.options.name, "ev" /* Event */)
2956
+ stream: this.resolveStreamName("ev" /* Event */),
2957
+ consumer: this.resolveConsumerName("ev" /* Event */)
2807
2958
  });
2808
2959
  }
2809
2960
  if (registry.hasRpcHandlers() && isJetStreamRpcMode(this.options.rpc)) {
2810
2961
  targets.push({
2811
2962
  kind: "cmd" /* Command */,
2812
- stream: streamName(this.options.name, "cmd" /* Command */),
2813
- consumer: consumerName(this.options.name, "cmd" /* Command */)
2963
+ stream: this.resolveStreamName("cmd" /* Command */),
2964
+ consumer: this.resolveConsumerName("cmd" /* Command */)
2814
2965
  });
2815
2966
  }
2816
2967
  if (registry.hasBroadcastHandlers()) {
2817
2968
  targets.push({
2818
2969
  kind: "broadcast" /* Broadcast */,
2819
- stream: streamName(this.options.name, "broadcast" /* Broadcast */),
2820
- consumer: consumerName(this.options.name, "broadcast" /* Broadcast */)
2970
+ stream: this.resolveStreamName("broadcast" /* Broadcast */),
2971
+ consumer: this.resolveConsumerName("broadcast" /* Broadcast */)
2821
2972
  });
2822
2973
  }
2823
2974
  return targets;
2824
2975
  }
2976
+ resolveStreamName(kind) {
2977
+ return this.names ? this.names.streamName(kind) : streamName(this.options.name, kind);
2978
+ }
2979
+ resolveConsumerName(kind) {
2980
+ return this.names ? this.names.consumerName(kind) : consumerName(this.options.name, kind);
2981
+ }
2825
2982
  subscribeToEvents() {
2826
2983
  this.eventBus.subscribe("connect" /* Connect */, this.onConnect);
2827
2984
  this.eventBus.subscribe("disconnect" /* Disconnect */, this.onDisconnect);
@@ -2856,7 +3013,7 @@ var JetstreamMetricsService = class {
2856
3013
  const subjectLabel = declared?.pattern ?? UNMATCHED_SUBJECT_LABEL;
2857
3014
  this.metrics?.rpcTimeoutTotal.labels({ subject: subjectLabel }).inc();
2858
3015
  };
2859
- // `_kind` collapses broadcast/ordered into MessageKind.Event we use
3016
+ // `_kind` collapses broadcast/ordered into MessageKind.Event; we use
2860
3017
  // declared.kind from PatternRegistry for the precise label instead.
2861
3018
  onMessageRouted = (subject, _kind) => {
2862
3019
  if (!this.metrics) return;
@@ -2866,7 +3023,7 @@ var JetstreamMetricsService = class {
2866
3023
  return;
2867
3024
  }
2868
3025
  this.metrics.messagesReceivedTotal.labels({
2869
- stream: streamName(this.options.name, declared.kind),
3026
+ stream: this.resolveStreamName(declared.kind),
2870
3027
  subject: declared.pattern,
2871
3028
  kind: STREAM_KIND_LABEL[declared.kind]
2872
3029
  }).inc();
@@ -2882,7 +3039,7 @@ var JetstreamMetricsService = class {
2882
3039
  };
2883
3040
  onHandlerCompleted = (pattern, kind, durationMs, status) => {
2884
3041
  if (!this.metrics) return;
2885
- const stream = streamName(this.options.name, kind);
3042
+ const stream = this.resolveStreamName(kind);
2886
3043
  const kindLabel = STREAM_KIND_LABEL[kind];
2887
3044
  const labels = { stream, subject: pattern, kind: kindLabel, status };
2888
3045
  this.metrics.messagesProcessedTotal.labels(labels).inc();
@@ -2908,7 +3065,8 @@ JetstreamMetricsService = __decorateClass([
2908
3065
  __decorateParam(3, Inject(JETSTREAM_OPTIONS)),
2909
3066
  __decorateParam(4, Optional()),
2910
3067
  __decorateParam(5, Optional()),
2911
- __decorateParam(5, Inject(JETSTREAM_CONNECTION))
3068
+ __decorateParam(5, Inject(JETSTREAM_CONNECTION)),
3069
+ __decorateParam(6, Optional())
2912
3070
  ], JetstreamMetricsService);
2913
3071
 
2914
3072
  // src/metrics/metrics.module.ts
@@ -2963,9 +3121,18 @@ var JetstreamMetricsModule = class {
2963
3121
  JETSTREAM_METRICS_PROM_CLIENT,
2964
3122
  JETSTREAM_OPTIONS,
2965
3123
  { token: PatternRegistry, optional: true },
2966
- { token: JETSTREAM_CONNECTION, optional: true }
3124
+ { token: JETSTREAM_CONNECTION, optional: true },
3125
+ { token: NameResolver, optional: true }
2967
3126
  ],
2968
- useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection) => new JetstreamMetricsService(eventBus, cfg, runtime, opts, patternRegistry, connection)
3127
+ useFactory: (eventBus, cfg, runtime, opts, patternRegistry, connection, names) => new JetstreamMetricsService(
3128
+ eventBus,
3129
+ cfg,
3130
+ runtime,
3131
+ opts,
3132
+ patternRegistry,
3133
+ connection,
3134
+ names
3135
+ )
2969
3136
  };
2970
3137
  return {
2971
3138
  module: JetstreamMetricsModule,
@@ -3026,31 +3193,22 @@ var JetstreamStrategy = class extends Server {
3026
3193
  this.started = false;
3027
3194
  }
3028
3195
  /**
3029
- * Override NestJS `Server.addHandler` to fail-fast on duplicate pattern registration.
3196
+ * Override NestJS `Server.addHandler` to fail fast on duplicate pattern registration.
3030
3197
  *
3031
- * The base class silently overwrites duplicate RPC handlers (last wins) and appends
3032
- * duplicate event handlers to a linked list. Both behaviors are hazardous in a
3033
- * JetStream context: silent overwrite drops a handler the user wrote, and double
3034
- * event dispatch double-acks/double-processes the same JetStream message.
3035
- *
3036
- * We treat any pattern collision as a fatal misconfiguration so it surfaces at
3037
- * bootstrap instead of in production traffic.
3198
+ * The base class silently overwrites duplicate RPC handlers and chains duplicate event
3199
+ * handlers, which would double-ack the same JetStream message. Any collision is treated
3200
+ * as a fatal misconfiguration so it surfaces at bootstrap, not in production traffic.
3038
3201
  */
3039
3202
  addHandler(pattern, callback, isEventHandler = false, extras = {}) {
3040
3203
  const normalizedPattern = this.normalizePattern(pattern);
3041
3204
  if (this.messageHandlers.has(normalizedPattern)) {
3042
3205
  throw new Error(
3043
- `Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice \u2014 find and remove the second declaration.`
3206
+ `Duplicate handler registered for pattern "${normalizedPattern}". Each @EventPattern() / @MessagePattern() value must be unique within a microservice; find and remove the second declaration.`
3044
3207
  );
3045
3208
  }
3046
3209
  super.addHandler(pattern, callback, isEventHandler, extras);
3047
3210
  }
3048
- /**
3049
- * Register event listener (required by Server base class).
3050
- *
3051
- * Stores callbacks for client use. Primary lifecycle events
3052
- * are routed through EventBus.
3053
- */
3211
+ /** Register event listener (required by Server base class); lifecycle events use EventBus. */
3054
3212
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
3055
3213
  on(event, callback) {
3056
3214
  const existing = this.listeners.get(event) ?? [];
@@ -3065,7 +3223,7 @@ var JetstreamStrategy = class extends Server {
3065
3223
  unwrap() {
3066
3224
  const nc = this.connection.unwrap;
3067
3225
  if (!nc) {
3068
- throw new Error("Not connected \u2014 transport has not started");
3226
+ throw new Error("Not connected; transport has not started");
3069
3227
  }
3070
3228
  return nc;
3071
3229
  }
@@ -3075,7 +3233,7 @@ var JetstreamStrategy = class extends Server {
3075
3233
  }
3076
3234
  async doListen(callback) {
3077
3235
  if (this.started) {
3078
- this.logger.warn("listen() called more than once \u2014 ignoring");
3236
+ this.logger.warn("listen() called more than once; ignoring");
3079
3237
  return;
3080
3238
  }
3081
3239
  this.started = true;
@@ -3166,7 +3324,7 @@ var JetstreamStrategy = class extends Server {
3166
3324
  };
3167
3325
 
3168
3326
  // src/server/core-rpc.server.ts
3169
- import { Logger as Logger11 } from "@nestjs/common";
3327
+ import { Logger as Logger12 } from "@nestjs/common";
3170
3328
  import { headers as natsHeaders2 } from "@nats-io/transport-node";
3171
3329
 
3172
3330
  // src/context/rpc.context.ts
@@ -3243,7 +3401,7 @@ var RpcContext = class extends BaseRpcContext {
3243
3401
  retry(opts) {
3244
3402
  this.assertJetStream("retry");
3245
3403
  if (this._shouldTerminate) {
3246
- throw new Error("Cannot retry \u2014 terminate() was already called");
3404
+ throw new Error("Cannot retry; terminate() was already called");
3247
3405
  }
3248
3406
  this._shouldRetry = true;
3249
3407
  this._retryDelay = opts?.delayMs;
@@ -3260,7 +3418,7 @@ var RpcContext = class extends BaseRpcContext {
3260
3418
  terminate(reason) {
3261
3419
  this.assertJetStream("terminate");
3262
3420
  if (this._shouldRetry) {
3263
- throw new Error("Cannot terminate \u2014 retry() was already called");
3421
+ throw new Error("Cannot terminate; retry() was already called");
3264
3422
  }
3265
3423
  this._shouldTerminate = true;
3266
3424
  this._terminateReason = reason;
@@ -3269,7 +3427,7 @@ var RpcContext = class extends BaseRpcContext {
3269
3427
  asJetStream() {
3270
3428
  return this.isJetStream() ? this.args[0] : null;
3271
3429
  }
3272
- /** Ensure the message is JetStream settlement actions are not available for Core NATS. */
3430
+ /** Ensure the message is JetStream; settlement actions are not available for Core NATS. */
3273
3431
  assertJetStream(method) {
3274
3432
  if (!this.isJetStream()) {
3275
3433
  throw new Error(`${method}() is only available for JetStream messages`);
@@ -3434,17 +3592,18 @@ var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
3434
3592
 
3435
3593
  // src/server/core-rpc.server.ts
3436
3594
  var CoreRpcServer = class {
3437
- constructor(options, connection, patternRegistry, codec, eventBus) {
3595
+ constructor(options, connection, patternRegistry, codec, eventBus, names) {
3438
3596
  this.connection = connection;
3439
3597
  this.patternRegistry = patternRegistry;
3440
3598
  this.codec = codec;
3441
3599
  this.eventBus = eventBus;
3600
+ this.names = names;
3442
3601
  const derived = deriveOtelAttrs(options);
3443
3602
  this.otel = derived.otel;
3444
3603
  this.serviceName = derived.serviceName;
3445
3604
  this.serverEndpoint = derived.serverEndpoint;
3446
3605
  }
3447
- logger = new Logger11("Jetstream:CoreRpc");
3606
+ logger = new Logger12("Jetstream:CoreRpc");
3448
3607
  subscription = null;
3449
3608
  otel;
3450
3609
  serviceName;
@@ -3452,7 +3611,7 @@ var CoreRpcServer = class {
3452
3611
  /** Start listening for RPC requests on the command subject. */
3453
3612
  async start() {
3454
3613
  const nc = await this.connection.getConnection();
3455
- const subject = `${this.serviceName}.cmd.>`;
3614
+ const subject = this.names ? this.names.filterSubject("cmd" /* Command */) : `${this.serviceName}.cmd.>`;
3456
3615
  const queue = `${this.serviceName}_cmd_queue`;
3457
3616
  this.subscription = nc.subscribe(subject, {
3458
3617
  queue,
@@ -3555,7 +3714,7 @@ var CoreRpcServer = class {
3555
3714
  };
3556
3715
 
3557
3716
  // src/server/infrastructure/stream.provider.ts
3558
- import { Logger as Logger13 } from "@nestjs/common";
3717
+ import { Logger as Logger14 } from "@nestjs/common";
3559
3718
  import {
3560
3719
  JetStreamApiError as JetStreamApiError2,
3561
3720
  RetentionPolicy as RetentionPolicy2,
@@ -3629,7 +3788,7 @@ var assertStorageBudget = async (jsm, serviceName, reservations, logger5) => {
3629
3788
  );
3630
3789
  }
3631
3790
  } catch (err) {
3632
- logger5.debug(`Storage preflight skipped \u2014 account info unavailable: ${String(err)}`);
3791
+ logger5.debug(`Storage preflight skipped; account info unavailable: ${String(err)}`);
3633
3792
  }
3634
3793
  };
3635
3794
 
@@ -3650,7 +3809,7 @@ var JetstreamProvisioningError = class _JetstreamProvisioningError extends Error
3650
3809
  numReplicas;
3651
3810
  reservation;
3652
3811
  constructor(fields) {
3653
- const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B \xD7 replicas=${fields.numReplicas}).` : "";
3812
+ const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B x replicas=${fields.numReplicas}).` : "";
3654
3813
  super(
3655
3814
  `JetStream ${fields.entity} provisioning failed for "${fields.target}" (kind=${fields.kind}): ${fields.errDescription} [err_code=${fields.errCode}].${reservationNote} ${fields.remediation}`,
3656
3815
  { cause: fields.cause }
@@ -3702,18 +3861,22 @@ var formatAge = (nanos) => {
3702
3861
  if (nanos >= NANOS_PER_HOUR) return `${(nanos / NANOS_PER_HOUR).toFixed(1)}h`;
3703
3862
  return `${(nanos / NANOS_PER_SECOND).toFixed(0)}s`;
3704
3863
  };
3705
- var formatProvisioningSummary = (serviceName, reservations) => {
3706
- const lines = [`Provisioning ${reservations.length} stream(s) for "${serviceName}":`];
3864
+ var formatProvisioningSummary = (serviceName, reservations, external = []) => {
3865
+ const totalStreams = reservations.length + external.length;
3866
+ const lines = [`Provisioning ${totalStreams} stream(s) for "${serviceName}":`];
3707
3867
  let totalFileMaxBytes = 0;
3708
3868
  for (const r of reservations) {
3709
3869
  if (r.storage === StorageType3.File) totalFileMaxBytes += r.maxBytes;
3710
3870
  const clusterReservation = r.maxBytes * r.numReplicas;
3711
3871
  lines.push(
3712
- ` \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)}`
3872
+ ` - ${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)}`
3713
3873
  );
3714
3874
  }
3875
+ for (const e of external) {
3876
+ lines.push(` - ${e.name} [${e.kind}] external (bound)`);
3877
+ }
3715
3878
  lines.push(
3716
- ` \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.`
3879
+ ` 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.`
3717
3880
  );
3718
3881
  return lines.join("\n");
3719
3882
  };
@@ -3774,7 +3937,7 @@ var isEqual = (a, b) => {
3774
3937
  };
3775
3938
 
3776
3939
  // src/server/infrastructure/stream-migration.ts
3777
- import { Logger as Logger12 } from "@nestjs/common";
3940
+ import { Logger as Logger13 } from "@nestjs/common";
3778
3941
  import {
3779
3942
  JetStreamApiError
3780
3943
  } from "@nats-io/jetstream";
@@ -3789,7 +3952,7 @@ var StreamMigration = class {
3789
3952
  this.sourcingTimeoutMs = sourcingTimeoutMs;
3790
3953
  this.peerWaitMs = peerWaitMs;
3791
3954
  }
3792
- logger = new Logger12("Jetstream:Stream");
3955
+ logger = new Logger13("Jetstream:Stream");
3793
3956
  async migrate(jsm, streamName2, newConfig) {
3794
3957
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
3795
3958
  const startTime = Date.now();
@@ -3808,7 +3971,7 @@ var StreamMigration = class {
3808
3971
  await jsm.streams.update(streamName2, { ...currentInfo.config, subjects: [] });
3809
3972
  drainedCount = (await jsm.streams.info(streamName2)).state.messages;
3810
3973
  if (drainedCount > 0) {
3811
- this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages \u2192 ${backupName}`);
3974
+ this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages -> ${backupName}`);
3812
3975
  await jsm.streams.add({
3813
3976
  ...currentInfo.config,
3814
3977
  name: backupName,
@@ -3829,7 +3992,7 @@ var StreamMigration = class {
3829
3992
  } catch (err) {
3830
3993
  if (originalDeleted) {
3831
3994
  this.logger.error(
3832
- `Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved \u2014 restoration resumes on the next startup.`
3995
+ `Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved; restoration resumes on the next startup.`
3833
3996
  );
3834
3997
  } else {
3835
3998
  await this.rollbackBeforeDelete(jsm, streamName2, currentInfo, backupName);
@@ -3897,7 +4060,7 @@ var StreamMigration = class {
3897
4060
  await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
3898
4061
  }
3899
4062
  /**
3900
- * Lag-based drain check live publishes cannot fake completion. A fresh
4063
+ * Lag-based drain check: live publishes cannot fake completion. A fresh
3901
4064
  * source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
3902
4065
  * hence the active guard.
3903
4066
  */
@@ -3916,13 +4079,13 @@ var StreamMigration = class {
3916
4079
  );
3917
4080
  }
3918
4081
  /**
3919
- * A backup present at migrate() start is a live peer migration wait it
4082
+ * A backup present at migrate() start is a live peer migration; wait it
3920
4083
  * out. Stale leftovers were already handled by recoverInterrupted().
3921
4084
  */
3922
4085
  async waitOutPeerMigration(jsm, backupName) {
3923
4086
  if (await this.tryInfo(jsm, backupName) === null) return false;
3924
4087
  this.logger.warn(
3925
- `Migration backup ${backupName} exists \u2014 another instance appears to be migrating; waiting`
4088
+ `Migration backup ${backupName} exists; another instance appears to be migrating; waiting`
3926
4089
  );
3927
4090
  const deadline = Date.now() + this.peerWaitMs;
3928
4091
  while (Date.now() < deadline) {
@@ -3943,7 +4106,7 @@ var StreamMigration = class {
3943
4106
  }
3944
4107
  } catch (rollbackErr) {
3945
4108
  this.logger.error(
3946
- `Rollback of ${streamName2} after a failed migration also failed \u2014 the stream may be left quiesced:`,
4109
+ `Rollback of ${streamName2} after a failed migration also failed; the stream may be left quiesced:`,
3947
4110
  rollbackErr
3948
4111
  );
3949
4112
  }
@@ -3967,7 +4130,7 @@ var StreamMigration = class {
3967
4130
  }
3968
4131
  };
3969
4132
 
3970
- // src/server/infrastructure/stream.provider.ts
4133
+ // src/server/infrastructure/subject-utils.ts
3971
4134
  var subjectCovers = (broad, narrow) => {
3972
4135
  if (broad === narrow) return false;
3973
4136
  const broadTokens = broad.split(".");
@@ -3979,16 +4142,21 @@ var subjectCovers = (broad, narrow) => {
3979
4142
  }
3980
4143
  return broadTokens.length === narrowTokens.length;
3981
4144
  };
4145
+ var coversOrEquals = (broad, subject) => broad === subject || subjectCovers(broad, subject);
4146
+
4147
+ // src/server/infrastructure/stream.provider.ts
3982
4148
  var StreamProvider = class {
3983
- constructor(options, connection) {
4149
+ constructor(options, connection, names, binder) {
3984
4150
  this.options = options;
3985
4151
  this.connection = connection;
4152
+ this.names = names;
4153
+ this.binder = binder;
3986
4154
  const derived = deriveOtelAttrs(options);
3987
4155
  this.otel = derived.otel;
3988
4156
  this.otelServiceName = derived.serviceName;
3989
4157
  this.otelEndpoint = derived.serverEndpoint;
3990
4158
  }
3991
- logger = new Logger13("Jetstream:Stream");
4159
+ logger = new Logger14("Jetstream:Stream");
3992
4160
  migration = new StreamMigration();
3993
4161
  otel;
3994
4162
  otelServiceName;
@@ -4002,42 +4170,48 @@ var StreamProvider = class {
4002
4170
  */
4003
4171
  async ensureStreams(kinds) {
4004
4172
  const jsm = await this.connection.getJetStreamManager();
4005
- const reservations = kinds.map((kind) => this.buildReservation(kind, this.buildConfig(kind)));
4173
+ const { autoKinds, externalKinds } = this.partitionByManagement(kinds);
4174
+ const reservations = autoKinds.map(
4175
+ (kind) => this.buildReservation(kind, this.buildConfig(kind))
4176
+ );
4177
+ const external = externalKinds.map((kind) => ({
4178
+ kind,
4179
+ name: this.names.streamName(kind)
4180
+ }));
4181
+ const dlqIsManual = !!this.options.dlq && resolveManagementMode(this.options, "dlq", "stream") === "manual" /* Manual */;
4006
4182
  if (this.options.dlq) {
4007
- reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
4183
+ if (dlqIsManual) {
4184
+ external.push({ kind: "dlq", name: this.names.dlqStreamName() });
4185
+ } else {
4186
+ reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
4187
+ }
4008
4188
  }
4009
4189
  this.logger.log(`
4010
- ${formatProvisioningSummary(this.options.name, reservations)}`);
4011
- if (this.options.provisioning?.preflightStorageCheck) {
4190
+ ${formatProvisioningSummary(this.options.name, reservations, external)}`);
4191
+ if (this.options.provisioning?.preflightStorageCheck && reservations.length > 0) {
4012
4192
  await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
4013
4193
  }
4014
- await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
4194
+ await Promise.all([
4195
+ ...autoKinds.map((kind) => this.ensureStream(jsm, kind)),
4196
+ ...externalKinds.map((kind) => this.bindStream(jsm, kind))
4197
+ ]);
4015
4198
  if (this.options.dlq) {
4016
- await this.ensureDlqStream(jsm);
4199
+ if (dlqIsManual) {
4200
+ await this.bindDlqStream(jsm);
4201
+ } else {
4202
+ await this.ensureDlqStream(jsm);
4203
+ }
4017
4204
  }
4018
4205
  }
4019
4206
  /** Get the stream name for a given kind. */
4020
4207
  getStreamName(kind) {
4021
- return streamName(this.options.name, kind);
4208
+ return this.names.streamName(kind);
4022
4209
  }
4023
4210
  /** Get the subjects pattern for a given kind. */
4024
4211
  getSubjects(kind) {
4025
- const name = internalName(this.options.name);
4026
- switch (kind) {
4027
- case "ev" /* Event */: {
4028
- const subjects = [`${name}.${"ev" /* Event */}.>`];
4029
- if (this.isSchedulingEnabled(kind)) {
4030
- subjects.push(`${name}._sch.>`);
4031
- }
4032
- return subjects;
4033
- }
4034
- case "cmd" /* Command */:
4035
- return [`${name}.${"cmd" /* Command */}.>`];
4036
- case "broadcast" /* Broadcast */:
4037
- return ["broadcast.>"];
4038
- case "ordered" /* Ordered */:
4039
- return [`${name}.${"ordered" /* Ordered */}.>`];
4040
- }
4212
+ const filter = this.names.filterSubject(kind);
4213
+ const dedicatedSchedule = kind === "ev" /* Event */ && this.isSchedulingEnabled(kind) && !this.names.hasCustomPrefix(kind);
4214
+ return dedicatedSchedule ? [filter, `${this.names.schedulePrefix(kind)}>`] : [filter];
4041
4215
  }
4042
4216
  /** Ensure a single stream exists, creating or updating as needed. */
4043
4217
  async ensureStream(jsm, kind) {
@@ -4114,7 +4288,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4114
4288
  }
4115
4289
  this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
4116
4290
  if (diff.hasTransportControlledConflicts) {
4117
- const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
4291
+ const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`).join(", ");
4118
4292
  throw new Error(
4119
4293
  `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.`
4120
4294
  );
@@ -4164,13 +4338,13 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4164
4338
  }
4165
4339
  logChanges(streamName2, diff, migrationEnabled) {
4166
4340
  for (const c of diff.changes) {
4167
- const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
4341
+ const detail = `${c.property}: ${JSON.stringify(c.current)} -> ${JSON.stringify(c.desired)}`;
4168
4342
  if (c.mutability === "transport-controlled") {
4169
4343
  this.logger.error(
4170
- `Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
4344
+ `Stream ${streamName2}: ${detail}; transport-controlled, cannot be changed`
4171
4345
  );
4172
4346
  } else if (c.mutability === "immutable" && !migrationEnabled) {
4173
- this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
4347
+ this.logger.warn(`Stream ${streamName2}: ${detail}; requires allowDestructiveMigration`);
4174
4348
  } else {
4175
4349
  this.logger.log(`Stream ${streamName2}: ${detail}`);
4176
4350
  }
@@ -4208,7 +4382,47 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4208
4382
  throw err;
4209
4383
  }
4210
4384
  }
4211
- /** The broadcast stream is global — every service in the cluster shares it. */
4385
+ partitionByManagement(kinds) {
4386
+ const autoKinds = [];
4387
+ const externalKinds = [];
4388
+ for (const kind of kinds) {
4389
+ if (resolveManagementMode(this.options, kind, "stream") === "manual" /* Manual */) {
4390
+ externalKinds.push(kind);
4391
+ } else {
4392
+ autoKinds.push(kind);
4393
+ }
4394
+ }
4395
+ return { autoKinds, externalKinds };
4396
+ }
4397
+ async bindStream(jsm, kind) {
4398
+ const name = this.names.streamName(kind);
4399
+ return withProvisioningSpan(
4400
+ this.otel,
4401
+ {
4402
+ serviceName: this.otelServiceName,
4403
+ endpoint: this.otelEndpoint,
4404
+ entity: "stream",
4405
+ name,
4406
+ action: "bind"
4407
+ },
4408
+ () => this.binder.bindStream(jsm, kind)
4409
+ );
4410
+ }
4411
+ async bindDlqStream(jsm) {
4412
+ const name = this.names.dlqStreamName();
4413
+ return withProvisioningSpan(
4414
+ this.otel,
4415
+ {
4416
+ serviceName: this.otelServiceName,
4417
+ endpoint: this.otelEndpoint,
4418
+ entity: "stream",
4419
+ name,
4420
+ action: "bind"
4421
+ },
4422
+ () => this.binder.bindDlqStream(jsm)
4423
+ );
4424
+ }
4425
+ /** The broadcast stream is global; every service in the cluster shares it. */
4212
4426
  isSharedStream(name) {
4213
4427
  return name === this.getStreamName("broadcast" /* Broadcast */);
4214
4428
  }
@@ -4228,13 +4442,11 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4228
4442
  };
4229
4443
  }
4230
4444
  /**
4231
- * Build the stream configuration for the Dead-Letter Queue (DLQ).
4232
- *
4233
- * Merges the library default DLQ config with user-provided overrides.
4234
- * Ensures transport-controlled settings like retention are safely decoupled.
4445
+ * Build the DLQ stream config: library defaults merged with user overrides,
4446
+ * with transport-controlled settings like retention stripped.
4235
4447
  */
4236
4448
  buildDlqConfig() {
4237
- const name = dlqStreamName(this.options.name);
4449
+ const name = this.names.dlqStreamName();
4238
4450
  const subjects = [name];
4239
4451
  const description = `JetStream DLQ stream for ${this.options.name}`;
4240
4452
  const overrides = this.options.dlq?.stream ?? {};
@@ -4267,22 +4479,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4267
4479
  }
4268
4480
  /** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
4269
4481
  getOverrides(kind) {
4270
- let overrides;
4271
- switch (kind) {
4272
- case "ev" /* Event */:
4273
- overrides = this.options.events?.stream ?? {};
4274
- break;
4275
- case "cmd" /* Command */:
4276
- overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
4277
- break;
4278
- case "broadcast" /* Broadcast */:
4279
- overrides = this.options.broadcast?.stream ?? {};
4280
- break;
4281
- case "ordered" /* Ordered */:
4282
- overrides = this.options.ordered?.stream ?? {};
4283
- break;
4284
- }
4285
- return this.stripTransportControlled(overrides);
4482
+ return this.stripTransportControlled(kindOptionsBlock(this.options, kind)?.stream ?? {});
4286
4483
  }
4287
4484
  /**
4288
4485
  * Remove transport-controlled properties from user overrides.
@@ -4292,7 +4489,7 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4292
4489
  stripTransportControlled(overrides) {
4293
4490
  if (!("retention" in overrides)) return overrides;
4294
4491
  this.logger.debug(
4295
- "Stripping user-provided retention override \u2014 retention is managed by the transport"
4492
+ "Stripping user-provided retention override; retention is managed by the transport"
4296
4493
  );
4297
4494
  const cleaned = { ...overrides };
4298
4495
  delete cleaned.retention;
@@ -4301,20 +4498,22 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4301
4498
  };
4302
4499
 
4303
4500
  // src/server/infrastructure/consumer.provider.ts
4304
- import { Logger as Logger14 } from "@nestjs/common";
4501
+ import { Logger as Logger15 } from "@nestjs/common";
4305
4502
  import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
4306
4503
  var ConsumerProvider = class {
4307
- constructor(options, connection, streamProvider, patternRegistry) {
4504
+ constructor(options, connection, streamProvider, patternRegistry, names, binder) {
4308
4505
  this.options = options;
4309
4506
  this.connection = connection;
4310
4507
  this.streamProvider = streamProvider;
4311
4508
  this.patternRegistry = patternRegistry;
4509
+ this.names = names;
4510
+ this.binder = binder;
4312
4511
  const derived = deriveOtelAttrs(options);
4313
4512
  this.otel = derived.otel;
4314
4513
  this.otelServiceName = derived.serviceName;
4315
4514
  this.otelEndpoint = derived.serverEndpoint;
4316
4515
  }
4317
- logger = new Logger14("Jetstream:Consumer");
4516
+ logger = new Logger15("Jetstream:Consumer");
4318
4517
  otel;
4319
4518
  otelServiceName;
4320
4519
  otelEndpoint;
@@ -4336,24 +4535,33 @@ var ConsumerProvider = class {
4336
4535
  }
4337
4536
  /** Get the consumer name for a given kind. */
4338
4537
  getConsumerName(kind) {
4339
- return consumerName(this.options.name, kind);
4538
+ return this.names.consumerName(kind);
4340
4539
  }
4341
4540
  /**
4342
4541
  * Ensure a single consumer exists with the desired config.
4343
- * Used at **startup** — creates or updates the consumer to match
4344
- * the current pod's configuration.
4542
+ * Startup path: creates or updates the consumer to match the current pod's configuration.
4345
4543
  */
4346
4544
  async ensureConsumer(jsm, kind) {
4347
4545
  const stream = this.streamProvider.getStreamName(kind);
4348
4546
  const config = this.buildConfig(kind);
4349
4547
  const name = config.durable_name;
4548
+ const spanAttrs = {
4549
+ serviceName: this.otelServiceName,
4550
+ endpoint: this.otelEndpoint,
4551
+ entity: "consumer",
4552
+ name
4553
+ };
4554
+ if (resolveManagementMode(this.options, kind, "consumer") === "manual" /* Manual */) {
4555
+ return withProvisioningSpan(
4556
+ this.otel,
4557
+ { ...spanAttrs, action: "bind" },
4558
+ () => this.binder.bindConsumer(jsm, kind)
4559
+ );
4560
+ }
4350
4561
  return withProvisioningSpan(
4351
4562
  this.otel,
4352
4563
  {
4353
- serviceName: this.otelServiceName,
4354
- endpoint: this.otelEndpoint,
4355
- entity: "consumer",
4356
- name,
4564
+ ...spanAttrs,
4357
4565
  action: "ensure"
4358
4566
  },
4359
4567
  async () => {
@@ -4374,28 +4582,40 @@ var ConsumerProvider = class {
4374
4582
  }
4375
4583
  /**
4376
4584
  * Recover a consumer that disappeared during runtime.
4377
- * Used by **self-healing** — creates if missing, but NEVER updates config.
4378
- *
4379
- * If a migration backup stream exists, another pod is mid-migration — we
4380
- * throw so the self-healing retry loop waits with backoff until migration
4381
- * completes and the backup is cleaned up.
4382
4585
  *
4383
- * This prevents old pods from:
4384
- * - Overwriting a newer pod's consumer config during rolling updates
4385
- * - Creating consumers during migration (which would consume and delete
4386
- * workqueue messages while they're being restored)
4586
+ * Self-healing path: creates if missing but never updates config, so an old pod
4587
+ * cannot overwrite a newer pod's config during rolling updates. If a migration
4588
+ * backup stream exists, throws so the retry loop backs off until migration completes;
4589
+ * creating a consumer mid-migration would eat workqueue messages being restored.
4387
4590
  */
4388
4591
  async recoverConsumer(jsm, kind) {
4389
4592
  const stream = this.streamProvider.getStreamName(kind);
4390
4593
  const config = this.buildConfig(kind);
4391
4594
  const name = config.durable_name;
4595
+ const spanAttrs = {
4596
+ serviceName: this.otelServiceName,
4597
+ endpoint: this.otelEndpoint,
4598
+ entity: "consumer",
4599
+ name
4600
+ };
4601
+ if (resolveManagementMode(this.options, kind, "consumer") === "manual" /* Manual */) {
4602
+ return withProvisioningSpan(this.otel, { ...spanAttrs, action: "rebind" }, async () => {
4603
+ try {
4604
+ return await jsm.consumers.info(stream, name);
4605
+ } catch (err) {
4606
+ if (err instanceof JetStreamApiError3 && err.apiError().err_code === 10014 /* ConsumerNotFound */) {
4607
+ throw new Error(
4608
+ `Consumer ${name} on ${stream} is externally managed and currently absent; waiting for it to be restored.`
4609
+ );
4610
+ }
4611
+ throw err;
4612
+ }
4613
+ });
4614
+ }
4392
4615
  return withProvisioningSpan(
4393
4616
  this.otel,
4394
4617
  {
4395
- serviceName: this.otelServiceName,
4396
- endpoint: this.otelEndpoint,
4397
- entity: "consumer",
4398
- name,
4618
+ ...spanAttrs,
4399
4619
  action: "recover"
4400
4620
  },
4401
4621
  async () => {
@@ -4431,9 +4651,7 @@ var ConsumerProvider = class {
4431
4651
  throw err;
4432
4652
  }
4433
4653
  }
4434
- /**
4435
- * Create a consumer, handling the race where another pod creates it first.
4436
- */
4654
+ /** Create a consumer, handling the race where another pod creates it first. */
4437
4655
  async createConsumer(jsm, stream, name, kind, config) {
4438
4656
  this.logger.log(`Creating consumer: ${name}`);
4439
4657
  const ctx = { entity: "consumer", name, kind };
@@ -4461,10 +4679,8 @@ var ConsumerProvider = class {
4461
4679
  }
4462
4680
  }
4463
4681
  /** Build consumer config by merging defaults with user overrides. */
4464
- // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
4465
4682
  buildConfig(kind) {
4466
- const name = this.getConsumerName(kind);
4467
- const serviceName = internalName(this.options.name);
4683
+ const durableName = this.getConsumerName(kind);
4468
4684
  const defaults = this.getDefaults(kind);
4469
4685
  const overrides = this.getOverrides(kind);
4470
4686
  if (kind === "broadcast" /* Broadcast */) {
@@ -4476,31 +4692,54 @@ var ConsumerProvider = class {
4476
4692
  return {
4477
4693
  ...defaults,
4478
4694
  ...overrides,
4479
- name,
4480
- durable_name: name,
4695
+ name: durableName,
4696
+ durable_name: durableName,
4481
4697
  filter_subject: broadcastPatterns[0]
4482
4698
  };
4483
4699
  }
4484
4700
  return {
4485
4701
  ...defaults,
4486
4702
  ...overrides,
4487
- name,
4488
- durable_name: name,
4703
+ name: durableName,
4704
+ durable_name: durableName,
4489
4705
  filter_subjects: broadcastPatterns
4490
4706
  };
4491
4707
  }
4492
4708
  if (kind !== "ev" /* Event */ && kind !== "cmd" /* Command */) {
4493
4709
  throw new Error(`Unexpected durable consumer kind: ${kind}`);
4494
4710
  }
4495
- const filter_subject = `${serviceName}.${kind}.>`;
4711
+ if (this.names.hasCustomPrefix(kind)) {
4712
+ return this.buildCustomPrefixConfig(kind, durableName, defaults, overrides);
4713
+ }
4714
+ const filter_subject = this.names.filterSubject(kind);
4496
4715
  return {
4497
4716
  ...defaults,
4498
4717
  ...overrides,
4499
- name,
4500
- durable_name: name,
4718
+ name: durableName,
4719
+ durable_name: durableName,
4501
4720
  filter_subject
4502
4721
  };
4503
4722
  }
4723
+ buildCustomPrefixConfig(kind, durableName, defaults, overrides) {
4724
+ const patterns = kind === "ev" /* Event */ ? this.patternRegistry.getEventPatterns() : this.patternRegistry.getCommandPatterns();
4725
+ const subjects = patterns.map((p) => this.names.subject(kind, p));
4726
+ if (subjects.length === 1) {
4727
+ return {
4728
+ ...defaults,
4729
+ ...overrides,
4730
+ name: durableName,
4731
+ durable_name: durableName,
4732
+ filter_subject: subjects[0]
4733
+ };
4734
+ }
4735
+ return {
4736
+ ...defaults,
4737
+ ...overrides,
4738
+ name: durableName,
4739
+ durable_name: durableName,
4740
+ filter_subjects: subjects
4741
+ };
4742
+ }
4504
4743
  /** Get default config for a consumer kind. */
4505
4744
  getDefaults(kind) {
4506
4745
  switch (kind) {
@@ -4521,26 +4760,12 @@ var ConsumerProvider = class {
4521
4760
  }
4522
4761
  /** Get user-provided overrides for a consumer kind. */
4523
4762
  getOverrides(kind) {
4524
- switch (kind) {
4525
- case "ev" /* Event */:
4526
- return this.options.events?.consumer ?? {};
4527
- case "cmd" /* Command */:
4528
- return this.options.rpc?.mode === "jetstream" ? this.options.rpc.consumer ?? {} : {};
4529
- case "broadcast" /* Broadcast */:
4530
- return this.options.broadcast?.consumer ?? {};
4531
- case "ordered" /* Ordered */:
4532
- throw new Error("Ordered consumers are ephemeral and should not use durable config");
4533
- /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
4534
- default: {
4535
- const _exhaustive = kind;
4536
- throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
4537
- }
4538
- }
4763
+ return kindOptionsBlock(this.options, kind)?.consumer ?? {};
4539
4764
  }
4540
4765
  };
4541
4766
 
4542
4767
  // src/server/infrastructure/message.provider.ts
4543
- import { Logger as Logger15 } from "@nestjs/common";
4768
+ import { Logger as Logger16 } from "@nestjs/common";
4544
4769
  import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
4545
4770
  import {
4546
4771
  catchError,
@@ -4559,7 +4784,7 @@ var MessageProvider = class {
4559
4784
  this.consumeOptionsMap = consumeOptionsMap;
4560
4785
  this.consumerRecoveryFn = consumerRecoveryFn;
4561
4786
  }
4562
- logger = new Logger15("Jetstream:Message");
4787
+ logger = new Logger16("Jetstream:Message");
4563
4788
  activeIterators = /* @__PURE__ */ new Set();
4564
4789
  orderedReadyResolve = null;
4565
4790
  orderedReadyReject = null;
@@ -4602,7 +4827,7 @@ var MessageProvider = class {
4602
4827
  /**
4603
4828
  * Start an ordered consumer for strict sequential delivery.
4604
4829
  *
4605
- * Unlike durable consumers, ordered consumers are ephemeral created at
4830
+ * Unlike durable consumers, ordered consumers are ephemeral: created at
4606
4831
  * consumption time, no durable state. nats.js handles auto-recreation.
4607
4832
  *
4608
4833
  * @param streamName - JetStream stream to consume from.
@@ -4816,7 +5041,7 @@ var MessageProvider = class {
4816
5041
  };
4817
5042
 
4818
5043
  // src/server/infrastructure/metadata.provider.ts
4819
- import { Logger as Logger16 } from "@nestjs/common";
5044
+ import { Logger as Logger17 } from "@nestjs/common";
4820
5045
  import { Kvm } from "@nats-io/kv";
4821
5046
  var MetadataProvider = class {
4822
5047
  constructor(options, connection) {
@@ -4825,7 +5050,7 @@ var MetadataProvider = class {
4825
5050
  this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
4826
5051
  this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
4827
5052
  }
4828
- logger = new Logger16("Jetstream:Metadata");
5053
+ logger = new Logger17("Jetstream:Metadata");
4829
5054
  bucketName;
4830
5055
  replicas;
4831
5056
  ttl;
@@ -4833,16 +5058,12 @@ var MetadataProvider = class {
4833
5058
  heartbeatTimer;
4834
5059
  cachedKv;
4835
5060
  /**
4836
- * Write handler metadata entries to the KV bucket and start heartbeat.
5061
+ * Write handler metadata entries to the KV bucket and start the heartbeat.
4837
5062
  *
4838
- * Creates the bucket if it doesn't exist (idempotent).
4839
- * Skips silently when entries map is empty.
4840
- * Starts a heartbeat interval that refreshes entries every `ttl / 2`
4841
- * to prevent TTL expiry while the pod is alive.
5063
+ * Creates the bucket if missing; skips silently when the map is empty.
5064
+ * Non-critical: errors are logged but do not prevent transport startup.
4842
5065
  *
4843
- * Non-critical errors are logged but do not prevent transport startup.
4844
- *
4845
- * @param entries Map of KV key → metadata object.
5066
+ * @param entries Map of KV key to metadata object.
4846
5067
  */
4847
5068
  async publish(entries) {
4848
5069
  if (entries.size === 0) return;
@@ -4918,396 +5139,626 @@ var MetadataProvider = class {
4918
5139
  };
4919
5140
 
4920
5141
  // src/server/routing/event.router.ts
4921
- import { Logger as Logger17 } from "@nestjs/common";
5142
+ import { Logger as Logger19 } from "@nestjs/common";
5143
+
5144
+ // src/server/routing/concurrency-gate.ts
5145
+ var BACKLOG_WARN_THRESHOLD = 1e3;
5146
+ var ConcurrencyGate = class {
5147
+ constructor(maxActive, route, parkTimer, logger5, label) {
5148
+ this.maxActive = maxActive;
5149
+ this.route = route;
5150
+ this.parkTimer = parkTimer;
5151
+ this.logger = logger5;
5152
+ this.label = label;
5153
+ }
5154
+ active = 0;
5155
+ backlogWarned = false;
5156
+ backlog = [];
5157
+ /** Entry point for each incoming message. */
5158
+ push(msg) {
5159
+ if (this.active >= this.maxActive) {
5160
+ this.backlog.push({
5161
+ msg,
5162
+ stopAckExtension: this.parkTimer ? this.parkTimer(msg) : null
5163
+ });
5164
+ if (!this.backlogWarned && this.backlog.length >= BACKLOG_WARN_THRESHOLD) {
5165
+ this.backlogWarned = true;
5166
+ this.logger.warn(
5167
+ `${this.label} backlog reached ${this.backlog.length} messages; consumer may be falling behind`
5168
+ );
5169
+ }
5170
+ return;
5171
+ }
5172
+ this.active++;
5173
+ const result = this.routeSafely(msg);
5174
+ if (result !== void 0) {
5175
+ this.trackAsync(result, msg);
5176
+ } else {
5177
+ this.active--;
5178
+ if (this.backlog.length > 0) this.drainBacklog();
5179
+ }
5180
+ }
5181
+ /** Stop parked timers and drop the backlog. */
5182
+ dispose() {
5183
+ for (const queued of this.backlog) {
5184
+ queued.stopAckExtension?.();
5185
+ }
5186
+ this.backlog.length = 0;
5187
+ }
5188
+ onAsyncDone = () => {
5189
+ this.active--;
5190
+ this.drainBacklog();
5191
+ };
5192
+ /** A throw here must not leak the concurrency slot or kill the subscription. */
5193
+ routeSafely(msg) {
5194
+ try {
5195
+ return this.route(msg);
5196
+ } catch (err) {
5197
+ this.logger.error(`Unexpected routing failure for ${msg.subject}:`, err);
5198
+ return void 0;
5199
+ }
5200
+ }
5201
+ trackAsync(result, msg) {
5202
+ void result.catch((err) => {
5203
+ this.logger.error(`Unexpected routing failure for ${msg.subject}:`, err);
5204
+ }).finally(this.onAsyncDone);
5205
+ }
5206
+ drainBacklog() {
5207
+ while (this.active < this.maxActive) {
5208
+ const next = this.backlog.shift();
5209
+ if (next === void 0) break;
5210
+ next.stopAckExtension?.();
5211
+ this.active++;
5212
+ const result = this.routeSafely(next.msg);
5213
+ if (result !== void 0) {
5214
+ this.trackAsync(result, next.msg);
5215
+ } else {
5216
+ this.active--;
5217
+ }
5218
+ }
5219
+ if (this.backlog.length < BACKLOG_WARN_THRESHOLD) this.backlogWarned = false;
5220
+ }
5221
+ };
5222
+
5223
+ // src/server/routing/dead-letter-capture.ts
5224
+ import { Logger as Logger18 } from "@nestjs/common";
4922
5225
  import { headers as natsHeaders3 } from "@nats-io/transport-node";
4923
5226
  var DLQ_PUBLISH_ATTEMPTS = 3;
4924
- var eventConsumeKindFor = (kind) => {
4925
- if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
4926
- if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
4927
- return "event" /* Event */;
4928
- };
4929
- var EventRouter = class {
4930
- constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
4931
- this.messageProvider = messageProvider;
5227
+ var DeadLetterCapture = class {
5228
+ constructor(patternRegistry, eventBus, deadLetterConfig, otel, serviceName, serverEndpoint, connection, options, names) {
4932
5229
  this.patternRegistry = patternRegistry;
4933
- this.codec = codec;
4934
5230
  this.eventBus = eventBus;
4935
5231
  this.deadLetterConfig = deadLetterConfig;
4936
- this.processingConfig = processingConfig;
4937
- this.ackWaitMap = ackWaitMap;
5232
+ this.otel = otel;
5233
+ this.serviceName = serviceName;
5234
+ this.serverEndpoint = serverEndpoint;
4938
5235
  this.connection = connection;
4939
5236
  this.options = options;
4940
- if (options) {
4941
- const derived = deriveOtelAttrs(options);
4942
- this.otel = derived.otel;
4943
- this.serviceName = derived.serviceName;
4944
- this.serverEndpoint = derived.serverEndpoint;
4945
- } else {
4946
- this.otel = resolveOtelOptions({ enabled: false });
4947
- this.serviceName = "";
4948
- this.serverEndpoint = null;
4949
- }
5237
+ this.names = names;
5238
+ }
5239
+ logger = new Logger18("Jetstream:DeadLetter");
5240
+ /** True when this delivery is the consumer's last attempt for the message. */
5241
+ isFinalDelivery(msg) {
5242
+ const maxDeliver = this.deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
5243
+ if (maxDeliver === void 0 || maxDeliver <= 0) return false;
5244
+ return msg.info.deliveryCount >= maxDeliver;
5245
+ }
5246
+ /** Emit the dead-letter event and route the message to the DLQ or the fallback callback. */
5247
+ async capture(msg, data, error) {
5248
+ const info = {
5249
+ subject: msg.subject,
5250
+ data,
5251
+ headers: msg.headers,
5252
+ error,
5253
+ deliveryCount: msg.info.deliveryCount,
5254
+ stream: msg.info.stream,
5255
+ streamSequence: msg.info.streamSequence,
5256
+ timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
5257
+ };
5258
+ await withDeadLetterSpan(
5259
+ {
5260
+ msg,
5261
+ // Surface the registered pattern so APM can filter dead letters by
5262
+ // handler; falls back to the raw subject when no handler matches.
5263
+ pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
5264
+ finalDeliveryCount: msg.info.deliveryCount,
5265
+ reason: error instanceof Error ? error.message : String(error),
5266
+ serviceName: this.serviceName,
5267
+ endpoint: this.serverEndpoint
5268
+ },
5269
+ this.otel,
5270
+ async () => {
5271
+ this.eventBus.emit("deadLetter" /* DeadLetter */, info);
5272
+ if (!this.options?.dlq) {
5273
+ await this.fallbackToOnDeadLetterCallback(info, msg);
5274
+ } else {
5275
+ await this.publishToDlq(msg, info, error);
5276
+ }
5277
+ }
5278
+ );
4950
5279
  }
4951
- logger = new Logger17("Jetstream:EventRouter");
4952
- subscriptions = [];
4953
- otel;
4954
- serviceName;
4955
- serverEndpoint;
4956
5280
  /**
4957
- * Update the max_deliver thresholds from actual NATS consumer configs.
4958
- * Called after consumers are ensured so the DLQ map reflects reality.
5281
+ * Publish the dead letter to the DLQ stream with diagnostic headers. On
5282
+ * success the `onDeadLetter` callback is notified and the message termed;
5283
+ * on failure everything falls back to the callback to avoid silent loss.
4959
5284
  */
4960
- updateMaxDeliverMap(consumerMaxDelivers) {
4961
- if (!this.deadLetterConfig) return;
4962
- this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
4963
- }
4964
- /** Start routing event, broadcast, and ordered messages to handlers. */
4965
- start() {
4966
- this.subscribeToStream(this.messageProvider.events$, "ev" /* Event */);
4967
- this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast" /* Broadcast */);
4968
- if (this.patternRegistry.hasOrderedHandlers()) {
4969
- this.subscribeToStream(this.messageProvider.ordered$, "ordered" /* Ordered */);
5285
+ async publishToDlq(msg, info, error) {
5286
+ const serviceName = this.options?.name;
5287
+ if (!this.connection || !serviceName) {
5288
+ this.logger.error(
5289
+ `Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
5290
+ );
5291
+ await this.fallbackToOnDeadLetterCallback(info, msg);
5292
+ return;
4970
5293
  }
4971
- }
4972
- /** Stop routing and unsubscribe from all streams. */
4973
- destroy() {
4974
- for (const sub of this.subscriptions) {
4975
- sub.unsubscribe();
5294
+ const dlqStreamOverride = this.options.dlq?.stream?.name;
5295
+ const destinationSubject = this.names ? this.names.dlqStreamName() : dlqStreamOverride ?? dlqStreamName(serviceName);
5296
+ const hdrs = this.buildDlqHeaders(msg);
5297
+ hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, this.extractErrorReason(error));
5298
+ hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
5299
+ hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
5300
+ hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
5301
+ hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
5302
+ try {
5303
+ await this.publishWithRetry(this.connection, destinationSubject, msg.data, hdrs);
5304
+ this.logger.log(`Message sent to DLQ: ${msg.subject}`);
5305
+ await this.notifyDeadLetterCallback(info, msg);
5306
+ } catch (publishErr) {
5307
+ this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
5308
+ await this.fallbackToOnDeadLetterCallback(info, msg);
4976
5309
  }
4977
- this.subscriptions.length = 0;
4978
5310
  }
4979
- /** Subscribe to a message stream and route each message to its handler. */
4980
- subscribeToStream(stream$, kind) {
4981
- const isOrdered = kind === "ordered" /* Ordered */;
4982
- const patternRegistry = this.patternRegistry;
4983
- const codec = this.codec;
4984
- const eventBus = this.eventBus;
4985
- const logger5 = this.logger;
4986
- const deadLetterConfig = this.deadLetterConfig;
4987
- const otel = this.otel;
4988
- const serviceName = this.serviceName;
4989
- const serverEndpoint = this.serverEndpoint;
4990
- const spanKind = eventConsumeKindFor(kind);
4991
- const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
4992
- const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
4993
- const concurrency = this.getConcurrency(kind);
4994
- const hasDlqCheck = deadLetterConfig !== void 0;
4995
- const reportHandlerCompleted = (msg, startedAt, status) => {
4996
- if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
4997
- const declared = patternRegistry.resolveDeclared(msg.subject);
4998
- const pattern = declared?.pattern ?? msg.subject;
4999
- const declaredKind = declared?.kind ?? kind;
5000
- const durationMs = performance.now() - startedAt;
5001
- eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
5002
- };
5003
- const isDeadLetter = (msg) => {
5004
- if (!hasDlqCheck) return false;
5005
- const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
5006
- if (maxDeliver === void 0 || maxDeliver <= 0) return false;
5007
- return msg.info.deliveryCount >= maxDeliver;
5008
- };
5009
- const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
5010
- const settleSuccess = (msg, ctx, data) => {
5011
- if (ctx.shouldTerminate) {
5012
- settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
5013
- msg.term(ctx.terminateReason);
5014
- });
5015
- return void 0;
5016
- }
5017
- if (ctx.shouldRetry) {
5018
- if (handleDeadLetter !== null && isDeadLetter(msg)) {
5019
- return handleDeadLetter(
5020
- msg,
5021
- data,
5022
- new Error("Retry requested on the final delivery attempt")
5311
+ /**
5312
+ * Past max_deliver the server never redelivers, so these in-process attempts
5313
+ * are the only second chance a dead letter gets. No artificial delay: an
5314
+ * unreachable broker already spaces attempts via its own request timeout.
5315
+ */
5316
+ async publishWithRetry(connection, subject, data, headers2) {
5317
+ let lastErr;
5318
+ for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
5319
+ try {
5320
+ await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
5321
+ return;
5322
+ } catch (err) {
5323
+ lastErr = err;
5324
+ if (attempt < DLQ_PUBLISH_ATTEMPTS) {
5325
+ this.logger.warn(
5326
+ `DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
5023
5327
  );
5024
5328
  }
5025
- settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5026
- msg.nak(ctx.retryDelay);
5027
- });
5028
- return void 0;
5029
5329
  }
5030
- settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
5031
- msg.ack();
5032
- });
5033
- return void 0;
5034
- };
5035
- const settleFailure = async (msg, data, err) => {
5036
- if (handleDeadLetter !== null && isDeadLetter(msg)) {
5037
- await handleDeadLetter(msg, data, err);
5038
- return;
5330
+ }
5331
+ throw lastErr;
5332
+ }
5333
+ /**
5334
+ * Copy headers for the DLQ republish, dropping NATS control headers: a
5335
+ * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
5336
+ */
5337
+ buildDlqHeaders(msg) {
5338
+ const hdrs = natsHeaders3();
5339
+ if (!msg.headers) return hdrs;
5340
+ for (const [k, v] of msg.headers) {
5341
+ if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
5342
+ for (const val of v) {
5343
+ hdrs.append(k, val);
5039
5344
  }
5040
- settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5041
- msg.nak();
5042
- });
5043
- };
5044
- const captureUnroutable = (capture, msg, err) => {
5045
- let data;
5345
+ }
5346
+ return hdrs;
5347
+ }
5348
+ async notifyDeadLetterCallback(info, msg) {
5349
+ if (this.deadLetterConfig.onDeadLetter) {
5046
5350
  try {
5047
- data = codec.decode(msg.data);
5048
- } catch {
5049
- data = void 0;
5351
+ await this.deadLetterConfig.onDeadLetter(info);
5352
+ } catch (hookErr) {
5353
+ this.logger.warn(
5354
+ `onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
5355
+ hookErr
5356
+ );
5050
5357
  }
5051
- return capture(msg, data, err).catch((captureErr) => {
5052
- logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
5358
+ }
5359
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5360
+ msg.term("Moved to DLQ stream");
5361
+ });
5362
+ }
5363
+ /**
5364
+ * Last resort: invoke onDeadLetter, then term on success. On failure the
5365
+ * message is nak'd: never redelivered past max_deliver, but preserved.
5366
+ */
5367
+ async fallbackToOnDeadLetterCallback(info, msg) {
5368
+ const onDeadLetter = this.deadLetterConfig.onDeadLetter;
5369
+ if (!onDeadLetter) {
5370
+ this.logger.error(
5371
+ `Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback); leaving the message in the stream`
5372
+ );
5373
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5374
+ msg.nak();
5053
5375
  });
5054
- };
5055
- const resolveEvent = (msg) => {
5056
- const subject = msg.subject;
5057
- try {
5058
- const handler = patternRegistry.getHandler(subject);
5059
- if (!handler) {
5060
- logger5.error(`No handler for subject: ${subject}`);
5061
- if (handleDeadLetter !== null) {
5062
- return captureUnroutable(
5063
- handleDeadLetter,
5064
- msg,
5065
- new Error(`No handler for event: ${subject}`)
5066
- );
5067
- }
5068
- msg.term(`No handler for event: ${subject}`);
5069
- return null;
5070
- }
5071
- let data;
5072
- try {
5073
- data = codec.decode(msg.data);
5074
- } catch (err) {
5075
- logger5.error(`Decode error for ${subject}:`, err);
5076
- if (handleDeadLetter !== null) {
5077
- return captureUnroutable(
5078
- handleDeadLetter,
5079
- msg,
5080
- new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
5081
- );
5082
- }
5083
- msg.term("Decode error");
5084
- return null;
5376
+ return;
5377
+ }
5378
+ try {
5379
+ await onDeadLetter(info);
5380
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5381
+ msg.term("Dead letter processed via fallback callback");
5382
+ });
5383
+ } catch (hookErr) {
5384
+ this.logger.error(
5385
+ `Fallback onDeadLetter callback failed for ${msg.subject}; the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
5386
+ hookErr
5387
+ );
5388
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5389
+ msg.nak();
5390
+ });
5391
+ }
5392
+ }
5393
+ extractErrorReason(error) {
5394
+ if (error instanceof Error) {
5395
+ return error.message;
5396
+ }
5397
+ if (typeof error === "object" && error !== null && "message" in error) {
5398
+ return String(error.message);
5399
+ }
5400
+ return String(error);
5401
+ }
5402
+ };
5403
+
5404
+ // src/server/routing/settlement.ts
5405
+ var statusForContext = (ctx) => {
5406
+ if (ctx.shouldTerminate) return "terminated";
5407
+ if (ctx.shouldRetry) return "retried";
5408
+ return "success";
5409
+ };
5410
+ var createSettlement = (logger5, capture) => {
5411
+ const settleSuccess = (msg, ctx, data) => {
5412
+ if (ctx.shouldTerminate) {
5413
+ settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
5414
+ msg.term(ctx.terminateReason);
5415
+ });
5416
+ return void 0;
5417
+ }
5418
+ if (ctx.shouldRetry) {
5419
+ if (capture?.isFinalDelivery(msg)) {
5420
+ return capture.capture(
5421
+ msg,
5422
+ data,
5423
+ new Error("Retry requested on the final delivery attempt")
5424
+ );
5425
+ }
5426
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5427
+ msg.nak(ctx.retryDelay);
5428
+ });
5429
+ return void 0;
5430
+ }
5431
+ settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
5432
+ msg.ack();
5433
+ });
5434
+ return void 0;
5435
+ };
5436
+ const settleFailure = async (msg, data, err) => {
5437
+ if (capture?.isFinalDelivery(msg)) {
5438
+ await capture.capture(msg, data, err);
5439
+ return;
5440
+ }
5441
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5442
+ msg.nak();
5443
+ });
5444
+ };
5445
+ return { settleSuccess, settleFailure };
5446
+ };
5447
+
5448
+ // src/server/routing/event-pipeline.ts
5449
+ var createHandlerReporter = (rctx) => {
5450
+ const { eventBus, patternRegistry, kind } = rctx;
5451
+ return (msg, startedAt, status) => {
5452
+ if (!eventBus.hasHook("handlerCompleted" /* HandlerCompleted */)) return;
5453
+ const declared = patternRegistry.resolveDeclared(msg.subject);
5454
+ const pattern = declared?.pattern ?? msg.subject;
5455
+ const declaredKind = declared?.kind ?? kind;
5456
+ const durationMs = performance.now() - startedAt;
5457
+ eventBus.emit("handlerCompleted" /* HandlerCompleted */, pattern, declaredKind, durationMs, status);
5458
+ };
5459
+ };
5460
+ var createEventResolver = (rctx) => {
5461
+ const { patternRegistry, codec, eventBus, logger: logger5, capture, kind } = rctx;
5462
+ const captureUnroutable = (activeCapture, msg, err) => {
5463
+ let data;
5464
+ try {
5465
+ data = codec.decode(msg.data);
5466
+ } catch {
5467
+ data = void 0;
5468
+ }
5469
+ return activeCapture.capture(msg, data, err).catch((captureErr) => {
5470
+ logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
5471
+ });
5472
+ };
5473
+ return (msg) => {
5474
+ const subject = msg.subject;
5475
+ try {
5476
+ const handler = patternRegistry.getHandler(subject);
5477
+ if (!handler) {
5478
+ logger5.error(`No handler for subject: ${subject}`);
5479
+ if (capture !== null) {
5480
+ return captureUnroutable(capture, msg, new Error(`No handler for event: ${subject}`));
5085
5481
  }
5086
- eventBus.emitMessageRouted(subject, "event" /* Event */);
5087
- return { handler, data };
5482
+ msg.term(`No handler for event: ${subject}`);
5483
+ return null;
5484
+ }
5485
+ let data;
5486
+ try {
5487
+ data = codec.decode(msg.data);
5088
5488
  } catch (err) {
5089
- logger5.error(`Unexpected error in ${kind} event router`, err);
5090
- try {
5091
- msg.term("Unexpected router error");
5092
- } catch (termErr) {
5093
- logger5.error(`Failed to terminate message ${subject}:`, termErr);
5489
+ logger5.error(`Decode error for ${subject}:`, err);
5490
+ if (capture !== null) {
5491
+ return captureUnroutable(
5492
+ capture,
5493
+ msg,
5494
+ new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
5495
+ );
5094
5496
  }
5497
+ msg.term("Decode error");
5095
5498
  return null;
5096
5499
  }
5097
- };
5098
- const statusForContext = (ctx) => {
5099
- if (ctx.shouldTerminate) return "terminated";
5100
- if (ctx.shouldRetry) return "retried";
5101
- return "success";
5102
- };
5103
- const handleSafe = (msg) => {
5104
- const resolved = resolveEvent(msg);
5105
- if (resolved === null) return void 0;
5106
- if (isPromiseLike2(resolved)) return resolved;
5107
- const { handler, data } = resolved;
5108
- const ctx = new RpcContext([msg]);
5109
- const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
5110
- const startedAt = performance.now();
5111
- let pending;
5500
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
5501
+ return { handler, data };
5502
+ } catch (err) {
5503
+ logger5.error(`Unexpected error in ${kind} event router`, err);
5112
5504
  try {
5113
- pending = withConsumeSpan(
5114
- {
5115
- subject: msg.subject,
5116
- msg,
5117
- info: msg.info,
5118
- kind: spanKind,
5119
- payloadBytes: msg.data.length,
5120
- handlerMetadata: { pattern: msg.subject },
5121
- serviceName,
5122
- endpoint: serverEndpoint
5123
- },
5124
- otel,
5125
- () => unwrapResult(handler(data, ctx))
5126
- );
5127
- } catch (err) {
5505
+ msg.term("Unexpected router error");
5506
+ } catch (termErr) {
5507
+ logger5.error(`Failed to terminate message ${subject}:`, termErr);
5508
+ }
5509
+ return null;
5510
+ }
5511
+ };
5512
+ };
5513
+ var createWorkqueuePipeline = (rctx) => {
5514
+ const { kind, spanKind, logger: logger5, eventBus, otel, serviceName, serverEndpoint } = rctx;
5515
+ const { ackExtensionInterval } = rctx;
5516
+ const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
5517
+ const reportHandlerCompleted = createHandlerReporter(rctx);
5518
+ const resolveEvent = createEventResolver(rctx);
5519
+ const { settleSuccess, settleFailure } = createSettlement(logger5, rctx.capture);
5520
+ return (msg) => {
5521
+ const resolved = resolveEvent(msg);
5522
+ if (resolved === null) return void 0;
5523
+ if (isPromiseLike2(resolved)) return resolved;
5524
+ const { handler, data } = resolved;
5525
+ const ctx = new RpcContext([msg]);
5526
+ const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
5527
+ const startedAt = performance.now();
5528
+ let pending;
5529
+ try {
5530
+ pending = withConsumeSpan(
5531
+ {
5532
+ subject: msg.subject,
5533
+ msg,
5534
+ info: msg.info,
5535
+ kind: spanKind,
5536
+ payloadBytes: msg.data.length,
5537
+ handlerMetadata: { pattern: msg.subject },
5538
+ serviceName,
5539
+ endpoint: serverEndpoint
5540
+ },
5541
+ otel,
5542
+ () => unwrapResult(handler(data, ctx))
5543
+ );
5544
+ } catch (err) {
5545
+ eventBus.emit(
5546
+ "error" /* Error */,
5547
+ err instanceof Error ? err : new Error(String(err)),
5548
+ `${kind}-handler:${msg.subject}`
5549
+ );
5550
+ reportHandlerCompleted(msg, startedAt, "error");
5551
+ return settleFailure(msg, data, err).finally(() => {
5552
+ if (stopAckExtension !== null) stopAckExtension();
5553
+ });
5554
+ }
5555
+ if (!isPromiseLike2(pending)) {
5556
+ const settled = settleSuccess(msg, ctx, data);
5557
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5558
+ if (settled === void 0) {
5559
+ if (stopAckExtension !== null) stopAckExtension();
5560
+ return void 0;
5561
+ }
5562
+ return settled.finally(() => {
5563
+ if (stopAckExtension !== null) stopAckExtension();
5564
+ });
5565
+ }
5566
+ return pending.then(
5567
+ async () => {
5568
+ try {
5569
+ await settleSuccess(msg, ctx, data);
5570
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5571
+ } finally {
5572
+ if (stopAckExtension !== null) stopAckExtension();
5573
+ }
5574
+ },
5575
+ async (err) => {
5128
5576
  eventBus.emit(
5129
5577
  "error" /* Error */,
5130
5578
  err instanceof Error ? err : new Error(String(err)),
5131
5579
  `${kind}-handler:${msg.subject}`
5132
5580
  );
5133
5581
  reportHandlerCompleted(msg, startedAt, "error");
5134
- return settleFailure(msg, data, err).finally(() => {
5135
- if (stopAckExtension !== null) stopAckExtension();
5136
- });
5137
- }
5138
- if (!isPromiseLike2(pending)) {
5139
- const settled = settleSuccess(msg, ctx, data);
5140
- reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5141
- if (settled === void 0) {
5582
+ try {
5583
+ await settleFailure(msg, data, err);
5584
+ } finally {
5142
5585
  if (stopAckExtension !== null) stopAckExtension();
5143
- return void 0;
5144
5586
  }
5145
- return settled.finally(() => {
5146
- if (stopAckExtension !== null) stopAckExtension();
5147
- });
5148
5587
  }
5149
- return pending.then(
5150
- async () => {
5151
- try {
5152
- await settleSuccess(msg, ctx, data);
5153
- reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5154
- } finally {
5155
- if (stopAckExtension !== null) stopAckExtension();
5156
- }
5157
- },
5158
- async (err) => {
5159
- eventBus.emit(
5160
- "error" /* Error */,
5161
- err instanceof Error ? err : new Error(String(err)),
5162
- `${kind}-handler:${msg.subject}`
5163
- );
5164
- reportHandlerCompleted(msg, startedAt, "error");
5165
- try {
5166
- await settleFailure(msg, data, err);
5167
- } finally {
5168
- if (stopAckExtension !== null) stopAckExtension();
5169
- }
5170
- }
5171
- );
5172
- };
5173
- const handleOrderedSafe = (msg) => {
5174
- const subject = msg.subject;
5175
- let handler;
5176
- let data;
5177
- try {
5178
- handler = patternRegistry.getHandler(subject);
5179
- if (!handler) {
5180
- logger5.error(`No handler for subject: ${subject}`);
5181
- return void 0;
5182
- }
5183
- try {
5184
- data = codec.decode(msg.data);
5185
- } catch (err) {
5186
- logger5.error(`Decode error for ${subject}:`, err);
5187
- return void 0;
5188
- }
5189
- eventBus.emitMessageRouted(subject, "event" /* Event */);
5190
- } catch (err) {
5191
- logger5.error(`Ordered handler error (${subject}):`, err);
5588
+ );
5589
+ };
5590
+ };
5591
+ var createOrderedPipeline = (rctx) => {
5592
+ const { spanKind, codec, logger: logger5, eventBus, patternRegistry, otel } = rctx;
5593
+ const { serviceName, serverEndpoint } = rctx;
5594
+ const reportHandlerCompleted = createHandlerReporter(rctx);
5595
+ return (msg) => {
5596
+ const subject = msg.subject;
5597
+ let handler;
5598
+ let data;
5599
+ try {
5600
+ handler = patternRegistry.getHandler(subject);
5601
+ if (!handler) {
5602
+ logger5.error(`No handler for subject: ${subject}`);
5192
5603
  return void 0;
5193
5604
  }
5194
- const ctx = new RpcContext([msg]);
5195
- const warnIfSettlementAttempted = () => {
5196
- if (ctx.shouldRetry || ctx.shouldTerminate) {
5197
- logger5.warn(
5198
- `retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
5199
- );
5200
- }
5201
- };
5202
- const startedAt = performance.now();
5203
- let pending;
5204
5605
  try {
5205
- pending = withConsumeSpan(
5206
- {
5207
- subject: msg.subject,
5208
- msg,
5209
- info: msg.info,
5210
- kind: spanKind,
5211
- payloadBytes: msg.data.length,
5212
- handlerMetadata: { pattern: msg.subject },
5213
- serviceName,
5214
- endpoint: serverEndpoint
5215
- },
5216
- otel,
5217
- () => unwrapResult(handler(data, ctx))
5218
- );
5606
+ data = codec.decode(msg.data);
5219
5607
  } catch (err) {
5220
- logger5.error(`Ordered handler error (${subject}):`, err);
5221
- reportHandlerCompleted(msg, startedAt, "error");
5608
+ logger5.error(`Decode error for ${subject}:`, err);
5222
5609
  return void 0;
5223
5610
  }
5224
- if (!isPromiseLike2(pending)) {
5225
- warnIfSettlementAttempted();
5226
- reportHandlerCompleted(msg, startedAt, "success");
5227
- return void 0;
5611
+ eventBus.emitMessageRouted(subject, "event" /* Event */);
5612
+ } catch (err) {
5613
+ logger5.error(`Ordered handler error (${subject}):`, err);
5614
+ return void 0;
5615
+ }
5616
+ const ctx = new RpcContext([msg]);
5617
+ const warnIfSettlementAttempted = () => {
5618
+ if (ctx.shouldRetry || ctx.shouldTerminate) {
5619
+ logger5.warn(
5620
+ `retry()/terminate() ignored for ordered message ${subject}; ordered consumers auto-acknowledge`
5621
+ );
5228
5622
  }
5229
- return pending.then(
5230
- () => {
5231
- warnIfSettlementAttempted();
5232
- reportHandlerCompleted(msg, startedAt, "success");
5623
+ };
5624
+ const startedAt = performance.now();
5625
+ let pending;
5626
+ try {
5627
+ pending = withConsumeSpan(
5628
+ {
5629
+ subject: msg.subject,
5630
+ msg,
5631
+ info: msg.info,
5632
+ kind: spanKind,
5633
+ payloadBytes: msg.data.length,
5634
+ handlerMetadata: { pattern: msg.subject },
5635
+ serviceName,
5636
+ endpoint: serverEndpoint
5233
5637
  },
5234
- (err) => {
5235
- logger5.error(`Ordered handler error (${subject}):`, err);
5236
- reportHandlerCompleted(msg, startedAt, "error");
5237
- }
5638
+ otel,
5639
+ () => unwrapResult(handler(data, ctx))
5238
5640
  );
5239
- };
5240
- const route = isOrdered ? handleOrderedSafe : handleSafe;
5241
- const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
5242
- const backlogWarnThreshold = 1e3;
5243
- let active = 0;
5244
- let backlogWarned = false;
5245
- const backlog = [];
5246
- const onAsyncDone = () => {
5247
- active--;
5248
- drainBacklog();
5249
- };
5250
- const routeSafely = (msg) => {
5251
- try {
5252
- return route(msg);
5253
- } catch (err) {
5254
- logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5255
- return void 0;
5256
- }
5257
- };
5258
- const trackAsync = (result, msg) => {
5259
- void result.catch((err) => {
5260
- logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5261
- }).finally(onAsyncDone);
5262
- };
5263
- const drainBacklog = () => {
5264
- while (active < maxActive) {
5265
- const next = backlog.shift();
5266
- if (next === void 0) return;
5267
- next.stopAckExtension?.();
5268
- active++;
5269
- const result = routeSafely(next.msg);
5270
- if (result !== void 0) {
5271
- trackAsync(result, next.msg);
5272
- } else {
5273
- active--;
5274
- }
5641
+ } catch (err) {
5642
+ logger5.error(`Ordered handler error (${subject}):`, err);
5643
+ reportHandlerCompleted(msg, startedAt, "error");
5644
+ return void 0;
5645
+ }
5646
+ if (!isPromiseLike2(pending)) {
5647
+ warnIfSettlementAttempted();
5648
+ reportHandlerCompleted(msg, startedAt, "success");
5649
+ return void 0;
5650
+ }
5651
+ return pending.then(
5652
+ () => {
5653
+ warnIfSettlementAttempted();
5654
+ reportHandlerCompleted(msg, startedAt, "success");
5655
+ },
5656
+ (err) => {
5657
+ logger5.error(`Ordered handler error (${subject}):`, err);
5658
+ reportHandlerCompleted(msg, startedAt, "error");
5275
5659
  }
5276
- if (backlog.length < backlogWarnThreshold) backlogWarned = false;
5660
+ );
5661
+ };
5662
+ };
5663
+
5664
+ // src/server/routing/event.router.ts
5665
+ var eventConsumeKindFor = (kind) => {
5666
+ if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
5667
+ if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
5668
+ return "event" /* Event */;
5669
+ };
5670
+ var EventRouter = class {
5671
+ constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options, names) {
5672
+ this.messageProvider = messageProvider;
5673
+ this.patternRegistry = patternRegistry;
5674
+ this.codec = codec;
5675
+ this.eventBus = eventBus;
5676
+ this.deadLetterConfig = deadLetterConfig;
5677
+ this.processingConfig = processingConfig;
5678
+ this.ackWaitMap = ackWaitMap;
5679
+ if (options) {
5680
+ const derived = deriveOtelAttrs(options);
5681
+ this.otel = derived.otel;
5682
+ this.serviceName = derived.serviceName;
5683
+ this.serverEndpoint = derived.serverEndpoint;
5684
+ } else {
5685
+ this.otel = resolveOtelOptions({ enabled: false });
5686
+ this.serviceName = "";
5687
+ this.serverEndpoint = null;
5688
+ }
5689
+ this.capture = deadLetterConfig ? new DeadLetterCapture(
5690
+ patternRegistry,
5691
+ eventBus,
5692
+ deadLetterConfig,
5693
+ this.otel,
5694
+ this.serviceName,
5695
+ this.serverEndpoint,
5696
+ connection,
5697
+ options,
5698
+ names
5699
+ ) : null;
5700
+ }
5701
+ logger = new Logger19("Jetstream:EventRouter");
5702
+ subscriptions = [];
5703
+ otel;
5704
+ serviceName;
5705
+ serverEndpoint;
5706
+ capture;
5707
+ /**
5708
+ * Update the max_deliver thresholds from actual NATS consumer configs.
5709
+ * Called after consumers are ensured so the DLQ map reflects reality.
5710
+ */
5711
+ updateMaxDeliverMap(consumerMaxDelivers) {
5712
+ if (!this.deadLetterConfig) return;
5713
+ this.deadLetterConfig.maxDeliverByStream = consumerMaxDelivers;
5714
+ }
5715
+ /** Start routing event, broadcast, and ordered messages to handlers. */
5716
+ start() {
5717
+ this.subscribeToStream(this.messageProvider.events$, "ev" /* Event */);
5718
+ this.subscribeToStream(this.messageProvider.broadcasts$, "broadcast" /* Broadcast */);
5719
+ if (this.patternRegistry.hasOrderedHandlers()) {
5720
+ this.subscribeToStream(this.messageProvider.ordered$, "ordered" /* Ordered */);
5721
+ }
5722
+ }
5723
+ /** Stop routing and unsubscribe from all streams. */
5724
+ destroy() {
5725
+ for (const sub of this.subscriptions) {
5726
+ sub.unsubscribe();
5727
+ }
5728
+ this.subscriptions.length = 0;
5729
+ }
5730
+ /** Assemble the pipeline and concurrency gate for one stream and subscribe. */
5731
+ subscribeToStream(stream$, kind) {
5732
+ const isOrdered = kind === "ordered" /* Ordered */;
5733
+ const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
5734
+ const rctx = {
5735
+ kind,
5736
+ spanKind: eventConsumeKindFor(kind),
5737
+ codec: this.codec,
5738
+ logger: this.logger,
5739
+ eventBus: this.eventBus,
5740
+ patternRegistry: this.patternRegistry,
5741
+ otel: this.otel,
5742
+ serviceName: this.serviceName,
5743
+ serverEndpoint: this.serverEndpoint,
5744
+ ackExtensionInterval,
5745
+ capture: this.capture
5277
5746
  };
5747
+ const route = isOrdered ? createOrderedPipeline(rctx) : createWorkqueuePipeline(rctx);
5748
+ const maxActive = isOrdered ? 1 : this.getConcurrency(kind) ?? Number.POSITIVE_INFINITY;
5749
+ const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
5750
+ const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
5751
+ const gate = new ConcurrencyGate(maxActive, route, parkTimer, this.logger, kind);
5278
5752
  const subscription = stream$.subscribe({
5279
- next: (msg) => {
5280
- if (active >= maxActive) {
5281
- backlog.push({
5282
- msg,
5283
- stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5284
- });
5285
- if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
5286
- backlogWarned = true;
5287
- logger5.warn(
5288
- `${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
5289
- );
5290
- }
5291
- return;
5292
- }
5293
- active++;
5294
- const result = routeSafely(msg);
5295
- if (result !== void 0) {
5296
- trackAsync(result, msg);
5297
- } else {
5298
- active--;
5299
- if (backlog.length > 0) drainBacklog();
5300
- }
5753
+ next: (msg) => {
5754
+ gate.push(msg);
5301
5755
  },
5302
5756
  error: (err) => {
5303
- logger5.error(`Stream error in ${kind} router`, err);
5757
+ this.logger.error(`Stream error in ${kind} router`, err);
5304
5758
  }
5305
5759
  });
5306
5760
  subscription.add(() => {
5307
- for (const queued of backlog) {
5308
- queued.stopAckExtension?.();
5309
- }
5310
- backlog.length = 0;
5761
+ gate.dispose();
5311
5762
  });
5312
5763
  this.subscriptions.push(subscription);
5313
5764
  }
@@ -5321,169 +5772,10 @@ var EventRouter = class {
5321
5772
  if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
5322
5773
  return void 0;
5323
5774
  }
5324
- /**
5325
- * Last resort: invoke onDeadLetter, then term on success. On failure the
5326
- * message is nak'd — never redelivered past max_deliver, but preserved.
5327
- */
5328
- async fallbackToOnDeadLetterCallback(info, msg) {
5329
- const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
5330
- if (!onDeadLetter) {
5331
- this.logger.error(
5332
- `Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
5333
- );
5334
- settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5335
- msg.nak();
5336
- });
5337
- return;
5338
- }
5339
- try {
5340
- await onDeadLetter(info);
5341
- settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5342
- msg.term("Dead letter processed via fallback callback");
5343
- });
5344
- } catch (hookErr) {
5345
- this.logger.error(
5346
- `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:`,
5347
- hookErr
5348
- );
5349
- settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5350
- msg.nak();
5351
- });
5352
- }
5353
- }
5354
- /**
5355
- * Copy headers for the DLQ republish, dropping NATS control headers — a
5356
- * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
5357
- */
5358
- buildDlqHeaders(msg) {
5359
- const hdrs = natsHeaders3();
5360
- if (!msg.headers) return hdrs;
5361
- for (const [k, v] of msg.headers) {
5362
- if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
5363
- for (const val of v) {
5364
- hdrs.append(k, val);
5365
- }
5366
- }
5367
- return hdrs;
5368
- }
5369
- /**
5370
- * Past max_deliver the server never redelivers, so these in-process attempts
5371
- * are the only second chance a dead letter gets. No artificial delay — an
5372
- * unreachable broker already spaces attempts via its own request timeout.
5373
- */
5374
- async publishToDlqWithRetry(connection, subject, data, headers2) {
5375
- let lastErr;
5376
- for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
5377
- try {
5378
- await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
5379
- return;
5380
- } catch (err) {
5381
- lastErr = err;
5382
- if (attempt < DLQ_PUBLISH_ATTEMPTS) {
5383
- this.logger.warn(
5384
- `DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
5385
- );
5386
- }
5387
- }
5388
- }
5389
- throw lastErr;
5390
- }
5391
- /**
5392
- * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
5393
- *
5394
- * Appends diagnostic metadata headers to the original message and preserves
5395
- * the primary payload. If publishing succeeds, it notifies the standard
5396
- * `onDeadLetter` callback and terminates the message. If it fails, it falls
5397
- * back to the callback entirely to prevent silent data loss.
5398
- */
5399
- async publishToDlq(msg, info, error) {
5400
- const serviceName = this.options?.name;
5401
- if (!this.connection || !serviceName) {
5402
- this.logger.error(
5403
- `Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
5404
- );
5405
- await this.fallbackToOnDeadLetterCallback(info, msg);
5406
- return;
5407
- }
5408
- const destinationSubject = dlqStreamName(serviceName);
5409
- const hdrs = this.buildDlqHeaders(msg);
5410
- let reason = String(error);
5411
- if (error instanceof Error) {
5412
- reason = error.message;
5413
- } else if (typeof error === "object" && error !== null && "message" in error) {
5414
- reason = String(error.message);
5415
- }
5416
- hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
5417
- hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
5418
- hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
5419
- hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
5420
- hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
5421
- try {
5422
- await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
5423
- this.logger.log(`Message sent to DLQ: ${msg.subject}`);
5424
- if (this.deadLetterConfig?.onDeadLetter) {
5425
- try {
5426
- await this.deadLetterConfig.onDeadLetter(info);
5427
- } catch (hookErr) {
5428
- this.logger.warn(
5429
- `onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
5430
- hookErr
5431
- );
5432
- }
5433
- }
5434
- settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5435
- msg.term("Moved to DLQ stream");
5436
- });
5437
- } catch (publishErr) {
5438
- this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
5439
- await this.fallbackToOnDeadLetterCallback(info, msg);
5440
- }
5441
- }
5442
- /**
5443
- * Orchestrates the handling of a message that has exhausted delivery limits.
5444
- *
5445
- * Emits a system event and delegates either to the robust DLQ stream publisher
5446
- * or directly to the fallback callback based on the active module configuration.
5447
- */
5448
- async handleDeadLetter(msg, data, error) {
5449
- const info = {
5450
- subject: msg.subject,
5451
- data,
5452
- headers: msg.headers,
5453
- error,
5454
- deliveryCount: msg.info.deliveryCount,
5455
- stream: msg.info.stream,
5456
- streamSequence: msg.info.streamSequence,
5457
- timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
5458
- };
5459
- await withDeadLetterSpan(
5460
- {
5461
- msg,
5462
- // Pattern resolution mirrors event-routing: when a registered
5463
- // pattern matches, surface it on the DLQ span so APM can filter
5464
- // dead letters by handler without parsing the subject. Falls back
5465
- // to the subject itself when no glob handler is in play.
5466
- pattern: this.patternRegistry.getHandler(msg.subject) ? msg.subject : void 0,
5467
- finalDeliveryCount: msg.info.deliveryCount,
5468
- reason: error instanceof Error ? error.message : String(error),
5469
- serviceName: this.serviceName,
5470
- endpoint: this.serverEndpoint
5471
- },
5472
- this.otel,
5473
- async () => {
5474
- this.eventBus.emit("deadLetter" /* DeadLetter */, info);
5475
- if (!this.options?.dlq) {
5476
- await this.fallbackToOnDeadLetterCallback(info, msg);
5477
- } else {
5478
- await this.publishToDlq(msg, info, error);
5479
- }
5480
- }
5481
- );
5482
- }
5483
5775
  };
5484
5776
 
5485
5777
  // src/server/routing/rpc.router.ts
5486
- import { Logger as Logger18 } from "@nestjs/common";
5778
+ import { Logger as Logger20 } from "@nestjs/common";
5487
5779
  import { headers } from "@nats-io/transport-node";
5488
5780
  var RpcRouter = class {
5489
5781
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap, options) {
@@ -5507,7 +5799,7 @@ var RpcRouter = class {
5507
5799
  this.serverEndpoint = null;
5508
5800
  }
5509
5801
  }
5510
- logger = new Logger18("Jetstream:RpcRouter");
5802
+ logger = new Logger20("Jetstream:RpcRouter");
5511
5803
  timeout;
5512
5804
  concurrency;
5513
5805
  resolvedAckExtensionInterval;
@@ -5698,75 +5990,18 @@ var RpcRouter = class {
5698
5990
  }
5699
5991
  );
5700
5992
  };
5701
- const backlogWarnThreshold = 1e3;
5702
- let active = 0;
5703
- let backlogWarned = false;
5704
- const backlog = [];
5705
- const onAsyncDone = () => {
5706
- active--;
5707
- drainBacklog();
5708
- };
5709
- const routeSafely = (msg) => {
5710
- try {
5711
- return handleSafe(msg);
5712
- } catch (err) {
5713
- logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5714
- return void 0;
5715
- }
5716
- };
5717
- const trackAsync = (result, msg) => {
5718
- void result.catch((err) => {
5719
- logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5720
- }).finally(onAsyncDone);
5721
- };
5722
- const drainBacklog = () => {
5723
- while (active < maxActive) {
5724
- const next = backlog.shift();
5725
- if (next === void 0) return;
5726
- next.stopAckExtension?.();
5727
- active++;
5728
- const result = routeSafely(next.msg);
5729
- if (result !== void 0) {
5730
- trackAsync(result, next.msg);
5731
- } else {
5732
- active--;
5733
- }
5734
- }
5735
- if (backlog.length < backlogWarnThreshold) backlogWarned = false;
5736
- };
5993
+ const parkTimer = hasAckExtension ? (msg) => startAckExtensionTimer(msg, ackExtensionInterval) : null;
5994
+ const gate = new ConcurrencyGate(maxActive, handleSafe, parkTimer, logger5, "RPC");
5737
5995
  this.subscription = this.messageProvider.commands$.subscribe({
5738
5996
  next: (msg) => {
5739
- if (active >= maxActive) {
5740
- backlog.push({
5741
- msg,
5742
- stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5743
- });
5744
- if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
5745
- backlogWarned = true;
5746
- logger5.warn(
5747
- `RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
5748
- );
5749
- }
5750
- return;
5751
- }
5752
- active++;
5753
- const result = routeSafely(msg);
5754
- if (result !== void 0) {
5755
- trackAsync(result, msg);
5756
- } else {
5757
- active--;
5758
- if (backlog.length > 0) drainBacklog();
5759
- }
5997
+ gate.push(msg);
5760
5998
  },
5761
5999
  error: (err) => {
5762
6000
  logger5.error("Stream error in RPC router", err);
5763
6001
  }
5764
6002
  });
5765
6003
  this.subscription.add(() => {
5766
- for (const queued of backlog) {
5767
- queued.stopAckExtension?.();
5768
- }
5769
- backlog.length = 0;
6004
+ gate.dispose();
5770
6005
  });
5771
6006
  }
5772
6007
  /** Stop routing and unsubscribe. */
@@ -5776,20 +6011,220 @@ var RpcRouter = class {
5776
6011
  }
5777
6012
  };
5778
6013
 
6014
+ // src/server/infrastructure/infrastructure-binder.ts
6015
+ import { Logger as Logger21 } from "@nestjs/common";
6016
+ import { JetStreamApiError as JetStreamApiError4, RetentionPolicy as RetentionPolicy3 } from "@nats-io/jetstream";
6017
+ var WORKQUEUE_KINDS = /* @__PURE__ */ new Set(["ev" /* Event */, "cmd" /* Command */]);
6018
+ var manualRemediation = (entity) => `Management mode is Manual; the ${entity} must be provisioned externally before boot.`;
6019
+ var isSchedulingEnabled = (options, kind) => kindOptionsBlock(options, kind)?.stream?.allow_msg_schedules === true;
6020
+ var resolveAckExtension = (options, kind) => kindOptionsBlock(options, kind)?.ackExtension;
6021
+ var filterCoversSubject = (filter_subject, filter_subjects, subject) => {
6022
+ if (filter_subject !== void 0) {
6023
+ return coversOrEquals(filter_subject, subject);
6024
+ }
6025
+ if (filter_subjects !== void 0) {
6026
+ return filter_subjects.some((f) => coversOrEquals(f, subject));
6027
+ }
6028
+ return true;
6029
+ };
6030
+ var InfrastructureBinder = class {
6031
+ constructor(options, names, registry) {
6032
+ this.options = options;
6033
+ this.names = names;
6034
+ this.registry = registry;
6035
+ }
6036
+ logger = new Logger21("Jetstream:Binder");
6037
+ async bindStream(jsm, kind) {
6038
+ const name = this.names.streamName(kind);
6039
+ const info = await this.fetchStream(jsm, name, kind);
6040
+ await this.warnOnOrphanedMigrationBackup(jsm, name);
6041
+ if (isSchedulingEnabled(this.options, kind)) {
6042
+ this.assertScheduleCoverage(info, kind);
6043
+ this.warnOnSchedulesDisabled(info, kind);
6044
+ }
6045
+ if (WORKQUEUE_KINDS.has(kind)) {
6046
+ this.warnOnRetention(info, kind);
6047
+ }
6048
+ return info;
6049
+ }
6050
+ async bindDlqStream(jsm) {
6051
+ const dlqName = this.names.dlqStreamName();
6052
+ const info = await this.fetchStream(jsm, dlqName, "dlq");
6053
+ await this.warnOnOrphanedMigrationBackup(jsm, dlqName);
6054
+ this.assertDlqSubjectCoverage(info);
6055
+ return info;
6056
+ }
6057
+ async bindConsumer(jsm, kind) {
6058
+ const info = await this.fetchConsumer(jsm, kind);
6059
+ this.assertHandlersCovered(info, kind);
6060
+ this.assertScheduleHoldersNotConsumed(info, kind);
6061
+ this.warnOnUnlimitedDelivery(info, kind);
6062
+ this.warnOnShortAckWait(info, kind);
6063
+ return info;
6064
+ }
6065
+ async fetchStream(jsm, name, kind) {
6066
+ try {
6067
+ return await jsm.streams.info(name);
6068
+ } catch (err) {
6069
+ if (err instanceof JetStreamApiError4 && err.apiError().err_code === 10059 /* StreamNotFound */) {
6070
+ const api = err.apiError();
6071
+ throw new JetstreamProvisioningError({
6072
+ entity: "stream",
6073
+ target: name,
6074
+ kind: String(kind),
6075
+ errCode: api.err_code,
6076
+ errDescription: api.description,
6077
+ remediation: manualRemediation("stream"),
6078
+ cause: err
6079
+ });
6080
+ }
6081
+ throw err;
6082
+ }
6083
+ }
6084
+ async fetchConsumer(jsm, kind) {
6085
+ const stream = this.names.streamName(kind);
6086
+ const consumer = this.names.consumerName(kind);
6087
+ try {
6088
+ return await jsm.consumers.info(stream, consumer);
6089
+ } catch (err) {
6090
+ if (err instanceof JetStreamApiError4 && err.apiError().err_code === 10014 /* ConsumerNotFound */) {
6091
+ const api = err.apiError();
6092
+ throw new JetstreamProvisioningError({
6093
+ entity: "consumer",
6094
+ target: `${consumer} on stream "${stream}"`,
6095
+ kind: String(kind),
6096
+ errCode: api.err_code,
6097
+ errDescription: api.description,
6098
+ remediation: manualRemediation("consumer"),
6099
+ cause: err
6100
+ });
6101
+ }
6102
+ throw err;
6103
+ }
6104
+ }
6105
+ assertHandlersCovered(info, kind) {
6106
+ const subjects = this.resolveHandlerSubjects(kind);
6107
+ if (subjects.length === 0) return;
6108
+ const { filter_subject, filter_subjects } = info.config;
6109
+ const uncovered = subjects.filter(
6110
+ (s) => !filterCoversSubject(filter_subject, filter_subjects, s)
6111
+ );
6112
+ if (uncovered.length > 0) {
6113
+ throw new Error(
6114
+ `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.`
6115
+ );
6116
+ }
6117
+ }
6118
+ assertDlqSubjectCoverage(info) {
6119
+ const dlqSubject = this.names.dlqStreamName();
6120
+ const covered = info.config.subjects.some((s) => coversOrEquals(s, dlqSubject));
6121
+ if (!covered) {
6122
+ throw new Error(
6123
+ `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.`
6124
+ );
6125
+ }
6126
+ }
6127
+ assertScheduleCoverage(info, kind) {
6128
+ const scheduleWildcard = `${this.names.schedulePrefix(kind)}>`;
6129
+ const covered = info.config.subjects.some((s) => coversOrEquals(s, scheduleWildcard));
6130
+ if (!covered) {
6131
+ throw new Error(
6132
+ `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.`
6133
+ );
6134
+ }
6135
+ }
6136
+ assertScheduleHoldersNotConsumed(info, kind) {
6137
+ if (!isSchedulingEnabled(this.options, kind)) return;
6138
+ const scheduleWildcard = `${this.names.schedulePrefix(kind)}>`;
6139
+ const { filter_subject, filter_subjects } = info.config;
6140
+ const filters = filter_subjects ?? (filter_subject !== void 0 ? [filter_subject] : []);
6141
+ const swallowing = filters.length === 0 ? ["<no filter, consumes the whole stream>"] : filters.filter((f) => coversOrEquals(f, scheduleWildcard));
6142
+ if (swallowing.length > 0) {
6143
+ throw new Error(
6144
+ `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.`
6145
+ );
6146
+ }
6147
+ }
6148
+ async warnOnOrphanedMigrationBackup(jsm, streamName2) {
6149
+ const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
6150
+ try {
6151
+ await jsm.streams.info(backupName);
6152
+ } catch {
6153
+ return;
6154
+ }
6155
+ this.logger.warn(
6156
+ `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.`
6157
+ );
6158
+ }
6159
+ warnOnSchedulesDisabled(info, kind) {
6160
+ if (info.config.allow_msg_schedules === true) return;
6161
+ this.logger.warn(
6162
+ `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.`
6163
+ );
6164
+ }
6165
+ warnOnRetention(info, kind) {
6166
+ if (info.config.retention !== RetentionPolicy3.Workqueue) {
6167
+ this.logger.warn(
6168
+ `Stream "${this.names.streamName(kind)}" (kind=${String(kind)}) retention is "${String(info.config.retention)}"; expected "workqueue" for reliable at-least-once delivery.`
6169
+ );
6170
+ }
6171
+ }
6172
+ warnOnUnlimitedDelivery(info, kind) {
6173
+ if (!this.options.dlq) return;
6174
+ const maxDeliver = info.config.max_deliver;
6175
+ if (maxDeliver === void 0 || maxDeliver <= 0) {
6176
+ this.logger.warn(
6177
+ `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.`
6178
+ );
6179
+ }
6180
+ }
6181
+ warnOnShortAckWait(info, kind) {
6182
+ const ackExtConfig = resolveAckExtension(this.options, kind);
6183
+ if (ackExtConfig === void 0 || ackExtConfig === false) return;
6184
+ const ackWaitNanos = info.config.ack_wait;
6185
+ const intervalMs = resolveAckExtensionInterval(ackExtConfig, ackWaitNanos);
6186
+ if (intervalMs === null) return;
6187
+ const ackWaitMs = ackWaitNanos !== void 0 ? ackWaitNanos / 1e6 : void 0;
6188
+ if (ackWaitMs !== void 0 && ackWaitMs < intervalMs) {
6189
+ this.logger.warn(
6190
+ `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.`
6191
+ );
6192
+ }
6193
+ }
6194
+ resolveHandlerSubjects(kind) {
6195
+ const patterns = this.registry.getPatternsByKind();
6196
+ switch (kind) {
6197
+ case "ev" /* Event */:
6198
+ return patterns.events.map((p) => this.names.subject("ev" /* Event */, p));
6199
+ case "cmd" /* Command */:
6200
+ return patterns.commands.map((p) => this.names.subject("cmd" /* Command */, p));
6201
+ case "broadcast" /* Broadcast */:
6202
+ return this.registry.getBroadcastPatterns();
6203
+ case "ordered" /* Ordered */:
6204
+ return this.registry.getOrderedSubjects();
6205
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
6206
+ default: {
6207
+ const _exhaustive = kind;
6208
+ throw new Error(`Unhandled StreamKind: ${String(_exhaustive)}`);
6209
+ }
6210
+ }
6211
+ }
6212
+ };
6213
+
5779
6214
  // src/shutdown/shutdown.manager.ts
5780
- import { Logger as Logger19 } from "@nestjs/common";
6215
+ import { Logger as Logger22 } from "@nestjs/common";
5781
6216
  var ShutdownManager = class {
5782
6217
  constructor(connection, eventBus, timeout) {
5783
6218
  this.connection = connection;
5784
6219
  this.eventBus = eventBus;
5785
6220
  this.timeout = timeout;
5786
6221
  }
5787
- logger = new Logger19("Jetstream:Shutdown");
6222
+ logger = new Logger22("Jetstream:Shutdown");
5788
6223
  shutdownPromise;
5789
6224
  /**
5790
6225
  * Execute the full shutdown sequence.
5791
6226
  *
5792
- * Idempotent concurrent or repeated calls return the same promise.
6227
+ * Idempotent: concurrent or repeated calls return the same promise.
5793
6228
  *
5794
6229
  * @param strategy Optional stoppable to close (stops consumers and subscriptions).
5795
6230
  */
@@ -5819,6 +6254,12 @@ var ShutdownManager = class {
5819
6254
 
5820
6255
  // src/jetstream.module.ts
5821
6256
  var JETSTREAM_ACK_WAIT_MAP = /* @__PURE__ */ Symbol("JETSTREAM_ACK_WAIT_MAP");
6257
+ var DESTRUCTIVE_MIGRATION_MANUAL_WARNING = "allowDestructiveMigration has no effect under provisioning.management: Manual; the library never migrates externally managed streams.";
6258
+ var warnIfManualWithDestructive = (options, logger5) => {
6259
+ if (options.allowDestructiveMigration && options.provisioning?.management === "manual" /* Manual */) {
6260
+ logger5.warn(DESTRUCTIVE_MIGRATION_MANUAL_WARNING);
6261
+ }
6262
+ };
5822
6263
  var JetstreamModule = class {
5823
6264
  constructor(shutdownManager, strategy) {
5824
6265
  this.shutdownManager = shutdownManager;
@@ -5848,7 +6289,8 @@ var JetstreamModule = class {
5848
6289
  PatternRegistry,
5849
6290
  ShutdownManager,
5850
6291
  JetstreamStrategy,
5851
- JetstreamHealthIndicator
6292
+ JetstreamHealthIndicator,
6293
+ NameResolver
5852
6294
  ]
5853
6295
  };
5854
6296
  }
@@ -5877,7 +6319,8 @@ var JetstreamModule = class {
5877
6319
  PatternRegistry,
5878
6320
  ShutdownManager,
5879
6321
  JetstreamStrategy,
5880
- JetstreamHealthIndicator
6322
+ JetstreamHealthIndicator,
6323
+ NameResolver
5881
6324
  ]
5882
6325
  };
5883
6326
  }
@@ -5894,10 +6337,23 @@ var JetstreamModule = class {
5894
6337
  const clientToken = getClientToken(options.name);
5895
6338
  const clientProvider = {
5896
6339
  provide: clientToken,
5897
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_CODEC, JETSTREAM_EVENT_BUS],
5898
- useFactory: (rootOptions, connection, rootCodec, eventBus) => {
6340
+ inject: [
6341
+ JETSTREAM_OPTIONS,
6342
+ JETSTREAM_CONNECTION,
6343
+ JETSTREAM_CODEC,
6344
+ JETSTREAM_EVENT_BUS,
6345
+ { token: NameResolver, optional: true }
6346
+ ],
6347
+ useFactory: (rootOptions, connection, rootCodec, eventBus, names) => {
5899
6348
  const codec = options.codec ?? rootCodec;
5900
- return new JetstreamClient(rootOptions, options.name, connection, codec, eventBus);
6349
+ return new JetstreamClient(
6350
+ rootOptions,
6351
+ options.name,
6352
+ connection,
6353
+ codec,
6354
+ eventBus,
6355
+ names ?? void 0
6356
+ );
5901
6357
  }
5902
6358
  };
5903
6359
  return {
@@ -5918,16 +6374,14 @@ var JetstreamModule = class {
5918
6374
  /** Create providers that depend on JETSTREAM_OPTIONS (shared by sync and async). */
5919
6375
  static createCoreDependentProviders() {
5920
6376
  return [
5921
- // EventBus — hook system with Logger fallback
5922
6377
  {
5923
6378
  provide: JETSTREAM_EVENT_BUS,
5924
6379
  inject: [JETSTREAM_OPTIONS],
5925
6380
  useFactory: (options) => {
5926
- const logger5 = new Logger20("Jetstream:Module");
6381
+ const logger5 = new Logger23("Jetstream:Module");
5927
6382
  return new EventBus(logger5, options.hooks);
5928
6383
  }
5929
6384
  },
5930
- // Codec — global encode/decode
5931
6385
  {
5932
6386
  provide: JETSTREAM_CODEC,
5933
6387
  inject: [JETSTREAM_OPTIONS],
@@ -5935,7 +6389,6 @@ var JetstreamModule = class {
5935
6389
  return options.codec ?? new JsonCodec();
5936
6390
  }
5937
6391
  },
5938
- // ConnectionProvider — single NATS connection
5939
6392
  {
5940
6393
  provide: JETSTREAM_CONNECTION,
5941
6394
  inject: [JETSTREAM_OPTIONS, JETSTREAM_EVENT_BUS],
@@ -5943,7 +6396,6 @@ var JetstreamModule = class {
5943
6396
  return new ConnectionProvider(options, eventBus);
5944
6397
  }
5945
6398
  },
5946
- // JetstreamHealthIndicator — health check for NATS connection
5947
6399
  {
5948
6400
  provide: JetstreamHealthIndicator,
5949
6401
  inject: [JETSTREAM_CONNECTION],
@@ -5951,7 +6403,6 @@ var JetstreamModule = class {
5951
6403
  return new JetstreamHealthIndicator(connection);
5952
6404
  }
5953
6405
  },
5954
- // ShutdownManager — graceful shutdown orchestration
5955
6406
  {
5956
6407
  provide: ShutdownManager,
5957
6408
  inject: [JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, JETSTREAM_OPTIONS],
@@ -5963,41 +6414,69 @@ var JetstreamModule = class {
5963
6414
  );
5964
6415
  }
5965
6416
  },
5966
- // Consumer infrastructure only created when consumer !== false.
5967
- // Providers return null when consumer is disabled (publisher-only mode).
5968
- // PatternRegistry — subject-to-handler mapping
6417
+ // Consumer infrastructure providers below return null when consumer === false
6418
+ // (publisher-only mode). NameResolver is the exception: clients need it too.
5969
6419
  {
5970
- provide: PatternRegistry,
6420
+ provide: NameResolver,
5971
6421
  inject: [JETSTREAM_OPTIONS],
5972
6422
  useFactory: (options) => {
6423
+ const logger5 = new Logger23("Jetstream:Module");
6424
+ warnIfManualWithDestructive(options, logger5);
6425
+ return new NameResolver(options);
6426
+ }
6427
+ },
6428
+ {
6429
+ provide: PatternRegistry,
6430
+ inject: [JETSTREAM_OPTIONS, NameResolver],
6431
+ useFactory: (options, names) => {
6432
+ if (options.consumer === false) return null;
6433
+ return new PatternRegistry(options, names);
6434
+ }
6435
+ },
6436
+ {
6437
+ provide: InfrastructureBinder,
6438
+ inject: [JETSTREAM_OPTIONS, NameResolver, PatternRegistry],
6439
+ useFactory: (options, names, registry) => {
5973
6440
  if (options.consumer === false) return null;
5974
- return new PatternRegistry(options);
6441
+ return new InfrastructureBinder(options, names, registry);
5975
6442
  }
5976
6443
  },
5977
- // StreamProvider — JetStream stream lifecycle
5978
6444
  {
5979
6445
  provide: StreamProvider,
5980
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
5981
- useFactory: (options, connection) => {
6446
+ inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, NameResolver, InfrastructureBinder],
6447
+ useFactory: (options, connection, names, binder) => {
5982
6448
  if (options.consumer === false) return null;
5983
- return new StreamProvider(options, connection);
6449
+ return new StreamProvider(options, connection, names, binder);
5984
6450
  }
5985
6451
  },
5986
- // ConsumerProvider JetStream consumer lifecycle (receives PatternRegistry for broadcast filtering)
6452
+ // ConsumerProvider needs PatternRegistry for broadcast filtering.
5987
6453
  {
5988
6454
  provide: ConsumerProvider,
5989
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, StreamProvider, PatternRegistry],
5990
- useFactory: (options, connection, streamProvider, patternRegistry) => {
6455
+ inject: [
6456
+ JETSTREAM_OPTIONS,
6457
+ JETSTREAM_CONNECTION,
6458
+ StreamProvider,
6459
+ PatternRegistry,
6460
+ NameResolver,
6461
+ InfrastructureBinder
6462
+ ],
6463
+ useFactory: (options, connection, streamProvider, patternRegistry, names, binder) => {
5991
6464
  if (options.consumer === false) return null;
5992
- return new ConsumerProvider(options, connection, streamProvider, patternRegistry);
6465
+ return new ConsumerProvider(
6466
+ options,
6467
+ connection,
6468
+ streamProvider,
6469
+ patternRegistry,
6470
+ names,
6471
+ binder
6472
+ );
5993
6473
  }
5994
6474
  },
5995
- // Shared ack_wait map populated by strategy after ensureConsumers()
6475
+ // Shared ack_wait map, populated by the strategy after ensureConsumers().
5996
6476
  {
5997
6477
  provide: JETSTREAM_ACK_WAIT_MAP,
5998
6478
  useFactory: () => /* @__PURE__ */ new Map()
5999
6479
  },
6000
- // MessageProvider — pull-based message consumption
6001
6480
  {
6002
6481
  provide: MessageProvider,
6003
6482
  inject: [
@@ -6036,7 +6515,6 @@ var JetstreamModule = class {
6036
6515
  return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
6037
6516
  }
6038
6517
  },
6039
- // EventRouter — routes event and broadcast messages to handlers
6040
6518
  {
6041
6519
  provide: EventRouter,
6042
6520
  inject: [
@@ -6046,9 +6524,10 @@ var JetstreamModule = class {
6046
6524
  JETSTREAM_CODEC,
6047
6525
  JETSTREAM_EVENT_BUS,
6048
6526
  JETSTREAM_ACK_WAIT_MAP,
6049
- JETSTREAM_CONNECTION
6527
+ JETSTREAM_CONNECTION,
6528
+ NameResolver
6050
6529
  ],
6051
- useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
6530
+ useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection, names) => {
6052
6531
  if (options.consumer === false) return null;
6053
6532
  const deadLetterConfig = options.onDeadLetter || options.dlq ? {
6054
6533
  maxDeliverByStream: /* @__PURE__ */ new Map(),
@@ -6073,11 +6552,11 @@ var JetstreamModule = class {
6073
6552
  processingConfig,
6074
6553
  ackWaitMap,
6075
6554
  connection,
6076
- options
6555
+ options,
6556
+ names
6077
6557
  );
6078
6558
  }
6079
6559
  },
6080
- // RpcRouter — routes RPC command messages in JetStream mode
6081
6560
  {
6082
6561
  provide: RpcRouter,
6083
6562
  inject: [
@@ -6108,7 +6587,6 @@ var JetstreamModule = class {
6108
6587
  );
6109
6588
  }
6110
6589
  },
6111
- // CoreRpcServer — RPC via NATS Core request/reply
6112
6590
  {
6113
6591
  provide: CoreRpcServer,
6114
6592
  inject: [
@@ -6116,14 +6594,14 @@ var JetstreamModule = class {
6116
6594
  JETSTREAM_CONNECTION,
6117
6595
  PatternRegistry,
6118
6596
  JETSTREAM_CODEC,
6119
- JETSTREAM_EVENT_BUS
6597
+ JETSTREAM_EVENT_BUS,
6598
+ NameResolver
6120
6599
  ],
6121
- useFactory: (options, connection, patternRegistry, codec, eventBus) => {
6600
+ useFactory: (options, connection, patternRegistry, codec, eventBus, names) => {
6122
6601
  if (options.consumer === false) return null;
6123
- return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
6602
+ return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus, names);
6124
6603
  }
6125
6604
  },
6126
- // MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
6127
6605
  {
6128
6606
  provide: MetadataProvider,
6129
6607
  inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
@@ -6132,7 +6610,6 @@ var JetstreamModule = class {
6132
6610
  return new MetadataProvider(options, connection);
6133
6611
  }
6134
6612
  },
6135
- // JetstreamStrategy — server-side transport (only when consumer enabled)
6136
6613
  {
6137
6614
  provide: JetstreamStrategy,
6138
6615
  inject: [
@@ -6257,6 +6734,7 @@ export {
6257
6734
  JetstreamTrace,
6258
6735
  JsonCodec,
6259
6736
  MIN_METADATA_TTL,
6737
+ ManagementMode,
6260
6738
  MessageKind,
6261
6739
  MsgpackCodec,
6262
6740
  NatsErrorCode,