@horizon-republic/nestjs-jetstream 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -29,12 +29,17 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
29
29
  // src/index.ts
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ DEFAULT_METADATA_BUCKET: () => DEFAULT_METADATA_BUCKET,
33
+ DEFAULT_METADATA_HISTORY: () => DEFAULT_METADATA_HISTORY,
34
+ DEFAULT_METADATA_REPLICAS: () => DEFAULT_METADATA_REPLICAS,
35
+ DEFAULT_METADATA_TTL: () => DEFAULT_METADATA_TTL,
32
36
  EventBus: () => EventBus,
33
37
  JETSTREAM_CODEC: () => JETSTREAM_CODEC,
34
38
  JETSTREAM_CONNECTION: () => JETSTREAM_CONNECTION,
35
39
  JETSTREAM_EVENT_BUS: () => JETSTREAM_EVENT_BUS,
36
40
  JETSTREAM_OPTIONS: () => JETSTREAM_OPTIONS,
37
41
  JetstreamClient: () => JetstreamClient,
42
+ JetstreamDlqHeader: () => JetstreamDlqHeader,
38
43
  JetstreamHeader: () => JetstreamHeader,
39
44
  JetstreamHealthIndicator: () => JetstreamHealthIndicator,
40
45
  JetstreamModule: () => JetstreamModule,
@@ -42,24 +47,28 @@ __export(index_exports, {
42
47
  JetstreamRecordBuilder: () => JetstreamRecordBuilder,
43
48
  JetstreamStrategy: () => JetstreamStrategy,
44
49
  JsonCodec: () => JsonCodec,
50
+ MIN_METADATA_TTL: () => MIN_METADATA_TTL,
45
51
  MessageKind: () => MessageKind,
46
52
  PatternPrefix: () => PatternPrefix,
47
53
  RpcContext: () => RpcContext,
48
54
  StreamKind: () => StreamKind,
49
55
  TransportEvent: () => TransportEvent,
56
+ buildBroadcastSubject: () => buildBroadcastSubject,
50
57
  buildSubject: () => buildSubject,
51
58
  consumerName: () => consumerName,
59
+ dlqStreamName: () => dlqStreamName,
52
60
  getClientToken: () => getClientToken,
53
61
  internalName: () => internalName,
54
62
  isCoreRpcMode: () => isCoreRpcMode,
55
63
  isJetStreamRpcMode: () => isJetStreamRpcMode,
64
+ metadataKey: () => metadataKey,
56
65
  streamName: () => streamName,
57
66
  toNanos: () => toNanos
58
67
  });
59
68
  module.exports = __toCommonJS(index_exports);
60
69
 
61
70
  // src/jetstream.module.ts
62
- var import_common12 = require("@nestjs/common");
71
+ var import_common14 = require("@nestjs/common");
63
72
 
64
73
  // src/client/jetstream.client.ts
65
74
  var import_common = require("@nestjs/common");
@@ -152,7 +161,7 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
152
161
  max_msgs_per_subject: 1e6,
153
162
  max_msgs: 1e7,
154
163
  max_bytes: 2 * GB,
155
- max_age: toNanos(1, "days"),
164
+ max_age: toNanos(1, "hours"),
156
165
  duplicate_window: toNanos(2, "minutes")
157
166
  };
158
167
  var DEFAULT_ORDERED_STREAM_CONFIG = {
@@ -167,6 +176,18 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
167
176
  max_age: toNanos(1, "days"),
168
177
  duplicate_window: toNanos(2, "minutes")
169
178
  };
179
+ var DEFAULT_DLQ_STREAM_CONFIG = {
180
+ ...baseStreamConfig,
181
+ retention: import_jetstream.RetentionPolicy.Workqueue,
182
+ allow_rollup_hdrs: false,
183
+ max_consumers: 100,
184
+ max_msg_size: 10 * MB,
185
+ max_msgs_per_subject: 5e6,
186
+ max_msgs: 5e7,
187
+ max_bytes: 5 * GB,
188
+ max_age: toNanos(30, "days"),
189
+ duplicate_window: toNanos(2, "minutes")
190
+ };
170
191
  var DEFAULT_EVENT_CONSUMER_CONFIG = {
171
192
  ack_wait: toNanos(10, "seconds"),
172
193
  max_deliver: 3,
@@ -194,6 +215,12 @@ var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
194
215
  var DEFAULT_RPC_TIMEOUT = 3e4;
195
216
  var DEFAULT_JETSTREAM_RPC_TIMEOUT = 18e4;
196
217
  var DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
218
+ var DEFAULT_METADATA_BUCKET = "handler_registry";
219
+ var DEFAULT_METADATA_REPLICAS = 1;
220
+ var DEFAULT_METADATA_HISTORY = 1;
221
+ var DEFAULT_METADATA_TTL = 3e4;
222
+ var MIN_METADATA_TTL = 5e3;
223
+ var metadataKey = (serviceName, kind, pattern) => `${serviceName}.${kind}.${pattern}`;
197
224
  var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
198
225
  JetstreamHeader2["CorrelationId"] = "x-correlation-id";
199
226
  JetstreamHeader2["ReplyTo"] = "x-reply-to";
@@ -202,6 +229,14 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
202
229
  JetstreamHeader2["Error"] = "x-error";
203
230
  return JetstreamHeader2;
204
231
  })(JetstreamHeader || {});
