@horizon-republic/nestjs-jetstream 2.8.0 → 2.9.1

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
@@ -7,11 +7,11 @@ var __export = (target, all) => {
7
7
  for (var name in all)
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
- var __copyProps = (to, from4, except, desc) => {
11
- if (from4 && typeof from4 === "object" || typeof from4 === "function") {
12
- for (let key of __getOwnPropNames(from4))
10
+ var __copyProps = (to, from2, except, desc) => {
11
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
12
+ for (let key of __getOwnPropNames(from2))
13
13
  if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from4[key], enumerable: !(desc = __getOwnPropDesc(from4, key)) || desc.enumerable });
14
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
15
15
  }
16
16
  return to;
17
17
  };
@@ -29,12 +29,26 @@ 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
- EventBus: () => EventBus,
32
+ DEFAULT_BROADCAST_CONSUMER_CONFIG: () => DEFAULT_BROADCAST_CONSUMER_CONFIG,
33
+ DEFAULT_BROADCAST_STREAM_CONFIG: () => DEFAULT_BROADCAST_STREAM_CONFIG,
34
+ DEFAULT_COMMAND_CONSUMER_CONFIG: () => DEFAULT_COMMAND_CONSUMER_CONFIG,
35
+ DEFAULT_COMMAND_STREAM_CONFIG: () => DEFAULT_COMMAND_STREAM_CONFIG,
36
+ DEFAULT_DLQ_STREAM_CONFIG: () => DEFAULT_DLQ_STREAM_CONFIG,
37
+ DEFAULT_EVENT_CONSUMER_CONFIG: () => DEFAULT_EVENT_CONSUMER_CONFIG,
38
+ DEFAULT_EVENT_STREAM_CONFIG: () => DEFAULT_EVENT_STREAM_CONFIG,
39
+ DEFAULT_JETSTREAM_RPC_TIMEOUT: () => DEFAULT_JETSTREAM_RPC_TIMEOUT,
40
+ DEFAULT_METADATA_BUCKET: () => DEFAULT_METADATA_BUCKET,
41
+ DEFAULT_METADATA_HISTORY: () => DEFAULT_METADATA_HISTORY,
42
+ DEFAULT_METADATA_REPLICAS: () => DEFAULT_METADATA_REPLICAS,
43
+ DEFAULT_METADATA_TTL: () => DEFAULT_METADATA_TTL,
44
+ DEFAULT_ORDERED_STREAM_CONFIG: () => DEFAULT_ORDERED_STREAM_CONFIG,
45
+ DEFAULT_RPC_TIMEOUT: () => DEFAULT_RPC_TIMEOUT,
46
+ DEFAULT_SHUTDOWN_TIMEOUT: () => DEFAULT_SHUTDOWN_TIMEOUT,
33
47
  JETSTREAM_CODEC: () => JETSTREAM_CODEC,
34
48
  JETSTREAM_CONNECTION: () => JETSTREAM_CONNECTION,
35
- JETSTREAM_EVENT_BUS: () => JETSTREAM_EVENT_BUS,
36
49
  JETSTREAM_OPTIONS: () => JETSTREAM_OPTIONS,
37
50
  JetstreamClient: () => JetstreamClient,
51
+ JetstreamDlqHeader: () => JetstreamDlqHeader,
38
52
  JetstreamHeader: () => JetstreamHeader,
39
53
  JetstreamHealthIndicator: () => JetstreamHealthIndicator,
40
54
  JetstreamModule: () => JetstreamModule,
@@ -42,24 +56,31 @@ __export(index_exports, {
42
56
  JetstreamRecordBuilder: () => JetstreamRecordBuilder,
43
57
  JetstreamStrategy: () => JetstreamStrategy,
44
58
  JsonCodec: () => JsonCodec,
59
+ MIN_METADATA_TTL: () => MIN_METADATA_TTL,
45
60
  MessageKind: () => MessageKind,
61
+ MsgpackCodec: () => MsgpackCodec,
62
+ NatsErrorCode: () => NatsErrorCode,
46
63
  PatternPrefix: () => PatternPrefix,
64
+ RESERVED_HEADERS: () => RESERVED_HEADERS,
47
65
  RpcContext: () => RpcContext,
48
66
  StreamKind: () => StreamKind,
49
67
  TransportEvent: () => TransportEvent,
68
+ buildBroadcastSubject: () => buildBroadcastSubject,
50
69
  buildSubject: () => buildSubject,
51
70
  consumerName: () => consumerName,
71
+ dlqStreamName: () => dlqStreamName,
52
72
  getClientToken: () => getClientToken,
53
73
  internalName: () => internalName,
54
74
  isCoreRpcMode: () => isCoreRpcMode,
55
75
  isJetStreamRpcMode: () => isJetStreamRpcMode,
76
+ metadataKey: () => metadataKey,
56
77
  streamName: () => streamName,
57
78
  toNanos: () => toNanos
58
79
  });
59
80
  module.exports = __toCommonJS(index_exports);
60
81
 
61
82
  // src/jetstream.module.ts
62
- var import_common12 = require("@nestjs/common");
83
+ var import_common14 = require("@nestjs/common");
63
84
 
64
85
  // src/client/jetstream.client.ts
65
86
  var import_common = require("@nestjs/common");
@@ -152,7 +173,7 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
152
173
  max_msgs_per_subject: 1e6,
153
174
  max_msgs: 1e7,
154
175
  max_bytes: 2 * GB,
155
- max_age: toNanos(1, "days"),
176
+ max_age: toNanos(1, "hours"),
156
177
  duplicate_window: toNanos(2, "minutes")
157
178
  };
158
179
  var DEFAULT_ORDERED_STREAM_CONFIG = {
@@ -167,6 +188,18 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
167
188
  max_age: toNanos(1, "days"),
168
189
  duplicate_window: toNanos(2, "minutes")
169
190
  };
191
+ var DEFAULT_DLQ_STREAM_CONFIG = {
192
+ ...baseStreamConfig,
193
+ retention: import_jetstream.RetentionPolicy.Workqueue,
194
+ allow_rollup_hdrs: false,
195
+ max_consumers: 100,
196
+ max_msg_size: 10 * MB,
197
+ max_msgs_per_subject: 5e6,
198
+ max_msgs: 5e7,
199
+ max_bytes: 5 * GB,
200
+ max_age: toNanos(30, "days"),
201
+ duplicate_window: toNanos(2, "minutes")
202
+ };
170
203
  var DEFAULT_EVENT_CONSUMER_CONFIG = {
171
204
  ack_wait: toNanos(10, "seconds"),
172
205
  max_deliver: 3,
@@ -194,6 +227,12 @@ var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
194
227
  var DEFAULT_RPC_TIMEOUT = 3e4;
195
228
  var DEFAULT_JETSTREAM_RPC_TIMEOUT = 18e4;
196
229
  var DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
230
+ var DEFAULT_METADATA_BUCKET = "handler_registry";
231
+ var DEFAULT_METADATA_REPLICAS = 1;
232
+ var DEFAULT_METADATA_HISTORY = 1;
233
+ var DEFAULT_METADATA_TTL = 3e4;
234
+ var MIN_METADATA_TTL = 5e3;
235
+ var metadataKey = (serviceName, kind, pattern) => `${serviceName}.${kind}.${pattern}`;
197
236
  var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
198
237
  JetstreamHeader2["CorrelationId"] = "x-correlation-id";
199
238
  JetstreamHeader2["ReplyTo"] = "x-reply-to";
@@ -202,6 +241,14 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
202
241
  JetstreamHeader2["Error"] = "x-error";
203
242
  return JetstreamHeader2;
204
243
  })(JetstreamHeader || {});
