@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.js CHANGED
@@ -14,7 +14,7 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
14
14
  import {
15
15
  Global,
16
16
  Inject,
17
- Logger as Logger12,
17
+ Logger as Logger14,
18
18
  Module,
19
19
  Optional
20
20
  } from "@nestjs/common";
@@ -121,7 +121,7 @@ var DEFAULT_BROADCAST_STREAM_CONFIG = {
121
121
  max_msgs_per_subject: 1e6,
122
122
  max_msgs: 1e7,
123
123
  max_bytes: 2 * GB,
124
- max_age: toNanos(1, "days"),
124
+ max_age: toNanos(1, "hours"),
125
125
  duplicate_window: toNanos(2, "minutes")
126
126
  };
127
127
  var DEFAULT_ORDERED_STREAM_CONFIG = {
@@ -136,6 +136,18 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
136
136
  max_age: toNanos(1, "days"),
137
137
  duplicate_window: toNanos(2, "minutes")
138
138
  };
139
+ var DEFAULT_DLQ_STREAM_CONFIG = {
140
+ ...baseStreamConfig,
141
+ retention: RetentionPolicy.Workqueue,
142
+ allow_rollup_hdrs: false,
143
+ max_consumers: 100,
144
+ max_msg_size: 10 * MB,
145
+ max_msgs_per_subject: 5e6,
146
+ max_msgs: 5e7,
147
+ max_bytes: 5 * GB,
148
+ max_age: toNanos(30, "days"),
149
+ duplicate_window: toNanos(2, "minutes")
150
+ };
139
151
  var DEFAULT_EVENT_CONSUMER_CONFIG = {
140
152
  ack_wait: toNanos(10, "seconds"),
141
153
  max_deliver: 3,
@@ -163,6 +175,12 @@ var DEFAULT_BROADCAST_CONSUMER_CONFIG = {
163
175
  var DEFAULT_RPC_TIMEOUT = 3e4;
164
176
  var DEFAULT_JETSTREAM_RPC_TIMEOUT = 18e4;
165
177
  var DEFAULT_SHUTDOWN_TIMEOUT = 1e4;
178
+ var DEFAULT_METADATA_BUCKET = "handler_registry";
179
+ var DEFAULT_METADATA_REPLICAS = 1;
180
+ var DEFAULT_METADATA_HISTORY = 1;
181
+ var DEFAULT_METADATA_TTL = 3e4;
182
+ var MIN_METADATA_TTL = 5e3;
183
+ var metadataKey = (serviceName, kind, pattern) => `${serviceName}.${kind}.${pattern}`;
166
184
  var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
167
185
  JetstreamHeader2["CorrelationId"] = "x-correlation-id";
168
186
  JetstreamHeader2["ReplyTo"] = "x-reply-to";
@@ -171,6 +189,14 @@ var JetstreamHeader = /* @__PURE__ */ ((JetstreamHeader2) => {
171
189
  JetstreamHeader2["Error"] = "x-error";
172
190
  return JetstreamHeader2;
173
191
  })(JetstreamHeader || {});
192
+ var JetstreamDlqHeader = /* @__PURE__ */ ((JetstreamDlqHeader2) => {
193
+ JetstreamDlqHeader2["DeadLetterReason"] = "x-dead-letter-reason";
194
+ JetstreamDlqHeader2["OriginalSubject"] = "x-original-subject";
195
+ JetstreamDlqHeader2["OriginalStream"] = "x-original-stream";
196
+ JetstreamDlqHeader2["FailedAt"] = "x-failed-at";
197
+ JetstreamDlqHeader2["DeliveryCount"] = "x-delivery-count";
198
+ return JetstreamDlqHeader2;
199
+ })(JetstreamDlqHeader || {});
174
200
  var RESERVED_HEADERS = /* @__PURE__ */ new Set([
175
201
  "x-correlation-id" /* CorrelationId */,
176
202
  "x-reply-to" /* ReplyTo */,
@@ -183,6 +209,9 @@ var streamName = (serviceName, kind) => {
183
209
  if (kind === "broadcast" /* Broadcast */) return "broadcast-stream";
184
210
  return `${internalName(serviceName)}_${kind}-stream`;
185
211
  };
212
+ var dlqStreamName = (serviceName) => {
213
+ return `${internalName(serviceName)}_dlq-stream`;
214
+ };
186
215
  var consumerName = (serviceName, kind) => {
187
216
  if (kind === "broadcast" /* Broadcast */) return `${internalName(serviceName)}_broadcast-consumer`;
188
217
  return `${internalName(serviceName)}_${kind}-consumer`;
@@ -197,12 +226,13 @@ var isCoreRpcMode = (rpc) => !rpc || rpc.mode === "core";
197
226
 
198
227
  // src/client/jetstream.record.ts
199
228
  var JetstreamRecord = class {
200
- constructor(data, headers2, timeout, messageId, schedule) {
229
+ constructor(data, headers2, timeout, messageId, schedule, ttl) {
201
230
  this.data = data;
202
231
  this.headers = headers2;
203
232
  this.timeout = timeout;
204
233
  this.messageId = messageId;
205
234
  this.schedule = schedule;
235
+ this.ttl = ttl;
206
236
  }
207
237
  };
208
238
  var JetstreamRecordBuilder = class {
@@ -211,6 +241,7 @@ var JetstreamRecordBuilder = class {
211
241
  timeout;
212
242
  messageId;
213
243
  scheduleOptions;
244
+ ttlDuration;
214
245
  constructor(data) {
215
246
  this.data = data;
216
247
  }
@@ -302,6 +333,33 @@ var JetstreamRecordBuilder = class {
302
333
  this.scheduleOptions = { at: new Date(ts) };
303
334
  return this;
304
335
  }
336
+ /**
337
+ * Set per-message TTL (time-to-live).
338
+ *
339
+ * The message expires individually after the specified duration,
340
+ * independent of the stream's `max_age`. Requires NATS >= 2.11 and
341
+ * `allow_msg_ttl: true` on the stream.
342
+ *
343
+ * Only meaningful for events (`client.emit()`). If used with RPC
344
+ * (`client.send()`), a warning is logged and the TTL is ignored.
345
+ *
346
+ * @param nanos - TTL in nanoseconds. Use {@link toNanos} for human-readable values.
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * import { toNanos } from '@horizon-republic/nestjs-jetstream';
351
+ *
352
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(30, 'minutes')).build();
353
+ * new JetstreamRecordBuilder(payload).ttl(toNanos(24, 'hours')).build();
354
+ * ```
355
+ */
356
+ ttl(nanos) {
357
+ if (!Number.isFinite(nanos) || nanos <= 0) {
358
+ throw new Error("TTL must be a positive finite value");
359
+ }
360
+ this.ttlDuration = nanosToGoDuration(nanos);
361
+ return this;
362
+ }
305
363
  /**
306
364
  * Build the immutable {@link JetstreamRecord}.
307
365
  *
@@ -314,7 +372,8 @@ var JetstreamRecordBuilder = class {
314
372
  new Map(this.headers),
315
373
  this.timeout,
316
374
  this.messageId,
317
- schedule
375
+ schedule,
376
+ this.ttlDuration
318
377
  );
319
378
  }
320
379
  /** Validate that a header key is not reserved. */
@@ -326,8 +385,20 @@ var JetstreamRecordBuilder = class {
326
385
  }
327
386
  }
328
387
  };
388
+ var NS_PER_MS = 1e6;
389
+ var NS_PER_S = 1e9;
390
+ var NS_PER_M = 60 * NS_PER_S;
391
+ var NS_PER_H = 60 * NS_PER_M;
392
+ var nanosToGoDuration = (nanos) => {
393
+ if (nanos >= NS_PER_H && nanos % NS_PER_H === 0) return `${nanos / NS_PER_H}h`;
394
+ if (nanos >= NS_PER_M && nanos % NS_PER_M === 0) return `${nanos / NS_PER_M}m`;
395
+ if (nanos >= NS_PER_S && nanos % NS_PER_S === 0) return `${nanos / NS_PER_S}s`;
396
+ if (nanos >= NS_PER_MS && nanos % NS_PER_MS === 0) return `${nanos / NS_PER_MS}ms`;
397
+ return `${nanos}ns`;
398
+ };
329
399
 
330
400
  // src/client/jetstream.client.ts
401
+ var BROADCAST_SUBJECT_PREFIX = "broadcast.";
331
402
  var JetstreamClient = class extends ClientProxy {
332
403
  constructor(rootOptions, targetServiceName, connection, codec, eventBus) {
333
404
  super();
@@ -337,12 +408,33 @@ var JetstreamClient = class extends ClientProxy {
337
408
  this.eventBus = eventBus;
338
409
  this.targetName = targetServiceName;
339
410
  this.callerName = internalName(this.rootOptions.name);
411
+ const targetInternal = internalName(targetServiceName);
412
+ this.eventSubjectPrefix = `${targetInternal}.${"ev" /* Event */}.`;
413
+ this.commandSubjectPrefix = `${targetInternal}.${"cmd" /* Command */}.`;
414
+ this.orderedSubjectPrefix = `${targetInternal}.${"ordered" /* Ordered */}.`;
415
+ this.isCoreMode = isCoreRpcMode(this.rootOptions.rpc);
416
+ this.defaultRpcTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? this.rootOptions.rpc?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT : this.rootOptions.rpc?.timeout ?? DEFAULT_RPC_TIMEOUT;
340
417
  }
341
418
  logger = new Logger("Jetstream:Client");
342
419
  /** Target service name this client sends messages to. */
343
420
  targetName;
344
421
  /** Pre-cached caller name derived from rootOptions.name, computed once in constructor. */
345
422
  callerName;
423
+ /**
424
+ * Subject prefixes of the form `{serviceName}__microservice.{kind}.` — one
425
+ * per stream kind this client may publish to. Built once in the constructor
426
+ * so producing a full subject is a single string concat with the user pattern.
427
+ */
428
+ eventSubjectPrefix;
429
+ commandSubjectPrefix;
430
+ orderedSubjectPrefix;
431
+ /**
432
+ * RPC configuration snapshots. The values are derived from rootOptions at
433
+ * construction time so the publish hot path never has to re-run
434
+ * isCoreRpcMode / getRpcTimeout on every call.
435
+ */
436
+ isCoreMode;
437
+ defaultRpcTimeout;
346
438
  /** Shared inbox for JetStream-mode RPC responses. */
347
439
  inbox = null;
348
440
  inboxSubscription = null;
@@ -352,6 +444,12 @@ var JetstreamClient = class extends ClientProxy {
352
444
  pendingTimeouts = /* @__PURE__ */ new Map();
353
445
  /** Subscription to connection status events for disconnect handling. */
354
446
  statusSubscription = null;
447
+ /**
448
+ * Cached readiness flag. Once `connect()` has wired the inbox and status
449
+ * subscription, subsequent publishes skip the `await connect()` microtask
450
+ * and reach for the underlying connection synchronously instead.
451
+ */
452
+ readyForPublish = false;
355
453
  /**
356
454
  * Establish connection. Called automatically by NestJS on first use.
357
455
  *
@@ -362,7 +460,7 @@ var JetstreamClient = class extends ClientProxy {
362
460
  */
363
461
  async connect() {
364
462
  const nc = await this.connection.getConnection();
365
- if (isJetStreamRpcMode(this.rootOptions.rpc) && !this.inboxSubscription) {
463
+ if (!this.isCoreMode && !this.inboxSubscription) {
366
464
  this.setupInbox(nc);
367
465
  }
368
466
  this.statusSubscription ??= this.connection.status$.subscribe((status) => {
@@ -370,12 +468,14 @@ var JetstreamClient = class extends ClientProxy {
370
468
  this.handleDisconnect();
371
469
  }
372
470
  });
471
+ this.readyForPublish = true;
373
472
  return nc;
374
473
  }
375
474
  /** Clean up resources: reject pending RPCs, unsubscribe from status events. */
376
475
  async close() {
377
476
  this.statusSubscription?.unsubscribe();
378
477
  this.statusSubscription = null;
478
+ this.readyForPublish = false;
379
479
  this.rejectPendingRpcs(new Error("Client closed"));
380
480
  }
381
481
  /**
@@ -399,8 +499,8 @@ var JetstreamClient = class extends ClientProxy {
399
499
  * set to the original event subject.
400
500
  */
401
501
  async dispatchEvent(packet) {
402
- await this.connect();
403
- const { data, hdrs, messageId, schedule } = this.extractRecordData(packet.data);
502
+ if (!this.readyForPublish) await this.connect();
503
+ const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
404
504
  const eventSubject = this.buildEventSubject(packet.pattern);
405
505
  const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
406
506
  if (schedule) {
@@ -408,6 +508,7 @@ var JetstreamClient = class extends ClientProxy {
408
508
  const ack = await this.connection.getJetStreamClient().publish(scheduleSubject, this.codec.encode(data), {
409
509
  headers: msgHeaders,
410
510
  msgID: messageId ?? nuid.next(),
511
+ ttl,
411
512
  schedule: {
412
513
  specification: schedule.at,
413
514
  target: eventSubject
@@ -421,7 +522,8 @@ var JetstreamClient = class extends ClientProxy {
421
522
  } else {
422
523
  const ack = await this.connection.getJetStreamClient().publish(eventSubject, this.codec.encode(data), {
423
524
  headers: msgHeaders,
424
- msgID: messageId ?? nuid.next()
525
+ msgID: messageId ?? nuid.next(),
526
+ ttl
425
527
  });
426
528
  if (ack.duplicate) {
427
529
  this.logger.warn(`Duplicate event publish detected: ${eventSubject} (seq: ${ack.seq})`);
@@ -436,19 +538,24 @@ var JetstreamClient = class extends ClientProxy {
436
538
  * JetStream mode: publishes to stream + waits for inbox response.
437
539
  */
438
540
  publish(packet, callback) {
439
- const subject = buildSubject(this.targetName, "cmd" /* Command */, packet.pattern);
440
- const { data, hdrs, timeout, messageId, schedule } = this.extractRecordData(packet.data);
541
+ const subject = this.commandSubjectPrefix + packet.pattern;
542
+ const { data, hdrs, timeout, messageId, schedule, ttl } = this.extractRecordData(packet.data);
441
543
  if (schedule) {
442
544
  this.logger.warn(
443
545
  "scheduleAt() is ignored for RPC (client.send()). Use client.emit() for scheduled events."
444
546
  );
445
547
  }
548
+ if (ttl) {
549
+ this.logger.warn(
550
+ "ttl() is ignored for RPC (client.send()). Use client.emit() for events with TTL."
551
+ );
552
+ }
446
553
  const onUnhandled = (err) => {
447
554
  this.logger.error("Unhandled publish error:", err);
448
555
  callback({ err: new Error("Internal transport error"), response: null, isDisposed: true });
449
556
  };
450
557
  let jetStreamCorrelationId = null;
451
- if (isCoreRpcMode(this.rootOptions.rpc)) {
558
+ if (this.isCoreMode) {
452
559
  this.publishCoreRpc(subject, data, hdrs, timeout, callback).catch(onUnhandled);
453
560
  } else {
454
561
  jetStreamCorrelationId = nuid.next();
@@ -473,8 +580,8 @@ var JetstreamClient = class extends ClientProxy {
473
580
  /** Core mode: nc.request() with timeout. */
474
581
  async publishCoreRpc(subject, data, customHeaders, timeout, callback) {
475
582
  try {
476
- const nc = await this.connect();
477
- const effectiveTimeout = timeout ?? this.getRpcTimeout();
583
+ const nc = this.readyForPublish ? this.connection.unwrap : await this.connect();
584
+ const effectiveTimeout = timeout ?? this.defaultRpcTimeout;
478
585
  const hdrs = this.buildHeaders(customHeaders, { subject });
479
586
  const response = await nc.request(subject, this.codec.encode(data), {
480
587
  timeout: effectiveTimeout,
@@ -496,10 +603,10 @@ var JetstreamClient = class extends ClientProxy {
496
603
  /** JetStream mode: publish to stream + wait for inbox response. */
497
604
  async publishJetStreamRpc(subject, data, callback, options) {
498
605
  const { headers: customHeaders, correlationId, messageId } = options;
499
- const effectiveTimeout = options.timeout ?? this.getRpcTimeout();
606
+ const effectiveTimeout = options.timeout ?? this.defaultRpcTimeout;
500
607
  this.pendingMessages.set(correlationId, callback);
501
608
  try {
502
- await this.connect();
609
+ if (!this.readyForPublish) await this.connect();
503
610
  if (!this.pendingMessages.has(correlationId)) return;
504
611
  if (!this.inbox) {
505
612
  this.pendingMessages.delete(correlationId);
@@ -545,6 +652,7 @@ var JetstreamClient = class extends ClientProxy {
545
652
  handleDisconnect() {
546
653
  this.rejectPendingRpcs(new Error("Connection lost"));
547
654
  this.inbox = null;
655
+ this.readyForPublish = false;
548
656
  }
549
657
  /** Reject all pending RPC callbacks, clear timeouts, and tear down inbox. */
550
658
  rejectPendingRpcs(error) {
@@ -558,6 +666,7 @@ var JetstreamClient = class extends ClientProxy {
558
666
  this.pendingTimeouts.clear();
559
667
  this.inboxSubscription?.unsubscribe();
560
668
  this.inboxSubscription = null;
669
+ this.inbox = null;
561
670
  }
562
671
  /** Setup shared inbox subscription for JetStream RPC responses. */
563
672
  setupInbox(nc) {
@@ -607,19 +716,22 @@ var JetstreamClient = class extends ClientProxy {
607
716
  this.pendingMessages.delete(correlationId);
608
717
  }
609
718
  }
610
- /** Build event subject — workqueue, broadcast, or ordered. */
719
+ /**
720
+ * Resolve a user pattern to a fully-qualified NATS subject, dispatching
721
+ * between the event, broadcast, and ordered prefixes.
722
+ *
723
+ * The leading-char check short-circuits the `startsWith` comparisons for
724
+ * patterns that cannot possibly carry a broadcast/ordered marker, which is
725
+ * the overwhelmingly common case.
726
+ */
611
727
  buildEventSubject(pattern) {
612
- if (pattern.startsWith("broadcast:" /* Broadcast */)) {
613
- return buildBroadcastSubject(pattern.slice("broadcast:" /* Broadcast */.length));
614
- }
615
- if (pattern.startsWith("ordered:" /* Ordered */)) {
616
- return buildSubject(
617
- this.targetName,
618
- "ordered" /* Ordered */,
619
- pattern.slice("ordered:" /* Ordered */.length)
620
- );
728
+ if (pattern.charCodeAt(0) === 98 && pattern.startsWith("broadcast:" /* Broadcast */)) {
729
+ return BROADCAST_SUBJECT_PREFIX + pattern.slice("broadcast:" /* Broadcast */.length);
730
+ }
731
+ if (pattern.charCodeAt(0) === 111 && pattern.startsWith("ordered:" /* Ordered */)) {
732
+ return this.orderedSubjectPrefix + pattern.slice("ordered:" /* Ordered */.length);
621
733
  }
622
- return buildSubject(this.targetName, "ev" /* Event */, pattern);
734
+ return this.eventSubjectPrefix + pattern;
623
735
  }
624
736
  /** Build NATS headers merging custom headers with transport headers. */
625
737
  buildHeaders(customHeaders, transport) {
@@ -644,10 +756,11 @@ var JetstreamClient = class extends ClientProxy {
644
756
  if (rawData instanceof JetstreamRecord) {
645
757
  return {
646
758
  data: rawData.data,
647
- hdrs: rawData.headers.size > 0 ? new Map(rawData.headers) : null,
759
+ hdrs: rawData.headers.size > 0 ? rawData.headers : null,
648
760
  timeout: rawData.timeout,
649
761
  messageId: rawData.messageId,
650
- schedule: rawData.schedule
762
+ schedule: rawData.schedule,
763
+ ttl: rawData.ttl
651
764
  };
652
765
  }
653
766
  return {
@@ -655,7 +768,8 @@ var JetstreamClient = class extends ClientProxy {
655
768
  hdrs: null,
656
769
  timeout: void 0,
657
770
  messageId: void 0,
658
- schedule: void 0
771
+ schedule: void 0,
772
+ ttl: void 0
659
773
  };
660
774
  }
661
775
  /**
@@ -685,11 +799,6 @@ var JetstreamClient = class extends ClientProxy {
685
799
  const pattern = withoutPrefix.slice(dotIndex + 1);
686
800
  return `${targetPrefix}_sch.${pattern}`;
687
801
  }
688
- getRpcTimeout() {
689
- if (!this.rootOptions.rpc) return DEFAULT_RPC_TIMEOUT;
690
- const defaultTimeout = isJetStreamRpcMode(this.rootOptions.rpc) ? DEFAULT_JETSTREAM_RPC_TIMEOUT : DEFAULT_RPC_TIMEOUT;
691
- return this.rootOptions.rpc.timeout ?? defaultTimeout;
692
- }
693
802
  };
694
803
 
695
804
  // src/codec/json.codec.ts
@@ -704,6 +813,19 @@ var JsonCodec = class {
704
813
  }
705
814
  };
706
815
 
816
+ // src/codec/msgpack.codec.ts
817
+ var MsgpackCodec = class {
818
+ constructor(packr) {
819
+ this.packr = packr;
820
+ }
821
+ encode(data) {
822
+ return this.packr.pack(data);
823
+ }
824
+ decode(data) {
825
+ return this.packr.unpack(data);
826
+ }
827
+ };
828
+
707
829
  // src/connection/connection.provider.ts
708
830
  import { Logger as Logger2 } from "@nestjs/common";
709
831
  import {
@@ -921,6 +1043,15 @@ var EventBus = class {
921
1043
  kind
922
1044
  );
923
1045
  }
1046
+ /**
1047
+ * Check whether a hook is registered for the given event.
1048
+ *
1049
+ * Used by the routing hot path to elide the emit call entirely when the
1050
+ * transport owner did not register a listener.
1051
+ */
1052
+ hasHook(event) {
1053
+ return this.hooks[event] !== void 0;
1054
+ }
924
1055
  callHook(event, hook, ...args) {
925
1056
  try {
926
1057
  const result = hook(...args);
@@ -975,9 +1106,14 @@ var JetstreamHealthIndicator = class {
975
1106
  * Returns `{ [key]: { status: 'up', ... } }` on success.
976
1107
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
977
1108
  *
1109
+ * The thrown error sets `isHealthCheckError: true` and `causes` — the
1110
+ * duck-type contract that Terminus `HealthCheckExecutor` uses to distinguish
1111
+ * health failures from unexpected exceptions. Works with both Terminus v10
1112
+ * (`instanceof HealthCheckError`) and v11+ (`error?.isHealthCheckError`).
1113
+ *
978
1114
  * @param key - Health indicator key (default: `'jetstream'`).
979
1115
  * @returns Object with status, server, and latency under the given key.
980
- * @throws Error with `{ [key]: { status: 'down' } }` when disconnected.
1116
+ * @throws Error with `isHealthCheckError`, `causes`, and `{ [key]: { status: 'down' } }`.
981
1117
  */
982
1118
  async isHealthy(key = "jetstream") {
983
1119
  const status = await this.check();
@@ -987,8 +1123,10 @@ var JetstreamHealthIndicator = class {
987
1123
  latency: status.latency
988
1124
  };
989
1125
  if (!status.connected) {
1126
+ const causes = { [key]: details };
990
1127
  throw Object.assign(new Error("Jetstream health check failed"), {
991
- [key]: details
1128
+ causes,
1129
+ isHealthCheckError: true
992
1130
  });
993
1131
  }
994
1132
  return { [key]: details };
@@ -1001,7 +1139,7 @@ JetstreamHealthIndicator = __decorateClass([
1001
1139
  // src/server/strategy.ts
1002
1140
  import { Server } from "@nestjs/microservices";
1003
1141
  var JetstreamStrategy = class extends Server {
1004
- constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map()) {
1142
+ constructor(options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap = /* @__PURE__ */ new Map(), metadataProvider) {
1005
1143
  super();
1006
1144
  this.options = options;
1007
1145
  this.connection = connection;
@@ -1013,6 +1151,7 @@ var JetstreamStrategy = class extends Server {
1013
1151
  this.rpcRouter = rpcRouter;
1014
1152
  this.coreRpcServer = coreRpcServer;
1015
1153
  this.ackWaitMap = ackWaitMap;
1154
+ this.metadataProvider = metadataProvider;
1016
1155
  }
1017
1156
  transportId = /* @__PURE__ */ Symbol("jetstream-transport");
1018
1157
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -1057,10 +1196,14 @@ var JetstreamStrategy = class extends Server {
1057
1196
  if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
1058
1197
  await this.coreRpcServer.start();
1059
1198
  }
1199
+ if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
1200
+ await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
1201
+ }
1060
1202
  callback();
1061
1203
  }
1062
- /** Stop all consumers, routers, and subscriptions. Called during shutdown. */
1204
+ /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
1063
1205
  close() {
1206
+ this.metadataProvider?.destroy();
1064
1207
  this.eventRouter.destroy();
1065
1208
  this.rpcRouter.destroy();
1066
1209
  this.coreRpcServer.stop();
@@ -1291,16 +1434,71 @@ var resolveAckExtensionInterval = (config, ackWaitNanos) => {
1291
1434
  const interval = Math.floor(ackWaitNanos / 1e6 / 2);
1292
1435
  return Math.max(interval, MIN_ACK_EXTENSION_INTERVAL);
1293
1436
  };
1437
+ var AckExtensionPool = class {
1438
+ entries = /* @__PURE__ */ new Set();
1439
+ handle = null;
1440
+ handleFireAt = 0;
1441
+ schedule(msg, interval) {
1442
+ const entry = {
1443
+ msg,
1444
+ interval,
1445
+ nextFireAt: Date.now() + interval,
1446
+ active: true
1447
+ };
1448
+ this.entries.add(entry);
1449
+ this.ensureWake(entry.nextFireAt);
1450
+ return entry;
1451
+ }
1452
+ cancel(entry) {
1453
+ if (!entry.active) return;
1454
+ entry.active = false;
1455
+ this.entries.delete(entry);
1456
+ if (this.entries.size === 0 && this.handle !== null) {
1457
+ clearTimeout(this.handle);
1458
+ this.handle = null;
1459
+ this.handleFireAt = 0;
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Ensure the shared timer will fire no later than `dueAt`. If an earlier
1464
+ * wake is already scheduled, leave it; otherwise replace it with a tighter one.
1465
+ */
1466
+ ensureWake(dueAt) {
1467
+ if (this.handle !== null && this.handleFireAt <= dueAt) return;
1468
+ if (this.handle !== null) clearTimeout(this.handle);
1469
+ const delay = Math.max(0, dueAt - Date.now());
1470
+ const handle = setTimeout(() => {
1471
+ this.tick();
1472
+ }, delay);
1473
+ if (typeof handle.unref === "function") handle.unref();
1474
+ this.handle = handle;
1475
+ this.handleFireAt = dueAt;
1476
+ }
1477
+ tick() {
1478
+ this.handle = null;
1479
+ this.handleFireAt = 0;
1480
+ const now = Date.now();
1481
+ let earliest = Infinity;
1482
+ for (const entry of this.entries) {
1483
+ if (!entry.active) continue;
1484
+ if (entry.nextFireAt <= now) {
1485
+ try {
1486
+ entry.msg.working();
1487
+ } catch {
1488
+ }
1489
+ entry.nextFireAt = now + entry.interval;
1490
+ }
1491
+ if (entry.nextFireAt < earliest) earliest = entry.nextFireAt;
1492
+ }
1493
+ if (earliest !== Infinity) this.ensureWake(earliest);
1494
+ }
1495
+ };
1496
+ var pool = new AckExtensionPool();
1294
1497
  var startAckExtensionTimer = (msg, interval) => {
1295
1498
  if (interval === null || interval <= 0) return null;
1296
- const timer2 = setInterval(() => {
1297
- try {
1298
- msg.working();
1299
- } catch {
1300
- }
1301
- }, interval);
1499
+ const entry = pool.schedule(msg, interval);
1302
1500
  return () => {
1303
- clearInterval(timer2);
1501
+ pool.cancel(entry);
1304
1502
  };
1305
1503
  };
1306
1504
 
@@ -1314,11 +1512,8 @@ var serializeError = (err) => {
1314
1512
 
1315
1513
  // src/utils/unwrap-result.ts
1316
1514
  import { isObservable } from "rxjs";
1317
- var RESOLVED_VOID = Promise.resolve(void 0);
1318
- var RESOLVED_NULL = Promise.resolve(null);
1319
1515
  var unwrapResult = (result) => {
1320
- if (result === void 0) return RESOLVED_VOID;
1321
- if (result === null) return RESOLVED_NULL;
1516
+ if (result === void 0 || result === null) return result;
1322
1517
  if (isObservable(result)) {
1323
1518
  return subscribeToFirst(result);
1324
1519
  }
@@ -1327,8 +1522,9 @@ var unwrapResult = (result) => {
1327
1522
  (resolved) => isObservable(resolved) ? subscribeToFirst(resolved) : resolved
1328
1523
  );
1329
1524
  }
1330
- return Promise.resolve(result);
1525
+ return result;
1331
1526
  };
1527
+ var isPromiseLike = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
1332
1528
  var subscribeToFirst = (obs) => new Promise((resolve, reject) => {
1333
1529
  let done = false;
1334
1530
  let subscription = null;
@@ -1416,7 +1612,8 @@ var CoreRpcServer = class {
1416
1612
  }
1417
1613
  const ctx = new RpcContext([msg]);
1418
1614
  try {
1419
- const result = await unwrapResult(handler(data, ctx));
1615
+ const raw = unwrapResult(handler(data, ctx));
1616
+ const result = isPromiseLike(raw) ? await raw : raw;
1420
1617
  msg.respond(this.codec.encode(result));
1421
1618
  } catch (err) {
1422
1619
  this.logger.error(`Handler error for Core RPC ${msg.subject}:`, err);
@@ -1436,24 +1633,180 @@ var CoreRpcServer = class {
1436
1633
  };
1437
1634
 
1438
1635
  // src/server/infrastructure/stream.provider.ts
1636
+ import { Logger as Logger6 } from "@nestjs/common";
1637
+ import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
1638
+
1639
+ // src/server/infrastructure/nats-error-codes.ts
1640
+ var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
1641
+ NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
1642
+ NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
1643
+ NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
1644
+ return NatsErrorCode2;
1645
+ })(NatsErrorCode || {});
1646
+
1647
+ // src/server/infrastructure/stream-config-diff.ts
1648
+ var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
1649
+ "retention"
1650
+ ]);
1651
+ var IMMUTABLE_PROPERTIES = /* @__PURE__ */ new Set([
1652
+ "storage"
1653
+ ]);
1654
+ var ENABLE_ONLY_PROPERTIES = /* @__PURE__ */ new Set([
1655
+ "allow_msg_schedules",
1656
+ "allow_msg_ttl",
1657
+ "deny_delete",
1658
+ "deny_purge"
1659
+ ]);
1660
+ var compareStreamConfig = (current, desired) => {
1661
+ const changes = [];
1662
+ for (const key of Object.keys(desired)) {
1663
+ const currentVal = current[key];
1664
+ const desiredVal = desired[key];
1665
+ if (isEqual(currentVal, desiredVal)) continue;
1666
+ changes.push({
1667
+ property: key,
1668
+ current: currentVal,
1669
+ desired: desiredVal,
1670
+ mutability: classifyMutability(key, currentVal, desiredVal)
1671
+ });
1672
+ }
1673
+ const hasImmutableChanges = changes.some((c) => c.mutability === "immutable");
1674
+ const hasMutableChanges = changes.some(
1675
+ (c) => c.mutability === "mutable" || c.mutability === "enable-only"
1676
+ );
1677
+ const hasTransportControlledConflicts = changes.some(
1678
+ (c) => c.mutability === "transport-controlled"
1679
+ );
1680
+ return {
1681
+ hasChanges: changes.length > 0,
1682
+ hasMutableChanges,
1683
+ hasImmutableChanges,
1684
+ hasTransportControlledConflicts,
1685
+ changes
1686
+ };
1687
+ };
1688
+ var classifyMutability = (key, current, desired) => {
1689
+ if (TRANSPORT_CONTROLLED_PROPERTIES.has(key)) return "transport-controlled";
1690
+ if (IMMUTABLE_PROPERTIES.has(key)) return "immutable";
1691
+ if (ENABLE_ONLY_PROPERTIES.has(key)) {
1692
+ return current === true && desired === false ? "immutable" : "enable-only";
1693
+ }
1694
+ return "mutable";
1695
+ };
1696
+ var isEqual = (a, b) => {
1697
+ if (a === b) return true;
1698
+ if (a == null && b == null) return true;
1699
+ return JSON.stringify(a) === JSON.stringify(b);
1700
+ };
1701
+
1702
+ // src/server/infrastructure/stream-migration.ts
1439
1703
  import { Logger as Logger5 } from "@nestjs/common";
1440
1704
  import { JetStreamApiError } from "@nats-io/jetstream";
1441
- var STREAM_NOT_FOUND = 10059;
1705
+ var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
1706
+ var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
1707
+ var SOURCING_POLL_INTERVAL_MS = 100;
1708
+ var StreamMigration = class {
1709
+ constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
1710
+ this.sourcingTimeoutMs = sourcingTimeoutMs;
1711
+ }
1712
+ logger = new Logger5("Jetstream:Stream");
1713
+ async migrate(jsm, streamName2, newConfig) {
1714
+ const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
1715
+ const startTime = Date.now();
1716
+ const currentInfo = await jsm.streams.info(streamName2);
1717
+ await this.cleanupOrphanedBackup(jsm, backupName);
1718
+ const messageCount = currentInfo.state.messages;
1719
+ this.logger.log(`Stream ${streamName2}: destructive migration started`);
1720
+ let originalDeleted = false;
1721
+ try {
1722
+ if (messageCount > 0) {
1723
+ this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
1724
+ await jsm.streams.add({
1725
+ ...currentInfo.config,
1726
+ name: backupName,
1727
+ subjects: [],
1728
+ sources: [{ name: streamName2 }]
1729
+ });
1730
+ await this.waitForSourcing(jsm, backupName, messageCount);
1731
+ }
1732
+ this.logger.log(` Phase 2/4: Deleting old stream`);
1733
+ await jsm.streams.delete(streamName2);
1734
+ originalDeleted = true;
1735
+ this.logger.log(` Phase 3/4: Creating stream with new config`);
1736
+ await jsm.streams.add(newConfig);
1737
+ if (messageCount > 0) {
1738
+ const backupInfo = await jsm.streams.info(backupName);
1739
+ await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
1740
+ this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
1741
+ await jsm.streams.update(streamName2, {
1742
+ ...newConfig,
1743
+ sources: [{ name: backupName }]
1744
+ });
1745
+ await this.waitForSourcing(jsm, streamName2, messageCount);
1746
+ await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
1747
+ await jsm.streams.delete(backupName);
1748
+ }
1749
+ } catch (err) {
1750
+ if (originalDeleted && messageCount > 0) {
1751
+ this.logger.error(
1752
+ `Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
1753
+ );
1754
+ } else {
1755
+ await this.cleanupOrphanedBackup(jsm, backupName);
1756
+ }
1757
+ throw err;
1758
+ }
1759
+ const durationMs = Date.now() - startTime;
1760
+ this.logger.log(
1761
+ `Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
1762
+ );
1763
+ }
1764
+ async waitForSourcing(jsm, streamName2, expectedCount) {
1765
+ const deadline = Date.now() + this.sourcingTimeoutMs;
1766
+ while (Date.now() < deadline) {
1767
+ const info = await jsm.streams.info(streamName2);
1768
+ if (info.state.messages >= expectedCount) return;
1769
+ await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
1770
+ }
1771
+ throw new Error(
1772
+ `Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
1773
+ );
1774
+ }
1775
+ async cleanupOrphanedBackup(jsm, backupName) {
1776
+ try {
1777
+ await jsm.streams.info(backupName);
1778
+ this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
1779
+ await jsm.streams.delete(backupName);
1780
+ } catch (err) {
1781
+ if (err instanceof JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
1782
+ return;
1783
+ }
1784
+ throw err;
1785
+ }
1786
+ }
1787
+ };
1788
+
1789
+ // src/server/infrastructure/stream.provider.ts
1442
1790
  var StreamProvider = class {
1443
1791
  constructor(options, connection) {
1444
1792
  this.options = options;
1445
1793
  this.connection = connection;
1446
1794
  }
1447
- logger = new Logger5("Jetstream:Stream");
1795
+ logger = new Logger6("Jetstream:Stream");
1796
+ migration = new StreamMigration();
1448
1797
  /**
1449
1798
  * Ensure all required streams exist with correct configuration.
1450
1799
  *
1451
1800
  * @param kinds Which stream kinds to create. Determined by the module based
1452
1801
  * on RPC mode and registered handler patterns.
1802
+ * If the dlq option is enabled, also ensures the DLQ stream exists.
1453
1803
  */
1454
1804
  async ensureStreams(kinds) {
1455
1805
  const jsm = await this.connection.getJetStreamManager();
1456
1806
  await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
1807
+ if (this.options.dlq) {
1808
+ await this.ensureDlqStream(jsm);
1809
+ }
1457
1810
  }
1458
1811
  /** Get the stream name for a given kind. */
1459
1812
  getStreamName(kind) {
@@ -1488,17 +1841,85 @@ var StreamProvider = class {
1488
1841
  const config = this.buildConfig(kind);
1489
1842
  this.logger.log(`Ensuring stream: ${config.name}`);
1490
1843
  try {
1491
- await jsm.streams.info(config.name);
1492
- this.logger.debug(`Stream exists, updating: ${config.name}`);
1493
- return await jsm.streams.update(config.name, config);
1844
+ const currentInfo = await jsm.streams.info(config.name);
1845
+ return await this.handleExistingStream(jsm, currentInfo, config);
1494
1846
  } catch (err) {
1495
- if (err instanceof JetStreamApiError && err.apiError().err_code === STREAM_NOT_FOUND) {
1847
+ if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
1496
1848
  this.logger.log(`Creating stream: ${config.name}`);
1497
1849
  return await jsm.streams.add(config);
1498
1850
  }
1499
1851
  throw err;
1500
1852
  }
1501
1853
  }
1854
+ /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
1855
+ async ensureDlqStream(jsm) {
1856
+ const config = this.buildDlqConfig();
1857
+ this.logger.log(`Ensuring DLQ stream: ${config.name}`);
1858
+ try {
1859
+ const currentInfo = await jsm.streams.info(config.name);
1860
+ return await this.handleExistingStream(jsm, currentInfo, config);
1861
+ } catch (err) {
1862
+ if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
1863
+ this.logger.log(`Creating DLQ stream: ${config.name}`);
1864
+ return await jsm.streams.add(config);
1865
+ }
1866
+ throw err;
1867
+ }
1868
+ }
1869
+ async handleExistingStream(jsm, currentInfo, config) {
1870
+ const diff = compareStreamConfig(currentInfo.config, config);
1871
+ if (!diff.hasChanges) {
1872
+ this.logger.debug(`Stream ${config.name}: no config changes`);
1873
+ return currentInfo;
1874
+ }
1875
+ this.logChanges(config.name, diff, !!this.options.allowDestructiveMigration);
1876
+ if (diff.hasTransportControlledConflicts) {
1877
+ const conflicts = diff.changes.filter((c) => c.mutability === "transport-controlled").map((c) => `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`).join(", ");
1878
+ throw new Error(
1879
+ `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.`
1880
+ );
1881
+ }
1882
+ if (!diff.hasImmutableChanges) {
1883
+ this.logger.debug(`Stream exists, updating: ${config.name}`);
1884
+ return await jsm.streams.update(config.name, config);
1885
+ }
1886
+ if (!this.options.allowDestructiveMigration) {
1887
+ this.logger.warn(
1888
+ `Stream ${config.name} has immutable config conflicts. Enable allowDestructiveMigration to recreate the stream.`
1889
+ );
1890
+ if (diff.hasMutableChanges) {
1891
+ const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
1892
+ return await jsm.streams.update(config.name, mutableConfig);
1893
+ }
1894
+ return currentInfo;
1895
+ }
1896
+ await this.migration.migrate(jsm, config.name, config);
1897
+ return await jsm.streams.info(config.name);
1898
+ }
1899
+ buildMutableOnlyConfig(config, currentConfig, diff) {
1900
+ const nonMutableKeys = new Set(
1901
+ diff.changes.filter((c) => c.mutability === "immutable" || c.mutability === "transport-controlled").map((c) => c.property)
1902
+ );
1903
+ const filtered = { ...config };
1904
+ for (const key of nonMutableKeys) {
1905
+ filtered[key] = currentConfig[key];
1906
+ }
1907
+ return filtered;
1908
+ }
1909
+ logChanges(streamName2, diff, migrationEnabled) {
1910
+ for (const c of diff.changes) {
1911
+ const detail = `${c.property}: ${JSON.stringify(c.current)} \u2192 ${JSON.stringify(c.desired)}`;
1912
+ if (c.mutability === "transport-controlled") {
1913
+ this.logger.error(
1914
+ `Stream ${streamName2}: ${detail} \u2014 transport-controlled, cannot be changed`
1915
+ );
1916
+ } else if (c.mutability === "immutable" && !migrationEnabled) {
1917
+ this.logger.warn(`Stream ${streamName2}: ${detail} \u2014 requires allowDestructiveMigration`);
1918
+ } else {
1919
+ this.logger.log(`Stream ${streamName2}: ${detail}`);
1920
+ }
1921
+ }
1922
+ }
1502
1923
  /** Build the full stream config by merging defaults with user overrides. */
1503
1924
  buildConfig(kind) {
1504
1925
  const name = this.getStreamName(kind);
@@ -1514,6 +1935,26 @@ var StreamProvider = class {
1514
1935
  description
1515
1936
  };
1516
1937
  }
1938
+ /**
1939
+ * Build the stream configuration for the Dead-Letter Queue (DLQ).
1940
+ *
1941
+ * Merges the library default DLQ config with user-provided overrides.
1942
+ * Ensures transport-controlled settings like retention are safely decoupled.
1943
+ */
1944
+ buildDlqConfig() {
1945
+ const name = dlqStreamName(this.options.name);
1946
+ const subjects = [name];
1947
+ const description = `JetStream DLQ stream for ${this.options.name}`;
1948
+ const overrides = this.options.dlq?.stream ?? {};
1949
+ const safeOverrides = this.stripTransportControlled(overrides);
1950
+ return {
1951
+ ...DEFAULT_DLQ_STREAM_CONFIG,
1952
+ ...safeOverrides,
1953
+ name,
1954
+ subjects,
1955
+ description
1956
+ };
1957
+ }
1517
1958
  /** Get default config for a stream kind. */
1518
1959
  getDefaults(kind) {
1519
1960
  switch (kind) {
@@ -1532,25 +1973,44 @@ var StreamProvider = class {
1532
1973
  const overrides = this.getOverrides(kind);
1533
1974
  return overrides.allow_msg_schedules === true;
1534
1975
  }
1535
- /** Get user-provided overrides for a stream kind. */
1976
+ /** Get user-provided overrides for a stream kind, stripping transport-controlled properties. */
1536
1977
  getOverrides(kind) {
1978
+ let overrides;
1537
1979
  switch (kind) {
1538
1980
  case "ev" /* Event */:
1539
- return this.options.events?.stream ?? {};
1981
+ overrides = this.options.events?.stream ?? {};
1982
+ break;
1540
1983
  case "cmd" /* Command */:
1541
- return this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
1984
+ overrides = this.options.rpc?.mode === "jetstream" ? this.options.rpc.stream ?? {} : {};
1985
+ break;
1542
1986
  case "broadcast" /* Broadcast */:
1543
- return this.options.broadcast?.stream ?? {};
1987
+ overrides = this.options.broadcast?.stream ?? {};
1988
+ break;
1544
1989
  case "ordered" /* Ordered */:
1545
- return this.options.ordered?.stream ?? {};
1990
+ overrides = this.options.ordered?.stream ?? {};
1991
+ break;
1546
1992
  }
1993
+ return this.stripTransportControlled(overrides);
1994
+ }
1995
+ /**
1996
+ * Remove transport-controlled properties from user overrides.
1997
+ * `retention` is managed by the transport (Workqueue/Limits per stream kind)
1998
+ * and silently stripped to protect users from misconfiguration.
1999
+ */
2000
+ stripTransportControlled(overrides) {
2001
+ if (!("retention" in overrides)) return overrides;
2002
+ this.logger.debug(
2003
+ "Stripping user-provided retention override \u2014 retention is managed by the transport"
2004
+ );
2005
+ const cleaned = { ...overrides };
2006
+ delete cleaned.retention;
2007
+ return cleaned;
1547
2008
  }
1548
2009
  };
1549
2010
 
1550
2011
  // src/server/infrastructure/consumer.provider.ts
1551
- import { Logger as Logger6 } from "@nestjs/common";
1552
- import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
1553
- var CONSUMER_NOT_FOUND = 10014;
2012
+ import { Logger as Logger7 } from "@nestjs/common";
2013
+ import { JetStreamApiError as JetStreamApiError3 } from "@nats-io/jetstream";
1554
2014
  var ConsumerProvider = class {
1555
2015
  constructor(options, connection, streamProvider, patternRegistry) {
1556
2016
  this.options = options;
@@ -1558,7 +2018,7 @@ var ConsumerProvider = class {
1558
2018
  this.streamProvider = streamProvider;
1559
2019
  this.patternRegistry = patternRegistry;
1560
2020
  }
1561
- logger = new Logger6("Jetstream:Consumer");
2021
+ logger = new Logger7("Jetstream:Consumer");
1562
2022
  /**
1563
2023
  * Ensure consumers exist for the specified kinds.
1564
2024
  *
@@ -1579,7 +2039,11 @@ var ConsumerProvider = class {
1579
2039
  getConsumerName(kind) {
1580
2040
  return consumerName(this.options.name, kind);
1581
2041
  }
1582
- /** Ensure a single consumer exists, creating if needed. */
2042
+ /**
2043
+ * Ensure a single consumer exists with the desired config.
2044
+ * Used at **startup** — creates or updates the consumer to match
2045
+ * the current pod's configuration.
2046
+ */
1583
2047
  async ensureConsumer(jsm, kind) {
1584
2048
  const stream = this.streamProvider.getStreamName(kind);
1585
2049
  const config = this.buildConfig(kind);
@@ -1590,13 +2054,74 @@ var ConsumerProvider = class {
1590
2054
  this.logger.debug(`Consumer exists, updating: ${name}`);
1591
2055
  return await jsm.consumers.update(stream, name, config);
1592
2056
  } catch (err) {
1593
- if (err instanceof JetStreamApiError2 && err.apiError().err_code === CONSUMER_NOT_FOUND) {
1594
- this.logger.log(`Creating consumer: ${name}`);
1595
- return await jsm.consumers.add(stream, config);
2057
+ if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
2058
+ throw err;
2059
+ }
2060
+ return await this.createConsumer(jsm, stream, name, config);
2061
+ }
2062
+ }
2063
+ /**
2064
+ * Recover a consumer that disappeared during runtime.
2065
+ * Used by **self-healing** — creates if missing, but NEVER updates config.
2066
+ *
2067
+ * If a migration backup stream exists, another pod is mid-migration — we
2068
+ * throw so the self-healing retry loop waits with backoff until migration
2069
+ * completes and the backup is cleaned up.
2070
+ *
2071
+ * This prevents old pods from:
2072
+ * - Overwriting a newer pod's consumer config during rolling updates
2073
+ * - Creating consumers during migration (which would consume and delete
2074
+ * workqueue messages while they're being restored)
2075
+ */
2076
+ async recoverConsumer(jsm, kind) {
2077
+ const stream = this.streamProvider.getStreamName(kind);
2078
+ const config = this.buildConfig(kind);
2079
+ const name = config.durable_name;
2080
+ this.logger.log(`Recovering consumer: ${name} on stream: ${stream}`);
2081
+ await this.assertNoMigrationInProgress(jsm, stream);
2082
+ try {
2083
+ return await jsm.consumers.info(stream, name);
2084
+ } catch (err) {
2085
+ if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
2086
+ throw err;
2087
+ }
2088
+ return await this.createConsumer(jsm, stream, name, config);
2089
+ }
2090
+ }
2091
+ /**
2092
+ * Throw if a migration backup stream exists for this stream.
2093
+ * The self-healing retry loop catches the error and retries with backoff,
2094
+ * naturally waiting until the migrating pod finishes and cleans up the backup.
2095
+ */
2096
+ async assertNoMigrationInProgress(jsm, stream) {
2097
+ const backupName = `${stream}${MIGRATION_BACKUP_SUFFIX}`;
2098
+ try {
2099
+ await jsm.streams.info(backupName);
2100
+ throw new Error(
2101
+ `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
2102
+ );
2103
+ } catch (err) {
2104
+ if (err instanceof JetStreamApiError3 && err.apiError().err_code === 10059 /* StreamNotFound */) {
2105
+ return;
1596
2106
  }
1597
2107
  throw err;
1598
2108
  }
1599
2109
  }
2110
+ /**
2111
+ * Create a consumer, handling the race where another pod creates it first.
2112
+ */
2113
+ async createConsumer(jsm, stream, name, config) {
2114
+ this.logger.log(`Creating consumer: ${name}`);
2115
+ try {
2116
+ return await jsm.consumers.add(stream, config);
2117
+ } catch (addErr) {
2118
+ if (addErr instanceof JetStreamApiError3 && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
2119
+ this.logger.debug(`Consumer ${name} created by another pod, using existing`);
2120
+ return await jsm.consumers.info(stream, name);
2121
+ }
2122
+ throw addErr;
2123
+ }
2124
+ }
1600
2125
  /** Build consumer config by merging defaults with user overrides. */
1601
2126
  // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
1602
2127
  buildConfig(kind) {
@@ -1649,6 +2174,7 @@ var ConsumerProvider = class {
1649
2174
  return DEFAULT_BROADCAST_CONSUMER_CONFIG;
1650
2175
  case "ordered" /* Ordered */:
1651
2176
  throw new Error("Ordered consumers are ephemeral and should not use durable config");
2177
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1652
2178
  default: {
1653
2179
  const _exhaustive = kind;
1654
2180
  throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
@@ -1666,6 +2192,7 @@ var ConsumerProvider = class {
1666
2192
  return this.options.broadcast?.consumer ?? {};
1667
2193
  case "ordered" /* Ordered */:
1668
2194
  throw new Error("Ordered consumers are ephemeral and should not use durable config");
2195
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1669
2196
  default: {
1670
2197
  const _exhaustive = kind;
1671
2198
  throw new Error(`Unexpected StreamKind: ${_exhaustive}`);
@@ -1675,7 +2202,7 @@ var ConsumerProvider = class {
1675
2202
  };
1676
2203
 
1677
2204
  // src/server/infrastructure/message.provider.ts
1678
- import { Logger as Logger7 } from "@nestjs/common";
2205
+ import { Logger as Logger8 } from "@nestjs/common";
1679
2206
  import { DeliverPolicy as DeliverPolicy2 } from "@nats-io/jetstream";
1680
2207
  import {
1681
2208
  catchError,
@@ -1689,12 +2216,13 @@ import {
1689
2216
  timer
1690
2217
  } from "rxjs";
1691
2218
  var MessageProvider = class {
1692
- constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map()) {
2219
+ constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
1693
2220
  this.connection = connection;
1694
2221
  this.eventBus = eventBus;
1695
2222
  this.consumeOptionsMap = consumeOptionsMap;
2223
+ this.consumerRecoveryFn = consumerRecoveryFn;
1696
2224
  }
1697
- logger = new Logger7("Jetstream:Message");
2225
+ logger = new Logger8("Jetstream:Message");
1698
2226
  activeIterators = /* @__PURE__ */ new Set();
1699
2227
  orderedReadyResolve = null;
1700
2228
  orderedReadyReject = null;
@@ -1797,20 +2325,49 @@ var MessageProvider = class {
1797
2325
  /** Single iteration: get consumer -> pull messages -> emit to subject. */
1798
2326
  async consumeOnce(kind, info, target$) {
1799
2327
  const js = this.connection.getJetStreamClient();
1800
- const consumer = await js.consumers.get(info.stream_name, info.name);
2328
+ let consumer;
2329
+ let consumerName2 = info.name;
2330
+ try {
2331
+ consumer = await js.consumers.get(info.stream_name, info.name);
2332
+ } catch (err) {
2333
+ if (this.isConsumerNotFound(err) && this.consumerRecoveryFn) {
2334
+ this.logger.warn(`Consumer ${info.name} not found, recreating...`);
2335
+ const recovered = await this.consumerRecoveryFn(kind);
2336
+ consumerName2 = recovered.name;
2337
+ this.logger.log(`Consumer ${consumerName2} recreated, resuming consumption`);
2338
+ consumer = await js.consumers.get(recovered.stream_name, consumerName2);
2339
+ } else {
2340
+ throw err;
2341
+ }
2342
+ }
1801
2343
  const defaults = { idle_heartbeat: 5e3 };
1802
2344
  const userOptions = this.consumeOptionsMap.get(kind) ?? {};
1803
- const messages = await consumer.consume({ ...defaults, ...userOptions });
1804
- this.activeIterators.add(messages);
1805
- this.monitorConsumerHealth(messages, info.name);
1806
- try {
1807
- for await (const msg of messages) {
2345
+ const messages = await consumer.consume({
2346
+ ...defaults,
2347
+ ...userOptions,
2348
+ callback: (msg) => {
1808
2349
  target$.next(msg);
1809
2350
  }
2351
+ });
2352
+ this.activeIterators.add(messages);
2353
+ this.monitorConsumerHealth(messages, consumerName2);
2354
+ try {
2355
+ await messages.closed();
1810
2356
  } finally {
1811
2357
  this.activeIterators.delete(messages);
1812
2358
  }
1813
2359
  }
2360
+ /**
2361
+ * Detect "consumer not found" errors from `js.consumers.get()`.
2362
+ *
2363
+ * Unlike JetStream Manager calls (which throw `JetStreamApiError`),
2364
+ * the JetStream client's `consumers.get()` throws a plain `Error`
2365
+ * with the error code embedded in the message text.
2366
+ */
2367
+ isConsumerNotFound(err) {
2368
+ if (!(err instanceof Error)) return false;
2369
+ return err.message.includes("consumer not found") || err.message.includes(String(10014 /* ConsumerNotFound */));
2370
+ }
1814
2371
  /** Get the target subject for a consumer kind. */
1815
2372
  getTargetSubject(kind) {
1816
2373
  switch (kind) {
@@ -1822,6 +2379,7 @@ var MessageProvider = class {
1822
2379
  return this.broadcastMessages$;
1823
2380
  case "ordered" /* Ordered */:
1824
2381
  return this.orderedMessages$;
2382
+ /* v8 ignore next 5 -- exhaustive switch guard, unreachable */
1825
2383
  default: {
1826
2384
  const _exhaustive = kind;
1827
2385
  throw new Error(`Unknown stream kind: ${_exhaustive}`);
@@ -1886,11 +2444,16 @@ var MessageProvider = class {
1886
2444
  })
1887
2445
  );
1888
2446
  }
1889
- /** Single iteration: create ordered consumer -> iterate messages. */
2447
+ /** Single iteration: create ordered consumer -> push messages into the subject. */
1890
2448
  async consumeOrderedOnce(streamName2, consumerOpts) {
1891
2449
  const js = this.connection.getJetStreamClient();
1892
2450
  const consumer = await js.consumers.get(streamName2, consumerOpts);
1893
- const messages = await consumer.consume();
2451
+ const orderedMessages$ = this.orderedMessages$;
2452
+ const messages = await consumer.consume({
2453
+ callback: (msg) => {
2454
+ orderedMessages$.next(msg);
2455
+ }
2456
+ });
1894
2457
  if (this.orderedReadyResolve) {
1895
2458
  this.orderedReadyResolve();
1896
2459
  this.orderedReadyResolve = null;
@@ -1898,17 +2461,117 @@ var MessageProvider = class {
1898
2461
  }
1899
2462
  this.activeIterators.add(messages);
1900
2463
  try {
1901
- for await (const msg of messages) {
1902
- this.orderedMessages$.next(msg);
1903
- }
2464
+ await messages.closed();
1904
2465
  } finally {
1905
2466
  this.activeIterators.delete(messages);
1906
2467
  }
1907
2468
  }
1908
2469
  };
1909
2470
 
2471
+ // src/server/infrastructure/metadata.provider.ts
2472
+ import { Logger as Logger9 } from "@nestjs/common";
2473
+ import { Kvm } from "@nats-io/kv";
2474
+ var MetadataProvider = class {
2475
+ constructor(options, connection) {
2476
+ this.connection = connection;
2477
+ this.bucketName = options.metadata?.bucket ?? DEFAULT_METADATA_BUCKET;
2478
+ this.replicas = options.metadata?.replicas ?? DEFAULT_METADATA_REPLICAS;
2479
+ this.ttl = Math.max(options.metadata?.ttl ?? DEFAULT_METADATA_TTL, MIN_METADATA_TTL);
2480
+ }
2481
+ logger = new Logger9("Jetstream:Metadata");
2482
+ bucketName;
2483
+ replicas;
2484
+ ttl;
2485
+ currentEntries;
2486
+ heartbeatTimer;
2487
+ cachedKv;
2488
+ /**
2489
+ * Write handler metadata entries to the KV bucket and start heartbeat.
2490
+ *
2491
+ * Creates the bucket if it doesn't exist (idempotent).
2492
+ * Skips silently when entries map is empty.
2493
+ * Starts a heartbeat interval that refreshes entries every `ttl / 2`
2494
+ * to prevent TTL expiry while the pod is alive.
2495
+ *
2496
+ * Non-critical — errors are logged but do not prevent transport startup.
2497
+ *
2498
+ * @param entries Map of KV key → metadata object.
2499
+ */
2500
+ async publish(entries) {
2501
+ if (entries.size === 0) return;
2502
+ try {
2503
+ const kv = await this.openBucket();
2504
+ await this.writeEntries(kv, entries);
2505
+ this.currentEntries = entries;
2506
+ this.startHeartbeat();
2507
+ this.logger.log(
2508
+ `Published ${entries.size} handler metadata entries to KV bucket "${this.bucketName}"`
2509
+ );
2510
+ } catch (err) {
2511
+ this.logger.error("Failed to publish handler metadata to KV", err);
2512
+ }
2513
+ }
2514
+ /**
2515
+ * Stop the heartbeat timer.
2516
+ *
2517
+ * After this call, entries will expire via TTL once the heartbeat window passes.
2518
+ * Called during transport shutdown (strategy.close()).
2519
+ */
2520
+ destroy() {
2521
+ if (this.heartbeatTimer) {
2522
+ clearInterval(this.heartbeatTimer);
2523
+ this.heartbeatTimer = void 0;
2524
+ }
2525
+ this.currentEntries = void 0;
2526
+ this.cachedKv = void 0;
2527
+ }
2528
+ /** Write entries to KV with per-entry error handling. */
2529
+ async writeEntries(kv, entries) {
2530
+ for (const [key, meta] of entries) {
2531
+ try {
2532
+ await kv.put(key, JSON.stringify(meta));
2533
+ } catch (err) {
2534
+ this.logger.error(`Failed to write metadata entry "${key}"`, err);
2535
+ }
2536
+ }
2537
+ }
2538
+ /** Start heartbeat interval that refreshes entries every ttl/2. */
2539
+ startHeartbeat() {
2540
+ if (this.heartbeatTimer) {
2541
+ clearInterval(this.heartbeatTimer);
2542
+ }
2543
+ const interval = Math.floor(this.ttl / 2);
2544
+ this.heartbeatTimer = setInterval(() => {
2545
+ void this.refreshEntries();
2546
+ }, interval);
2547
+ this.heartbeatTimer.unref();
2548
+ }
2549
+ /** Refresh all current entries in KV (heartbeat tick). */
2550
+ async refreshEntries() {
2551
+ if (!this.currentEntries || this.currentEntries.size === 0) return;
2552
+ try {
2553
+ const kv = await this.openBucket();
2554
+ await this.writeEntries(kv, this.currentEntries);
2555
+ } catch (err) {
2556
+ this.logger.error("Failed to refresh handler metadata in KV", err);
2557
+ }
2558
+ }
2559
+ /** Create or open the KV bucket (cached after first call). */
2560
+ async openBucket() {
2561
+ if (this.cachedKv) return this.cachedKv;
2562
+ const js = this.connection.getJetStreamClient();
2563
+ const kvm = new Kvm(js);
2564
+ this.cachedKv = await kvm.create(this.bucketName, {
2565
+ history: DEFAULT_METADATA_HISTORY,
2566
+ replicas: this.replicas,
2567
+ ttl: this.ttl
2568
+ });
2569
+ return this.cachedKv;
2570
+ }
2571
+ };
2572
+
1910
2573
  // src/server/routing/pattern-registry.ts
1911
- import { Logger as Logger8 } from "@nestjs/common";
2574
+ import { Logger as Logger10 } from "@nestjs/common";
1912
2575
  var HANDLER_LABELS = {
1913
2576
  ["broadcast" /* Broadcast */]: "broadcast" /* Broadcast */,
1914
2577
  ["ordered" /* Ordered */]: "ordered" /* Ordered */,
@@ -1919,7 +2582,7 @@ var PatternRegistry = class {
1919
2582
  constructor(options) {
1920
2583
  this.options = options;
1921
2584
  }
1922
- logger = new Logger8("Jetstream:PatternRegistry");
2585
+ logger = new Logger10("Jetstream:PatternRegistry");
1923
2586
  registry = /* @__PURE__ */ new Map();
1924
2587
  // Cached after registerHandlers() — the registry is immutable from that point
1925
2588
  cachedPatterns = null;
@@ -1927,6 +2590,7 @@ var PatternRegistry = class {
1927
2590
  _hasCommands = false;
1928
2591
  _hasBroadcasts = false;
1929
2592
  _hasOrdered = false;
2593
+ _hasMetadata = false;
1930
2594
  /**
1931
2595
  * Register all handlers from the NestJS strategy.
1932
2596
  *
@@ -1939,6 +2603,7 @@ var PatternRegistry = class {
1939
2603
  const isEvent = handler.isEventHandler ?? false;
1940
2604
  const isBroadcast = !!extras?.broadcast;
1941
2605
  const isOrdered = !!extras?.ordered;
2606
+ const meta = extras?.meta;
1942
2607
  if (isBroadcast && isOrdered) {
1943
2608
  throw new Error(
1944
2609
  `Handler "${pattern}" cannot be both broadcast and ordered. Use one or the other.`
@@ -1955,7 +2620,8 @@ var PatternRegistry = class {
1955
2620
  pattern,
1956
2621
  isEvent: isEvent && !isOrdered,
1957
2622
  isBroadcast,
1958
- isOrdered
2623
+ isOrdered,
2624
+ meta
1959
2625
  });
1960
2626
  this.logger.debug(`Registered ${HANDLER_LABELS[kind]}: ${pattern} -> ${fullSubject}`);
1961
2627
  }
@@ -1964,6 +2630,7 @@ var PatternRegistry = class {
1964
2630
  this._hasCommands = this.cachedPatterns.commands.length > 0;
1965
2631
  this._hasBroadcasts = this.cachedPatterns.broadcasts.length > 0;
1966
2632
  this._hasOrdered = this.cachedPatterns.ordered.length > 0;
2633
+ this._hasMetadata = [...this.registry.values()].some((entry) => entry.meta !== void 0);
1967
2634
  this.logSummary();
1968
2635
  }
1969
2636
  /** Find handler for a full NATS subject. */
@@ -1992,6 +2659,26 @@ var PatternRegistry = class {
1992
2659
  (p) => buildSubject(this.options.name, "ordered" /* Ordered */, p)
1993
2660
  );
1994
2661
  }
2662
+ /** Check if any registered handler has metadata. */
2663
+ hasMetadata() {
2664
+ return this._hasMetadata;
2665
+ }
2666
+ /**
2667
+ * Get handler metadata entries for KV publishing.
2668
+ *
2669
+ * Returns a map of KV key -> metadata object for all handlers that have `meta`.
2670
+ * Key format: `{serviceName}.{kind}.{pattern}`.
2671
+ */
2672
+ getMetadataEntries() {
2673
+ const entries = /* @__PURE__ */ new Map();
2674
+ for (const entry of this.registry.values()) {
2675
+ if (!entry.meta) continue;
2676
+ const kind = this.resolveStreamKind(entry);
2677
+ const key = metadataKey(this.options.name, kind, entry.pattern);
2678
+ entries.set(key, entry.meta);
2679
+ }
2680
+ return entries;
2681
+ }
1995
2682
  /** Get patterns grouped by kind (cached after registration). */
1996
2683
  getPatternsByKind() {
1997
2684
  const patterns = this.cachedPatterns ?? this.buildPatternsByKind();
@@ -2031,6 +2718,12 @@ var PatternRegistry = class {
2031
2718
  }
2032
2719
  return { events, commands, broadcasts, ordered };
2033
2720
  }
2721
+ resolveStreamKind(entry) {
2722
+ if (entry.isBroadcast) return "broadcast" /* Broadcast */;
2723
+ if (entry.isOrdered) return "ordered" /* Ordered */;
2724
+ if (entry.isEvent) return "ev" /* Event */;
2725
+ return "cmd" /* Command */;
2726
+ }
2034
2727
  logSummary() {
2035
2728
  const { events, commands, broadcasts, ordered } = this.getPatternsByKind();
2036
2729
  const parts = [
@@ -2046,10 +2739,10 @@ var PatternRegistry = class {
2046
2739
  };
2047
2740
 
2048
2741
  // src/server/routing/event.router.ts
2049
- import { Logger as Logger9 } from "@nestjs/common";
2050
- import { concatMap, from as from2, mergeMap } from "rxjs";
2742
+ import { Logger as Logger11 } from "@nestjs/common";
2743
+ import { headers as natsHeaders3 } from "@nats-io/transport-node";
2051
2744
  var EventRouter = class {
2052
- constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap) {
2745
+ constructor(messageProvider, patternRegistry, codec, eventBus, deadLetterConfig, processingConfig, ackWaitMap, connection, options) {
2053
2746
  this.messageProvider = messageProvider;
2054
2747
  this.patternRegistry = patternRegistry;
2055
2748
  this.codec = codec;
@@ -2057,8 +2750,10 @@ var EventRouter = class {
2057
2750
  this.deadLetterConfig = deadLetterConfig;
2058
2751
  this.processingConfig = processingConfig;
2059
2752
  this.ackWaitMap = ackWaitMap;
2753
+ this.connection = connection;
2754
+ this.options = options;
2060
2755
  }
2061
- logger = new Logger9("Jetstream:EventRouter");
2756
+ logger = new Logger11("Jetstream:EventRouter");
2062
2757
  subscriptions = [];
2063
2758
  /**
2064
2759
  * Update the max_deliver thresholds from actual NATS consumer configs.
@@ -2083,15 +2778,194 @@ var EventRouter = class {
2083
2778
  }
2084
2779
  this.subscriptions.length = 0;
2085
2780
  }
2086
- /** Subscribe to a message stream and route each message. */
2781
+ /** Subscribe to a message stream and route each message to its handler. */
2087
2782
  subscribeToStream(stream$, kind) {
2088
2783
  const isOrdered = kind === "ordered" /* Ordered */;
2784
+ const patternRegistry = this.patternRegistry;
2785
+ const codec = this.codec;
2786
+ const eventBus = this.eventBus;
2787
+ const logger = this.logger;
2788
+ const deadLetterConfig = this.deadLetterConfig;
2089
2789
  const ackExtensionInterval = isOrdered ? null : resolveAckExtensionInterval(this.getAckExtensionConfig(kind), this.ackWaitMap?.get(kind));
2790
+ const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
2090
2791
  const concurrency = this.getConcurrency(kind);
2091
- const route = (msg) => from2(
2092
- isOrdered ? this.handleOrderedSafe(msg) : this.handleSafe(msg, ackExtensionInterval, kind)
2093
- );
2094
- const subscription = stream$.pipe(isOrdered ? concatMap(route) : mergeMap(route, concurrency)).subscribe();
2792
+ const hasDlqCheck = deadLetterConfig !== void 0;
2793
+ const emitRouted = eventBus.hasHook("messageRouted" /* MessageRouted */);
2794
+ const isDeadLetter = (msg) => {
2795
+ if (!hasDlqCheck) return false;
2796
+ const maxDeliver = deadLetterConfig.maxDeliverByStream?.get(msg.info.stream);
2797
+ if (maxDeliver === void 0 || maxDeliver <= 0) return false;
2798
+ return msg.info.deliveryCount >= maxDeliver;
2799
+ };
2800
+ const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
2801
+ const settleSuccess = (msg, ctx) => {
2802
+ if (ctx.shouldTerminate) msg.term(ctx.terminateReason);
2803
+ else if (ctx.shouldRetry) msg.nak(ctx.retryDelay);
2804
+ else msg.ack();
2805
+ };
2806
+ const settleFailure = async (msg, data, err) => {
2807
+ if (handleDeadLetter !== null && isDeadLetter(msg)) {
2808
+ await handleDeadLetter(msg, data, err);
2809
+ return;
2810
+ }
2811
+ msg.nak();
2812
+ };
2813
+ const resolveEvent = (msg) => {
2814
+ const subject = msg.subject;
2815
+ try {
2816
+ const handler = patternRegistry.getHandler(subject);
2817
+ if (!handler) {
2818
+ msg.term(`No handler for event: ${subject}`);
2819
+ logger.error(`No handler for subject: ${subject}`);
2820
+ return null;
2821
+ }
2822
+ let data;
2823
+ try {
2824
+ data = codec.decode(msg.data);
2825
+ } catch (err) {
2826
+ msg.term("Decode error");
2827
+ logger.error(`Decode error for ${subject}:`, err);
2828
+ return null;
2829
+ }
2830
+ if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
2831
+ return { handler, data };
2832
+ } catch (err) {
2833
+ logger.error(`Unexpected error in ${kind} event router`, err);
2834
+ try {
2835
+ msg.term("Unexpected router error");
2836
+ } catch (termErr) {
2837
+ logger.error(`Failed to terminate message ${subject}:`, termErr);
2838
+ }
2839
+ return null;
2840
+ }
2841
+ };
2842
+ const handleSafe = (msg) => {
2843
+ const resolved = resolveEvent(msg);
2844
+ if (resolved === null) return void 0;
2845
+ const { handler, data } = resolved;
2846
+ const ctx = new RpcContext([msg]);
2847
+ const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
2848
+ let pending;
2849
+ try {
2850
+ pending = unwrapResult(handler(data, ctx));
2851
+ } catch (err) {
2852
+ logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
2853
+ if (stopAckExtension !== null) stopAckExtension();
2854
+ return settleFailure(msg, data, err);
2855
+ }
2856
+ if (!isPromiseLike(pending)) {
2857
+ settleSuccess(msg, ctx);
2858
+ if (stopAckExtension !== null) stopAckExtension();
2859
+ return void 0;
2860
+ }
2861
+ return pending.then(
2862
+ () => {
2863
+ settleSuccess(msg, ctx);
2864
+ if (stopAckExtension !== null) stopAckExtension();
2865
+ },
2866
+ async (err) => {
2867
+ logger.error(`Event handler error (${msg.subject}) in ${kind} router:`, err);
2868
+ try {
2869
+ await settleFailure(msg, data, err);
2870
+ } finally {
2871
+ if (stopAckExtension !== null) stopAckExtension();
2872
+ }
2873
+ }
2874
+ );
2875
+ };
2876
+ const handleOrderedSafe = (msg) => {
2877
+ const subject = msg.subject;
2878
+ let handler;
2879
+ let data;
2880
+ try {
2881
+ handler = patternRegistry.getHandler(subject);
2882
+ if (!handler) {
2883
+ logger.error(`No handler for subject: ${subject}`);
2884
+ return void 0;
2885
+ }
2886
+ try {
2887
+ data = codec.decode(msg.data);
2888
+ } catch (err) {
2889
+ logger.error(`Decode error for ${subject}:`, err);
2890
+ return void 0;
2891
+ }
2892
+ if (emitRouted) eventBus.emitMessageRouted(subject, "event" /* Event */);
2893
+ } catch (err) {
2894
+ logger.error(`Ordered handler error (${subject}):`, err);
2895
+ return void 0;
2896
+ }
2897
+ const ctx = new RpcContext([msg]);
2898
+ const warnIfSettlementAttempted = () => {
2899
+ if (ctx.shouldRetry || ctx.shouldTerminate) {
2900
+ logger.warn(
2901
+ `retry()/terminate() ignored for ordered message ${subject} \u2014 ordered consumers auto-acknowledge`
2902
+ );
2903
+ }
2904
+ };
2905
+ let pending;
2906
+ try {
2907
+ pending = unwrapResult(handler(data, ctx));
2908
+ } catch (err) {
2909
+ logger.error(`Ordered handler error (${subject}):`, err);
2910
+ return void 0;
2911
+ }
2912
+ if (!isPromiseLike(pending)) {
2913
+ warnIfSettlementAttempted();
2914
+ return void 0;
2915
+ }
2916
+ return pending.then(warnIfSettlementAttempted, (err) => {
2917
+ logger.error(`Ordered handler error (${subject}):`, err);
2918
+ });
2919
+ };
2920
+ const route = isOrdered ? handleOrderedSafe : handleSafe;
2921
+ const maxActive = isOrdered ? 1 : concurrency ?? Number.POSITIVE_INFINITY;
2922
+ const backlogWarnThreshold = 1e3;
2923
+ let active = 0;
2924
+ let backlogWarned = false;
2925
+ const backlog = [];
2926
+ const onAsyncDone = () => {
2927
+ active--;
2928
+ drainBacklog();
2929
+ };
2930
+ const drainBacklog = () => {
2931
+ while (active < maxActive) {
2932
+ const next = backlog.shift();
2933
+ if (next === void 0) return;
2934
+ active++;
2935
+ const result = route(next);
2936
+ if (result !== void 0) {
2937
+ void result.finally(onAsyncDone);
2938
+ } else {
2939
+ active--;
2940
+ }
2941
+ }
2942
+ if (backlog.length < backlogWarnThreshold) backlogWarned = false;
2943
+ };
2944
+ const subscription = stream$.subscribe({
2945
+ next: (msg) => {
2946
+ if (active >= maxActive) {
2947
+ backlog.push(msg);
2948
+ if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
2949
+ backlogWarned = true;
2950
+ logger.warn(
2951
+ `${kind} backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
2952
+ );
2953
+ }
2954
+ return;
2955
+ }
2956
+ active++;
2957
+ const result = route(msg);
2958
+ if (result !== void 0) {
2959
+ void result.finally(onAsyncDone);
2960
+ } else {
2961
+ active--;
2962
+ if (backlog.length > 0) drainBacklog();
2963
+ }
2964
+ },
2965
+ error: (err) => {
2966
+ logger.error(`Stream error in ${kind} router`, err);
2967
+ }
2968
+ });
2095
2969
  this.subscriptions.push(subscription);
2096
2970
  }
2097
2971
  getConcurrency(kind) {
@@ -2104,87 +2978,94 @@ var EventRouter = class {
2104
2978
  if (kind === "broadcast" /* Broadcast */) return this.processingConfig?.broadcast?.ackExtension;
2105
2979
  return void 0;
2106
2980
  }
2107
- /** Handle a single event message with error isolation. */
2108
- async handleSafe(msg, ackExtensionInterval, kind) {
2109
- try {
2110
- const resolved = this.decodeMessage(msg);
2111
- if (!resolved) return;
2112
- await this.executeHandler(
2113
- resolved.handler,
2114
- resolved.data,
2115
- resolved.ctx,
2116
- msg,
2117
- ackExtensionInterval
2118
- );
2119
- } catch (err) {
2120
- this.logger.error(`Unexpected error in ${kind} event router`, err);
2981
+ /** Handle a dead letter: invoke callback, then term or nak based on result. */
2982
+ /**
2983
+ * Fallback execution for a dead letter when DLQ is disabled, or when
2984
+ * publishing to the DLQ stream fails (due to network or NATS errors).
2985
+ *
2986
+ * Triggers the user-provided `onDeadLetter` hook for logging/alerting.
2987
+ * On success, terminates the message. On error, leaves it unacknowledged (nak)
2988
+ * so NATS can retry the delivery on the next cycle.
2989
+ */
2990
+ async fallbackToOnDeadLetterCallback(info, msg) {
2991
+ if (!this.deadLetterConfig) {
2992
+ msg.term("Dead letter config unavailable");
2993
+ return;
2121
2994
  }
2122
- }
2123
- /** Handle an ordered message with error isolation. */
2124
- async handleOrderedSafe(msg) {
2125
2995
  try {
2126
- const resolved = this.decodeMessage(msg, true);
2127
- if (!resolved) return;
2128
- await unwrapResult(resolved.handler(resolved.data, resolved.ctx));
2129
- if (resolved.ctx.shouldRetry || resolved.ctx.shouldTerminate) {
2130
- this.logger.warn(
2131
- `retry()/terminate() ignored for ordered message ${msg.subject} \u2014 ordered consumers auto-acknowledge`
2132
- );
2133
- }
2134
- } catch (err) {
2135
- this.logger.error(`Ordered handler error (${msg.subject}):`, err);
2996
+ await this.deadLetterConfig.onDeadLetter(info);
2997
+ msg.term("Dead letter processed via fallback callback");
2998
+ } catch (hookErr) {
2999
+ this.logger.error(
3000
+ `Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
3001
+ hookErr
3002
+ );
3003
+ msg.nak();
2136
3004
  }
2137
3005
  }
2138
- /** Resolve handler, decode payload, and build context. Returns null on failure. */
2139
- decodeMessage(msg, isOrdered = false) {
2140
- const handler = this.patternRegistry.getHandler(msg.subject);
2141
- if (!handler) {
2142
- if (!isOrdered) msg.term(`No handler for event: ${msg.subject}`);
2143
- this.logger.error(`No handler for subject: ${msg.subject}`);
2144
- return null;
3006
+ /**
3007
+ * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
3008
+ *
3009
+ * Appends diagnostic metadata headers to the original message and preserves
3010
+ * the primary payload. If publishing succeeds, it notifies the standard
3011
+ * `onDeadLetter` callback and terminates the message. If it fails, it falls
3012
+ * back to the callback entirely to prevent silent data loss.
3013
+ */
3014
+ async publishToDlq(msg, info, error) {
3015
+ const serviceName = this.options?.name;
3016
+ if (!this.connection || !serviceName) {
3017
+ this.logger.error(
3018
+ `Cannot publish to DLQ for ${msg.subject}: Connection or Module Options unavailable`
3019
+ );
3020
+ await this.fallbackToOnDeadLetterCallback(info, msg);
3021
+ return;
2145
3022
  }
2146
- let data;
2147
- try {
2148
- data = this.codec.decode(msg.data);
2149
- } catch (err) {
2150
- if (!isOrdered) msg.term("Decode error");
2151
- this.logger.error(`Decode error for ${msg.subject}:`, err);
2152
- return null;
3023
+ const destinationSubject = dlqStreamName(serviceName);
3024
+ const hdrs = natsHeaders3();
3025
+ if (msg.headers) {
3026
+ for (const [k, v] of msg.headers) {
3027
+ for (const val of v) {
3028
+ hdrs.append(k, val);
3029
+ }
3030
+ }
2153
3031
  }
2154
- this.eventBus.emitMessageRouted(msg.subject, "event" /* Event */);
2155
- return { handler, data, ctx: new RpcContext([msg]) };
2156
- }
2157
- /** Execute handler, then ack on success or nak/dead-letter on failure. */
2158
- async executeHandler(handler, data, ctx, msg, ackExtensionInterval) {
2159
- const stopAckExtension = startAckExtensionTimer(msg, ackExtensionInterval);
3032
+ let reason = String(error);
3033
+ if (error instanceof Error) {
3034
+ reason = error.message;
3035
+ } else if (typeof error === "object" && error !== null && "message" in error) {
3036
+ reason = String(error.message);
3037
+ }
3038
+ hdrs.set("x-dead-letter-reason" /* DeadLetterReason */, reason);
3039
+ hdrs.set("x-original-subject" /* OriginalSubject */, msg.subject);
3040
+ hdrs.set("x-original-stream" /* OriginalStream */, msg.info.stream);
3041
+ hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
3042
+ hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
2160
3043
  try {
2161
- await unwrapResult(handler(data, ctx));
2162
- if (ctx.shouldTerminate) {
2163
- msg.term(ctx.terminateReason);
2164
- } else if (ctx.shouldRetry) {
2165
- msg.nak(ctx.retryDelay);
2166
- } else {
2167
- msg.ack();
2168
- }
2169
- } catch (err) {
2170
- this.logger.error(`Event handler error (${msg.subject}):`, err);
2171
- if (this.isDeadLetter(msg)) {
2172
- await this.handleDeadLetter(msg, data, err);
2173
- } else {
2174
- msg.nak();
3044
+ const js = this.connection.getJetStreamClient();
3045
+ await js.publish(destinationSubject, msg.data, { headers: hdrs });
3046
+ this.logger.log(`Message sent to DLQ: ${msg.subject}`);
3047
+ if (this.deadLetterConfig?.onDeadLetter) {
3048
+ try {
3049
+ await this.deadLetterConfig.onDeadLetter(info);
3050
+ } catch (hookErr) {
3051
+ this.logger.warn(
3052
+ `onDeadLetter callback failed after successful DLQ publish for ${msg.subject}`,
3053
+ hookErr
3054
+ );
3055
+ }
2175
3056
  }
2176
- } finally {
2177
- stopAckExtension?.();
3057
+ msg.term("Moved to DLQ stream");
3058
+ } catch (publishErr) {
3059
+ this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
3060
+ await this.fallbackToOnDeadLetterCallback(info, msg);
2178
3061
  }
2179
3062
  }
2180
- /** Check if the message has exhausted all delivery attempts. */
2181
- isDeadLetter(msg) {
2182
- if (!this.deadLetterConfig) return false;
2183
- const maxDeliver = this.deadLetterConfig.maxDeliverByStream.get(msg.info.stream);
2184
- if (maxDeliver === void 0 || maxDeliver <= 0) return false;
2185
- return msg.info.deliveryCount >= maxDeliver;
2186
- }
2187
- /** Handle a dead letter: invoke callback, then term or nak based on result. */
3063
+ /**
3064
+ * Orchestrates the handling of a message that has exhausted delivery limits.
3065
+ *
3066
+ * Emits a system event and delegates either to the robust DLQ stream publisher
3067
+ * or directly to the fallback callback based on the active module configuration.
3068
+ */
2188
3069
  async handleDeadLetter(msg, data, error) {
2189
3070
  const info = {
2190
3071
  subject: msg.subject,
@@ -2197,24 +3078,17 @@ var EventRouter = class {
2197
3078
  timestamp: new Date(msg.info.timestampNanos / 1e6).toISOString()
2198
3079
  };
2199
3080
  this.eventBus.emit("deadLetter" /* DeadLetter */, info);
2200
- if (!this.deadLetterConfig) {
2201
- msg.term("Dead letter config unavailable");
2202
- return;
2203
- }
2204
- try {
2205
- await this.deadLetterConfig.onDeadLetter(info);
2206
- msg.term("Dead letter processed");
2207
- } catch (hookErr) {
2208
- this.logger.error(`onDeadLetter callback failed for ${msg.subject}, nak for retry:`, hookErr);
2209
- msg.nak();
3081
+ if (!this.options?.dlq) {
3082
+ await this.fallbackToOnDeadLetterCallback(info, msg);
3083
+ } else {
3084
+ await this.publishToDlq(msg, info, error);
2210
3085
  }
2211
3086
  }
2212
3087
  };
2213
3088
 
2214
3089
  // src/server/routing/rpc.router.ts
2215
- import { Logger as Logger10 } from "@nestjs/common";
3090
+ import { Logger as Logger12 } from "@nestjs/common";
2216
3091
  import { headers } from "@nats-io/transport-node";
2217
- import { from as from3, mergeMap as mergeMap2 } from "rxjs";
2218
3092
  var RpcRouter = class {
2219
3093
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, rpcOptions, ackWaitMap) {
2220
3094
  this.messageProvider = messageProvider;
@@ -2227,7 +3101,7 @@ var RpcRouter = class {
2227
3101
  this.timeout = rpcOptions?.timeout ?? DEFAULT_JETSTREAM_RPC_TIMEOUT;
2228
3102
  this.concurrency = rpcOptions?.concurrency;
2229
3103
  }
2230
- logger = new Logger10("Jetstream:RpcRouter");
3104
+ logger = new Logger12("Jetstream:RpcRouter");
2231
3105
  timeout;
2232
3106
  concurrency;
2233
3107
  resolvedAckExtensionInterval;
@@ -2245,105 +3119,203 @@ var RpcRouter = class {
2245
3119
  /** Start routing command messages to handlers. */
2246
3120
  async start() {
2247
3121
  this.cachedNc = await this.connection.getConnection();
2248
- this.subscription = this.messageProvider.commands$.pipe(mergeMap2((msg) => from3(this.handleSafe(msg)), this.concurrency)).subscribe();
2249
- }
2250
- /** Stop routing and unsubscribe. */
2251
- destroy() {
2252
- this.subscription?.unsubscribe();
2253
- this.subscription = null;
2254
- }
2255
- /** Handle a single RPC command message with error isolation. */
2256
- async handleSafe(msg) {
2257
- try {
2258
- const handler = this.patternRegistry.getHandler(msg.subject);
2259
- if (!handler) {
2260
- msg.term(`No handler for RPC: ${msg.subject}`);
2261
- this.logger.error(`No handler for RPC subject: ${msg.subject}`);
2262
- return;
2263
- }
2264
- const { headers: msgHeaders } = msg;
2265
- const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
2266
- const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
2267
- if (!replyTo || !correlationId) {
2268
- msg.term("Missing required headers (reply-to or correlation-id)");
2269
- this.logger.error(`Missing headers for RPC: ${msg.subject}`);
2270
- return;
2271
- }
2272
- let data;
2273
- try {
2274
- data = this.codec.decode(msg.data);
2275
- } catch (err) {
2276
- msg.term("Decode error");
2277
- this.logger.error(`Decode error for RPC ${msg.subject}:`, err);
2278
- return;
2279
- }
2280
- this.eventBus.emitMessageRouted(msg.subject, "rpc" /* Rpc */);
2281
- await this.executeHandler(handler, data, msg, replyTo, correlationId);
2282
- } catch (err) {
2283
- this.logger.error("Unexpected error in RPC router", err);
2284
- }
2285
- }
2286
- /** Execute handler, publish response, settle message. */
2287
- async executeHandler(handler, data, msg, replyTo, correlationId) {
2288
- const nc = this.cachedNc ?? await this.connection.getConnection();
2289
- const ctx = new RpcContext([msg]);
2290
- let settled = false;
2291
- const stopAckExtension = startAckExtensionTimer(msg, this.ackExtensionInterval);
2292
- const timeoutId = setTimeout(() => {
2293
- if (settled) return;
2294
- settled = true;
2295
- stopAckExtension?.();
2296
- this.logger.error(`RPC timeout (${this.timeout}ms): ${msg.subject}`);
2297
- this.eventBus.emit("rpcTimeout" /* RpcTimeout */, msg.subject, correlationId);
2298
- msg.term("Handler timeout");
2299
- }, this.timeout);
2300
- try {
2301
- const result = await unwrapResult(handler(data, ctx));
2302
- if (settled) return;
2303
- settled = true;
2304
- clearTimeout(timeoutId);
2305
- stopAckExtension?.();
2306
- msg.ack();
3122
+ const nc = this.cachedNc;
3123
+ const patternRegistry = this.patternRegistry;
3124
+ const codec = this.codec;
3125
+ const eventBus = this.eventBus;
3126
+ const logger = this.logger;
3127
+ const timeout = this.timeout;
3128
+ const ackExtensionInterval = this.ackExtensionInterval;
3129
+ const hasAckExtension = ackExtensionInterval !== null && ackExtensionInterval > 0;
3130
+ const maxActive = this.concurrency ?? Number.POSITIVE_INFINITY;
3131
+ const emitRpcTimeout = (subject, correlationId) => {
3132
+ eventBus.emit("rpcTimeout" /* RpcTimeout */, subject, correlationId);
3133
+ };
3134
+ const publishReply = (replyTo, correlationId, payload) => {
2307
3135
  try {
2308
3136
  const hdrs = headers();
2309
3137
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
2310
- nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
3138
+ nc.publish(replyTo, codec.encode(payload), { headers: hdrs });
2311
3139
  } catch (publishErr) {
2312
- this.logger.error(`Failed to publish RPC response for ${msg.subject}`, publishErr);
3140
+ logger.error(`Failed to publish RPC response`, publishErr);
2313
3141
  }
2314
- } catch (err) {
2315
- if (settled) return;
2316
- settled = true;
2317
- clearTimeout(timeoutId);
2318
- stopAckExtension?.();
3142
+ };
3143
+ const publishErrorReply = (replyTo, correlationId, subject, err) => {
2319
3144
  try {
2320
3145
  const hdrs = headers();
2321
3146
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
2322
3147
  hdrs.set("x-error" /* Error */, "true");
2323
- nc.publish(replyTo, this.codec.encode(serializeError(err)), { headers: hdrs });
3148
+ nc.publish(replyTo, codec.encode(serializeError(err)), { headers: hdrs });
2324
3149
  } catch (encodeErr) {
2325
- this.logger.error(`Failed to encode RPC error for ${msg.subject}`, encodeErr);
3150
+ logger.error(`Failed to encode RPC error for ${subject}`, encodeErr);
2326
3151
  }
2327
- msg.term(`Handler error: ${msg.subject}`);
2328
- }
3152
+ };
3153
+ const resolveCommand = (msg) => {
3154
+ const subject = msg.subject;
3155
+ try {
3156
+ const handler = patternRegistry.getHandler(subject);
3157
+ if (!handler) {
3158
+ msg.term(`No handler for RPC: ${subject}`);
3159
+ logger.error(`No handler for RPC subject: ${subject}`);
3160
+ return null;
3161
+ }
3162
+ const msgHeaders = msg.headers;
3163
+ const replyTo = msgHeaders?.get("x-reply-to" /* ReplyTo */);
3164
+ const correlationId = msgHeaders?.get("x-correlation-id" /* CorrelationId */);
3165
+ if (!replyTo || !correlationId) {
3166
+ msg.term("Missing required headers (reply-to or correlation-id)");
3167
+ logger.error(`Missing headers for RPC: ${subject}`);
3168
+ return null;
3169
+ }
3170
+ let data;
3171
+ try {
3172
+ data = codec.decode(msg.data);
3173
+ } catch (err) {
3174
+ msg.term("Decode error");
3175
+ logger.error(`Decode error for RPC ${subject}:`, err);
3176
+ return null;
3177
+ }
3178
+ eventBus.emitMessageRouted(subject, "rpc" /* Rpc */);
3179
+ return { handler, data, replyTo, correlationId };
3180
+ } catch (err) {
3181
+ logger.error("Unexpected error in RPC router", err);
3182
+ try {
3183
+ msg.term("Unexpected router error");
3184
+ } catch (termErr) {
3185
+ logger.error(`Failed to terminate RPC message ${subject}:`, termErr);
3186
+ }
3187
+ return null;
3188
+ }
3189
+ };
3190
+ const handleSafe = (msg) => {
3191
+ const resolved = resolveCommand(msg);
3192
+ if (resolved === null) return void 0;
3193
+ const { handler, data, replyTo, correlationId } = resolved;
3194
+ const subject = msg.subject;
3195
+ const ctx = new RpcContext([msg]);
3196
+ const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
3197
+ let pending;
3198
+ try {
3199
+ pending = unwrapResult(handler(data, ctx));
3200
+ } catch (err) {
3201
+ if (stopAckExtension !== null) stopAckExtension();
3202
+ logger.error(`RPC handler error (${subject}):`, err);
3203
+ publishErrorReply(replyTo, correlationId, subject, err);
3204
+ msg.term(`Handler error: ${subject}`);
3205
+ return void 0;
3206
+ }
3207
+ if (!isPromiseLike(pending)) {
3208
+ if (stopAckExtension !== null) stopAckExtension();
3209
+ msg.ack();
3210
+ publishReply(replyTo, correlationId, pending);
3211
+ return void 0;
3212
+ }
3213
+ let settled = false;
3214
+ const timeoutId = setTimeout(() => {
3215
+ if (settled) return;
3216
+ settled = true;
3217
+ if (stopAckExtension !== null) stopAckExtension();
3218
+ logger.error(`RPC timeout (${timeout}ms): ${subject}`);
3219
+ emitRpcTimeout(subject, correlationId);
3220
+ msg.term("Handler timeout");
3221
+ }, timeout);
3222
+ return pending.then(
3223
+ (result) => {
3224
+ if (settled) return;
3225
+ settled = true;
3226
+ clearTimeout(timeoutId);
3227
+ if (stopAckExtension !== null) stopAckExtension();
3228
+ msg.ack();
3229
+ publishReply(replyTo, correlationId, result);
3230
+ },
3231
+ (err) => {
3232
+ if (settled) return;
3233
+ settled = true;
3234
+ clearTimeout(timeoutId);
3235
+ if (stopAckExtension !== null) stopAckExtension();
3236
+ logger.error(`RPC handler error (${subject}):`, err);
3237
+ publishErrorReply(replyTo, correlationId, subject, err);
3238
+ msg.term(`Handler error: ${subject}`);
3239
+ }
3240
+ );
3241
+ };
3242
+ const backlogWarnThreshold = 1e3;
3243
+ let active = 0;
3244
+ let backlogWarned = false;
3245
+ const backlog = [];
3246
+ const onAsyncDone = () => {
3247
+ active--;
3248
+ drainBacklog();
3249
+ };
3250
+ const drainBacklog = () => {
3251
+ while (active < maxActive) {
3252
+ const next = backlog.shift();
3253
+ if (next === void 0) return;
3254
+ active++;
3255
+ const result = handleSafe(next);
3256
+ if (result !== void 0) {
3257
+ void result.finally(onAsyncDone);
3258
+ } else {
3259
+ active--;
3260
+ }
3261
+ }
3262
+ if (backlog.length < backlogWarnThreshold) backlogWarned = false;
3263
+ };
3264
+ this.subscription = this.messageProvider.commands$.subscribe({
3265
+ next: (msg) => {
3266
+ if (active >= maxActive) {
3267
+ backlog.push(msg);
3268
+ if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
3269
+ backlogWarned = true;
3270
+ logger.warn(
3271
+ `RPC backlog reached ${backlog.length} messages \u2014 consumer may be falling behind`
3272
+ );
3273
+ }
3274
+ return;
3275
+ }
3276
+ active++;
3277
+ const result = handleSafe(msg);
3278
+ if (result !== void 0) {
3279
+ void result.finally(onAsyncDone);
3280
+ } else {
3281
+ active--;
3282
+ if (backlog.length > 0) drainBacklog();
3283
+ }
3284
+ },
3285
+ error: (err) => {
3286
+ logger.error("Stream error in RPC router", err);
3287
+ }
3288
+ });
3289
+ }
3290
+ /** Stop routing and unsubscribe. */
3291
+ destroy() {
3292
+ this.subscription?.unsubscribe();
3293
+ this.subscription = null;
2329
3294
  }
2330
3295
  };
2331
3296
 
2332
3297
  // src/shutdown/shutdown.manager.ts
2333
- import { Logger as Logger11 } from "@nestjs/common";
3298
+ import { Logger as Logger13 } from "@nestjs/common";
2334
3299
  var ShutdownManager = class {
2335
3300
  constructor(connection, eventBus, timeout) {
2336
3301
  this.connection = connection;
2337
3302
  this.eventBus = eventBus;
2338
3303
  this.timeout = timeout;
2339
3304
  }
2340
- logger = new Logger11("Jetstream:Shutdown");
3305
+ logger = new Logger13("Jetstream:Shutdown");
3306
+ shutdownPromise;
2341
3307
  /**
2342
3308
  * Execute the full shutdown sequence.
2343
3309
  *
3310
+ * Idempotent — concurrent or repeated calls return the same promise.
3311
+ *
2344
3312
  * @param strategy Optional stoppable to close (stops consumers and subscriptions).
2345
3313
  */
2346
3314
  async shutdown(strategy) {
3315
+ this.shutdownPromise ??= this.doShutdown(strategy);
3316
+ return this.shutdownPromise;
3317
+ }
3318
+ async doShutdown(strategy) {
2347
3319
  this.eventBus.emit("shutdownStart" /* ShutdownStart */);
2348
3320
  this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
2349
3321
  strategy?.close();
@@ -2478,7 +3450,7 @@ var JetstreamModule = class {
2478
3450
  provide: JETSTREAM_EVENT_BUS,
2479
3451
  inject: [JETSTREAM_OPTIONS],
2480
3452
  useFactory: (options) => {
2481
- const logger = new Logger12("Jetstream:Module");
3453
+ const logger = new Logger14("Jetstream:Module");
2482
3454
  return new EventBus(logger, options.hooks);
2483
3455
  }
2484
3456
  },
@@ -2557,8 +3529,8 @@ var JetstreamModule = class {
2557
3529
  // MessageProvider — pull-based message consumption
2558
3530
  {
2559
3531
  provide: MessageProvider,
2560
- inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS],
2561
- useFactory: (options, connection, eventBus) => {
3532
+ inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION, JETSTREAM_EVENT_BUS, ConsumerProvider],
3533
+ useFactory: (options, connection, eventBus, consumerProvider) => {
2562
3534
  if (options.consumer === false) return null;
2563
3535
  const consumeOptionsMap = /* @__PURE__ */ new Map();
2564
3536
  if (options.events?.consume)
@@ -2568,7 +3540,11 @@ var JetstreamModule = class {
2568
3540
  if (options.rpc?.mode === "jetstream" && options.rpc.consume) {
2569
3541
  consumeOptionsMap.set("cmd" /* Command */, options.rpc.consume);
2570
3542
  }
2571
- return new MessageProvider(connection, eventBus, consumeOptionsMap);
3543
+ const consumerRecoveryFn = consumerProvider ? async (kind) => {
3544
+ const jsm = await connection.getJetStreamManager();
3545
+ return consumerProvider.recoverConsumer(jsm, kind);
3546
+ } : void 0;
3547
+ return new MessageProvider(connection, eventBus, consumeOptionsMap, consumerRecoveryFn);
2572
3548
  }
2573
3549
  },
2574
3550
  // EventRouter — routes event and broadcast messages to handlers
@@ -2580,9 +3556,10 @@ var JetstreamModule = class {
2580
3556
  PatternRegistry,
2581
3557
  JETSTREAM_CODEC,
2582
3558
  JETSTREAM_EVENT_BUS,
2583
- JETSTREAM_ACK_WAIT_MAP
3559
+ JETSTREAM_ACK_WAIT_MAP,
3560
+ JETSTREAM_CONNECTION
2584
3561
  ],
2585
- useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap) => {
3562
+ useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
2586
3563
  if (options.consumer === false) return null;
2587
3564
  const deadLetterConfig = options.onDeadLetter ? {
2588
3565
  maxDeliverByStream: /* @__PURE__ */ new Map(),
@@ -2605,7 +3582,9 @@ var JetstreamModule = class {
2605
3582
  eventBus,
2606
3583
  deadLetterConfig,
2607
3584
  processingConfig,
2608
- ackWaitMap
3585
+ ackWaitMap,
3586
+ connection,
3587
+ options
2609
3588
  );
2610
3589
  }
2611
3590
  },
@@ -2654,6 +3633,15 @@ var JetstreamModule = class {
2654
3633
  return new CoreRpcServer(options, connection, patternRegistry, codec, eventBus);
2655
3634
  }
2656
3635
  },
3636
+ // MetadataProvider — handler metadata KV registry (decoupled from stream/consumer infra)
3637
+ {
3638
+ provide: MetadataProvider,
3639
+ inject: [JETSTREAM_OPTIONS, JETSTREAM_CONNECTION],
3640
+ useFactory: (options, connection) => {
3641
+ if (options.consumer === false) return null;
3642
+ return new MetadataProvider(options, connection);
3643
+ }
3644
+ },
2657
3645
  // JetstreamStrategy — server-side transport (only when consumer enabled)
2658
3646
  {
2659
3647
  provide: JetstreamStrategy,
@@ -2667,9 +3655,10 @@ var JetstreamModule = class {
2667
3655
  EventRouter,
2668
3656
  RpcRouter,
2669
3657
  CoreRpcServer,
2670
- JETSTREAM_ACK_WAIT_MAP
3658
+ JETSTREAM_ACK_WAIT_MAP,
3659
+ MetadataProvider
2671
3660
  ],
2672
- useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap) => {
3661
+ useFactory: (options, connection, patternRegistry, streamProvider, consumerProvider, messageProvider, eventRouter, rpcRouter, coreRpcServer, ackWaitMap, metadataProvider) => {
2673
3662
  if (options.consumer === false) return null;
2674
3663
  return new JetstreamStrategy(
2675
3664
  options,
@@ -2681,7 +3670,8 @@ var JetstreamModule = class {
2681
3670
  eventRouter,
2682
3671
  rpcRouter,
2683
3672
  coreRpcServer,
2684
- ackWaitMap
3673
+ ackWaitMap,
3674
+ metadataProvider
2685
3675
  );
2686
3676
  }
2687
3677
  }
@@ -2748,12 +3738,26 @@ JetstreamModule = __decorateClass([
2748
3738
  __decorateParam(1, Inject(JetstreamStrategy))
2749
3739
  ], JetstreamModule);
2750
3740
  export {
2751
- EventBus,
3741
+ DEFAULT_BROADCAST_CONSUMER_CONFIG,
3742
+ DEFAULT_BROADCAST_STREAM_CONFIG,
3743
+ DEFAULT_COMMAND_CONSUMER_CONFIG,
3744
+ DEFAULT_COMMAND_STREAM_CONFIG,
3745
+ DEFAULT_DLQ_STREAM_CONFIG,
3746
+ DEFAULT_EVENT_CONSUMER_CONFIG,
3747
+ DEFAULT_EVENT_STREAM_CONFIG,
3748
+ DEFAULT_JETSTREAM_RPC_TIMEOUT,
3749
+ DEFAULT_METADATA_BUCKET,
3750
+ DEFAULT_METADATA_HISTORY,
3751
+ DEFAULT_METADATA_REPLICAS,
3752
+ DEFAULT_METADATA_TTL,
3753
+ DEFAULT_ORDERED_STREAM_CONFIG,
3754
+ DEFAULT_RPC_TIMEOUT,
3755
+ DEFAULT_SHUTDOWN_TIMEOUT,
2752
3756
  JETSTREAM_CODEC,
2753
3757
  JETSTREAM_CONNECTION,
2754
- JETSTREAM_EVENT_BUS,
2755
3758
  JETSTREAM_OPTIONS,
2756
3759
  JetstreamClient,
3760
+ JetstreamDlqHeader,
2757
3761
  JetstreamHeader,
2758
3762
  JetstreamHealthIndicator,
2759
3763
  JetstreamModule,
@@ -2761,17 +3765,24 @@ export {
2761
3765
  JetstreamRecordBuilder,
2762
3766
  JetstreamStrategy,
2763
3767
  JsonCodec,
3768
+ MIN_METADATA_TTL,
2764
3769
  MessageKind,
3770
+ MsgpackCodec,
3771
+ NatsErrorCode,
2765
3772
  PatternPrefix,
3773
+ RESERVED_HEADERS,
2766
3774
  RpcContext,
2767
3775
  StreamKind,
2768
3776
  TransportEvent,
3777
+ buildBroadcastSubject,
2769
3778
  buildSubject,
2770
3779
  consumerName,
3780
+ dlqStreamName,
2771
3781
  getClientToken,
2772
3782
  internalName,
2773
3783
  isCoreRpcMode,
2774
3784
  isJetStreamRpcMode,
3785
+ metadataKey,
2775
3786
  streamName,
2776
3787
  toNanos
2777
3788
  };