232
+ var JetstreamDlqHeader = /* @__PURE__ */ ((JetstreamDlqHeader2) => {
233
+ JetstreamDlqHeader2["DeadLetterReason"] = "x-dead-letter-reason";
234
+ JetstreamDlqHeader2["OriginalSubject"] = "x-original-subject";
235
+ JetstreamDlqHeader2["OriginalStream"] = "x-original-stream";
236
+ JetstreamDlqHeader2["FailedAt"] = "x-failed-at";
237
+ JetstreamDlqHeader2["DeliveryCount"] = "x-delivery-count";
238
+ return JetstreamDlqHeader2;
239
+ })(JetstreamDlqHeader || {});
205
240
  var RESERVED_HEADERS = /* @__PURE__ */ new Set([
206
241
  "x-correlation-id" /* CorrelationId */,
207
242
  "x-reply-to" /* ReplyTo */,
@@ -214,6 +249,9 @@ var streamName = (serviceName, kind) => {
214
249
  if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
215
250
  return `${internalName(serviceName)}_${kind}-stream`;
216
251
  };
252
+ var dlqStreamName = (serviceName) => {
253
+ return `${internalName(serviceName)}_dlq-stream`;
254
+ };
217
255
  var consumerName = (serviceName, kind) => {
218
256
  if (kind === "broadcast" /* Broadcast */) return `${internalName(serviceName)}_broadcast-consumer`;
219
257
  return `${internalName(serviceName)}_${kind}-consumer`;
@@ -228,12 +266,13 @@ var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
228
266
 
229
267
  // src/client/jetstream.record.ts
230
268
  var JetstreamRecord = class {
231
- constructor(data, headers2, timeout, messageId, schedule) {
269
+ constructor(data, headers2, timeout, messageId, schedule, ttl) {
232
270
  this.data = data;
233
271
  this.headers = headers2;
234
272
  this.timeout = timeout;
235
273
  this.messageId = messageId;
236
274
  this.schedule = schedule;
275
+ this.ttl = ttl;
237
276
  }
238
277
  };
239
278
  var JetstreamRecordBuilder = class {
@@ -242,6 +281,7 @@ var JetstreamRecordBuilder = class {
242
281
  timeout;
243
282
  messageId;
244
283
  scheduleOptions;
284
+ ttlDuration;
245
285
  constructor(data) {
246
286
  this.data = data;
247
287
  }
@@ -333,6 +373,33 @@ var JetstreamRecordBuilder = class {
333
373
  this.scheduleOptions = { at: new Date(ts) };
334
374
  return this;
335
375
  }
376
+ /**
377
+ * Set per-message TTL (time-to-live).
378
+ *
379
+ * The message expires individually after the specified duration,
380
+ * independent of the stream's `max_age`. Requires NATS >= 2.11 and
381
+ * `allow_msg_ttl: true` on the stream.
382
+ *
383
+ * Only meaningful for events (`client.emit()`). If used with RPC
384
+ * (`client.send()`), a warning is logged and the TTL is ignored.
385
+ *
386
+ * @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * import { toNanos } from '@horizon-republic/nestjs-jetstream';
391
+ *
392
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
393
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
394
+ * ```
395
+ */
396
+ ttl(nanos) {
397
+ if (!Number.isFinite(nanos) || nanos <= 0) {
398
+ throw new Error("TTL must be a positive finite value");
399
+ }
400
+ this.ttlDuration = nanosToGoDuration(nanos);
401
+ return this;
402
+ }
336
403
  /**
337
404
  * Build the immutable {@link JetstreamRecord}.
338
405
  *
@@ -345,7 +412,8 @@ var JetstreamRecordBuilder = class {
345
412
  new Map(this.headers),
346
413
  this.timeout,
347
414
  this.messageId,
348
- schedule
415
+ schedule,
416
+ this.ttlDuration
349
417
  );
350
418
  }
351
419
  /** Validate that a header key is not reserved. */
@@ -357,6 +425,17 @@ var JetstreamRecordBuilder = class {
357
425
  }
358
426
  }
359
427
  };
428
+ var NS_PER_MS = 1e6;
429
+ var NS_PER_S = 1e9;
430
+ var NS_PER_M = 60 * NS_PER_S;
431
+ var NS_PER_H = 60 * NS_PER_M;
432
+ var nanosToGoDuration = (nanos) => {
433
+ if (nanos >= NS_PER_H && nanos % NS_PER_H === 0) return `${nanos / NS_PER_H}h`;
434
+ if (nanos >= NS_PER_M && nanos % NS_PER_M === 0) return `${nanos / NS_PER_M}m`;
435
+ if (nanos >= NS_PER_S && nanos % NS_PER_S === 0) return `${nanos / NS_PER_S}s`;
436
+ if (nanos >= NS_PER_MS && nanos % NS_PER_MS === 0) return `${nanos / NS_PER_MS}ms`;
437
+ return `${nanos}ns`;
438
+ };
360
439
 
361
440
  // src/client/jetstream.client.ts
362
441
  var JetstreamClient = class extends import_microservices.ClientProxy {
@@ -431,7 +510,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
431
510
  */
432
511
  async dispatchEvent(packet) {
433
512
  await this.connect();
434
- const { data, hdrs, messageId, schedule } = this.extractRecordData(packet.data);
513
+ const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
435
514
  const eventSubject = this.buildEventSubject(packet.pattern);
436
515
  const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
437
516
  if (schedule) {
@@ -439,6 +518,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
439
518
  const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
440
519
  headers: msgHeaders,
441
520
  msgID: messageId ?? import_nuid.nuid.next(),
521
+ ttl,
442
522
  schedule: {
443
523
  specification: schedule.at,
444
524
  target: eventSubject
@@ -452,7 +532,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
452
532
  } else {
453
533
  const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
454
534
  headers: msgHeaders,
455
- msgID: messageId ?? import_nuid.nuid.next()
535
+ msgID: messageId ?? import_nuid.nuid.next(),
536
+ ttl
456
537
  });
457
538
  if (ack.duplicate) {
458
539
  this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
@@ -468,12 +549,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
468
549
  */
469
550
  publish(packet, callback) {
470
551
  const subject = buildSubject(this.targetName, "cmd" /* Command */, packet.pattern);
471
- const { data, hdrs, timeout, messageId, schedule } = this.extractRecordData(packet.data);
552
+ const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
472
553
  if (schedule) {
473
554
  this.logger.warn(
474
555
  "scheduleAt() is ignored for RPC (client.send()). Use client.emit() for scheduled events."
475
556
  );
476
557
  }
558
+ if (ttl) {
559
+ this.logger.warn(
560
+ "ttl() is ignored for RPC (client.send()). Use client.emit() for events with TTL."
561
+ );
562
+ }
477
563
  const onUnhandled = (err) => {
478
564
  this.logger.error("Unhandled publish error:", err);
479
565
  callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
@@ -589,6 +675,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
589
675
  this.pendingTimeouts.clear();
590
676
  this.inboxSubscription?.unsubscribe();
591
677
  this.inboxSubscription = null;
678
+ this.inbox = null;
592
679
  }
593
680
  /** Setup shared inbox subscription for JetStream RPC responses. */
594
681
  setupInbox(nc) {
@@ -678,7 +765,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
678
765
  hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
679
766
  timeout: rawData.timeout,
680
767
  messageId: rawData.messageId,
681
- schedule: rawData.schedule
768
+ schedule: rawData.schedule,
769
+ ttl: rawData.ttl
682
770
  };
683
771
  }
684
772
  return {
@@ -686,7 +774,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
686
774
  hdrs: null,
687
775
  timeout: void 0,
688
776
  messageId: void 0,
689
- schedule: void 0
777
+ schedule: void 0,
778
+ ttl: void 0
690
779
  };
691
780
  }
692
781
  /**
@@ -1001,9 +1090,14 @@ var JetstreamHealthIndicator = class {
1001
1090
  * Returns `{ [key]: { status: 'up', ... } }` on success.
1002
1091
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
1003
1092
  *
1093
+ * The thrown error sets `isHealthCheckError: true` and `causes` — the
1094
+ * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
1095
+ * health failures from unexpected exceptions. Works with both Terminus v10
1096
+ * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
1097
+ *
1004
1098
  * @param key - Health indicator key (default: `'jetstream'`).
1005
1099
  * @returns Object with status, server, and latency under the given key.
1006
- * @throws Error with `{ [key]: { status: 'down' } }` when disconnected.
1100
+ * @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
1007
1101
  */
1008
1102
  async isHealthy(key = "jetstream") {
1009
1103
  const status = await this.check();
@@ -1013,8 +1107,10 @@ var JetstreamHealthIndicator = class {
1013
1107
  latency: status.latency
1014
1108
  };
1015
1109
  if (!status.connected) {
1110
+ const causes = { [key]: details };
1016
1111
  throw Object.assign(new Error("Jetstream health check failed"), {
1017
- [key]: details
1112
+ causes,
1113
+ isHealthCheckError: true
1018
1114
  });
1019
1115
  }
1020
1116
  return { [key]: details };
@@ -1027,7 +1123,7 @@ JetstreamHealthIndicator = __decorateClass([
1027
1123
  // src/server/strategy.ts
1028
1124
  var import_microservices2 = require("@nestjs/microservices");
1029
1125
  var JetstreamStrategy = class extends import_microservices2.Server {
1030
- constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map()) {
1126
+ constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map(), metadataProvider) {
1031
1127
  super();
1032
1128
  this.options = options;
1033
1129
  this.connection = connection;
@@ -1039,6 +1135,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
1039
1135
  this.rpcRouter = rpcRouter;
1040
1136
  this.coreRpcServer = coreRpcServer;
1041
1137
  this.ackWaitMap = ackWaitMap;
1138
+ this.metadataProvider = metadataProvider;
1042
1139
  }
1043
1140
  transportId = /* @__PURE__ */ Symbol("jetstream-transport");
1044
1141
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -1083,10 +1180,14 @@ var JetstreamStrategy = class extends import_microservices2.Server {
1083
1180
  if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
1084
1181
  await this.coreRpcServer.start();
1085
1182
  }
1183
+ if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
1184
+ await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
1185
+ }
1086
1186
  callback();
1087
1187
  }
1088
- /** Stop all consumers, routers, and subscriptions. Called during shutdown. */
1188
+ /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
1089
1189
  close() {
1190
+ this.metadataProvider?.destroy();
1090
1191
  this.eventRouter.destroy();
1091
1192
  this.rpcRouter.destroy();
1092
1193
  this.coreRpcServer.stop();
@@ -1462,24 +1563,172 @@ var CoreRpcServer = class {
1462
1563
  };
1463
1564
 
1464
1565
  // src/server/infrastructure/stream.provider.ts
1566
+ var import_common6 = require("@nestjs/common");
1567
+ var import_jetstream14 = require("@nats-io/jetstream");
1568
+
1569
+ // src/server/infrastructure/stream-config-diff.ts
1570
+ var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
1571
+ "retention"
1572
+ ]);
1573
+ var IMMUTABLE_PROPERTIES = /* @__PURE__ */ new Set([
1574
+ "storage"
1575
+ ]);
1576
+ var ENABLE_ONLY_PROPERTIES = /* @__PURE__ */ new Set([
1577
+ "allow_msg_schedules",
1578
+ "allow_msg_ttl",
1579
+ "deny_delete",
1580
+ "deny_purge"
1581
+ ]);
1582
+ var compareStreamConfig = (current, desired) => {
1583
+ const changes = [];
1584
+ for (const key of Object.keys(desired)) {
1585
+ const currentVal = current[key];
1586
+ const desiredVal = desired[key];
1587
+ if (isEqual(currentVal, desiredVal)) continue;
1588
+ changes.push({
1589
+ property: key,
1590
+ current: currentVal,
1591
+ desired: desiredVal,
1592
+ mutability: classifyMutability(key, currentVal, desiredVal)
1593
+ });
1594
+ }
1595
+ const hasImmutableChanges = changes.some((c) => c.mutability === "immutable");
1596
+ const hasMutableChanges = changes.some(
1597
+ (c) => c.mutability === "mutable" || c.mutability === "enable-only"
1598
+ );
1599
+ const hasTransportControlledConflicts = changes.some(
1600
+ (c) => c.mutability === "transport-controlled"
1601
+ );
1602
+ return {
1603
+ hasChanges: changes.length > 0,
1604
+ hasMutableChanges,
1605
+ hasImmutableChanges,
1606
+ hasTransportControlledConflicts,
1607
+ changes
1608
+ };
1609
+ };
1610
+ var classifyMutability = (key, current, desired) => {
1611
+ if (TRANSPORT_CONTROLLED_PROPERTIES.has(key)) return "transport-controlled";
1612
+ if (IMMUTABLE_PROPERTIES.has(key)) return "immutable";
1613
+ if (ENABLE_ONLY_PROPERTIES.has(key)) {
1614
+ return current === true && desired === false ? "immutable" : "enable-only";
1615
+ }
1616
+ return "mutable";
1617
+ };
1618
+ var isEqual = (a, b) => {
1619
+ if (a === b) return true;
1620
+ if (a == null && b == null) return true;
1621
+ return JSON.stringify(a) === JSON.stringify(b);
1622
+ };
1623
+
1624
+ // src/server/infrastructure/stream-migration.ts
1465
1625
  var import_common5 = require("@nestjs/common");
1466
1626
  var import_jetstream13 = require("@nats-io/jetstream");
1467
- var STREAM_NOT_FOUND = 10059;
1627
+ var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
1628
+ var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
1629
+ var SOURCING_POLL_INTERVAL_MS = 100;
1630
+ var StreamMigration = class {
1631
+ constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
1632
+ this.sourcingTimeoutMs = sourcingTimeoutMs;
1633
+ }
1634
+ logger = new import_common5.Logger("Jetstream:Stream");
1635
+ async migrate(jsm, streamName2, newConfig) {
1636
+ const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
1637
+ const startTime = Date.now();
1638
+ const currentInfo = await jsm.streams.info(streamName2);
1639
+ await this.cleanupOrphanedBackup(jsm, backupName);
1640
+ const messageCount = currentInfo.state.messages;
1641
+ this.logger.log(`Stream ${streamName2}: destructive migration started`);
1642
+ let originalDeleted = false;
1643
+ try {
1644
+ if (messageCount > 0) {
1645
+ this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
1646
+ await jsm.streams.add({
1647
+ ...currentInfo.config,
1648
+ name: backupName,
1649
+ subjects: [],
1650
+ sources: [{ name: streamName2 }]
1651
+ });
1652
+ await this.waitForSourcing(jsm, backupName, messageCount);
1653
+ }
1654
+ this.logger.log(` Phase 2/4: Deleting old stream`);
1655
+ await jsm.streams.delete(streamName2);
1656
+ originalDeleted = true;
1657
+ this.logger.log(` Phase 3/4: Creating stream with new config`);
1658
+ await jsm.streams.add(newConfig);
1659
+ if (messageCount > 0) {
1660
+ const backupInfo = await jsm.streams.info(backupName);
1661
+ await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
1662
+ this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
1663
+ await jsm.streams.update(streamName2, {
1664
+ ...newConfig,
1665
+ sources: [{ name: backupName }]
1666
+ });
1667
+ await this.waitForSourcing(jsm, streamName2, messageCount);
1668
+ await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
1669
+ await jsm.streams.delete(backupName);
1670
+ }
1671
+ } catch (err) {
1672
+ if (originalDeleted && messageCount > 0) {
1673
+ this.logger.error(
1674
+ `Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
1675
+ );
1676
+ } else {
1677
+ await this.cleanupOrphanedBackup(jsm, backupName);
1678
+ }
1679
+ throw err;
1680
+ }
1681
+ const durationMs = Date.now() - startTime;
1682
+ this.logger.log(
1683
+ `Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
1684
+ );
1685
+ }
1686
+ async waitForSourcing(jsm, streamName2, expectedCount) {
1687
+ const deadline = Date.now() + this.sourcingTimeoutMs;
1688
+ while (Date.now() < deadline) {
1689
+ const info = await jsm.streams.info(streamName2);
1690
+ if (info.state.messages >= expectedCount) return;
1691
+ await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
1692
+ }
1693
+ throw new Error(
1694
+ `Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
1695
+ );
1696
+ }
1697
+ async cleanupOrphanedBackup(jsm, backupName) {
1698
+ try {
1699
+ await jsm.streams.info(backupName);
1700
+ this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
1701
+ await jsm.streams.delete(backupName);
1702
+ } catch (err) {
1703
+ if (err instanceof import_jetstream13.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
1704
+ return;
1705
+ }
1706
+ throw err;
1707
+ }
1708
+ }
1709
+ };
1710
+
1711
+ // src/server/infrastructure/stream.provider.ts
1468
1712
  var StreamProvider = class {
1469
1713
  constructor(options, connection) {
1470
1714
  this.options = options;
1471
1715
  this.connection = connection;
1472
1716
  }
1473
- logger = new import_common5.Logger("Jetstream:Stream");
1717
+ logger = new import_common6.Logger("Jetstream:Stream");
1718
+ migration = new StreamMigration();
1474
1719
  /**
1475
1720
  * Ensure all required streams exist with correct configuration.
1476
1721
  *
1477
1722
  * @param kinds Which stream kinds to create. Determined by the module based
1478
1723
  * on RPC mode and registered handler patterns.
1724
+ * If the dlq option is enabled, also ensures the DLQ stream exists.
1479
1725
  */
1480
1726
  async ensureStreams(kinds) {
1481
1727
  const jsm = await this.connection.getJetStreamManager();
1482
1728
  await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
1729
+ if (this.options.dlq) {
1730
+ await this.ensureDlqStream(jsm);
1731
+ }
1483
1732
  }
1484
1733
  /** Get the stream name for a given kind. */
1485
1734
  getStreamName(kind) {
@@ -1514,17 +1763,85 @@ var StreamProvider = class {
1514
1763
  const config = this.buildConfig(kind);
1515
1764
  this.logger.log(`Ensuring stream: ${config.name}`);
1516
1765
  try {
1517
- await jsm.streams.info(config.name);
1518
- this.logger.debug(`Stream exists, updating: ${config.name}`);
1519
- return await jsm.streams.update(config.name, config);
1766
+ const currentInfo = await jsm.streams.info(config.name);
1767
+ return await this.handleExistingStream(jsm, currentInfo, config);
1520
1768
  } catch (err) {
1521
- if (err instanceof import_jetstream13.JetStreamApiError && err.apiError().err_code === STREAM_NOT_FOUND) {
1769
+ if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
1522
1770
  this.logger.log(`Creating stream: ${config.name}`);
1523
1771
  return await jsm.streams.add(config);
1524
1772
  }
1525
1773
  throw err;
1526
1774
  }
1527
1775
  }
1776
+ /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
1777
+ async ensureDlqStream(jsm) {
1778
+ const config = this.buildDlqConfig();
1779
+ this.logger.log(`Ensuring DLQ stream: ${config.name}`);
1780
+ try {
1781
+ const currentInfo = await jsm.streams.info(config.name);
1782
+ return await this.handleExistingStream(jsm, currentInfo, config);
1783
+ } catch (err) {
1784
+ if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
1785
+ this.logger.log(`Creating DLQ stream: ${config.name}`);
1786
+ return await jsm.streams.add(config);
1787
+ }
1788
+ throw err;
1789
+ }
1790
+ }
1791
+ async handleExistingStream(jsm, currentInfo, config) {
1792
+ const diff = compareStreamConfig(currentInfo.config, config);
1793
+ if (!diff.hasChanges) {
1794
+ this.logger.debug(`Stream ${config.name}: no config changes`);
1795
+ return currentInfo;
1796
+ }
1797
+ this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
1798
+ if (diff.hasTransportControlledConflicts) {
1799
+ const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
1800
+ throw new Error(
1801
+ `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.`
1802
+ );
1803
+ }
1804
+ if (!diff.hasImmutableChanges) {
1805
+ this.logger.debug(`Stream exists, updating: ${config.name}`);
1806
+ return await jsm.streams.update(config.name, config);
1807
+ }
1808
+ if (!this.options.allowDestructiveMigration) {
1809
+ this.logger.warn(
1810
+ `Stream ${config.name} has immutable config conflicts. Enable allowDestructiveMigration to recreate the stream.`
1811
+ );
1812
+ if (diff.hasMutableChanges) {
1813
+ const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
1814
+ return await jsm.streams.update(config.name, mutableConfig);
1815
+ }
1816
+ return currentInfo;
1817
+ }
1818
+ await this.migration.migrate(jsm, config.name, config);
1819
+ return await jsm.streams.info(config.name);
1820
+ }
1821
+ buildMutableOnlyConfig(config, currentConfig, diff) {
1822
+ const nonMutableKeys = new Set(
1823
+ diff.changes.filter((c) => c.mutability === "immutable" || c.mutability === "transport-controlled").map((c) => c.property)
1824
+ );
1825
+ const filtered = { ...config };
1826
+ for (const key of nonMutableKeys) {
1827
+ filtered[key] = currentConfig[key];
1828
+ }
1829
+ return filtered;
1830
+ }
1831
+ logChanges(streamName2, diff, migrationEnabled) {
1832
+ for (const c of diff.changes) {
1833
+ const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
1834
+ if (c.mutability === "transport-controlled") {
1835
+ this.logger.error(
1836
+ `Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
1837
+ );
1838
+ } else if (c.mutability === "immutable" && !migrationEnabled) {
1839
+ this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
1840
+ } else {
1841
+ this.logger.log(`Stream ${streamName2}: ${detail}`);
1842
+ }
1843
+ }
1844
+ }
1528
1845
  /** Build the full stream config by merging defaults with user overrides. */
1529
1846
  buildConfig(kind) {
1530
1847
  const name = this.getStreamName(kind);
@@ -1540,6 +1857,26 @@ var StreamProvider = class {
1540
1857
  description
1541
1858
  };
1542
1859
  }
1860
+ /**
1861
+ * Build the stream configuration for the Dead-Letter Queue (DLQ).
1862
+ *
1863
+ * Merges the library default DLQ config with user-provided overrides.
1864
+ * Ensures transport-controlled settings like retention are safely decoupled.
1865
+ */
1866
+ buildDlqConfig() {
1867
+ const name = dlqStreamName(this.options.name);
1868
+ const subjects = [name];
1869
+ const description = `JetStream DLQ stream for ${this.options.name}`;
1870
+ const overrides = this.options.dlq?.stream ?? {};
1871
+ const safeOverrides = this.stripTransportControlled(overrides);
1872
+ return {
1873
+ ...DEFAULT_DLQ_STREAM_CONFIG,
1874
+ ...safeOverrides,
1875
+ name,
1876
+ subjects,
1877
+ description
1878
+ };
1879
+ }
1543
1880
  /** Get default config for a stream kind. */
1544
1881
  getDefaults(kind) {
1545
1882
  switch (kind) {
@@ -1558,25 +1895,44 @@ var StreamProvider = class {
1558
1895
  const overrides = this.getOverrides(kind);
1559
1896
  return overrides.allow_msg_schedules === true;
1560
1897
  }
1561
- /** Get user-provided overrides for a stream kind. */
1898
+ /** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
1562
1899
  getOverrides(kind) {
1900
+ let overrides;
1563
1901
  switch (kind) {
1564
1902
  case "ev" /* Event */:
1565
- return this.options.events?.stream ?? {};
1903
+ overrides = this.options.events?.stream ?? {};
1904
+ break;
1566
1905
  case "cmd" /* Command */:
1567
- return this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
1906
+ overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
1907
+ break;
1568
1908
  case "broadcast" /* Broadcast */:
1569
- return this.options.broadcast?.stream ?? {};
1909
+ overrides = this.options.broadcast?.stream ?? {};
1910
+ break;
1570
1911
  case "ordered" /* Ordered */:
1571
- return this.options.ordered?.stream ?? {};
1912
+ overrides = this.options.ordered?.stream ?? {};
1913
+ break;
1572
1914
  }
1915
+ return this.stripTransportControlled(overrides);
1916
+ }
1917
+ /**
1918
+ * Remove transport-controlled properties from user overrides.
1919
+ * `retention` is managed by the transport (Workqueue/Limits per stream kind)
1920
+ * and silently stripped to protect users from misconfiguration.
1921
+ */
1922
+ stripTransportControlled(overrides) {
1923
+ if (!("retention" in overrides)) return overrides;
1924
+ this.logger.debug(
1925
+ "Stripping user-provided retention override \u2014 retention is managed by the transport"
1926
+ );
1927
+ const cleaned = { ...overrides };
1928
+ delete cleaned.retention;
1929
+ return cleaned;
1573
1930
  }
1574
1931
  };
1575
1932
 
1576
1933
  // src/server/infrastructure/consumer.provider.ts
1577
- var import_common6 = require("@nestjs/common");
1578
- var import_jetstream15 = require("@nats-io/jetstream");
1579
- var CONSUMER_NOT_FOUND = 10014;
1934
+ var import_common7 = require("@nestjs/common");
1935
+ var import_jetstream16 = require("@nats-io/jetstream");
1580
1936
  var ConsumerProvider = class {
1581
1937
  constructor(options, connection, streamProvider, patternRegistry) {
1582
1938
  this.options = options;
@@ -1584,7 +1940,7 @@ var ConsumerProvider = class {
1584
1940
  this.streamProvider = streamProvider;
1585
1941
  this.patternRegistry = patternRegistry;
1586
1942
  }
1587
- logger = new import_common6.Logger("Jetstream:Consumer");
1943
+ logger = new import_common7.Logger("Jetstream:Consumer");
1588
1944
  /**
1589
1945
  * Ensure consumers exist for the specified kinds.
1590
1946
  *
@@ -1605,7 +1961,11 @@ var ConsumerProvider = class {
1605
1961
  getConsumerName(kind) {
1606
1962
  return consumerName(this.options.name, kind);
1607
1963
  }
1608
- /** Ensure a single consumer exists, creating if needed. */
1964
+ /**
1965
+ * Ensure a single consumer exists with the desired config.
1966
+ * Used at **startup** — creates or updates the consumer to match
1967
+ * the current pod's configuration.
1968
+ */
1609
1969
  async ensureConsumer(jsm, kind) {
1610
1970
  const stream = this.streamProvider.getStreamName(kind);
1611
1971
  const config = this.buildConfig(kind);
@@ -1616,13 +1976,74 @@ var ConsumerProvider = class {
1616
1976
  this.logger.debug(`Consumer exists, updating: ${name}`);
1617
1977
  return await jsm.consumers.update(stream, name, config);
1618
1978
  } catch (err) {
1619
- if (err instanceof import_jetstream15.JetStreamApiError && err.apiError().err_code === CONSUMER_NOT_FOUND) {
1620
- this.logger.log(`Creating consumer: ${name}`);
1621
- return await jsm.consumers.add(stream, config);
1979
+ if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
1980
+ throw err;
1981
+ }
1982
+ return await this.createConsumer(jsm, stream, name, config);
1983
+ }
1984
+ }
1985
+ /**
1986
+ * Recover a consumer that disappeared during runtime.
1987
+ * Used by **self-healing** — creates if missing, but NEVER updates config.
1988
+ *
1989
+ * If a migration backup stream exists, another pod is mid-migration — we
1990
+ * throw so the self-healing retry loop waits with backoff until migration
1991
+ * completes and the backup is cleaned up.
1992
+ *
1993
+ * This prevents old pods from:
1994
+ * - Overwriting a newer pod's consumer config during rolling updates
1995
+ * - Creating consumers during migration (which would consume and delete
1996
+ * workqueue messages while they're being restored)
1997
+ */
1998
+ async recoverConsumer(jsm, kind) {
1999
+ const stream = this.streamProvider.getStreamName(kind);
2000
+ const config = this.buildConfig(kind);
2001
+ const name = config.durable_name;
2002
+ this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
2003
+ await this.assertNoMigrationInProgress(jsm, stream);
2004
+ try {
2005
+ return await jsm.consumers.info(stream, name);
2006
+ } catch (err) {
2007
+ if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
2008
+ throw err;
2009
+ }
2010
+ return await this.createConsumer(jsm, stream, name, config);
2011
+ }
2012
+ }
2013
+ /**
2014
+ * Throw if a migration backup stream exists for this stream.
2015
+ * The self-healing retry loop catches the error and retries with backoff,
2016
+ * naturally waiting until the migrating pod finishes and cleans up the backup.
2017
+ */
2018
+ async assertNoMigrationInProgress(jsm, stream) {
2019
+ const backupName = `${stream}${MIGRATION_BACKUP_SUFFIX}`;
2020
+ try {
2021
+ await jsm.streams.info(backupName);
2022
+ throw new Error(
2023
+ `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
2024
+ );
2025
+ } catch (err) {
2026
+ if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
2027
+ return;
1622
2028
  }
1623
2029
  throw err;
1624
2030
  }
1625
2031
  }
2032
+ /**
2033
+ * Create a consumer, handling the race where another pod creates it first.
2034
+ */
2035
+ async createConsumer(jsm, stream, name, config) {
2036
+ this.logger.log(`Creating consumer: ${name}`);
2037
+ try {
2038
+ return await jsm.consumers.add(stream, config);
2039
+ } catch (addErr) {
2040
+ if (addErr instanceof import_jetstream16.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
2041
+ this.logger.debug(`Consumer ${name} created by another pod, using existing`);
2042
+ return await jsm.consumers.info(stream, name);
2043
+ }
2044
+ throw addErr;
2045
+ }
2046
+ }
1626
2047
  /** Build consumer config by merging defaults with user overrides. */
1627
2048
  // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
1628
2049
  buildConfig(kind) {
@@ -1675,6 +2096,7 @@ var ConsumerProvider = class {
1675
2096
  return DEFAULT_BROADCAST_CONSUMER_CONFIG;
1676
2097
  case "ordered" /* Ordered */:
1677
2098
  throw new Error("Ordered consumers are ephemeral and should not use durable config");
2099
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1678
2100
  default: {
1679
2101
  const _exhaustive = kind;
1680
2102
  throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
@@ -1692,6 +2114,7 @@ var ConsumerProvider = class {
1692
2114
  return this.options.broadcast?.consumer ?? {};
1693
2115
  case "ordered" /* Ordered */:
1694
2116
  throw new Error("Ordered consumers are ephemeral and should not use durable config");
2117
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1695
2118
  default: {
1696
2119
  const _exhaustive = kind;
1697
2120
  throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
@@ -1701,16 +2124,17 @@ var ConsumerProvider = class {
1701
2124
  };
1702
2125
 
1703
2126
  // src/server/infrastructure/message.provider.ts
1704
- var import_common7 = require("@nestjs/common");
1705
- var import_jetstream17 = require("@nats-io/jetstream");
2127
+ var import_common8 = require("@nestjs/common");
2128
+ var import_jetstream18 = require("@nats-io/jetstream");
1706
2129
  var import_rxjs3 = require("rxjs");
1707
2130
  var MessageProvider = class {
1708
- constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
2131
+ constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
1709
2132
  this.connection = connection;
1710
2133
  this.eventBus = eventBus;
1711
2134
  this.consumeOptionsMap = consumeOptionsMap;
2135
+ this.consumerRecoveryFn = consumerRecoveryFn;
1712
2136
  }
1713
- logger = new import_common7.Logger("Jetstream:Message");
2137
+ logger = new import_common8.Logger("Jetstream:Message");
1714
2138
  activeIterators = /* @__PURE__ */ new Set();
1715
2139
  orderedReadyResolve = null;
1716
2140
  orderedReadyReject = null;
@@ -1762,7 +2186,7 @@ var MessageProvider = class {
1762
2186
  */
1763
2187
  async startOrdered(streamName2, filterSubjects, orderedConfig) {
1764
2188
  const consumerOpts = { filter_subjects: filterSubjects };
1765
- if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream17.DeliverPolicy.All) {
2189
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream18.DeliverPolicy.All) {
1766
2190
  consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
1767
2191
  }
1768
2192
  if (orderedConfig?.optStartSeq !== void 0) {
@@ -1813,12 +2237,26 @@ var MessageProvider = class {
1813
2237
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
1814
2238
  async consumeOnce(kind, info, target$) {
1815
2239
  const js = this.connection.getJetStreamClient();
1816
- const consumer = await js.consumers.get(info.stream_name, info.name);
2240
+ let consumer;
2241
+ let consumerName2 = info.name;
2242
+ try {
2243
+ consumer = await js.consumers.get(info.stream_name, info.name);
2244
+ } catch (err) {
2245
+ if (this.isConsumerNotFound(err) && this.consumerRecoveryFn) {
2246
+ this.logger.warn(`Consumer ${info.name} not found, recreating...`);
2247
+ const recovered = await this.consumerRecoveryFn(kind);
2248
+ consumerName2 = recovered.name;
2249
+ this.logger.log(`Consumer ${consumerName2} recreated, resuming consumption`);
2250
+ consumer = await js.consumers.get(recovered.stream_name, consumerName2);
2251
+ } else {
2252
+ throw err;
2253
+ }
2254
+ }
1817
2255
  const defaults = { idle_heartbeat: 5e3 };
1818
2256
  const userOptions = this.consumeOptionsMap.get(kind) ?? {};
1819
2257
  const messages = await consumer.consume({ ...defaults, ...userOptions });
1820
2258
  this.activeIterators.add(messages);
1821
- this.monitorConsumerHealth(messages, info.name);
2259
+ this.monitorConsumerHealth(messages, consumerName2);
1822
2260
  try {
1823
2261
  for await (const msg of messages) {
1824
2262
  target$.next(msg);
@@ -1827,6 +2265,17 @@ var MessageProvider = class {
1827
2265
  this.activeIterators.delete(messages);
1828
2266
  }
1829
2267
  }
2268
+ /**
2269
+ * Detect "consumer not found" errors from `js.consumers.get()`.
2270
+ *
2271
+ * Unlike JetStream Manager calls (which throw `JetStreamApiError`),
2272
+ * the JetStream client's `consumers.get()` throws a plain `Error`
2273
+ * with the error code embedded in the message text.
2274
+ */
2275
+ isConsumerNotFound(err) {
2276
+ if (!(err instanceof Error)) return false;
2277
+ return err.message.includes("consumer not found") || err.message.includes(String(10014 /* ConsumerNotFound */));
2278
+ }
1830
2279
  /** Get the target subject for a consumer kind. */
1831
2280
  getTargetSubject(kind) {
1832
2281
  switch (kind) {
@@ -1838,6 +2287,7 @@ var MessageProvider = class {
1838
2287
  return this.broadcastMessages$;
1839
2288
  case "ordered" /* Ordered */:
1840
2289
  return this.orderedMessages$;
2290
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1841
2291
  default: {
1842
2292
  const _exhaustive = kind;
1843
2293
  throw new Error(`Unknown stream kind: ${_exhaustive}`);
@@ -1923,8 +2373,110 @@ var MessageProvider = class {
1923
2373
  }
1924
2374
  };
1925
2375
 
2376
+ // src/server/infrastructure/metadata.provider.ts
2377
+ var import_common9 = require("@nestjs/common");
2378
+ var import_kv = require("@nats-io/kv");
2379
+ var MetadataProvider = class {
2380
+ constructor(options, connection) {
2381
+ this.connection = connection;
2382
+ this.bucketName = options.metadata?.bucket ?? DEFAULT_METADATA_BUCKET;
2383
+ this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
2384
+ this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
2385
+ }
2386
+ logger = new import_common9.Logger("Jetstream:Metadata");
2387
+ bucketName;
2388
+ replicas;
2389
+ ttl;
2390
+ currentEntries;
2391
+ heartbeatTimer;
2392
+ cachedKv;
2393
+ /**
2394
+ * Write handler metadata entries to the KV bucket and start heartbeat.
2395
+ *
2396
+ * Creates the bucket if it doesn't exist (idempotent).
2397
+ * Skips silently when entries map is empty.
2398
+ * Starts a heartbeat interval that refreshes entries every `ttl / 2`
2399
+ * to prevent TTL expiry while the pod is alive.
2400
+ *
2401
+ * Non-critical — errors are logged but do not prevent transport startup.
2402
+ *
2403
+ * @param entries Map of KV key → metadata object.
2404
+ */
2405
+ async publish(entries) {
2406
+ if (entries.size === 0) return;
2407
+ try {
2408
+ const kv = await this.openBucket();
2409
+ await this.writeEntries(kv, entries);
2410
+ this.currentEntries = entries;
2411
+ this.startHeartbeat();
2412
+ this.logger.log(
2413
+ `Published ${entries.size} handler metadata entries to KV bucket "${this.bucketName}"`
2414
+ );
2415
+ } catch (err) {
2416
+ this.logger.error("Failed to publish handler metadata to KV", err);
2417
+ }
2418
+ }
2419
+ /**
2420
+ * Stop the heartbeat timer.
2421
+ *
2422
+ * After this call, entries will expire via TTL once the heartbeat window passes.
2423
+ * Called during transport shutdown (strategy.close()).
2424
+ */
2425
+ destroy() {
2426
+ if (this.heartbeatTimer) {
2427
+ clearInterval(this.heartbeatTimer);
2428
+ this.heartbeatTimer = void 0;
2429
+ }
2430
+ this.currentEntries = void 0;
2431
+ this.cachedKv = void 0;
2432
+ }
2433
+ /** Write entries to KV with per-entry error handling. */
2434
+ async writeEntries(kv, entries) {
2435
+ for (const [key, meta] of entries) {
2436
+ try {
2437
+ await kv.put(key, JSON.stringify(meta));
2438
+ } catch (err) {
2439
+ this.logger.error(`Failed to write metadata entry "${key}"`, err);
2440
+ }
2441
+ }
2442
+ }
2443
+ /** Start heartbeat interval that refreshes entries every ttl/2. */
2444
+ startHeartbeat() {
2445
+ if (this.heartbeatTimer) {
2446
+ clearInterval(this.heartbeatTimer);
2447
+ }
2448
+ const interval = Math.floor(this.ttl / 2);
2449
+ this.heartbeatTimer = setInterval(() => {
2450
+ void this.refreshEntries();
2451
+ }, interval);
2452
+ this.heartbeatTimer.unref();
2453
+ }
2454
+ /** Refresh all current entries in KV (heartbeat tick). */
2455
+ async refreshEntries() {
2456
+ if (!this.currentEntries || this.currentEntries.size === 0) return;
2457
+ try {
2458
+ const kv = await this.openBucket();
2459
+ await this.writeEntries(kv, this.currentEntries);
2460
+ } catch (err) {
2461
+ this.logger.error("Failed to refresh handler metadata in KV", err);
2462
+ }
2463
+ }
2464
+ /** Create or open the KV bucket (cached after first call). */
2465
+ async openBucket() {
2466
+ if (this.cachedKv) return this.cachedKv;
2467
+ const js = this.connection.getJetStreamClient();
2468
+ const kvm = new import_kv.Kvm(js);
2469
+ this.cachedKv = await kvm.create(this.bucketName, {
2470
+ history: DEFAULT_METADATA_HISTORY,
2471
+ replicas: this.replicas,
2472
+ ttl: this.ttl
2473
+ });
2474
+ return this.cachedKv;
2475
+ }
2476
+ };
2477
+
1926
2478
  // src/server/routing/pattern-registry.ts
1927
- var import_common8 = require("@nestjs/common");
2479
+ var import_common10 = require("@nestjs/common");
1928
2480
  var HANDLER_LABELS = {
1929
2481
  ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
1930
2482
  ["ordered" /* Ordered */]: "ordered" /* Ordered */,
@@ -1935,7 +2487,7 @@ var PatternRegistry = class {
1935
2487
  constructor(options) {
1936
2488
  this.options = options;
1937
2489
  }
1938
- logger = new import_common8.Logger("Jetstream:PatternRegistry");
2490
+ logger = new import_common10.Logger("Jetstream:PatternRegistry");
1939
2491
  registry = /* @__PURE__ */ new Map();
1940
2492
  // Cached after registerHandlers() — the registry is immutable from that point
1941
2493
  cachedPatterns = null;
@@ -1943,6 +2495,7 @@ var PatternRegistry = class {
1943
2495
  _hasCommands = false;
1944
2496
  _hasBroadcasts = false;
1945
2497
  _hasOrdered = false;
2498
+ _hasMetadata = false;
1946
2499
  /**
1947
2500
  * Register all handlers from the NestJS strategy.
1948
2501
  *
@@ -1955,6 +2508,7 @@ var PatternRegistry = class {
1955
2508
  const isEvent = handler.isEventHandler ?? false;
1956
2509
  const isBroadcast = !!extras?.broadcast;
1957
2510
  const isOrdered = !!extras?.ordered;
2511
+ const meta = extras?.meta;
1958
2512
  if (isBroadcast && isOrdered) {
1959
2513
  throw new Error(
1960
2514
  `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
@@ -1971,7 +2525,8 @@ var PatternRegistry = class {
1971
2525
  pattern,
1972
2526
  isEvent: isEvent && !isOrdered,
1973
2527
  isBroadcast,
1974
- isOrdered
2528
+ isOrdered,
2529
+ meta
1975
2530
  });
1976
2531
  this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
1977
2532
  }
@@ -1980,6 +2535,7 @@ var PatternRegistry = class {
1980
2535
  this._hasCommands = this.cachedPatterns.commands.length > 0;
1981
2536
  this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
1982
2537
  this._hasOrdered = this.cachedPatterns.ordered.length > 0;
2538
+ this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
1983
2539
  this.logSummary();
1984
2540
  }
1985
2541
  /** Find handler for a full NATS subject. */
@@ -2008,6 +2564,26 @@ var PatternRegistry = class {
2008
2564
  (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2009
2565
  );
2010
2566
  }
2567
+ /** Check if any registered handler has metadata. */
2568
+ hasMetadata() {
2569
+ return this._hasMetadata;
2570
+ }
2571
+ /**
2572
+ * Get handler metadata entries for KV publishing.
2573
+ *
2574
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
2575
+ * Key format: `{serviceName}.{kind}.{pattern}`.
2576
+ */
2577
+ getMetadataEntries() {
2578
+ const entries = /* @__PURE__ */ new Map();
2579
+ for (const entry of this.registry.values()) {
2580
+ if (!entry.meta) continue;
2581
+ const kind = this.resolveStreamKind(entry);
2582
+ const key = metadataKey(this.options.name, kind, entry.pattern);
2583
+ entries.set(key, entry.meta);
2584
+ }
2585
+ return entries;
2586
+ }
2011
2587
  /** Get patterns grouped by kind (cached after registration). */
2012
2588
  getPatternsByKind() {
2013
2589
  const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
@@ -2047,6 +2623,12 @@ var PatternRegistry = class {
2047
2623
  }
2048
2624
  return { events, commands, broadcasts, ordered };
2049
2625
  }
2626
+ resolveStreamKind(entry) {
2627
+ if (entry.isBroadcast) return "broadcast" /* Broadcast */;
2628
+ if (entry.isOrdered) return "ordered" /* Ordered */;
2629
+ if (entry.isEvent) return "ev" /* Event */;
2630
+ return "cmd" /* Command */;
2631
+ }
2050
2632
  logSummary() {
2051
2633
  const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
2052
2634
  const parts = [
@@ -2062,10 +2644,11 @@ var PatternRegistry = class {
2062
2644
  };
2063
2645
 
2064
2646
  // src/server/routing/event.router.ts
2065
- var import_common9 = require("@nestjs/common");
2647
+ var import_common11 = require("@nestjs/common");
2066
2648
  var import_rxjs4 = require("rxjs");
2649
+ var import_transport_node4 = require("@nats-io/transport-node");
2067
2650
  var EventRouter = class {
2068
- constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
2651
+ constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
2069
2652
  this.messageProvider = messageProvider;
2070
2653
  this.patternRegistry = patternRegistry;
2071
2654
  this.codec = codec;
@@ -2073,8 +2656,10 @@ var EventRouter = class {
2073
2656
  this.deadLetterConfig = deadLetterConfig;
2074
2657
  this.processingConfig = processingConfig;
2075
2658
  this.ackWaitMap = ackWaitMap;
2659
+ this.connection = connection;
2660
+ this.options = options;
2076
2661
  }
2077
- logger = new import_common9.Logger("Jetstream:EventRouter");
2662
+ logger = new import_common11.Logger("Jetstream:EventRouter");
2078
2663
  subscriptions = [];
2079
2664
  /**
2080
2665
  * Update the max_deliver thresholds from actual NATS consumer configs.
@@ -2201,6 +2786,93 @@ var EventRouter = class {
2201
2786
  return msg.info.deliveryCount >= maxDeliver;
2202
2787
  }
2203
2788
  /** Handle a dead letter: invoke callback, then term or nak based on result. */
2789
+ /**
2790
+ * Fallback execution for a dead letter when DLQ is disabled, or when
2791
+ * publishing to the DLQ stream fails (due to network or NATS errors).
2792
+ *
2793
+ * Triggers the user-provided `onDeadLetter` hook for logging/alerting.
2794
+ * On success, terminates the message. On error, leaves it unacknowledged (nak)
2795
+ * so NATS can retry the delivery on the next cycle.
2796
+ */
2797
+ async fallbackToOnDeadLetterCallback(info, msg) {
2798
+ if (!this.deadLetterConfig) {
2799
+ msg.term("Dead letter config unavailable");
2800
+ return;
2801
+ }
2802
+ try {
2803
+ await this.deadLetterConfig.onDeadLetter(info);
2804
+ msg.term("Dead letter processed via fallback callback");
2805
+ } catch (hookErr) {
2806
+ this.logger.error(
2807
+ `Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
2808
+ hookErr
2809
+ );
2810
+ msg.nak();
2811
+ }
2812
+ }
2813
+ /**
2814
+ * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
2815
+ *
2816
+ * Appends diagnostic metadata headers to the original message and preserves
2817
+ * the primary payload. If publishing succeeds, it notifies the standard
2818
+ * `onDeadLetter` callback and terminates the message. If it fails, it falls
2819
+ * back to the callback entirely to prevent silent data loss.
2820
+ */
2821
+ async publishToDlq(msg, info, error) {
2822
+ const serviceName = this.options?.name;
2823
+ if (!this.connection || !serviceName) {
2824
+ this.logger.error(
2825
+ `Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
2826
+ );
2827
+ await this.fallbackToOnDeadLetterCallback(info, msg);
2828
+ return;
2829
+ }
2830
+ const destinationSubject = dlqStreamName(serviceName);
2831
+ const hdrs = (0, import_transport_node4.headers)();
2832
+ if (msg.headers) {
2833
+ for (const [k, v] of msg.headers) {
2834
+ for (const val of v) {
2835
+ hdrs.append(k, val);
2836
+ }
2837
+ }
2838
+ }
2839
+ let reason = String(error);
2840
+ if (error instanceof Error) {
2841
+ reason = error.message;
2842
+ } else if (typeof error === "object" && error !== null && "message" in error) {
2843
+ reason = String(error.message);
2844
+ }
2845
+ hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
2846
+ hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
2847
+ hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
2848
+ hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
2849
+ hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
2850
+ try {
2851
+ const js = this.connection.getJetStreamClient();
2852
+ await js.publish(destinationSubject, msg.data, { headers: hdrs });
2853
+ this.logger.log(`Message sent to DLQ: ${msg.subject}`);
2854
+ if (this.deadLetterConfig?.onDeadLetter) {
2855
+ try {
2856
+ await this.deadLetterConfig.onDeadLetter(info);
2857
+ } catch (hookErr) {
2858
+ this.logger.warn(
2859
+ `onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
2860
+ hookErr
2861
+ );
2862
+ }
2863
+ }
2864
+ msg.term("Moved to DLQ stream");
2865
+ } catch (publishErr) {
2866
+ this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
2867
+ await this.fallbackToOnDeadLetterCallback(info, msg);
2868
+ }
2869
+ }
2870
+ /**
2871
+ * Orchestrates the handling of a message that has exhausted delivery limits.
2872
+ *
2873
+ * Emits a system event and delegates either to the robust DLQ stream publisher
2874
+ * or directly to the fallback callback based on the active module configuration.
2875
+ */
2204
2876
  async handleDeadLetter(msg, data, error) {
2205
2877
  const info = {
2206
2878
  subject: msg.subject,
@@ -2213,23 +2885,17 @@ var EventRouter = class {
2213
2885
  timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
2214
2886
  };
2215
2887
  this.eventBus.emit("deadLetter" /* DeadLetter */, info);
2216
- if (!this.deadLetterConfig) {
2217
- msg.term("Dead letter config unavailable");
2218
- return;
2219
- }
2220
- try {
2221
- await this.deadLetterConfig.onDeadLetter(info);
2222
- msg.term("Dead letter processed");
2223
- } catch (hookErr) {
2224
- this.logger.error(`onDeadLetter callback failed for ${msg.subject}, nak for retry:`, hookErr);
2225
- msg.nak();
2888
+ if (!this.options?.dlq) {
2889
+ await this.fallbackToOnDeadLetterCallback(info, msg);
2890
+ } else {
2891
+ await this.publishToDlq(msg, info, error);
2226
2892
  }
2227
2893
  }
2228
2894
  };
2229
2895
 
2230
2896
  // src/server/routing/rpc.router.ts
2231
- var import_common10 = require("@nestjs/common");
2232
- var import_transport_node4 = require("@nats-io/transport-node");
2897
+ var import_common12 = require("@nestjs/common");
2898
+ var import_transport_node5 = require("@nats-io/transport-node");
2233
2899
  var import_rxjs5 = require("rxjs");
2234
2900
  var RpcRouter = class {
2235
2901
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
@@ -2243,7 +2909,7 @@ var RpcRouter = class {
2243
2909
  this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
2244
2910
  this.concurrency = rpcOptions?.concurrency;
2245
2911
  }
2246
- logger = new import_common10.Logger("Jetstream:RpcRouter");
2912
+ logger = new import_common12.Logger("Jetstream:RpcRouter");
2247
2913
  timeout;
2248
2914
  concurrency;
2249
2915
  resolvedAckExtensionInterval;
@@ -2321,7 +2987,7 @@ var RpcRouter = class {
2321
2987
  stopAckExtension?.();
2322
2988
  msg.ack();
2323
2989
  try {
2324
- const hdrs = (0, import_transport_node4.headers)();
2990
+ const hdrs = (0, import_transport_node5.headers)();
2325
2991
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
2326
2992
  nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
2327
2993
  } catch (publishErr) {
@@ -2333,7 +2999,7 @@ var RpcRouter = class {
2333
2999
  clearTimeout(timeoutId);
2334
3000
  stopAckExtension?.();
2335
3001
  try {
2336
- const hdrs = (0, import_transport_node4.headers)();
3002
+ const hdrs = (0, import_transport_node5.headers)();
2337
3003
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
2338
3004
  hdrs.set("x-error" /* Error */, "true");
2339
3005
  nc.publish(replyTo, this.codec.encode(serializeError(err)), { headers: hdrs });
@@ -2346,20 +3012,27 @@ var RpcRouter = class {
2346
3012
  };
2347
3013
 
2348
3014
  // src/shutdown/shutdown.manager.ts
2349
- var import_common11 = require("@nestjs/common");
3015
+ var import_common13 = require("@nestjs/common");
2350
3016
  var ShutdownManager = class {
2351
3017
  constructor(connection, eventBus, timeout) {
2352
3018
  this.connection = connection;
2353
3019
  this.eventBus = eventBus;
2354
3020
  this.timeout = timeout;
2355
3021
  }
2356
- logger = new import_common11.Logger("Jetstream:Shutdown");
3022
+ logger = new import_common13.Logger("Jetstream:Shutdown");
3023
+ shutdownPromise;
2357
3024
  /**
2358
3025
  * Execute the full shutdown sequence.
2359
3026
  *
3027
+ * Idempotent — concurrent or repeated calls return the same promise.
3028
+ *
2360
3029
  * @param strategy Optional stoppable to close (stops consumers and subscriptions).
2361
3030
  */
2362
3031
  async shutdown(strategy) {
3032
+ this.shutdownPromise ??= this.doShutdown(strategy);
3033
+ return this.shutdownPromise;
3034
+ }
3035
+ async doShutdown(strategy) {
2363
3036
  this.eventBus.emit("shutdownStart" /* ShutdownStart */);
2364
3037
  this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
2365
3038
  strategy?.close();
@@ -2494,7 +3167,7 @@ var JetstreamModule = class {
2494
3167
  provide: JETSTREAM_EVENT_BUS,
2495
3168
  inject: [JETSTREAM_OPTIONS],
2496
3169
  useFactory: (options) => {
2497
- const logger = new import_common12.Logger("Jetstream:Module");
3170
+ const logger = new import_common14.Logger("Jetstream:Module");
2498
3171
  return new EventBus(logger, options.hooks);
2499
3172
  }
2500
3173
  },
@@ -2573,8 +3246,8 @@ var JetstreamModule = class {
2573
3246
  // MessageProvider — pull-based message consumption
2574
3247
  {
2575
3248
  provide: MessageProvider,
2576
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
2577
- useFactory: (options, connection, eventBus) => {
3249
+ inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
3250
+ useFactory: (options, connection, eventBus, consumerProvider) => {
2578
3251
  if (options.consumer === false) return null;
2579
3252
  const consumeOptionsMap = /* @__PURE__ */ new Map();
2580
3253
  if (options.events?.consume)
@@ -2584,7 +3257,11 @@ var JetstreamModule = class {
2584
3257
  if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
2585
3258
  consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
2586
3259
  }
2587
- return new MessageProvider(connection, eventBus, consumeOptionsMap);
3260
+ const consumerRecoveryFn = consumerProvider ? async (kind) => {
3261
+ const jsm = await connection.getJetStreamManager();
3262
+ return consumerProvider.recoverConsumer(jsm, kind);
3263
+ } : void 0;
3264
+ return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
2588
3265
  }
2589
3266
  },
2590
3267
  // EventRouter — routes event and broadcast messages to handlers
@@ -2596,9 +3273,10 @@ var JetstreamModule = class {
2596
3273
  PatternRegistry,
2597
3274
  JETSTREAM_CODEC,
2598
3275
  JETSTREAM_EVENT_BUS,
2599
- JETSTREAM_ACK_WAIT_MAP
3276
+ JETSTREAM_ACK_WAIT_MAP,
3277
+ JETSTREAM_CONNECTION
2600
3278
  ],
2601
- useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
3279
+ useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
2602
3280
  if (options.consumer === false) return null;
2603
3281
  const deadLetterConfig = options.onDeadLetter ? {
2604
3282
  maxDeliverByStream: /* @__PURE__ */ new Map(),
@@ -2621,7 +3299,9 @@ var JetstreamModule = class {
2621
3299
  eventBus,
2622
3300
  deadLetterConfig,
2623
3301
  processingConfig,
2624
- ackWaitMap
3302
+ ackWaitMap,
3303
+ connection,
3304
+ options
2625
3305
  );
2626
3306
  }
2627
3307
  },
@@ -2670,6 +3350,15 @@ var JetstreamModule = class {
2670
3350
  return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
2671
3351
  }
2672
3352
  },
3353
+ // MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
3354
+ {
3355
+ provide: MetadataProvider,
3356
+ inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
3357
+ useFactory: (options, connection) => {
3358
+ if (options.consumer === false) return null;
3359
+ return new MetadataProvider(options, connection);
3360
+ }
3361
+ },
2673
3362
  // JetstreamStrategy — server-side transport (only when consumer enabled)
2674
3363
  {
2675
3364
  provide: JetstreamStrategy,
@@ -2683,9 +3372,10 @@ var JetstreamModule = class {
2683
3372
  EventRouter,
2684
3373
  RpcRouter,
2685
3374
  CoreRpcServer,
2686
- JETSTREAM_ACK_WAIT_MAP
3375
+ JETSTREAM_ACK_WAIT_MAP,
3376
+ MetadataProvider
2687
3377
  ],
2688
- useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
3378
+ useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap, metadataProvider) => {
2689
3379
  if (options.consumer === false) return null;
2690
3380
  return new JetstreamStrategy(
2691
3381
  options,
@@ -2697,7 +3387,8 @@ var JetstreamModule = class {
2697
3387
  eventRouter,
2698
3388
  rpcRouter,
2699
3389
  coreRpcServer,
2700
- ackWaitMap
3390
+ ackWaitMap,
3391
+ metadataProvider
2701
3392
  );
2702
3393
  }
2703
3394
  }
@@ -2756,21 +3447,26 @@ var JetstreamModule = class {
2756
3447
  }
2757
3448
  };
2758
3449
  JetstreamModule = __decorateClass([
2759
- (0, import_common12.Global)(),
2760
- (0, import_common12.Module)({}),
2761
- __decorateParam(0, (0, import_common12.Optional)()),
2762
- __decorateParam(0, (0, import_common12.Inject)(ShutdownManager)),
2763
- __decorateParam(1, (0, import_common12.Optional)()),
2764
- __decorateParam(1, (0, import_common12.Inject)(JetstreamStrategy))
3450
+ (0, import_common14.Global)(),
3451
+ (0, import_common14.Module)({}),
3452
+ __decorateParam(0, (0, import_common14.Optional)()),
3453
+ __decorateParam(0, (0, import_common14.Inject)(ShutdownManager)),
3454
+ __decorateParam(1, (0, import_common14.Optional)()),
3455
+ __decorateParam(1, (0, import_common14.Inject)(JetstreamStrategy))
2765
3456
  ], JetstreamModule);
2766
3457
  // Annotate the CommonJS export names for ESM import in node:
2767
3458
  0 && (module.exports = {
3459
+ DEFAULT_METADATA_BUCKET,
3460
+ DEFAULT_METADATA_HISTORY,
3461
+ DEFAULT_METADATA_REPLICAS,
3462
+ DEFAULT_METADATA_TTL,
2768
3463
  EventBus,
2769
3464
  JETSTREAM_CODEC,
2770
3465
  JETSTREAM_CONNECTION,
2771
3466
  JETSTREAM_EVENT_BUS,
2772
3467
  JETSTREAM_OPTIONS,
2773
3468
  JetstreamClient,
3469
+ JetstreamDlqHeader,
2774
3470
  JetstreamHeader,
2775
3471
  JetstreamHealthIndicator,
2776
3472
  JetstreamModule,
@@ -2778,17 +3474,21 @@ JetstreamModule = __decorateClass([
2778
3474
  JetstreamRecordBuilder,
2779
3475
  JetstreamStrategy,
2780
3476
  JsonCodec,
3477
+ MIN_METADATA_TTL,
2781
3478
  MessageKind,
2782
3479
  PatternPrefix,
2783
3480
  RpcContext,
2784
3481
  StreamKind,
2785
3482
  TransportEvent,
3483
+ buildBroadcastSubject,
2786
3484
  buildSubject,
2787
3485
  consumerName,
3486
+ dlqStreamName,
2788
3487
  getClientToken,
2789
3488
  internalName,
2790
3489
  isCoreRpcMode,
2791
3490
  isJetStreamRpcMode,
3491
+ metadataKey,
2792
3492
  streamName,
2793
3493
  toNanos
2794
3494
  });