244
+ var JetstreamDlqHeader = /* @__PURE__ */ ((JetstreamDlqHeader2) => {
245
+ JetstreamDlqHeader2["DeadLetterReason"] = "x-dead-letter-reason";
246
+ JetstreamDlqHeader2["OriginalSubject"] = "x-original-subject";
247
+ JetstreamDlqHeader2["OriginalStream"] = "x-original-stream";
248
+ JetstreamDlqHeader2["FailedAt"] = "x-failed-at";
249
+ JetstreamDlqHeader2["DeliveryCount"] = "x-delivery-count";
250
+ return JetstreamDlqHeader2;
251
+ })(JetstreamDlqHeader || {});
205
252
  var RESERVED_HEADERS = /* @__PURE__ */ new Set([
206
253
  "x-correlation-id" /* CorrelationId */,
207
254
  "x-reply-to" /* ReplyTo */,
@@ -214,6 +261,9 @@ var streamName = (serviceName, kind) => {
214
261
  if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
215
262
  return `${internalName(serviceName)}_${kind}-stream`;
216
263
  };
264
+ var dlqStreamName = (serviceName) => {
265
+ return `${internalName(serviceName)}_dlq-stream`;
266
+ };
217
267
  var consumerName = (serviceName, kind) => {
218
268
  if (kind === "broadcast" /* Broadcast */) return `${internalName(serviceName)}_broadcast-consumer`;
219
269
  return `${internalName(serviceName)}_${kind}-consumer`;
@@ -228,12 +278,13 @@ var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
228
278
 
229
279
  // src/client/jetstream.record.ts
230
280
  var JetstreamRecord = class {
231
- constructor(data, headers2, timeout, messageId, schedule) {
281
+ constructor(data, headers2, timeout, messageId, schedule, ttl) {
232
282
  this.data = data;
233
283
  this.headers = headers2;
234
284
  this.timeout = timeout;
235
285
  this.messageId = messageId;
236
286
  this.schedule = schedule;
287
+ this.ttl = ttl;
237
288
  }
238
289
  };
239
290
  var JetstreamRecordBuilder = class {
@@ -242,6 +293,7 @@ var JetstreamRecordBuilder = class {
242
293
  timeout;
243
294
  messageId;
244
295
  scheduleOptions;
296
+ ttlDuration;
245
297
  constructor(data) {
246
298
  this.data = data;
247
299
  }
@@ -333,6 +385,33 @@ var JetstreamRecordBuilder = class {
333
385
  this.scheduleOptions = { at: new Date(ts) };
334
386
  return this;
335
387
  }
388
+ /**
389
+ * Set per-message TTL (time-to-live).
390
+ *
391
+ * The message expires individually after the specified duration,
392
+ * independent of the stream's `max_age`. Requires NATS >= 2.11 and
393
+ * `allow_msg_ttl: true` on the stream.
394
+ *
395
+ * Only meaningful for events (`client.emit()`). If used with RPC
396
+ * (`client.send()`), a warning is logged and the TTL is ignored.
397
+ *
398
+ * @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * import { toNanos } from '@horizon-republic/nestjs-jetstream';
403
+ *
404
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
405
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
406
+ * ```
407
+ */
408
+ ttl(nanos) {
409
+ if (!Number.isFinite(nanos) || nanos <= 0) {
410
+ throw new Error("TTL must be a positive finite value");
411
+ }
412
+ this.ttlDuration = nanosToGoDuration(nanos);
413
+ return this;
414
+ }
336
415
  /**
337
416
  * Build the immutable {@link JetstreamRecord}.
338
417
  *
@@ -345,7 +424,8 @@ var JetstreamRecordBuilder = class {
345
424
  new Map(this.headers),
346
425
  this.timeout,
347
426
  this.messageId,
348
- schedule
427
+ schedule,
428
+ this.ttlDuration
349
429
  );
350
430
  }
351
431
  /** Validate that a header key is not reserved. */
@@ -357,8 +437,20 @@ var JetstreamRecordBuilder = class {
357
437
  }
358
438
  }
359
439
  };
440
+ var NS_PER_MS = 1e6;
441
+ var NS_PER_S = 1e9;
442
+ var NS_PER_M = 60 * NS_PER_S;
443
+ var NS_PER_H = 60 * NS_PER_M;
444
+ var nanosToGoDuration = (nanos) => {
445
+ if (nanos >= NS_PER_H && nanos % NS_PER_H === 0) return `${nanos / NS_PER_H}h`;
446
+ if (nanos >= NS_PER_M && nanos % NS_PER_M === 0) return `${nanos / NS_PER_M}m`;
447
+ if (nanos >= NS_PER_S && nanos % NS_PER_S === 0) return `${nanos / NS_PER_S}s`;
448
+ if (nanos >= NS_PER_MS && nanos % NS_PER_MS === 0) return `${nanos / NS_PER_MS}ms`;
449
+ return `${nanos}ns`;
450
+ };
360
451
 
361
452
  // src/client/jetstream.client.ts
453
+ var BROADCAST_SUBJECT_PREFIX = "broadcast.";
362
454
  var JetstreamClient = class extends import_microservices.ClientProxy {
363
455
  constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
364
456
  super();
@@ -368,12 +460,33 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
368
460
  this.eventBus = eventBus;
369
461
  this.targetName = targetServiceName;
370
462
  this.callerName = internalName(this.rootOptions.name);
463
+ const targetInternal = internalName(targetServiceName);
464
+ this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
465
+ this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
466
+ this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
467
+ this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
468
+ this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
371
469
  }
372
470
  logger = new import_common.Logger("Jetstream:Client");
373
471
  /** Target service name this client sends messages to. */
374
472
  targetName;
375
473
  /** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
376
474
  callerName;
475
+ /**
476
+ * Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
477
+ * per stream kind this client may publish to. Built once in the constructor
478
+ * so producing a full subject is a single string concat with the user pattern.
479
+ */
480
+ eventSubjectPrefix;
481
+ commandSubjectPrefix;
482
+ orderedSubjectPrefix;
483
+ /**
484
+ * RPC configuration snapshots. The values are derived from rootOptions at
485
+ * construction time so the publish hot path never has to re-run
486
+ * isCoreRpcMode / getRpcTimeout on every call.
487
+ */
488
+ isCoreMode;
489
+ defaultRpcTimeout;
377
490
  /** Shared inbox for JetStream-mode RPC responses. */
378
491
  inbox = null;
379
492
  inboxSubscription = null;
@@ -383,6 +496,12 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
383
496
  pendingTimeouts = /* @__PURE__ */ new Map();
384
497
  /** Subscription to connection status events for disconnect handling. */
385
498
  statusSubscription = null;
499
+ /**
500
+ * Cached readiness flag. Once `connect()` has wired the inbox and status
501
+ * subscription, subsequent publishes skip the `await connect()` microtask
502
+ * and reach for the underlying connection synchronously instead.
503
+ */
504
+ readyForPublish = false;
386
505
  /**
387
506
  * Establish connection. Called automatically by NestJS on first use.
388
507
  *
@@ -393,7 +512,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
393
512
  */
394
513
  async connect() {
395
514
  const nc = await this.connection.getConnection();
396
- if (isJetStreamRpcMode(this.rootOptions.rpc) && !this.inboxSubscription) {
515
+ if (!this.isCoreMode && !this.inboxSubscription) {
397
516
  this.setupInbox(nc);
398
517
  }
399
518
  this.statusSubscription ??= this.connection.status$.subscribe((status) => {
@@ -401,12 +520,14 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
401
520
  this.handleDisconnect();
402
521
  }
403
522
  });
523
+ this.readyForPublish = true;
404
524
  return nc;
405
525
  }
406
526
  /** Clean up resources: reject pending RPCs, unsubscribe from status events. */
407
527
  async close() {
408
528
  this.statusSubscription?.unsubscribe();
409
529
  this.statusSubscription = null;
530
+ this.readyForPublish = false;
410
531
  this.rejectPendingRpcs(new Error("Client closed"));
411
532
  }
412
533
  /**
@@ -430,8 +551,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
430
551
  * set to the original event subject.
431
552
  */
432
553
  async dispatchEvent(packet) {
433
- await this.connect();
434
- const { data, hdrs, messageId, schedule } = this.extractRecordData(packet.data);
554
+ if (!this.readyForPublish) await this.connect();
555
+ const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
435
556
  const eventSubject = this.buildEventSubject(packet.pattern);
436
557
  const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
437
558
  if (schedule) {
@@ -439,6 +560,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
439
560
  const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
440
561
  headers: msgHeaders,
441
562
  msgID: messageId ?? import_nuid.nuid.next(),
563
+ ttl,
442
564
  schedule: {
443
565
  specification: schedule.at,
444
566
  target: eventSubject
@@ -452,7 +574,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
452
574
  } else {
453
575
  const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
454
576
  headers: msgHeaders,
455
- msgID: messageId ?? import_nuid.nuid.next()
577
+ msgID: messageId ?? import_nuid.nuid.next(),
578
+ ttl
456
579
  });
457
580
  if (ack.duplicate) {
458
581
  this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
@@ -467,19 +590,24 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
467
590
  * JetStream mode: publishes to stream + waits for inbox response.
468
591
  */
469
592
  publish(packet, callback) {
470
- const subject = buildSubject(this.targetName, "cmd" /* Command */, packet.pattern);
471
- const { data, hdrs, timeout, messageId, schedule } = this.extractRecordData(packet.data);
593
+ const subject = this.commandSubjectPrefix + packet.pattern;
594
+ const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
472
595
  if (schedule) {
473
596
  this.logger.warn(
474
597
  "scheduleAt() is ignored for RPC (client.send()). Use client.emit() for scheduled events."
475
598
  );
476
599
  }
600
+ if (ttl) {
601
+ this.logger.warn(
602
+ "ttl() is ignored for RPC (client.send()). Use client.emit() for events with TTL."
603
+ );
604
+ }
477
605
  const onUnhandled = (err) => {
478
606
  this.logger.error("Unhandled publish error:", err);
479
607
  callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
480
608
  };
481
609
  let jetStreamCorrelationId = null;
482
- if (isCoreRpcMode(this.rootOptions.rpc)) {
610
+ if (this.isCoreMode) {
483
611
  this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
484
612
  } else {
485
613
  jetStreamCorrelationId = import_nuid.nuid.next();
@@ -504,8 +632,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
504
632
  /** Core mode: nc.request() with timeout. */
505
633
  async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
506
634
  try {
507
- const nc = await this.connect();
508
- const effectiveTimeout = timeout ?? this.getRpcTimeout();
635
+ const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
636
+ const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
509
637
  const hdrs = this.buildHeaders(customHeaders, { subject });
510
638
  const response = await nc.request(subject, this.codec.encode(data), {
511
639
  timeout: effectiveTimeout,
@@ -527,10 +655,10 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
527
655
  /** JetStream mode: publish to stream + wait for inbox response. */
528
656
  async publishJetStreamRpc(subject, data, callback, options) {
529
657
  const { headers: customHeaders, correlationId, messageId } = options;
530
- const effectiveTimeout = options.timeout ?? this.getRpcTimeout();
658
+ const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
531
659
  this.pendingMessages.set(correlationId, callback);
532
660
  try {
533
- await this.connect();
661
+ if (!this.readyForPublish) await this.connect();
534
662
  if (!this.pendingMessages.has(correlationId)) return;
535
663
  if (!this.inbox) {
536
664
  this.pendingMessages.delete(correlationId);
@@ -576,6 +704,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
576
704
  handleDisconnect() {
577
705
  this.rejectPendingRpcs(new Error("Connection lost"));
578
706
  this.inbox = null;
707
+ this.readyForPublish = false;
579
708
  }
580
709
  /** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
581
710
  rejectPendingRpcs(error) {
@@ -589,6 +718,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
589
718
  this.pendingTimeouts.clear();
590
719
  this.inboxSubscription?.unsubscribe();
591
720
  this.inboxSubscription = null;
721
+ this.inbox = null;
592
722
  }
593
723
  /** Setup shared inbox subscription for JetStream RPC responses. */
594
724
  setupInbox(nc) {
@@ -638,19 +768,22 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
638
768
  this.pendingMessages.delete(correlationId);
639
769
  }
640
770
  }
641
- /** Build event subject — workqueue, broadcast, or ordered. */
771
+ /**
772
+ * Resolve a user pattern to a fully-qualified NATS subject, dispatching
773
+ * between the event, broadcast, and ordered prefixes.
774
+ *
775
+ * The leading-char check short-circuits the `startsWith` comparisons for
776
+ * patterns that cannot possibly carry a broadcast/ordered marker, which is
777
+ * the overwhelmingly common case.
778
+ */
642
779
  buildEventSubject(pattern) {
643
- if (pattern.startsWith("broadcast:" /* Broadcast */)) {
644
- return buildBroadcastSubject(pattern.slice("broadcast:" /* Broadcast */.length));
645
- }
646
- if (pattern.startsWith("ordered:" /* Ordered */)) {
647
- return buildSubject(
648
- this.targetName,
649
- "ordered" /* Ordered */,
650
- pattern.slice("ordered:" /* Ordered */.length)
651
- );
780
+ if (pattern.charCodeAt(0) === 98 && pattern.startsWith("broadcast:" /* Broadcast */)) {
781
+ return BROADCAST_SUBJECT_PREFIX + pattern.slice("broadcast:" /* Broadcast */.length);
652
782
  }
653
- return buildSubject(this.targetName, "ev" /* Event */, pattern);
783
+ if (pattern.charCodeAt(0) === 111 && pattern.startsWith("ordered:" /* Ordered */)) {
784
+ return this.orderedSubjectPrefix + pattern.slice("ordered:" /* Ordered */.length);
785
+ }
786
+ return this.eventSubjectPrefix + pattern;
654
787
  }
655
788
  /** Build NATS headers merging custom headers with transport headers. */
656
789
  buildHeaders(customHeaders, transport) {
@@ -675,10 +808,11 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
675
808
  if (rawData instanceof JetstreamRecord) {
676
809
  return {
677
810
  data: rawData.data,
678
- hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
811
+ hdrs: rawData.headers.size > 0 ? rawData.headers : null,
679
812
  timeout: rawData.timeout,
680
813
  messageId: rawData.messageId,
681
- schedule: rawData.schedule
814
+ schedule: rawData.schedule,
815
+ ttl: rawData.ttl
682
816
  };
683
817
  }
684
818
  return {
@@ -686,7 +820,8 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
686
820
  hdrs: null,
687
821
  timeout: void 0,
688
822
  messageId: void 0,
689
- schedule: void 0
823
+ schedule: void 0,
824
+ ttl: void 0
690
825
  };
691
826
  }
692
827
  /**
@@ -716,11 +851,6 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
716
851
  const pattern = withoutPrefix.slice(dotIndex + 1);
717
852
  return `${targetPrefix}_sch.${pattern}`;
718
853
  }
719
- getRpcTimeout() {
720
- if (!this.rootOptions.rpc) return DEFAULT_RPC_TIMEOUT;
721
- const defaultTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? DEFAULT_JETSTREAM_RPC_TIMEOUT : DEFAULT_RPC_TIMEOUT;
722
- return this.rootOptions.rpc.timeout ?? defaultTimeout;
723
- }
724
854
  };
725
855
 
726
856
  // src/codec/json.codec.ts
@@ -735,6 +865,19 @@ var JsonCodec = class {
735
865
  }
736
866
  };
737
867
 
868
+ // src/codec/msgpack.codec.ts
869
+ var MsgpackCodec = class {
870
+ constructor(packr) {
871
+ this.packr = packr;
872
+ }
873
+ encode(data) {
874
+ return this.packr.pack(data);
875
+ }
876
+ decode(data) {
877
+ return this.packr.unpack(data);
878
+ }
879
+ };
880
+
738
881
  // src/connection/connection.provider.ts
739
882
  var import_common2 = require("@nestjs/common");
740
883
  var import_transport_node2 = require("@nats-io/transport-node");
@@ -947,6 +1090,15 @@ var EventBus = class {
947
1090
  kind
948
1091
  );
949
1092
  }
1093
+ /**
1094
+ * Check whether a hook is registered for the given event.
1095
+ *
1096
+ * Used by the routing hot path to elide the emit call entirely when the
1097
+ * transport owner did not register a listener.
1098
+ */
1099
+ hasHook(event) {
1100
+ return this.hooks[event] !== void 0;
1101
+ }
950
1102
  callHook(event, hook, ...args) {
951
1103
  try {
952
1104
  const result = hook(...args);
@@ -1001,9 +1153,14 @@ var JetstreamHealthIndicator = class {
1001
1153
  * Returns `{ [key]: { status: 'up', ... } }` on success.
1002
1154
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
1003
1155
  *
1156
+ * The thrown error sets `isHealthCheckError: true` and `causes` — the
1157
+ * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
1158
+ * health failures from unexpected exceptions. Works with both Terminus v10
1159
+ * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
1160
+ *
1004
1161
  * @param key - Health indicator key (default: `'jetstream'`).
1005
1162
  * @returns Object with status, server, and latency under the given key.
1006
- * @throws Error with `{ [key]: { status: 'down' } }` when disconnected.
1163
+ * @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
1007
1164
  */
1008
1165
  async isHealthy(key = "jetstream") {
1009
1166
  const status = await this.check();
@@ -1013,8 +1170,10 @@ var JetstreamHealthIndicator = class {
1013
1170
  latency: status.latency
1014
1171
  };
1015
1172
  if (!status.connected) {
1173
+ const causes = { [key]: details };
1016
1174
  throw Object.assign(new Error("Jetstream health check failed"), {
1017
- [key]: details
1175
+ causes,
1176
+ isHealthCheckError: true
1018
1177
  });
1019
1178
  }
1020
1179
  return { [key]: details };
@@ -1027,7 +1186,7 @@ JetstreamHealthIndicator = __decorateClass([
1027
1186
  // src/server/strategy.ts
1028
1187
  var import_microservices2 = require("@nestjs/microservices");
1029
1188
  var JetstreamStrategy = class extends import_microservices2.Server {
1030
- constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map()) {
1189
+ constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map(), metadataProvider) {
1031
1190
  super();
1032
1191
  this.options = options;
1033
1192
  this.connection = connection;
@@ -1039,6 +1198,7 @@ var JetstreamStrategy = class extends import_microservices2.Server {
1039
1198
  this.rpcRouter = rpcRouter;
1040
1199
  this.coreRpcServer = coreRpcServer;
1041
1200
  this.ackWaitMap = ackWaitMap;
1201
+ this.metadataProvider = metadataProvider;
1042
1202
  }
1043
1203
  transportId = /* @__PURE__ */ Symbol("jetstream-transport");
1044
1204
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -1083,10 +1243,14 @@ var JetstreamStrategy = class extends import_microservices2.Server {
1083
1243
  if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
1084
1244
  await this.coreRpcServer.start();
1085
1245
  }
1246
+ if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
1247
+ await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
1248
+ }
1086
1249
  callback();
1087
1250
  }
1088
- /** Stop all consumers, routers, and subscriptions. Called during shutdown. */
1251
+ /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
1089
1252
  close() {
1253
+ this.metadataProvider?.destroy();
1090
1254
  this.eventRouter.destroy();
1091
1255
  this.rpcRouter.destroy();
1092
1256
  this.coreRpcServer.stop();
@@ -1317,16 +1481,71 @@ var resolveAckExtensionInterval = (config, ackWaitNanos) => {
1317
1481
  const interval = Math.floor(ackWaitNanos / 1e6 / 2);
1318
1482
  return Math.max(interval, MIN_ACK_EXTENSION_INTERVAL);
1319
1483
  };
1484
+ var AckExtensionPool = class {
1485
+ entries = /* @__PURE__ */ new Set();
1486
+ handle = null;
1487
+ handleFireAt = 0;
1488
+ schedule(msg, interval) {
1489
+ const entry = {
1490
+ msg,
1491
+ interval,
1492
+ nextFireAt: Date.now() + interval,
1493
+ active: true
1494
+ };
1495
+ this.entries.add(entry);
1496
+ this.ensureWake(entry.nextFireAt);
1497
+ return entry;
1498
+ }
1499
+ cancel(entry) {
1500
+ if (!entry.active) return;
1501
+ entry.active = false;
1502
+ this.entries.delete(entry);
1503
+ if (this.entries.size === 0 && this.handle !== null) {
1504
+ clearTimeout(this.handle);
1505
+ this.handle = null;
1506
+ this.handleFireAt = 0;
1507
+ }
1508
+ }
1509
+ /**
1510
+ * Ensure the shared timer will fire no later than `dueAt`. If an earlier
1511
+ * wake is already scheduled, leave it; otherwise replace it with a tighter one.
1512
+ */
1513
+ ensureWake(dueAt) {
1514
+ if (this.handle !== null && this.handleFireAt <= dueAt) return;
1515
+ if (this.handle !== null) clearTimeout(this.handle);
1516
+ const delay = Math.max(0, dueAt - Date.now());
1517
+ const handle = setTimeout(() => {
1518
+ this.tick();
1519
+ }, delay);
1520
+ if (typeof handle.unref === "function") handle.unref();
1521
+ this.handle = handle;
1522
+ this.handleFireAt = dueAt;
1523
+ }
1524
+ tick() {
1525
+ this.handle = null;
1526
+ this.handleFireAt = 0;
1527
+ const now = Date.now();
1528
+ let earliest = Infinity;
1529
+ for (const entry of this.entries) {
1530
+ if (!entry.active) continue;
1531
+ if (entry.nextFireAt <= now) {
1532
+ try {
1533
+ entry.msg.working();
1534
+ } catch {
1535
+ }
1536
+ entry.nextFireAt = now + entry.interval;
1537
+ }
1538
+ if (entry.nextFireAt < earliest) earliest = entry.nextFireAt;
1539
+ }
1540
+ if (earliest !== Infinity) this.ensureWake(earliest);
1541
+ }
1542
+ };
1543
+ var pool = new AckExtensionPool();
1320
1544
  var startAckExtensionTimer = (msg, interval) => {
1321
1545
  if (interval === null || interval <= 0) return null;
1322
- const timer2 = setInterval(() => {
1323
- try {
1324
- msg.working();
1325
- } catch {
1326
- }
1327
- }, interval);
1546
+ const entry = pool.schedule(msg, interval);
1328
1547
  return () => {
1329
- clearInterval(timer2);
1548
+ pool.cancel(entry);
1330
1549
  };
1331
1550
  };
1332
1551
 
@@ -1340,11 +1559,8 @@ var serializeError = (err) => {
1340
1559
 
1341
1560
  // src/utils/unwrap-result.ts
1342
1561
  var import_rxjs2 = require("rxjs");
1343
- var RESOLVED_VOID = Promise.resolve(void 0);
1344
- var RESOLVED_NULL = Promise.resolve(null);
1345
1562
  var unwrapResult = (result) => {
1346
- if (result === void 0) return RESOLVED_VOID;
1347
- if (result === null) return RESOLVED_NULL;
1563
+ if (result === void 0 || result === null) return result;
1348
1564
  if ((0, import_rxjs2.isObservable)(result)) {
1349
1565
  return subscribeToFirst(result);
1350
1566
  }
@@ -1353,8 +1569,9 @@ var unwrapResult = (result) => {
1353
1569
  (resolved) => (0, import_rxjs2.isObservable)(resolved) ? subscribeToFirst(resolved) : resolved
1354
1570
  );
1355
1571
  }
1356
- return Promise.resolve(result);
1572
+ return result;
1357
1573
  };
1574
+ var isPromiseLike = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
1358
1575
  var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
1359
1576
  let done = false;
1360
1577
  let subscription = null;
@@ -1442,7 +1659,8 @@ var CoreRpcServer = class {
1442
1659
  }
1443
1660
  const ctx = new RpcContext([msg]);
1444
1661
  try {
1445
- const result = await unwrapResult(handler(data, ctx));
1662
+ const raw = unwrapResult(handler(data, ctx));
1663
+ const result = isPromiseLike(raw) ? await raw : raw;
1446
1664
  msg.respond(this.codec.encode(result));
1447
1665
  } catch (err) {
1448
1666
  this.logger.error(`Handler error for Core RPC ${msg.subject}:`, err);
@@ -1462,24 +1680,180 @@ var CoreRpcServer = class {
1462
1680
  };
1463
1681
 
1464
1682
  // src/server/infrastructure/stream.provider.ts
1683
+ var import_common6 = require("@nestjs/common");
1684
+ var import_jetstream14 = require("@nats-io/jetstream");
1685
+
1686
+ // src/server/infrastructure/nats-error-codes.ts
1687
+ var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
1688
+ NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
1689
+ NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
1690
+ NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
1691
+ return NatsErrorCode2;
1692
+ })(NatsErrorCode || {});
1693
+
1694
+ // src/server/infrastructure/stream-config-diff.ts
1695
+ var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
1696
+ "retention"
1697
+ ]);
1698
+ var IMMUTABLE_PROPERTIES = /* @__PURE__ */ new Set([
1699
+ "storage"
1700
+ ]);
1701
+ var ENABLE_ONLY_PROPERTIES = /* @__PURE__ */ new Set([
1702
+ "allow_msg_schedules",
1703
+ "allow_msg_ttl",
1704
+ "deny_delete",
1705
+ "deny_purge"
1706
+ ]);
1707
+ var compareStreamConfig = (current, desired) => {
1708
+ const changes = [];
1709
+ for (const key of Object.keys(desired)) {
1710
+ const currentVal = current[key];
1711
+ const desiredVal = desired[key];
1712
+ if (isEqual(currentVal, desiredVal)) continue;
1713
+ changes.push({
1714
+ property: key,
1715
+ current: currentVal,
1716
+ desired: desiredVal,
1717
+ mutability: classifyMutability(key, currentVal, desiredVal)
1718
+ });
1719
+ }
1720
+ const hasImmutableChanges = changes.some((c) => c.mutability === "immutable");
1721
+ const hasMutableChanges = changes.some(
1722
+ (c) => c.mutability === "mutable" || c.mutability === "enable-only"
1723
+ );
1724
+ const hasTransportControlledConflicts = changes.some(
1725
+ (c) => c.mutability === "transport-controlled"
1726
+ );
1727
+ return {
1728
+ hasChanges: changes.length > 0,
1729
+ hasMutableChanges,
1730
+ hasImmutableChanges,
1731
+ hasTransportControlledConflicts,
1732
+ changes
1733
+ };
1734
+ };
1735
+ var classifyMutability = (key, current, desired) => {
1736
+ if (TRANSPORT_CONTROLLED_PROPERTIES.has(key)) return "transport-controlled";
1737
+ if (IMMUTABLE_PROPERTIES.has(key)) return "immutable";
1738
+ if (ENABLE_ONLY_PROPERTIES.has(key)) {
1739
+ return current === true && desired === false ? "immutable" : "enable-only";
1740
+ }
1741
+ return "mutable";
1742
+ };
1743
+ var isEqual = (a, b) => {
1744
+ if (a === b) return true;
1745
+ if (a == null && b == null) return true;
1746
+ return JSON.stringify(a) === JSON.stringify(b);
1747
+ };
1748
+
1749
+ // src/server/infrastructure/stream-migration.ts
1465
1750
  var import_common5 = require("@nestjs/common");
1466
1751
  var import_jetstream13 = require("@nats-io/jetstream");
1467
- var STREAM_NOT_FOUND = 10059;
1752
+ var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
1753
+ var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
1754
+ var SOURCING_POLL_INTERVAL_MS = 100;
1755
+ var StreamMigration = class {
1756
+ constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
1757
+ this.sourcingTimeoutMs = sourcingTimeoutMs;
1758
+ }
1759
+ logger = new import_common5.Logger("Jetstream:Stream");
1760
+ async migrate(jsm, streamName2, newConfig) {
1761
+ const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
1762
+ const startTime = Date.now();
1763
+ const currentInfo = await jsm.streams.info(streamName2);
1764
+ await this.cleanupOrphanedBackup(jsm, backupName);
1765
+ const messageCount = currentInfo.state.messages;
1766
+ this.logger.log(`Stream ${streamName2}: destructive migration started`);
1767
+ let originalDeleted = false;
1768
+ try {
1769
+ if (messageCount > 0) {
1770
+ this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
1771
+ await jsm.streams.add({
1772
+ ...currentInfo.config,
1773
+ name: backupName,
1774
+ subjects: [],
1775
+ sources: [{ name: streamName2 }]
1776
+ });
1777
+ await this.waitForSourcing(jsm, backupName, messageCount);
1778
+ }
1779
+ this.logger.log(` Phase 2/4: Deleting old stream`);
1780
+ await jsm.streams.delete(streamName2);
1781
+ originalDeleted = true;
1782
+ this.logger.log(` Phase 3/4: Creating stream with new config`);
1783
+ await jsm.streams.add(newConfig);
1784
+ if (messageCount > 0) {
1785
+ const backupInfo = await jsm.streams.info(backupName);
1786
+ await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
1787
+ this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
1788
+ await jsm.streams.update(streamName2, {
1789
+ ...newConfig,
1790
+ sources: [{ name: backupName }]
1791
+ });
1792
+ await this.waitForSourcing(jsm, streamName2, messageCount);
1793
+ await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
1794
+ await jsm.streams.delete(backupName);
1795
+ }
1796
+ } catch (err) {
1797
+ if (originalDeleted && messageCount > 0) {
1798
+ this.logger.error(
1799
+ `Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
1800
+ );
1801
+ } else {
1802
+ await this.cleanupOrphanedBackup(jsm, backupName);
1803
+ }
1804
+ throw err;
1805
+ }
1806
+ const durationMs = Date.now() - startTime;
1807
+ this.logger.log(
1808
+ `Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
1809
+ );
1810
+ }
1811
+ async waitForSourcing(jsm, streamName2, expectedCount) {
1812
+ const deadline = Date.now() + this.sourcingTimeoutMs;
1813
+ while (Date.now() < deadline) {
1814
+ const info = await jsm.streams.info(streamName2);
1815
+ if (info.state.messages >= expectedCount) return;
1816
+ await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
1817
+ }
1818
+ throw new Error(
1819
+ `Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
1820
+ );
1821
+ }
1822
+ async cleanupOrphanedBackup(jsm, backupName) {
1823
+ try {
1824
+ await jsm.streams.info(backupName);
1825
+ this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
1826
+ await jsm.streams.delete(backupName);
1827
+ } catch (err) {
1828
+ if (err instanceof import_jetstream13.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
1829
+ return;
1830
+ }
1831
+ throw err;
1832
+ }
1833
+ }
1834
+ };
1835
+
1836
+ // src/server/infrastructure/stream.provider.ts
1468
1837
  var StreamProvider = class {
1469
1838
  constructor(options, connection) {
1470
1839
  this.options = options;
1471
1840
  this.connection = connection;
1472
1841
  }
1473
- logger = new import_common5.Logger("Jetstream:Stream");
1842
+ logger = new import_common6.Logger("Jetstream:Stream");
1843
+ migration = new StreamMigration();
1474
1844
  /**
1475
1845
  * Ensure all required streams exist with correct configuration.
1476
1846
  *
1477
1847
  * @param kinds Which stream kinds to create. Determined by the module based
1478
1848
  * on RPC mode and registered handler patterns.
1849
+ * If the dlq option is enabled, also ensures the DLQ stream exists.
1479
1850
  */
1480
1851
  async ensureStreams(kinds) {
1481
1852
  const jsm = await this.connection.getJetStreamManager();
1482
1853
  await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
1854
+ if (this.options.dlq) {
1855
+ await this.ensureDlqStream(jsm);
1856
+ }
1483
1857
  }
1484
1858
  /** Get the stream name for a given kind. */
1485
1859
  getStreamName(kind) {
@@ -1514,17 +1888,85 @@ var StreamProvider = class {
1514
1888
  const config = this.buildConfig(kind);
1515
1889
  this.logger.log(`Ensuring stream: ${config.name}`);
1516
1890
  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);
1891
+ const currentInfo = await jsm.streams.info(config.name);
1892
+ return await this.handleExistingStream(jsm, currentInfo, config);
1520
1893
  } catch (err) {
1521
- if (err instanceof import_jetstream13.JetStreamApiError && err.apiError().err_code === STREAM_NOT_FOUND) {
1894
+ if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
1522
1895
  this.logger.log(`Creating stream: ${config.name}`);
1523
1896
  return await jsm.streams.add(config);
1524
1897
  }
1525
1898
  throw err;
1526
1899
  }
1527
1900
  }
1901
+ /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
1902
+ async ensureDlqStream(jsm) {
1903
+ const config = this.buildDlqConfig();
1904
+ this.logger.log(`Ensuring DLQ stream: ${config.name}`);
1905
+ try {
1906
+ const currentInfo = await jsm.streams.info(config.name);
1907
+ return await this.handleExistingStream(jsm, currentInfo, config);
1908
+ } catch (err) {
1909
+ if (err instanceof import_jetstream14.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
1910
+ this.logger.log(`Creating DLQ stream: ${config.name}`);
1911
+ return await jsm.streams.add(config);
1912
+ }
1913
+ throw err;
1914
+ }
1915
+ }
1916
+ async handleExistingStream(jsm, currentInfo, config) {
1917
+ const diff = compareStreamConfig(currentInfo.config, config);
1918
+ if (!diff.hasChanges) {
1919
+ this.logger.debug(`Stream ${config.name}: no config changes`);
1920
+ return currentInfo;
1921
+ }
1922
+ this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
1923
+ if (diff.hasTransportControlledConflicts) {
1924
+ const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
1925
+ throw new Error(
1926
+ `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.`
1927
+ );
1928
+ }
1929
+ if (!diff.hasImmutableChanges) {
1930
+ this.logger.debug(`Stream exists, updating: ${config.name}`);
1931
+ return await jsm.streams.update(config.name, config);
1932
+ }
1933
+ if (!this.options.allowDestructiveMigration) {
1934
+ this.logger.warn(
1935
+ `Stream ${config.name} has immutable config conflicts. Enable allowDestructiveMigration to recreate the stream.`
1936
+ );
1937
+ if (diff.hasMutableChanges) {
1938
+ const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
1939
+ return await jsm.streams.update(config.name, mutableConfig);
1940
+ }
1941
+ return currentInfo;
1942
+ }
1943
+ await this.migration.migrate(jsm, config.name, config);
1944
+ return await jsm.streams.info(config.name);
1945
+ }
1946
+ buildMutableOnlyConfig(config, currentConfig, diff) {
1947
+ const nonMutableKeys = new Set(
1948
+ diff.changes.filter((c) => c.mutability === "immutable" || c.mutability === "transport-controlled").map((c) => c.property)
1949
+ );
1950
+ const filtered = { ...config };
1951
+ for (const key of nonMutableKeys) {
1952
+ filtered[key] = currentConfig[key];
1953
+ }
1954
+ return filtered;
1955
+ }
1956
+ logChanges(streamName2, diff, migrationEnabled) {
1957
+ for (const c of diff.changes) {
1958
+ const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
1959
+ if (c.mutability === "transport-controlled") {
1960
+ this.logger.error(
1961
+ `Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
1962
+ );
1963
+ } else if (c.mutability === "immutable" && !migrationEnabled) {
1964
+ this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
1965
+ } else {
1966
+ this.logger.log(`Stream ${streamName2}: ${detail}`);
1967
+ }
1968
+ }
1969
+ }
1528
1970
  /** Build the full stream config by merging defaults with user overrides. */
1529
1971
  buildConfig(kind) {
1530
1972
  const name = this.getStreamName(kind);
@@ -1540,6 +1982,26 @@ var StreamProvider = class {
1540
1982
  description
1541
1983
  };
1542
1984
  }
1985
+ /**
1986
+ * Build the stream configuration for the Dead-Letter Queue (DLQ).
1987
+ *
1988
+ * Merges the library default DLQ config with user-provided overrides.
1989
+ * Ensures transport-controlled settings like retention are safely decoupled.
1990
+ */
1991
+ buildDlqConfig() {
1992
+ const name = dlqStreamName(this.options.name);
1993
+ const subjects = [name];
1994
+ const description = `JetStream DLQ stream for ${this.options.name}`;
1995
+ const overrides = this.options.dlq?.stream ?? {};
1996
+ const safeOverrides = this.stripTransportControlled(overrides);
1997
+ return {
1998
+ ...DEFAULT_DLQ_STREAM_CONFIG,
1999
+ ...safeOverrides,
2000
+ name,
2001
+ subjects,
2002
+ description
2003
+ };
2004
+ }
1543
2005
  /** Get default config for a stream kind. */
1544
2006
  getDefaults(kind) {
1545
2007
  switch (kind) {
@@ -1558,25 +2020,44 @@ var StreamProvider = class {
1558
2020
  const overrides = this.getOverrides(kind);
1559
2021
  return overrides.allow_msg_schedules === true;
1560
2022
  }
1561
- /** Get user-provided overrides for a stream kind. */
2023
+ /** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
1562
2024
  getOverrides(kind) {
2025
+ let overrides;
1563
2026
  switch (kind) {
1564
2027
  case "ev" /* Event */:
1565
- return this.options.events?.stream ?? {};
2028
+ overrides = this.options.events?.stream ?? {};
2029
+ break;
1566
2030
  case "cmd" /* Command */:
1567
- return this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
2031
+ overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
2032
+ break;
1568
2033
  case "broadcast" /* Broadcast */:
1569
- return this.options.broadcast?.stream ?? {};
2034
+ overrides = this.options.broadcast?.stream ?? {};
2035
+ break;
1570
2036
  case "ordered" /* Ordered */:
1571
- return this.options.ordered?.stream ?? {};
2037
+ overrides = this.options.ordered?.stream ?? {};
2038
+ break;
1572
2039
  }
2040
+ return this.stripTransportControlled(overrides);
2041
+ }
2042
+ /**
2043
+ * Remove transport-controlled properties from user overrides.
2044
+ * `retention` is managed by the transport (Workqueue/Limits per stream kind)
2045
+ * and silently stripped to protect users from misconfiguration.
2046
+ */
2047
+ stripTransportControlled(overrides) {
2048
+ if (!("retention" in overrides)) return overrides;
2049
+ this.logger.debug(
2050
+ "Stripping user-provided retention override \u2014 retention is managed by the transport"
2051
+ );
2052
+ const cleaned = { ...overrides };
2053
+ delete cleaned.retention;
2054
+ return cleaned;
1573
2055
  }
1574
2056
  };
1575
2057
 
1576
2058
  // 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;
2059
+ var import_common7 = require("@nestjs/common");
2060
+ var import_jetstream16 = require("@nats-io/jetstream");
1580
2061
  var ConsumerProvider = class {
1581
2062
  constructor(options, connection, streamProvider, patternRegistry) {
1582
2063
  this.options = options;
@@ -1584,7 +2065,7 @@ var ConsumerProvider = class {
1584
2065
  this.streamProvider = streamProvider;
1585
2066
  this.patternRegistry = patternRegistry;
1586
2067
  }
1587
- logger = new import_common6.Logger("Jetstream:Consumer");
2068
+ logger = new import_common7.Logger("Jetstream:Consumer");
1588
2069
  /**
1589
2070
  * Ensure consumers exist for the specified kinds.
1590
2071
  *
@@ -1605,7 +2086,11 @@ var ConsumerProvider = class {
1605
2086
  getConsumerName(kind) {
1606
2087
  return consumerName(this.options.name, kind);
1607
2088
  }
1608
- /** Ensure a single consumer exists, creating if needed. */
2089
+ /**
2090
+ * Ensure a single consumer exists with the desired config.
2091
+ * Used at **startup** — creates or updates the consumer to match
2092
+ * the current pod's configuration.
2093
+ */
1609
2094
  async ensureConsumer(jsm, kind) {
1610
2095
  const stream = this.streamProvider.getStreamName(kind);
1611
2096
  const config = this.buildConfig(kind);
@@ -1616,13 +2101,74 @@ var ConsumerProvider = class {
1616
2101
  this.logger.debug(`Consumer exists, updating: ${name}`);
1617
2102
  return await jsm.consumers.update(stream, name, config);
1618
2103
  } 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);
2104
+ if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
2105
+ throw err;
2106
+ }
2107
+ return await this.createConsumer(jsm, stream, name, config);
2108
+ }
2109
+ }
2110
+ /**
2111
+ * Recover a consumer that disappeared during runtime.
2112
+ * Used by **self-healing** — creates if missing, but NEVER updates config.
2113
+ *
2114
+ * If a migration backup stream exists, another pod is mid-migration — we
2115
+ * throw so the self-healing retry loop waits with backoff until migration
2116
+ * completes and the backup is cleaned up.
2117
+ *
2118
+ * This prevents old pods from:
2119
+ * - Overwriting a newer pod's consumer config during rolling updates
2120
+ * - Creating consumers during migration (which would consume and delete
2121
+ * workqueue messages while they're being restored)
2122
+ */
2123
+ async recoverConsumer(jsm, kind) {
2124
+ const stream = this.streamProvider.getStreamName(kind);
2125
+ const config = this.buildConfig(kind);
2126
+ const name = config.durable_name;
2127
+ this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
2128
+ await this.assertNoMigrationInProgress(jsm, stream);
2129
+ try {
2130
+ return await jsm.consumers.info(stream, name);
2131
+ } catch (err) {
2132
+ if (!(err instanceof import_jetstream16.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
2133
+ throw err;
2134
+ }
2135
+ return await this.createConsumer(jsm, stream, name, config);
2136
+ }
2137
+ }
2138
+ /**
2139
+ * Throw if a migration backup stream exists for this stream.
2140
+ * The self-healing retry loop catches the error and retries with backoff,
2141
+ * naturally waiting until the migrating pod finishes and cleans up the backup.
2142
+ */
2143
+ async assertNoMigrationInProgress(jsm, stream) {
2144
+ const backupName = `${stream}${MIGRATION_BACKUP_SUFFIX}`;
2145
+ try {
2146
+ await jsm.streams.info(backupName);
2147
+ throw new Error(
2148
+ `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
2149
+ );
2150
+ } catch (err) {
2151
+ if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
2152
+ return;
1622
2153
  }
1623
2154
  throw err;
1624
2155
  }
1625
2156
  }
2157
+ /**
2158
+ * Create a consumer, handling the race where another pod creates it first.
2159
+ */
2160
+ async createConsumer(jsm, stream, name, config) {
2161
+ this.logger.log(`Creating consumer: ${name}`);
2162
+ try {
2163
+ return await jsm.consumers.add(stream, config);
2164
+ } catch (addErr) {
2165
+ if (addErr instanceof import_jetstream16.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
2166
+ this.logger.debug(`Consumer ${name} created by another pod, using existing`);
2167
+ return await jsm.consumers.info(stream, name);
2168
+ }
2169
+ throw addErr;
2170
+ }
2171
+ }
1626
2172
  /** Build consumer config by merging defaults with user overrides. */
1627
2173
  // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
1628
2174
  buildConfig(kind) {
@@ -1675,6 +2221,7 @@ var ConsumerProvider = class {
1675
2221
  return DEFAULT_BROADCAST_CONSUMER_CONFIG;
1676
2222
  case "ordered" /* Ordered */:
1677
2223
  throw new Error("Ordered consumers are ephemeral and should not use durable config");
2224
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1678
2225
  default: {
1679
2226
  const _exhaustive = kind;
1680
2227
  throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
@@ -1692,6 +2239,7 @@ var ConsumerProvider = class {
1692
2239
  return this.options.broadcast?.consumer ?? {};
1693
2240
  case "ordered" /* Ordered */:
1694
2241
  throw new Error("Ordered consumers are ephemeral and should not use durable config");
2242
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1695
2243
  default: {
1696
2244
  const _exhaustive = kind;
1697
2245
  throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
@@ -1701,16 +2249,17 @@ var ConsumerProvider = class {
1701
2249
  };
1702
2250
 
1703
2251
  // src/server/infrastructure/message.provider.ts
1704
- var import_common7 = require("@nestjs/common");
1705
- var import_jetstream17 = require("@nats-io/jetstream");
2252
+ var import_common8 = require("@nestjs/common");
2253
+ var import_jetstream18 = require("@nats-io/jetstream");
1706
2254
  var import_rxjs3 = require("rxjs");
1707
2255
  var MessageProvider = class {
1708
- constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
2256
+ constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
1709
2257
  this.connection = connection;
1710
2258
  this.eventBus = eventBus;
1711
2259
  this.consumeOptionsMap = consumeOptionsMap;
2260
+ this.consumerRecoveryFn = consumerRecoveryFn;
1712
2261
  }
1713
- logger = new import_common7.Logger("Jetstream:Message");
2262
+ logger = new import_common8.Logger("Jetstream:Message");
1714
2263
  activeIterators = /* @__PURE__ */ new Set();
1715
2264
  orderedReadyResolve = null;
1716
2265
  orderedReadyReject = null;
@@ -1762,7 +2311,7 @@ var MessageProvider = class {
1762
2311
  */
1763
2312
  async startOrdered(streamName2, filterSubjects, orderedConfig) {
1764
2313
  const consumerOpts = { filter_subjects: filterSubjects };
1765
- if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream17.DeliverPolicy.All) {
2314
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream18.DeliverPolicy.All) {
1766
2315
  consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
1767
2316
  }
1768
2317
  if (orderedConfig?.optStartSeq !== void 0) {
@@ -1813,20 +2362,49 @@ var MessageProvider = class {
1813
2362
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
1814
2363
  async consumeOnce(kind, info, target$) {
1815
2364
  const js = this.connection.getJetStreamClient();
1816
- const consumer = await js.consumers.get(info.stream_name, info.name);
2365
+ let consumer;
2366
+ let consumerName2 = info.name;
2367
+ try {
2368
+ consumer = await js.consumers.get(info.stream_name, info.name);
2369
+ } catch (err) {
2370
+ if (this.isConsumerNotFound(err) && this.consumerRecoveryFn) {
2371
+ this.logger.warn(`Consumer ${info.name} not found, recreating...`);
2372
+ const recovered = await this.consumerRecoveryFn(kind);
2373
+ consumerName2 = recovered.name;
2374
+ this.logger.log(`Consumer ${consumerName2} recreated, resuming consumption`);
2375
+ consumer = await js.consumers.get(recovered.stream_name, consumerName2);
2376
+ } else {
2377
+ throw err;
2378
+ }
2379
+ }
1817
2380
  const defaults = { idle_heartbeat: 5e3 };
1818
2381
  const userOptions = this.consumeOptionsMap.get(kind) ?? {};
1819
- const messages = await consumer.consume({ ...defaults, ...userOptions });
1820
- this.activeIterators.add(messages);
1821
- this.monitorConsumerHealth(messages, info.name);
1822
- try {
1823
- for await (const msg of messages) {
2382
+ const messages = await consumer.consume({
2383
+ ...defaults,
2384
+ ...userOptions,
2385
+ callback: (msg) => {
1824
2386
  target$.next(msg);
1825
2387
  }
2388
+ });
2389
+ this.activeIterators.add(messages);
2390
+ this.monitorConsumerHealth(messages, consumerName2);
2391
+ try {
2392
+ await messages.closed();
1826
2393
  } finally {
1827
2394
  this.activeIterators.delete(messages);
1828
2395
  }
1829
2396
  }
2397
+ /**
2398
+ * Detect "consumer not found" errors from `js.consumers.get()`.
2399
+ *
2400
+ * Unlike JetStream Manager calls (which throw `JetStreamApiError`),
2401
+ * the JetStream client's `consumers.get()` throws a plain `Error`
2402
+ * with the error code embedded in the message text.
2403
+ */
2404
+ isConsumerNotFound(err) {
2405
+ if (!(err instanceof Error)) return false;
2406
+ return err.message.includes("consumer not found") || err.message.includes(String(10014 /* ConsumerNotFound */));
2407
+ }
1830
2408
  /** Get the target subject for a consumer kind. */
1831
2409
  getTargetSubject(kind) {
1832
2410
  switch (kind) {
@@ -1838,6 +2416,7 @@ var MessageProvider = class {
1838
2416
  return this.broadcastMessages$;
1839
2417
  case "ordered" /* Ordered */:
1840
2418
  return this.orderedMessages$;
2419
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1841
2420
  default: {
1842
2421
  const _exhaustive = kind;
1843
2422
  throw new Error(`Unknown stream kind: ${_exhaustive}`);
@@ -1902,11 +2481,16 @@ var MessageProvider = class {
1902
2481
  })
1903
2482
  );
1904
2483
  }
1905
- /** Single iteration: create ordered consumer -> iterate messages. */
2484
+ /** Single iteration: create ordered consumer -> push messages into the subject. */
1906
2485
  async consumeOrderedOnce(streamName2, consumerOpts) {
1907
2486
  const js = this.connection.getJetStreamClient();
1908
2487
  const consumer = await js.consumers.get(streamName2, consumerOpts);
1909
- const messages = await consumer.consume();
2488
+ const orderedMessages$ = this.orderedMessages$;
2489
+ const messages = await consumer.consume({
2490
+ callback: (msg) => {
2491
+ orderedMessages$.next(msg);
2492
+ }
2493
+ });
1910
2494
  if (this.orderedReadyResolve) {
1911
2495
  this.orderedReadyResolve();
1912
2496
  this.orderedReadyResolve = null;
@@ -1914,17 +2498,117 @@ var MessageProvider = class {
1914
2498
  }
1915
2499
  this.activeIterators.add(messages);
1916
2500
  try {
1917
- for await (const msg of messages) {
1918
- this.orderedMessages$.next(msg);
1919
- }
2501
+ await messages.closed();
1920
2502
  } finally {
1921
2503
  this.activeIterators.delete(messages);
1922
2504
  }
1923
2505
  }
1924
2506
  };
1925
2507
 
2508
+ // src/server/infrastructure/metadata.provider.ts
2509
+ var import_common9 = require("@nestjs/common");
2510
+ var import_kv = require("@nats-io/kv");
2511
+ var MetadataProvider = class {
2512
+ constructor(options, connection) {
2513
+ this.connection = connection;
2514
+ this.bucketName = options.metadata?.bucket ?? DEFAULT_METADATA_BUCKET;
2515
+ this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
2516
+ this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
2517
+ }
2518
+ logger = new import_common9.Logger("Jetstream:Metadata");
2519
+ bucketName;
2520
+ replicas;
2521
+ ttl;
2522
+ currentEntries;
2523
+ heartbeatTimer;
2524
+ cachedKv;
2525
+ /**
2526
+ * Write handler metadata entries to the KV bucket and start heartbeat.
2527
+ *
2528
+ * Creates the bucket if it doesn't exist (idempotent).
2529
+ * Skips silently when entries map is empty.
2530
+ * Starts a heartbeat interval that refreshes entries every `ttl / 2`
2531
+ * to prevent TTL expiry while the pod is alive.
2532
+ *
2533
+ * Non-critical — errors are logged but do not prevent transport startup.
2534
+ *
2535
+ * @param entries Map of KV key → metadata object.
2536
+ */
2537
+ async publish(entries) {
2538
+ if (entries.size === 0) return;
2539
+ try {
2540
+ const kv = await this.openBucket();
2541
+ await this.writeEntries(kv, entries);
2542
+ this.currentEntries = entries;
2543
+ this.startHeartbeat();
2544
+ this.logger.log(
2545
+ `Published ${entries.size} handler metadata entries to KV bucket "${this.bucketName}"`
2546
+ );
2547
+ } catch (err) {
2548
+ this.logger.error("Failed to publish handler metadata to KV", err);
2549
+ }
2550
+ }
2551
+ /**
2552
+ * Stop the heartbeat timer.
2553
+ *
2554
+ * After this call, entries will expire via TTL once the heartbeat window passes.
2555
+ * Called during transport shutdown (strategy.close()).
2556
+ */
2557
+ destroy() {
2558
+ if (this.heartbeatTimer) {
2559
+ clearInterval(this.heartbeatTimer);
2560
+ this.heartbeatTimer = void 0;
2561
+ }
2562
+ this.currentEntries = void 0;
2563
+ this.cachedKv = void 0;
2564
+ }
2565
+ /** Write entries to KV with per-entry error handling. */
2566
+ async writeEntries(kv, entries) {
2567
+ for (const [key, meta] of entries) {
2568
+ try {
2569
+ await kv.put(key, JSON.stringify(meta));
2570
+ } catch (err) {
2571
+ this.logger.error(`Failed to write metadata entry "${key}"`, err);
2572
+ }
2573
+ }
2574
+ }
2575
+ /** Start heartbeat interval that refreshes entries every ttl/2. */
2576
+ startHeartbeat() {
2577
+ if (this.heartbeatTimer) {
2578
+ clearInterval(this.heartbeatTimer);
2579
+ }
2580
+ const interval = Math.floor(this.ttl / 2);
2581
+ this.heartbeatTimer = setInterval(() => {
2582
+ void this.refreshEntries();
2583
+ }, interval);
2584
+ this.heartbeatTimer.unref();
2585
+ }
2586
+ /** Refresh all current entries in KV (heartbeat tick). */
2587
+ async refreshEntries() {
2588
+ if (!this.currentEntries || this.currentEntries.size === 0) return;
2589
+ try {
2590
+ const kv = await this.openBucket();
2591
+ await this.writeEntries(kv, this.currentEntries);
2592
+ } catch (err) {
2593
+ this.logger.error("Failed to refresh handler metadata in KV", err);
2594
+ }
2595
+ }
2596
+ /** Create or open the KV bucket (cached after first call). */
2597
+ async openBucket() {
2598
+ if (this.cachedKv) return this.cachedKv;
2599
+ const js = this.connection.getJetStreamClient();
2600
+ const kvm = new import_kv.Kvm(js);
2601
+ this.cachedKv = await kvm.create(this.bucketName, {
2602
+ history: DEFAULT_METADATA_HISTORY,
2603
+ replicas: this.replicas,
2604
+ ttl: this.ttl
2605
+ });
2606
+ return this.cachedKv;
2607
+ }
2608
+ };
2609
+
1926
2610
  // src/server/routing/pattern-registry.ts
1927
- var import_common8 = require("@nestjs/common");
2611
+ var import_common10 = require("@nestjs/common");
1928
2612
  var HANDLER_LABELS = {
1929
2613
  ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
1930
2614
  ["ordered" /* Ordered */]: "ordered" /* Ordered */,
@@ -1935,7 +2619,7 @@ var PatternRegistry = class {
1935
2619
  constructor(options) {
1936
2620
  this.options = options;
1937
2621
  }
1938
- logger = new import_common8.Logger("Jetstream:PatternRegistry");
2622
+ logger = new import_common10.Logger("Jetstream:PatternRegistry");
1939
2623
  registry = /* @__PURE__ */ new Map();
1940
2624
  // Cached after registerHandlers() — the registry is immutable from that point
1941
2625
  cachedPatterns = null;
@@ -1943,6 +2627,7 @@ var PatternRegistry = class {
1943
2627
  _hasCommands = false;
1944
2628
  _hasBroadcasts = false;
1945
2629
  _hasOrdered = false;
2630
+ _hasMetadata = false;
1946
2631
  /**
1947
2632
  * Register all handlers from the NestJS strategy.
1948
2633
  *
@@ -1955,6 +2640,7 @@ var PatternRegistry = class {
1955
2640
  const isEvent = handler.isEventHandler ?? false;
1956
2641
  const isBroadcast = !!extras?.broadcast;
1957
2642
  const isOrdered = !!extras?.ordered;
2643
+ const meta = extras?.meta;
1958
2644
  if (isBroadcast && isOrdered) {
1959
2645
  throw new Error(
1960
2646
  `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
@@ -1971,7 +2657,8 @@ var PatternRegistry = class {
1971
2657
  pattern,
1972
2658
  isEvent: isEvent && !isOrdered,
1973
2659
  isBroadcast,
1974
- isOrdered
2660
+ isOrdered,
2661
+ meta
1975
2662
  });
1976
2663
  this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
1977
2664
  }
@@ -1980,6 +2667,7 @@ var PatternRegistry = class {
1980
2667
  this._hasCommands = this.cachedPatterns.commands.length > 0;
1981
2668
  this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
1982
2669
  this._hasOrdered = this.cachedPatterns.ordered.length > 0;
2670
+ this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
1983
2671
  this.logSummary();
1984
2672
  }
1985
2673
  /** Find handler for a full NATS subject. */
@@ -2008,6 +2696,26 @@ var PatternRegistry = class {
2008
2696
  (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
2009
2697
  );
2010
2698
  }
2699
+ /** Check if any registered handler has metadata. */
2700
+ hasMetadata() {
2701
+ return this._hasMetadata;
2702
+ }
2703
+ /**
2704
+ * Get handler metadata entries for KV publishing.
2705
+ *
2706
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
2707
+ * Key format: `{serviceName}.{kind}.{pattern}`.
2708
+ */
2709
+ getMetadataEntries() {
2710
+ const entries = /* @__PURE__ */ new Map();
2711
+ for (const entry of this.registry.values()) {
2712
+ if (!entry.meta) continue;
2713
+ const kind = this.resolveStreamKind(entry);
2714
+ const key = metadataKey(this.options.name, kind, entry.pattern);
2715
+ entries.set(key, entry.meta);
2716
+ }
2717
+ return entries;
2718
+ }
2011
2719
  /** Get patterns grouped by kind (cached after registration). */
2012
2720
  getPatternsByKind() {
2013
2721
  const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
@@ -2047,6 +2755,12 @@ var PatternRegistry = class {
2047
2755
  }
2048
2756
  return { events, commands, broadcasts, ordered };
2049
2757
  }
2758
+ resolveStreamKind(entry) {
2759
+ if (entry.isBroadcast) return "broadcast" /* Broadcast */;
2760
+ if (entry.isOrdered) return "ordered" /* Ordered */;
2761
+ if (entry.isEvent) return "ev" /* Event */;
2762
+ return "cmd" /* Command */;
2763
+ }
2050
2764
  logSummary() {
2051
2765
  const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
2052
2766
  const parts = [
@@ -2062,10 +2776,10 @@ var PatternRegistry = class {
2062
2776
  };
2063
2777
 
2064
2778
  // src/server/routing/event.router.ts
2065
- var import_common9 = require("@nestjs/common");
2066
- var import_rxjs4 = require("rxjs");
2779
+ var import_common11 = require("@nestjs/common");
2780
+ var import_transport_node4 = require("@nats-io/transport-node");
2067
2781
  var EventRouter = class {
2068
- constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
2782
+ constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
2069
2783
  this.messageProvider = messageProvider;
2070
2784
  this.patternRegistry = patternRegistry;
2071
2785
  this.codec = codec;
@@ -2073,8 +2787,10 @@ var EventRouter = class {
2073
2787
  this.deadLetterConfig = deadLetterConfig;
2074
2788
  this.processingConfig = processingConfig;
2075
2789
  this.ackWaitMap = ackWaitMap;
2790
+ this.connection = connection;
2791
+ this.options = options;
2076
2792
  }
2077
- logger = new import_common9.Logger("Jetstream:EventRouter");
2793
+ logger = new import_common11.Logger("Jetstream:EventRouter");
2078
2794
  subscriptions = [];
2079
2795
  /**
2080
2796
  * Update the max_deliver thresholds from actual NATS consumer configs.
@@ -2099,15 +2815,194 @@ var EventRouter = class {
2099
2815
  }
2100
2816
  this.subscriptions.length = 0;
2101
2817
  }
2102
- /** Subscribe to a message stream and route each message. */
2818
+ /** Subscribe to a message stream and route each message to its handler. */
2103
2819
  subscribeToStream(stream$, kind) {
2104
2820
  const isOrdered = kind === "ordered" /* Ordered */;
2821
+ const patternRegistry = this.patternRegistry;
2822
+ const codec = this.codec;
2823
+ const eventBus = this.eventBus;
2824
+ const logger = this.logger;
2825
+ const deadLetterConfig = this.deadLetterConfig;
2105
2826
  const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
2827
+ const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
2106
2828
  const concurrency = this.getConcurrency(kind);
2107
- const route = (msg) => (0, import_rxjs4.from)(
2108
- isOrdered ? this.handleOrderedSafe(msg) : this.handleSafe(msg, ackExtensionInterval, kind)
2109
- );
2110
- const subscription = stream$.pipe(isOrdered ? (0, import_rxjs4.concatMap)(route) : (0, import_rxjs4.mergeMap)(route, concurrency)).subscribe();
2829
+ const hasDlqCheck = deadLetterConfig !== void 0;
2830
+ const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
2831
+ const isDeadLetter = (msg) => {
2832
+ if (!hasDlqCheck) return false;
2833
+ const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
2834
+ if (maxDeliver === void 0 || maxDeliver <= 0) return false;
2835
+ return msg.info.deliveryCount >= maxDeliver;
2836
+ };
2837
+ const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
2838
+ const settleSuccess = (msg, ctx) => {
2839
+ if (ctx.shouldTerminate) msg.term(ctx.terminateReason);
2840
+ else if (ctx.shouldRetry) msg.nak(ctx.retryDelay);
2841
+ else msg.ack();
2842
+ };
2843
+ const settleFailure = async (msg, data, err) => {
2844
+ if (handleDeadLetter !== null && isDeadLetter(msg)) {
2845
+ await handleDeadLetter(msg, data, err);
2846
+ return;
2847
+ }
2848
+ msg.nak();
2849
+ };
2850
+ const resolveEvent = (msg) => {
2851
+ const subject = msg.subject;
2852
+ try {
2853
+ const handler = patternRegistry.getHandler(subject);
2854
+ if (!handler) {
2855
+ msg.term(`No handler for event: ${subject}`);
2856
+ logger.error(`No handler for subject: ${subject}`);
2857
+ return null;
2858
+ }
2859
+ let data;
2860
+ try {
2861
+ data = codec.decode(msg.data);
2862
+ } catch (err) {
2863
+ msg.term("Decode error");
2864
+ logger.error(`Decode error for ${subject}:`, err);
2865
+ return null;
2866
+ }
2867
+ if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
2868
+ return { handler, data };
2869
+ } catch (err) {
2870
+ logger.error(`Unexpected error in ${kind} event router`, err);
2871
+ try {
2872
+ msg.term("Unexpected router error");
2873
+ } catch (termErr) {
2874
+ logger.error(`Failed to terminate message ${subject}:`, termErr);
2875
+ }
2876
+ return null;
2877
+ }
2878
+ };
2879
+ const handleSafe = (msg) => {
2880
+ const resolved = resolveEvent(msg);
2881
+ if (resolved === null) return void 0;
2882
+ const { handler, data } = resolved;
2883
+ const ctx = new RpcContext([msg]);
2884
+ const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
2885
+ let pending;
2886
+ try {
2887
+ pending = unwrapResult(handler(data, ctx));
2888
+ } catch (err) {
2889
+ logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
2890
+ if (stopAckExtension !== null) stopAckExtension();
2891
+ return settleFailure(msg, data, err);
2892
+ }
2893
+ if (!isPromiseLike(pending)) {
2894
+ settleSuccess(msg, ctx);
2895
+ if (stopAckExtension !== null) stopAckExtension();
2896
+ return void 0;
2897
+ }
2898
+ return pending.then(
2899
+ () => {
2900
+ settleSuccess(msg, ctx);
2901
+ if (stopAckExtension !== null) stopAckExtension();
2902
+ },
2903
+ async (err) => {
2904
+ logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
2905
+ try {
2906
+ await settleFailure(msg, data, err);
2907
+ } finally {
2908
+ if (stopAckExtension !== null) stopAckExtension();
2909
+ }
2910
+ }
2911
+ );
2912
+ };
2913
+ const handleOrderedSafe = (msg) => {
2914
+ const subject = msg.subject;
2915
+ let handler;
2916
+ let data;
2917
+ try {
2918
+ handler = patternRegistry.getHandler(subject);
2919
+ if (!handler) {
2920
+ logger.error(`No handler for subject: ${subject}`);
2921
+ return void 0;
2922
+ }
2923
+ try {
2924
+ data = codec.decode(msg.data);
2925
+ } catch (err) {
2926
+ logger.error(`Decode error for ${subject}:`, err);
2927
+ return void 0;
2928
+ }
2929
+ if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
2930
+ } catch (err) {
2931
+ logger.error(`Ordered handler error (${subject}):`, err);
2932
+ return void 0;
2933
+ }
2934
+ const ctx = new RpcContext([msg]);
2935
+ const warnIfSettlementAttempted = () => {
2936
+ if (ctx.shouldRetry || ctx.shouldTerminate) {
2937
+ logger.warn(
2938
+ `retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
2939
+ );
2940
+ }
2941
+ };
2942
+ let pending;
2943
+ try {
2944
+ pending = unwrapResult(handler(data, ctx));
2945
+ } catch (err) {
2946
+ logger.error(`Ordered handler error (${subject}):`, err);
2947
+ return void 0;
2948
+ }
2949
+ if (!isPromiseLike(pending)) {
2950
+ warnIfSettlementAttempted();
2951
+ return void 0;
2952
+ }
2953
+ return pending.then(warnIfSettlementAttempted, (err) => {
2954
+ logger.error(`Ordered handler error (${subject}):`, err);
2955
+ });
2956
+ };
2957
+ const route = isOrdered ? handleOrderedSafe : handleSafe;
2958
+ const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
2959
+ const backlogWarnThreshold = 1e3;
2960
+ let active = 0;
2961
+ let backlogWarned = false;
2962
+ const backlog = [];
2963
+ const onAsyncDone = () => {
2964
+ active--;
2965
+ drainBacklog();
2966
+ };
2967
+ const drainBacklog = () => {
2968
+ while (active < maxActive) {
2969
+ const next = backlog.shift();
2970
+ if (next === void 0) return;
2971
+ active++;
2972
+ const result = route(next);
2973
+ if (result !== void 0) {
2974
+ void result.finally(onAsyncDone);
2975
+ } else {
2976
+ active--;
2977
+ }
2978
+ }
2979
+ if (backlog.length < backlogWarnThreshold) backlogWarned = false;
2980
+ };
2981
+ const subscription = stream$.subscribe({
2982
+ next: (msg) => {
2983
+ if (active >= maxActive) {
2984
+ backlog.push(msg);
2985
+ if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
2986
+ backlogWarned = true;
2987
+ logger.warn(
2988
+ `${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
2989
+ );
2990
+ }
2991
+ return;
2992
+ }
2993
+ active++;
2994
+ const result = route(msg);
2995
+ if (result !== void 0) {
2996
+ void result.finally(onAsyncDone);
2997
+ } else {
2998
+ active--;
2999
+ if (backlog.length > 0) drainBacklog();
3000
+ }
3001
+ },
3002
+ error: (err) => {
3003
+ logger.error(`Stream error in ${kind} router`, err);
3004
+ }
3005
+ });
2111
3006
  this.subscriptions.push(subscription);
2112
3007
  }
2113
3008
  getConcurrency(kind) {
@@ -2120,87 +3015,94 @@ var EventRouter = class {
2120
3015
  if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
2121
3016
  return void 0;
2122
3017
  }
2123
- /** Handle a single event message with error isolation. */
2124
- async handleSafe(msg, ackExtensionInterval, kind) {
2125
- try {
2126
- const resolved = this.decodeMessage(msg);
2127
- if (!resolved) return;
2128
- await this.executeHandler(
2129
- resolved.handler,
2130
- resolved.data,
2131
- resolved.ctx,
2132
- msg,
2133
- ackExtensionInterval
2134
- );
2135
- } catch (err) {
2136
- this.logger.error(`Unexpected error in ${kind} event router`, err);
3018
+ /** Handle a dead letter: invoke callback, then term or nak based on result. */
3019
+ /**
3020
+ * Fallback execution for a dead letter when DLQ is disabled, or when
3021
+ * publishing to the DLQ stream fails (due to network or NATS errors).
3022
+ *
3023
+ * Triggers the user-provided `onDeadLetter` hook for logging/alerting.
3024
+ * On success, terminates the message. On error, leaves it unacknowledged (nak)
3025
+ * so NATS can retry the delivery on the next cycle.
3026
+ */
3027
+ async fallbackToOnDeadLetterCallback(info, msg) {
3028
+ if (!this.deadLetterConfig) {
3029
+ msg.term("Dead letter config unavailable");
3030
+ return;
2137
3031
  }
2138
- }
2139
- /** Handle an ordered message with error isolation. */
2140
- async handleOrderedSafe(msg) {
2141
3032
  try {
2142
- const resolved = this.decodeMessage(msg, true);
2143
- if (!resolved) return;
2144
- await unwrapResult(resolved.handler(resolved.data, resolved.ctx));
2145
- if (resolved.ctx.shouldRetry || resolved.ctx.shouldTerminate) {
2146
- this.logger.warn(
2147
- `retry()/terminate() ignored for ordered message ${msg.subject} \u2014 ordered consumers auto-acknowledge`
2148
- );
2149
- }
2150
- } catch (err) {
2151
- this.logger.error(`Ordered handler error (${msg.subject}):`, err);
3033
+ await this.deadLetterConfig.onDeadLetter(info);
3034
+ msg.term("Dead letter processed via fallback callback");
3035
+ } catch (hookErr) {
3036
+ this.logger.error(
3037
+ `Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
3038
+ hookErr
3039
+ );
3040
+ msg.nak();
2152
3041
  }
2153
3042
  }
2154
- /** Resolve handler, decode payload, and build context. Returns null on failure. */
2155
- decodeMessage(msg, isOrdered = false) {
2156
- const handler = this.patternRegistry.getHandler(msg.subject);
2157
- if (!handler) {
2158
- if (!isOrdered) msg.term(`No handler for event: ${msg.subject}`);
2159
- this.logger.error(`No handler for subject: ${msg.subject}`);
2160
- return null;
3043
+ /**
3044
+ * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
3045
+ *
3046
+ * Appends diagnostic metadata headers to the original message and preserves
3047
+ * the primary payload. If publishing succeeds, it notifies the standard
3048
+ * `onDeadLetter` callback and terminates the message. If it fails, it falls
3049
+ * back to the callback entirely to prevent silent data loss.
3050
+ */
3051
+ async publishToDlq(msg, info, error) {
3052
+ const serviceName = this.options?.name;
3053
+ if (!this.connection || !serviceName) {
3054
+ this.logger.error(
3055
+ `Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
3056
+ );
3057
+ await this.fallbackToOnDeadLetterCallback(info, msg);
3058
+ return;
2161
3059
  }
2162
- let data;
2163
- try {
2164
- data = this.codec.decode(msg.data);
2165
- } catch (err) {
2166
- if (!isOrdered) msg.term("Decode error");
2167
- this.logger.error(`Decode error for ${msg.subject}:`, err);
2168
- return null;
3060
+ const destinationSubject = dlqStreamName(serviceName);
3061
+ const hdrs = (0, import_transport_node4.headers)();
3062
+ if (msg.headers) {
3063
+ for (const [k, v] of msg.headers) {
3064
+ for (const val of v) {
3065
+ hdrs.append(k, val);
3066
+ }
3067
+ }
2169
3068
  }
2170
- this.eventBus.emitMessageRouted(msg.subject, "event" /* Event */);
2171
- return { handler, data, ctx: new RpcContext([msg]) };
2172
- }
2173
- /** Execute handler, then ack on success or nak/dead-letter on failure. */
2174
- async executeHandler(handler, data, ctx, msg, ackExtensionInterval) {
2175
- const stopAckExtension = startAckExtensionTimer(msg, ackExtensionInterval);
3069
+ let reason = String(error);
3070
+ if (error instanceof Error) {
3071
+ reason = error.message;
3072
+ } else if (typeof error === "object" && error !== null && "message" in error) {
3073
+ reason = String(error.message);
3074
+ }
3075
+ hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
3076
+ hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
3077
+ hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
3078
+ hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
3079
+ hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
2176
3080
  try {
2177
- await unwrapResult(handler(data, ctx));
2178
- if (ctx.shouldTerminate) {
2179
- msg.term(ctx.terminateReason);
2180
- } else if (ctx.shouldRetry) {
2181
- msg.nak(ctx.retryDelay);
2182
- } else {
2183
- msg.ack();
2184
- }
2185
- } catch (err) {
2186
- this.logger.error(`Event handler error (${msg.subject}):`, err);
2187
- if (this.isDeadLetter(msg)) {
2188
- await this.handleDeadLetter(msg, data, err);
2189
- } else {
2190
- msg.nak();
3081
+ const js = this.connection.getJetStreamClient();
3082
+ await js.publish(destinationSubject, msg.data, { headers: hdrs });
3083
+ this.logger.log(`Message sent to DLQ: ${msg.subject}`);
3084
+ if (this.deadLetterConfig?.onDeadLetter) {
3085
+ try {
3086
+ await this.deadLetterConfig.onDeadLetter(info);
3087
+ } catch (hookErr) {
3088
+ this.logger.warn(
3089
+ `onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
3090
+ hookErr
3091
+ );
3092
+ }
2191
3093
  }
2192
- } finally {
2193
- stopAckExtension?.();
3094
+ msg.term("Moved to DLQ stream");
3095
+ } catch (publishErr) {
3096
+ this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
3097
+ await this.fallbackToOnDeadLetterCallback(info, msg);
2194
3098
  }
2195
3099
  }
2196
- /** Check if the message has exhausted all delivery attempts. */
2197
- isDeadLetter(msg) {
2198
- if (!this.deadLetterConfig) return false;
2199
- const maxDeliver = this.deadLetterConfig.maxDeliverByStream.get(msg.info.stream);
2200
- if (maxDeliver === void 0 || maxDeliver <= 0) return false;
2201
- return msg.info.deliveryCount >= maxDeliver;
2202
- }
2203
- /** Handle a dead letter: invoke callback, then term or nak based on result. */
3100
+ /**
3101
+ * Orchestrates the handling of a message that has exhausted delivery limits.
3102
+ *
3103
+ * Emits a system event and delegates either to the robust DLQ stream publisher
3104
+ * or directly to the fallback callback based on the active module configuration.
3105
+ */
2204
3106
  async handleDeadLetter(msg, data, error) {
2205
3107
  const info = {
2206
3108
  subject: msg.subject,
@@ -2213,24 +3115,17 @@ var EventRouter = class {
2213
3115
  timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
2214
3116
  };
2215
3117
  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();
3118
+ if (!this.options?.dlq) {
3119
+ await this.fallbackToOnDeadLetterCallback(info, msg);
3120
+ } else {
3121
+ await this.publishToDlq(msg, info, error);
2226
3122
  }
2227
3123
  }
2228
3124
  };
2229
3125
 
2230
3126
  // src/server/routing/rpc.router.ts
2231
- var import_common10 = require("@nestjs/common");
2232
- var import_transport_node4 = require("@nats-io/transport-node");
2233
- var import_rxjs5 = require("rxjs");
3127
+ var import_common12 = require("@nestjs/common");
3128
+ var import_transport_node5 = require("@nats-io/transport-node");
2234
3129
  var RpcRouter = class {
2235
3130
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
2236
3131
  this.messageProvider = messageProvider;
@@ -2243,7 +3138,7 @@ var RpcRouter = class {
2243
3138
  this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
2244
3139
  this.concurrency = rpcOptions?.concurrency;
2245
3140
  }
2246
- logger = new import_common10.Logger("Jetstream:RpcRouter");
3141
+ logger = new import_common12.Logger("Jetstream:RpcRouter");
2247
3142
  timeout;
2248
3143
  concurrency;
2249
3144
  resolvedAckExtensionInterval;
@@ -2261,105 +3156,203 @@ var RpcRouter = class {
2261
3156
  /** Start routing command messages to handlers. */
2262
3157
  async start() {
2263
3158
  this.cachedNc = await this.connection.getConnection();
2264
- this.subscription = this.messageProvider.commands$.pipe((0, import_rxjs5.mergeMap)((msg) => (0, import_rxjs5.from)(this.handleSafe(msg)), this.concurrency)).subscribe();
2265
- }
2266
- /** Stop routing and unsubscribe. */
2267
- destroy() {
2268
- this.subscription?.unsubscribe();
2269
- this.subscription = null;
2270
- }
2271
- /** Handle a single RPC command message with error isolation. */
2272
- async handleSafe(msg) {
2273
- try {
2274
- const handler = this.patternRegistry.getHandler(msg.subject);
2275
- if (!handler) {
2276
- msg.term(`No handler for RPC: ${msg.subject}`);
2277
- this.logger.error(`No handler for RPC subject: ${msg.subject}`);
2278
- return;
2279
- }
2280
- const { headers: msgHeaders } = msg;
2281
- const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
2282
- const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
2283
- if (!replyTo || !correlationId) {
2284
- msg.term("Missing required headers (reply-to or correlation-id)");
2285
- this.logger.error(`Missing headers for RPC: ${msg.subject}`);
2286
- return;
2287
- }
2288
- let data;
2289
- try {
2290
- data = this.codec.decode(msg.data);
2291
- } catch (err) {
2292
- msg.term("Decode error");
2293
- this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
2294
- return;
2295
- }
2296
- this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
2297
- await this.executeHandler(handler, data, msg, replyTo, correlationId);
2298
- } catch (err) {
2299
- this.logger.error("Unexpected error in RPC router", err);
2300
- }
2301
- }
2302
- /** Execute handler, publish response, settle message. */
2303
- async executeHandler(handler, data, msg, replyTo, correlationId) {
2304
- const nc = this.cachedNc ?? await this.connection.getConnection();
2305
- const ctx = new RpcContext([msg]);
2306
- let settled = false;
2307
- const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
2308
- const timeoutId = setTimeout(() => {
2309
- if (settled) return;
2310
- settled = true;
2311
- stopAckExtension?.();
2312
- this.logger.error(`RPC timeout (${this.timeout}ms): ${msg.subject}`);
2313
- this.eventBus.emit("rpcTimeout" /* RpcTimeout */, msg.subject, correlationId);
2314
- msg.term("Handler timeout");
2315
- }, this.timeout);
2316
- try {
2317
- const result = await unwrapResult(handler(data, ctx));
2318
- if (settled) return;
2319
- settled = true;
2320
- clearTimeout(timeoutId);
2321
- stopAckExtension?.();
2322
- msg.ack();
3159
+ const nc = this.cachedNc;
3160
+ const patternRegistry = this.patternRegistry;
3161
+ const codec = this.codec;
3162
+ const eventBus = this.eventBus;
3163
+ const logger = this.logger;
3164
+ const timeout = this.timeout;
3165
+ const ackExtensionInterval = this.ackExtensionInterval;
3166
+ const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
3167
+ const maxActive = this.concurrency ?? Number.POSITIVE_INFINITY;
3168
+ const emitRpcTimeout = (subject, correlationId) => {
3169
+ eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
3170
+ };
3171
+ const publishReply = (replyTo, correlationId, payload) => {
2323
3172
  try {
2324
- const hdrs = (0, import_transport_node4.headers)();
3173
+ const hdrs = (0, import_transport_node5.headers)();
2325
3174
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
2326
- nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
3175
+ nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
2327
3176
  } catch (publishErr) {
2328
- this.logger.error(`Failed to publish RPC response for ${msg.subject}`, publishErr);
3177
+ logger.error(`Failed to publish RPC response`, publishErr);
2329
3178
  }
2330
- } catch (err) {
2331
- if (settled) return;
2332
- settled = true;
2333
- clearTimeout(timeoutId);
2334
- stopAckExtension?.();
3179
+ };
3180
+ const publishErrorReply = (replyTo, correlationId, subject, err) => {
2335
3181
  try {
2336
- const hdrs = (0, import_transport_node4.headers)();
3182
+ const hdrs = (0, import_transport_node5.headers)();
2337
3183
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
2338
3184
  hdrs.set("x-error" /* Error */, "true");
2339
- nc.publish(replyTo, this.codec.encode(serializeError(err)), { headers: hdrs });
3185
+ nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
2340
3186
  } catch (encodeErr) {
2341
- this.logger.error(`Failed to encode RPC error for ${msg.subject}`, encodeErr);
3187
+ logger.error(`Failed to encode RPC error for ${subject}`, encodeErr);
2342
3188
  }
2343
- msg.term(`Handler error: ${msg.subject}`);
2344
- }
3189
+ };
3190
+ const resolveCommand = (msg) => {
3191
+ const subject = msg.subject;
3192
+ try {
3193
+ const handler = patternRegistry.getHandler(subject);
3194
+ if (!handler) {
3195
+ msg.term(`No handler for RPC: ${subject}`);
3196
+ logger.error(`No handler for RPC subject: ${subject}`);
3197
+ return null;
3198
+ }
3199
+ const msgHeaders = msg.headers;
3200
+ const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
3201
+ const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
3202
+ if (!replyTo || !correlationId) {
3203
+ msg.term("Missing required headers (reply-to or correlation-id)");
3204
+ logger.error(`Missing headers for RPC: ${subject}`);
3205
+ return null;
3206
+ }
3207
+ let data;
3208
+ try {
3209
+ data = codec.decode(msg.data);
3210
+ } catch (err) {
3211
+ msg.term("Decode error");
3212
+ logger.error(`Decode error for RPC ${subject}:`, err);
3213
+ return null;
3214
+ }
3215
+ eventBus.emitMessageRouted(subject, "rpc" /* Rpc */);
3216
+ return { handler, data, replyTo, correlationId };
3217
+ } catch (err) {
3218
+ logger.error("Unexpected error in RPC router", err);
3219
+ try {
3220
+ msg.term("Unexpected router error");
3221
+ } catch (termErr) {
3222
+ logger.error(`Failed to terminate RPC message ${subject}:`, termErr);
3223
+ }
3224
+ return null;
3225
+ }
3226
+ };
3227
+ const handleSafe = (msg) => {
3228
+ const resolved = resolveCommand(msg);
3229
+ if (resolved === null) return void 0;
3230
+ const { handler, data, replyTo, correlationId } = resolved;
3231
+ const subject = msg.subject;
3232
+ const ctx = new RpcContext([msg]);
3233
+ const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
3234
+ let pending;
3235
+ try {
3236
+ pending = unwrapResult(handler(data, ctx));
3237
+ } catch (err) {
3238
+ if (stopAckExtension !== null) stopAckExtension();
3239
+ logger.error(`RPC handler error (${subject}):`, err);
3240
+ publishErrorReply(replyTo, correlationId, subject, err);
3241
+ msg.term(`Handler error: ${subject}`);
3242
+ return void 0;
3243
+ }
3244
+ if (!isPromiseLike(pending)) {
3245
+ if (stopAckExtension !== null) stopAckExtension();
3246
+ msg.ack();
3247
+ publishReply(replyTo, correlationId, pending);
3248
+ return void 0;
3249
+ }
3250
+ let settled = false;
3251
+ const timeoutId = setTimeout(() => {
3252
+ if (settled) return;
3253
+ settled = true;
3254
+ if (stopAckExtension !== null) stopAckExtension();
3255
+ logger.error(`RPC timeout (${timeout}ms): ${subject}`);
3256
+ emitRpcTimeout(subject, correlationId);
3257
+ msg.term("Handler timeout");
3258
+ }, timeout);
3259
+ return pending.then(
3260
+ (result) => {
3261
+ if (settled) return;
3262
+ settled = true;
3263
+ clearTimeout(timeoutId);
3264
+ if (stopAckExtension !== null) stopAckExtension();
3265
+ msg.ack();
3266
+ publishReply(replyTo, correlationId, result);
3267
+ },
3268
+ (err) => {
3269
+ if (settled) return;
3270
+ settled = true;
3271
+ clearTimeout(timeoutId);
3272
+ if (stopAckExtension !== null) stopAckExtension();
3273
+ logger.error(`RPC handler error (${subject}):`, err);
3274
+ publishErrorReply(replyTo, correlationId, subject, err);
3275
+ msg.term(`Handler error: ${subject}`);
3276
+ }
3277
+ );
3278
+ };
3279
+ const backlogWarnThreshold = 1e3;
3280
+ let active = 0;
3281
+ let backlogWarned = false;
3282
+ const backlog = [];
3283
+ const onAsyncDone = () => {
3284
+ active--;
3285
+ drainBacklog();
3286
+ };
3287
+ const drainBacklog = () => {
3288
+ while (active < maxActive) {
3289
+ const next = backlog.shift();
3290
+ if (next === void 0) return;
3291
+ active++;
3292
+ const result = handleSafe(next);
3293
+ if (result !== void 0) {
3294
+ void result.finally(onAsyncDone);
3295
+ } else {
3296
+ active--;
3297
+ }
3298
+ }
3299
+ if (backlog.length < backlogWarnThreshold) backlogWarned = false;
3300
+ };
3301
+ this.subscription = this.messageProvider.commands$.subscribe({
3302
+ next: (msg) => {
3303
+ if (active >= maxActive) {
3304
+ backlog.push(msg);
3305
+ if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
3306
+ backlogWarned = true;
3307
+ logger.warn(
3308
+ `RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
3309
+ );
3310
+ }
3311
+ return;
3312
+ }
3313
+ active++;
3314
+ const result = handleSafe(msg);
3315
+ if (result !== void 0) {
3316
+ void result.finally(onAsyncDone);
3317
+ } else {
3318
+ active--;
3319
+ if (backlog.length > 0) drainBacklog();
3320
+ }
3321
+ },
3322
+ error: (err) => {
3323
+ logger.error("Stream error in RPC router", err);
3324
+ }
3325
+ });
3326
+ }
3327
+ /** Stop routing and unsubscribe. */
3328
+ destroy() {
3329
+ this.subscription?.unsubscribe();
3330
+ this.subscription = null;
2345
3331
  }
2346
3332
  };
2347
3333
 
2348
3334
  // src/shutdown/shutdown.manager.ts
2349
- var import_common11 = require("@nestjs/common");
3335
+ var import_common13 = require("@nestjs/common");
2350
3336
  var ShutdownManager = class {
2351
3337
  constructor(connection, eventBus, timeout) {
2352
3338
  this.connection = connection;
2353
3339
  this.eventBus = eventBus;
2354
3340
  this.timeout = timeout;
2355
3341
  }
2356
- logger = new import_common11.Logger("Jetstream:Shutdown");
3342
+ logger = new import_common13.Logger("Jetstream:Shutdown");
3343
+ shutdownPromise;
2357
3344
  /**
2358
3345
  * Execute the full shutdown sequence.
2359
3346
  *
3347
+ * Idempotent — concurrent or repeated calls return the same promise.
3348
+ *
2360
3349
  * @param strategy Optional stoppable to close (stops consumers and subscriptions).
2361
3350
  */
2362
3351
  async shutdown(strategy) {
3352
+ this.shutdownPromise ??= this.doShutdown(strategy);
3353
+ return this.shutdownPromise;
3354
+ }
3355
+ async doShutdown(strategy) {
2363
3356
  this.eventBus.emit("shutdownStart" /* ShutdownStart */);
2364
3357
  this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
2365
3358
  strategy?.close();
@@ -2494,7 +3487,7 @@ var JetstreamModule = class {
2494
3487
  provide: JETSTREAM_EVENT_BUS,
2495
3488
  inject: [JETSTREAM_OPTIONS],
2496
3489
  useFactory: (options) => {
2497
- const logger = new import_common12.Logger("Jetstream:Module");
3490
+ const logger = new import_common14.Logger("Jetstream:Module");
2498
3491
  return new EventBus(logger, options.hooks);
2499
3492
  }
2500
3493
  },
@@ -2573,8 +3566,8 @@ var JetstreamModule = class {
2573
3566
  // MessageProvider — pull-based message consumption
2574
3567
  {
2575
3568
  provide: MessageProvider,
2576
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
2577
- useFactory: (options, connection, eventBus) => {
3569
+ inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
3570
+ useFactory: (options, connection, eventBus, consumerProvider) => {
2578
3571
  if (options.consumer === false) return null;
2579
3572
  const consumeOptionsMap = /* @__PURE__ */ new Map();
2580
3573
  if (options.events?.consume)
@@ -2584,7 +3577,11 @@ var JetstreamModule = class {
2584
3577
  if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
2585
3578
  consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
2586
3579
  }
2587
- return new MessageProvider(connection, eventBus, consumeOptionsMap);
3580
+ const consumerRecoveryFn = consumerProvider ? async (kind) => {
3581
+ const jsm = await connection.getJetStreamManager();
3582
+ return consumerProvider.recoverConsumer(jsm, kind);
3583
+ } : void 0;
3584
+ return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
2588
3585
  }
2589
3586
  },
2590
3587
  // EventRouter — routes event and broadcast messages to handlers
@@ -2596,9 +3593,10 @@ var JetstreamModule = class {
2596
3593
  PatternRegistry,
2597
3594
  JETSTREAM_CODEC,
2598
3595
  JETSTREAM_EVENT_BUS,
2599
- JETSTREAM_ACK_WAIT_MAP
3596
+ JETSTREAM_ACK_WAIT_MAP,
3597
+ JETSTREAM_CONNECTION
2600
3598
  ],
2601
- useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
3599
+ useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
2602
3600
  if (options.consumer === false) return null;
2603
3601
  const deadLetterConfig = options.onDeadLetter ? {
2604
3602
  maxDeliverByStream: /* @__PURE__ */ new Map(),
@@ -2621,7 +3619,9 @@ var JetstreamModule = class {
2621
3619
  eventBus,
2622
3620
  deadLetterConfig,
2623
3621
  processingConfig,
2624
- ackWaitMap
3622
+ ackWaitMap,
3623
+ connection,
3624
+ options
2625
3625
  );
2626
3626
  }
2627
3627
  },
@@ -2670,6 +3670,15 @@ var JetstreamModule = class {
2670
3670
  return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
2671
3671
  }
2672
3672
  },
3673
+ // MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
3674
+ {
3675
+ provide: MetadataProvider,
3676
+ inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
3677
+ useFactory: (options, connection) => {
3678
+ if (options.consumer === false) return null;
3679
+ return new MetadataProvider(options, connection);
3680
+ }
3681
+ },
2673
3682
  // JetstreamStrategy — server-side transport (only when consumer enabled)
2674
3683
  {
2675
3684
  provide: JetstreamStrategy,
@@ -2683,9 +3692,10 @@ var JetstreamModule = class {
2683
3692
  EventRouter,
2684
3693
  RpcRouter,
2685
3694
  CoreRpcServer,
2686
- JETSTREAM_ACK_WAIT_MAP
3695
+ JETSTREAM_ACK_WAIT_MAP,
3696
+ MetadataProvider
2687
3697
  ],
2688
- useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
3698
+ useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap, metadataProvider) => {
2689
3699
  if (options.consumer === false) return null;
2690
3700
  return new JetstreamStrategy(
2691
3701
  options,
@@ -2697,7 +3707,8 @@ var JetstreamModule = class {
2697
3707
  eventRouter,
2698
3708
  rpcRouter,
2699
3709
  coreRpcServer,
2700
- ackWaitMap
3710
+ ackWaitMap,
3711
+ metadataProvider
2701
3712
  );
2702
3713
  }
2703
3714
  }
@@ -2756,21 +3767,35 @@ var JetstreamModule = class {
2756
3767
  }
2757
3768
  };
2758
3769
  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))
3770
+ (0, import_common14.Global)(),
3771
+ (0, import_common14.Module)({}),
3772
+ __decorateParam(0, (0, import_common14.Optional)()),
3773
+ __decorateParam(0, (0, import_common14.Inject)(ShutdownManager)),
3774
+ __decorateParam(1, (0, import_common14.Optional)()),
3775
+ __decorateParam(1, (0, import_common14.Inject)(JetstreamStrategy))
2765
3776
  ], JetstreamModule);
2766
3777
  // Annotate the CommonJS export names for ESM import in node:
2767
3778
  0 && (module.exports = {
2768
- EventBus,
3779
+ DEFAULT_BROADCAST_CONSUMER_CONFIG,
3780
+ DEFAULT_BROADCAST_STREAM_CONFIG,
3781
+ DEFAULT_COMMAND_CONSUMER_CONFIG,
3782
+ DEFAULT_COMMAND_STREAM_CONFIG,
3783
+ DEFAULT_DLQ_STREAM_CONFIG,
3784
+ DEFAULT_EVENT_CONSUMER_CONFIG,
3785
+ DEFAULT_EVENT_STREAM_CONFIG,
3786
+ DEFAULT_JETSTREAM_RPC_TIMEOUT,
3787
+ DEFAULT_METADATA_BUCKET,
3788
+ DEFAULT_METADATA_HISTORY,
3789
+ DEFAULT_METADATA_REPLICAS,
3790
+ DEFAULT_METADATA_TTL,
3791
+ DEFAULT_ORDERED_STREAM_CONFIG,
3792
+ DEFAULT_RPC_TIMEOUT,
3793
+ DEFAULT_SHUTDOWN_TIMEOUT,
2769
3794
  JETSTREAM_CODEC,
2770
3795
  JETSTREAM_CONNECTION,
2771
- JETSTREAM_EVENT_BUS,
2772
3796
  JETSTREAM_OPTIONS,
2773
3797
  JetstreamClient,
3798
+ JetstreamDlqHeader,
2774
3799
  JetstreamHeader,
2775
3800
  JetstreamHealthIndicator,
2776
3801
  JetstreamModule,
@@ -2778,17 +3803,24 @@ JetstreamModule = __decorateClass([
2778
3803
  JetstreamRecordBuilder,
2779
3804
  JetstreamStrategy,
2780
3805
  JsonCodec,
3806
+ MIN_METADATA_TTL,
2781
3807
  MessageKind,
3808
+ MsgpackCodec,
3809
+ NatsErrorCode,
2782
3810
  PatternPrefix,
3811
+ RESERVED_HEADERS,
2783
3812
  RpcContext,
2784
3813
  StreamKind,
2785
3814
  TransportEvent,
3815
+ buildBroadcastSubject,
2786
3816
  buildSubject,
2787
3817
  consumerName,
3818
+ dlqStreamName,
2788
3819
  getClientToken,
2789
3820
  internalName,
2790
3821
  isCoreRpcMode,
2791
3822
  isJetStreamRpcMode,
3823
+ metadataKey,
2792
3824
  streamName,
2793
3825
  toNanos
2794
3826
  });