@horizon-republic/nestjs-jetstream 2.11.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -64,6 +64,7 @@ __export(index_exports, {
64
64
  JetstreamHeader: () => JetstreamHeader,
65
65
  JetstreamHealthIndicator: () => JetstreamHealthIndicator,
66
66
  JetstreamModule: () => JetstreamModule,
67
+ JetstreamProvisioningError: () => JetstreamProvisioningError,
67
68
  JetstreamRecord: () => JetstreamRecord,
68
69
  JetstreamRecordBuilder: () => JetstreamRecordBuilder,
69
70
  JetstreamStrategy: () => JetstreamStrategy,
@@ -210,7 +211,7 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
210
211
  };
211
212
  var DEFAULT_DLQ_STREAM_CONFIG = {
212
213
  ...baseStreamConfig,
213
- retention: import_jetstream.RetentionPolicy.Workqueue,
214
+ retention: import_jetstream.RetentionPolicy.Limits,
214
215
  allow_rollup_hdrs: false,
215
216
  max_consumers: 100,
216
217
  max_msg_size: 10 * MB,
@@ -274,6 +275,7 @@ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
274
275
  "x-reply-to" /* ReplyTo */,
275
276
  "x-error" /* Error */
276
277
  ]);
278
+ var NATS_CONTROL_HEADER_PREFIX = "nats-";
277
279
  var internalName = (name) => `${name}__microservice`;
278
280
  var buildSubject = (serviceName, kind, pattern) => `${internalName(serviceName)}.${kind}.${pattern}`;
279
281
  var buildBroadcastSubject = (pattern) => `broadcast.${pattern}`;
@@ -326,6 +328,9 @@ var ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE = "jetstream.rpc.reply.error.code";
326
328
  var ATTR_JETSTREAM_PROVISIONING_ENTITY = "jetstream.provisioning.entity";
327
329
  var ATTR_JETSTREAM_PROVISIONING_ACTION = "jetstream.provisioning.action";
328
330
  var ATTR_JETSTREAM_PROVISIONING_NAME = "jetstream.provisioning.name";
331
+ var ATTR_JETSTREAM_PROVISIONING_MAX_BYTES = "jetstream.provisioning.max_bytes";
332
+ var ATTR_JETSTREAM_PROVISIONING_NUM_REPLICAS = "jetstream.provisioning.num_replicas";
333
+ var ATTR_JETSTREAM_PROVISIONING_RESERVATION = "jetstream.provisioning.reservation_bytes";
329
334
  var ATTR_JETSTREAM_SELF_HEALING_REASON = "jetstream.self_healing.reason";
330
335
  var ATTR_JETSTREAM_MIGRATION_REASON = "jetstream.migration.reason";
331
336
  var ATTR_JETSTREAM_DEAD_LETTER_REASON = "jetstream.dead_letter.reason";
@@ -593,7 +598,7 @@ var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ct
593
598
 
594
599
  // src/otel/tracer.ts
595
600
  var import_api2 = require("@opentelemetry/api");
596
- var PACKAGE_VERSION = true ? "2.11.1" : "0.0.0";
601
+ var PACKAGE_VERSION = true ? "2.12.0" : "0.0.0";
597
602
  var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
598
603
 
599
604
  // src/otel/carrier.ts
@@ -1182,7 +1187,10 @@ var withProvisioningSpan = (config, ctx, op) => wrapInfra(
1182
1187
  {
1183
1188
  [ATTR_JETSTREAM_PROVISIONING_ENTITY]: ctx.entity,
1184
1189
  [ATTR_JETSTREAM_PROVISIONING_ACTION]: ctx.action,
1185
- [ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name
1190
+ [ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name,
1191
+ [ATTR_JETSTREAM_PROVISIONING_MAX_BYTES]: ctx.maxBytes,
1192
+ [ATTR_JETSTREAM_PROVISIONING_NUM_REPLICAS]: ctx.numReplicas,
1193
+ [ATTR_JETSTREAM_PROVISIONING_RESERVATION]: ctx.reservation
1186
1194
  },
1187
1195
  op
1188
1196
  );
@@ -1358,7 +1366,13 @@ var JetstreamRecordBuilder = class {
1358
1366
  * lockstep. `RESERVED_HEADERS` is defined as an all-lowercase set.
1359
1367
  */
1360
1368
  validateHeaderKey(key) {
1361
- if (RESERVED_HEADERS.has(key.toLowerCase())) {
1369
+ const normalized = key.toLowerCase();
1370
+ if (normalized.startsWith(NATS_CONTROL_HEADER_PREFIX)) {
1371
+ throw new Error(
1372
+ `Header "${key}" is reserved for the NATS server and cannot be set manually. Use setMessageId() for deduplication, ttl() for per-message expiry, and scheduleAt() for delayed delivery.`
1373
+ );
1374
+ }
1375
+ if (RESERVED_HEADERS.has(normalized)) {
1362
1376
  throw new Error(
1363
1377
  `Header "${key}" is reserved by the JetStream transport and cannot be set manually. Reserved headers: ${[...RESERVED_HEADERS].join(", ")}`
1364
1378
  );
@@ -1504,13 +1518,18 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1504
1518
  async dispatchEvent(packet) {
1505
1519
  if (!this.readyForPublish) await this.connect();
1506
1520
  const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
1521
+ const publishKind = detectEventKind(packet.pattern);
1522
+ if (schedule && publishKind === "ordered" /* Ordered */) {
1523
+ throw new Error(
1524
+ `scheduleAt() is not supported for ordered events (pattern: ${packet.pattern}). Scheduled delivery is available for workqueue events and broadcasts.`
1525
+ );
1526
+ }
1507
1527
  const eventSubject = this.buildEventSubject(packet.pattern);
1508
1528
  const publishSubject = schedule ? this.buildScheduleSubject(eventSubject) : eventSubject;
1509
1529
  const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
1510
1530
  const encoded = this.codec.encode(data);
1511
1531
  const effectiveMsgId = messageId ?? import_nuid.nuid.next();
1512
1532
  const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
1513
- const publishKind = detectEventKind(packet.pattern);
1514
1533
  const declaredPattern = declaredEventPattern(packet.pattern);
1515
1534
  const streamKind = eventStreamKind(publishKind);
1516
1535
  const startedAt = performance.now();
@@ -1542,10 +1561,10 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1542
1561
  const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1543
1562
  headers: msgHeaders,
1544
1563
  msgID: effectiveMsgId,
1545
- ttl,
1546
1564
  schedule: {
1547
1565
  specification: schedule.at,
1548
- target: eventSubject
1566
+ target: eventSubject,
1567
+ ttl
1549
1568
  }
1550
1569
  });
1551
1570
  warnIfDuplicate("scheduled", ack2);
@@ -1735,13 +1754,18 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1735
1754
  });
1736
1755
  return;
1737
1756
  }
1738
- await import_api8.context.with(
1757
+ const ack = await import_api8.context.with(
1739
1758
  spanHandle.activeContext,
1740
1759
  () => this.connection.getJetStreamClient().publish(subject, encoded, {
1741
1760
  headers: hdrs,
1742
1761
  msgID: messageId ?? import_nuid.nuid.next()
1743
1762
  })
1744
1763
  );
1764
+ if (ack.duplicate) {
1765
+ throw new Error(
1766
+ `Duplicate RPC publish for ${subject}: the messageId was already used within the stream dedup window, so the reply belongs to the original request`
1767
+ );
1768
+ }
1745
1769
  this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1746
1770
  } catch (err) {
1747
1771
  const existingTimeout = this.pendingTimeouts.get(correlationId);
@@ -1911,13 +1935,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1911
1935
  * uses a separate `_sch` namespace that is NOT matched by any consumer filter.
1912
1936
  * NATS holds the message and publishes it to the target subject after the delay.
1913
1937
  *
1938
+ * A unique per-message suffix is appended because the server stores schedules
1939
+ * as rollup messages — one active schedule per subject (ADR-51). Without it,
1940
+ * concurrent schedules of the same pattern would silently replace each other.
1941
+ *
1914
1942
  * Examples:
1915
- * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder`
1916
- * - `broadcast.config.updated` → `broadcast._sch.config.updated`
1943
+ * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder.<nuid>`
1944
+ * - `broadcast.config.updated` → `broadcast._sch.config.updated.<nuid>`
1917
1945
  */
1918
1946
  buildScheduleSubject(eventSubject) {
1919
1947
  if (eventSubject.startsWith("broadcast.")) {
1920
- return eventSubject.replace("broadcast.", "broadcast._sch.");
1948
+ return `${eventSubject.replace("broadcast.", "broadcast._sch.")}.${import_nuid.nuid.next()}`;
1921
1949
  }
1922
1950
  const targetPrefix = `${internalName(this.targetName)}.`;
1923
1951
  if (!eventSubject.startsWith(targetPrefix)) {
@@ -1929,7 +1957,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1929
1957
  throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
1930
1958
  }
1931
1959
  const pattern = withoutPrefix.slice(dotIndex + 1);
1932
- return `${targetPrefix}_sch.${pattern}`;
1960
+ return `${targetPrefix}_sch.${pattern}.${import_nuid.nuid.next()}`;
1933
1961
  }
1934
1962
  };
1935
1963
 
@@ -1941,6 +1969,7 @@ var JsonCodec = class {
1941
1969
  return encoder.encode(JSON.stringify(data));
1942
1970
  }
1943
1971
  decode(data) {
1972
+ if (data.length === 0) return void 0;
1944
1973
  return JSON.parse(decoder.decode(data));
1945
1974
  }
1946
1975
  };
@@ -1954,6 +1983,7 @@ var MsgpackCodec = class {
1954
1983
  return this.packr.pack(data);
1955
1984
  }
1956
1985
  decode(data) {
1986
+ if (data.length === 0) return void 0;
1957
1987
  return this.packr.unpack(data);
1958
1988
  }
1959
1989
  };
@@ -3024,43 +3054,11 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3024
3054
  * Called by NestJS when `connectMicroservice()` is used, or internally by the module.
3025
3055
  */
3026
3056
  async listen(callback) {
3027
- if (this.started) {
3028
- this.logger.warn("listen() called more than once \u2014 ignoring");
3029
- return;
3030
- }
3031
- this.started = true;
3032
- this.patternRegistry.registerHandlers(this.getHandlers());
3033
- const { streams: streamKinds, durableConsumers: durableKinds } = this.resolveRequiredKinds();
3034
- if (streamKinds.length > 0) {
3035
- await this.streamProvider.ensureStreams(streamKinds);
3036
- if (durableKinds.length > 0) {
3037
- const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
3038
- this.populateAckWaitMap(consumers);
3039
- this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
3040
- this.messageProvider.start(consumers);
3041
- }
3042
- if (this.patternRegistry.hasOrderedHandlers()) {
3043
- const orderedStreamName = this.streamProvider.getStreamName("ordered" /* Ordered */);
3044
- await this.messageProvider.startOrdered(
3045
- orderedStreamName,
3046
- this.patternRegistry.getOrderedSubjects(),
3047
- this.options.ordered
3048
- );
3049
- }
3050
- if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
3051
- this.eventRouter.start();
3052
- }
3053
- if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3054
- await this.rpcRouter.start();
3055
- }
3056
- }
3057
- if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3058
- await this.coreRpcServer.start();
3059
- }
3060
- if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
3061
- await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
3057
+ try {
3058
+ await this.doListen(callback);
3059
+ } catch (err) {
3060
+ callback(err);
3062
3061
  }
3063
- callback();
3064
3062
  }
3065
3063
  /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
3066
3064
  close() {
@@ -3119,6 +3117,33 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3119
3117
  getPatternRegistry() {
3120
3118
  return this.patternRegistry;
3121
3119
  }
3120
+ async doListen(callback) {
3121
+ if (this.started) {
3122
+ this.logger.warn("listen() called more than once \u2014 ignoring");
3123
+ return;
3124
+ }
3125
+ this.started = true;
3126
+ this.patternRegistry.registerHandlers(this.getHandlers());
3127
+ const { streams: streamKinds, durableConsumers: durableKinds } = this.resolveRequiredKinds();
3128
+ if (streamKinds.length > 0) {
3129
+ await this.streamProvider.ensureStreams(streamKinds);
3130
+ let consumers = null;
3131
+ if (durableKinds.length > 0) {
3132
+ consumers = await this.consumerProvider.ensureConsumers(durableKinds);
3133
+ this.populateAckWaitMap(consumers);
3134
+ this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
3135
+ }
3136
+ await this.startRouters();
3137
+ await this.startConsumption(consumers);
3138
+ }
3139
+ if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3140
+ await this.coreRpcServer.start();
3141
+ }
3142
+ if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
3143
+ await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
3144
+ }
3145
+ callback();
3146
+ }
3122
3147
  /** Determine which streams and durable consumers are needed. */
3123
3148
  resolveRequiredKinds() {
3124
3149
  const streams = [];
@@ -3140,7 +3165,29 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3140
3165
  }
3141
3166
  return { streams, durableConsumers };
3142
3167
  }
3143
- /** Populate the shared ack_wait map from actual NATS consumer configs. */
3168
+ /** Subscribe the event and RPC routers to the message subjects. */
3169
+ async startRouters() {
3170
+ if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
3171
+ this.eventRouter.start();
3172
+ }
3173
+ if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3174
+ await this.rpcRouter.start();
3175
+ }
3176
+ }
3177
+ /** Begin durable and ordered consumption; routers must already be subscribed. */
3178
+ async startConsumption(consumers) {
3179
+ if (consumers !== null) {
3180
+ this.messageProvider.start(consumers);
3181
+ }
3182
+ if (this.patternRegistry.hasOrderedHandlers()) {
3183
+ const orderedStreamName = this.streamProvider.getStreamName("ordered" /* Ordered */);
3184
+ await this.messageProvider.startOrdered(
3185
+ orderedStreamName,
3186
+ this.patternRegistry.getOrderedSubjects(),
3187
+ this.options.ordered
3188
+ );
3189
+ }
3190
+ }
3144
3191
  populateAckWaitMap(consumers) {
3145
3192
  for (const [kind, info] of consumers) {
3146
3193
  if (info.config.ack_wait) {
@@ -3379,6 +3426,15 @@ var serializeError = (err) => {
3379
3426
  return err;
3380
3427
  };
3381
3428
 
3429
+ // src/utils/settle-quietly.ts
3430
+ var settleQuietly = (logger5, label, action) => {
3431
+ try {
3432
+ action();
3433
+ } catch (err) {
3434
+ logger5.error(label, err);
3435
+ }
3436
+ };
3437
+
3382
3438
  // src/utils/unwrap-result.ts
3383
3439
  var import_rxjs2 = require("rxjs");
3384
3440
  var unwrapResult = (result) => {
@@ -3544,16 +3600,164 @@ var CoreRpcServer = class {
3544
3600
 
3545
3601
  // src/server/infrastructure/stream.provider.ts
3546
3602
  var import_common14 = require("@nestjs/common");
3547
- var import_jetstream17 = require("@nats-io/jetstream");
3603
+ var import_jetstream19 = require("@nats-io/jetstream");
3548
3604
 
3549
3605
  // src/server/infrastructure/nats-error-codes.ts
3550
3606
  var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
3551
3607
  NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
3552
3608
  NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
3553
3609
  NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
3610
+ NatsErrorCode2[NatsErrorCode2["StorageResourcesExceeded"] = 10047] = "StorageResourcesExceeded";
3611
+ NatsErrorCode2[NatsErrorCode2["NoSuitablePeers"] = 10005] = "NoSuitablePeers";
3554
3612
  return NatsErrorCode2;
3555
3613
  })(NatsErrorCode || {});
3556
3614
 
3615
+ // src/server/infrastructure/provisioning-budget.ts
3616
+ var import_jetstream16 = require("@nats-io/jetstream");
3617
+ var GIB = 1024 ** 3;
3618
+ var fmt = (bytes) => `${(bytes / GIB).toFixed(2)} GiB`;
3619
+ var resolveTierBudget = (info, replicas) => {
3620
+ const tier = info.tiers?.[`R${replicas}`];
3621
+ const limits = tier?.limits ?? info.limits;
3622
+ return {
3623
+ maxStorage: limits?.max_storage ?? 0,
3624
+ reserved: tier?.reserved_storage ?? info.reserved_storage ?? 0,
3625
+ tiered: tier !== void 0
3626
+ };
3627
+ };
3628
+ var groupByReplicas = (reservations) => {
3629
+ const groups = /* @__PURE__ */ new Map();
3630
+ for (const r of reservations) {
3631
+ if (r.storage !== import_jetstream16.StorageType.File) continue;
3632
+ const prev = groups.get(r.numReplicas) ?? 0;
3633
+ groups.set(r.numReplicas, prev + r.maxBytes * r.numReplicas);
3634
+ }
3635
+ return groups;
3636
+ };
3637
+ var assertStorageBudget = async (jsm, serviceName, reservations, logger5) => {
3638
+ try {
3639
+ const info = await jsm.getAccountInfo();
3640
+ const groups = groupByReplicas(reservations);
3641
+ let limitNotSetWarned = false;
3642
+ let okReserved = 0;
3643
+ let anyWarned = false;
3644
+ for (const [replicas, incremental] of groups) {
3645
+ const { maxStorage, reserved, tiered } = resolveTierBudget(info, replicas);
3646
+ const tierNote = tiered ? ` (tier R${replicas})` : "";
3647
+ if (maxStorage <= 0) {
3648
+ if (!limitNotSetWarned) {
3649
+ limitNotSetWarned = true;
3650
+ logger5.warn(
3651
+ `Storage preflight for "${serviceName}": account file-storage limit not set (max_storage=${maxStorage}); the server max_file_store cannot be verified from the client.`
3652
+ );
3653
+ }
3654
+ continue;
3655
+ }
3656
+ const remaining = maxStorage - reserved;
3657
+ if (incremental > remaining) {
3658
+ anyWarned = true;
3659
+ logger5.warn(
3660
+ `Storage preflight for "${serviceName}"${tierNote}: needs ~${fmt(incremental)} but only ~${fmt(remaining)} remains (reserved ${fmt(reserved)} / limit ${fmt(maxStorage)}). Provisioning will likely fail with insufficient storage. Lower max_bytes/num_replicas, or raise the account/server storage limit.`
3661
+ );
3662
+ continue;
3663
+ }
3664
+ okReserved += incremental;
3665
+ }
3666
+ if (!anyWarned && !limitNotSetWarned && okReserved > 0) {
3667
+ logger5.log(
3668
+ `Storage preflight for "${serviceName}" OK: reserving ~${fmt(okReserved)} across file-backed streams within account limits.`
3669
+ );
3670
+ }
3671
+ } catch (err) {
3672
+ logger5.debug(`Storage preflight skipped \u2014 account info unavailable: ${String(err)}`);
3673
+ }
3674
+ };
3675
+
3676
+ // src/server/infrastructure/provisioning-error.ts
3677
+ var REMEDIATION = {
3678
+ [10047 /* StorageResourcesExceeded */]: "Aggregate stream reservation exceeds the server `max_file_store` (or account `max_storage`). Lower `max_bytes`/`num_replicas` for this service, or raise `max_file_store` on the NATS servers.",
3679
+ [10005 /* NoSuitablePeers */]: "Fewer healthy peers than `num_replicas`, or no peer has enough reserved storage headroom. Reduce replicas or add/repair cluster nodes."
3680
+ };
3681
+ var GENERIC_REMEDIATION = "Inspect the NATS server logs and JetStream account limits for the underlying cause.";
3682
+ var JetstreamProvisioningError = class _JetstreamProvisioningError extends Error {
3683
+ entity;
3684
+ target;
3685
+ kind;
3686
+ errCode;
3687
+ errDescription;
3688
+ remediation;
3689
+ maxBytes;
3690
+ numReplicas;
3691
+ reservation;
3692
+ constructor(fields) {
3693
+ const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B \xD7 replicas=${fields.numReplicas}).` : "";
3694
+ super(
3695
+ `JetStream ${fields.entity} provisioning failed for "${fields.target}" (kind=${fields.kind}): ${fields.errDescription} [err_code=${fields.errCode}].${reservationNote} ${fields.remediation}`,
3696
+ { cause: fields.cause }
3697
+ );
3698
+ this.name = "JetstreamProvisioningError";
3699
+ this.entity = fields.entity;
3700
+ this.target = fields.target;
3701
+ this.kind = fields.kind;
3702
+ this.errCode = fields.errCode;
3703
+ this.errDescription = fields.errDescription;
3704
+ this.remediation = fields.remediation;
3705
+ this.maxBytes = fields.maxBytes;
3706
+ this.numReplicas = fields.numReplicas;
3707
+ this.reservation = fields.reservation;
3708
+ Object.setPrototypeOf(this, _JetstreamProvisioningError.prototype);
3709
+ }
3710
+ };
3711
+ var mapProvisioningError = (err, ctx) => {
3712
+ const api = err.apiError();
3713
+ const remediation = REMEDIATION[api.err_code] ?? GENERIC_REMEDIATION;
3714
+ const reservation = ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0;
3715
+ return new JetstreamProvisioningError({
3716
+ entity: ctx.entity,
3717
+ target: ctx.name,
3718
+ kind: ctx.kind,
3719
+ errCode: api.err_code,
3720
+ errDescription: api.description,
3721
+ remediation,
3722
+ maxBytes: ctx.maxBytes,
3723
+ numReplicas: ctx.numReplicas,
3724
+ reservation,
3725
+ cause: err
3726
+ });
3727
+ };
3728
+
3729
+ // src/server/infrastructure/provisioning-summary.ts
3730
+ var import_jetstream17 = require("@nats-io/jetstream");
3731
+ var GIB2 = 1024 ** 3;
3732
+ var NANOS_PER_SECOND = 1e9;
3733
+ var NANOS_PER_HOUR = 3600 * NANOS_PER_SECOND;
3734
+ var NANOS_PER_DAY = 86400 * NANOS_PER_SECOND;
3735
+ var formatBytes = (bytes) => {
3736
+ if (bytes <= 0) return "0 B";
3737
+ return `${(bytes / GIB2).toFixed(2)} GiB`;
3738
+ };
3739
+ var formatAge = (nanos) => {
3740
+ if (nanos <= 0) return "unlimited";
3741
+ if (nanos >= NANOS_PER_DAY) return `${(nanos / NANOS_PER_DAY).toFixed(1)}d`;
3742
+ if (nanos >= NANOS_PER_HOUR) return `${(nanos / NANOS_PER_HOUR).toFixed(1)}h`;
3743
+ return `${(nanos / NANOS_PER_SECOND).toFixed(0)}s`;
3744
+ };
3745
+ var formatProvisioningSummary = (serviceName, reservations) => {
3746
+ const lines = [`Provisioning ${reservations.length} stream(s) for "${serviceName}":`];
3747
+ let totalFileMaxBytes = 0;
3748
+ for (const r of reservations) {
3749
+ if (r.storage === import_jetstream17.StorageType.File) totalFileMaxBytes += r.maxBytes;
3750
+ const clusterReservation = r.maxBytes * r.numReplicas;
3751
+ lines.push(
3752
+ ` \u2022 ${r.name} [${r.kind}] storage=${r.storage} replicas=${r.numReplicas} max_bytes=${formatBytes(r.maxBytes)} max_age=${formatAge(r.maxAge)} retention=${r.retention} \u2192 cluster reservation ${formatBytes(clusterReservation)}`
3753
+ );
3754
+ }
3755
+ lines.push(
3756
+ ` \u03A3 per-node file-backed footprint \u2248 ${formatBytes(totalFileMaxBytes)} (sum of max_bytes; worst case replicas = nodes). Ensure the NATS server max_file_store accommodates the sum across ALL services.`
3757
+ );
3758
+ return lines.join("\n");
3759
+ };
3760
+
3557
3761
  // src/server/infrastructure/stream-config-diff.ts
3558
3762
  var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
3559
3763
  "retention"
@@ -3611,85 +3815,199 @@ var isEqual = (a, b) => {
3611
3815
 
3612
3816
  // src/server/infrastructure/stream-migration.ts
3613
3817
  var import_common13 = require("@nestjs/common");
3614
- var import_jetstream16 = require("@nats-io/jetstream");
3818
+ var import_jetstream18 = require("@nats-io/jetstream");
3615
3819
  var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
3616
3820
  var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
3617
3821
  var SOURCING_POLL_INTERVAL_MS = 100;
3822
+ var DEFAULT_PEER_WAIT_MS = 6e4;
3823
+ var ACTIVE_MIGRATION_GRACE_MS = 9e4;
3824
+ var MIGRATION_STARTED_AT_KEY = "nestjs-jetstream-migration-started-at";
3618
3825
  var StreamMigration = class {
3619
- constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
3826
+ constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS, peerWaitMs = DEFAULT_PEER_WAIT_MS) {
3620
3827
  this.sourcingTimeoutMs = sourcingTimeoutMs;
3828
+ this.peerWaitMs = peerWaitMs;
3621
3829
  }
3622
3830
  logger = new import_common13.Logger("Jetstream:Stream");
3623
3831
  async migrate(jsm, streamName2, newConfig) {
3624
3832
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
3625
3833
  const startTime = Date.now();
3834
+ const peerFinished = await this.waitOutPeerMigration(jsm, backupName);
3626
3835
  const currentInfo = await jsm.streams.info(streamName2);
3627
- await this.cleanupOrphanedBackup(jsm, backupName);
3628
- const messageCount = currentInfo.state.messages;
3836
+ if (peerFinished && !compareStreamConfig(currentInfo.config, newConfig).hasImmutableChanges) {
3837
+ this.logger.log(`Stream ${streamName2}: migration completed by another instance`);
3838
+ await jsm.streams.update(streamName2, newConfig);
3839
+ return;
3840
+ }
3629
3841
  this.logger.log(`Stream ${streamName2}: destructive migration started`);
3630
3842
  let originalDeleted = false;
3843
+ let drainedCount = 0;
3631
3844
  try {
3632
- if (messageCount > 0) {
3633
- this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
3845
+ this.logger.log(` Phase 1/4: Quiescing ${streamName2} (publishes rejected during migration)`);
3846
+ await jsm.streams.update(streamName2, { ...currentInfo.config, subjects: [] });
3847
+ drainedCount = (await jsm.streams.info(streamName2)).state.messages;
3848
+ if (drainedCount > 0) {
3849
+ this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages \u2192 ${backupName}`);
3634
3850
  await jsm.streams.add({
3635
3851
  ...currentInfo.config,
3636
3852
  name: backupName,
3637
3853
  subjects: [],
3638
- sources: [{ name: streamName2 }]
3854
+ sources: [{ name: streamName2 }],
3855
+ metadata: { [MIGRATION_STARTED_AT_KEY]: (/* @__PURE__ */ new Date()).toISOString() }
3639
3856
  });
3640
- await this.waitForSourcing(jsm, backupName, messageCount);
3857
+ await this.waitForSourceDrained(jsm, backupName, streamName2, drainedCount);
3641
3858
  }
3642
- this.logger.log(` Phase 2/4: Deleting old stream`);
3859
+ this.logger.log(` Phase 3/4: Recreating ${streamName2} with the new config`);
3643
3860
  await jsm.streams.delete(streamName2);
3644
3861
  originalDeleted = true;
3645
- this.logger.log(` Phase 3/4: Creating stream with new config`);
3646
3862
  await jsm.streams.add(newConfig);
3647
- if (messageCount > 0) {
3648
- const backupInfo = await jsm.streams.info(backupName);
3649
- await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
3650
- this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
3651
- await jsm.streams.update(streamName2, {
3652
- ...newConfig,
3653
- sources: [{ name: backupName }]
3654
- });
3655
- await this.waitForSourcing(jsm, streamName2, messageCount);
3656
- await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
3657
- await jsm.streams.delete(backupName);
3863
+ if (drainedCount > 0) {
3864
+ this.logger.log(` Phase 4/4: Restoring ${drainedCount} messages from backup`);
3865
+ await this.restoreFromBackup(jsm, streamName2, newConfig, backupName);
3658
3866
  }
3659
3867
  } catch (err) {
3660
- if (originalDeleted && messageCount > 0) {
3868
+ if (originalDeleted) {
3661
3869
  this.logger.error(
3662
- `Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
3870
+ `Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved \u2014 restoration resumes on the next startup.`
3663
3871
  );
3664
3872
  } else {
3665
- await this.cleanupOrphanedBackup(jsm, backupName);
3873
+ await this.rollbackBeforeDelete(jsm, streamName2, currentInfo, backupName);
3666
3874
  }
3667
3875
  throw err;
3668
3876
  }
3669
3877
  const durationMs = Date.now() - startTime;
3670
3878
  this.logger.log(
3671
- `Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
3879
+ `Stream ${streamName2}: migration complete (${drainedCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
3880
+ );
3881
+ }
3882
+ /**
3883
+ * Detect and finish a migration that a previous process left unfinished.
3884
+ * Safe against concurrent instances: a backup fresh enough to belong to a
3885
+ * live migration is left alone.
3886
+ *
3887
+ * @returns true when recovery work was performed.
3888
+ */
3889
+ async recoverInterrupted(jsm, streamName2, desiredConfig) {
3890
+ const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
3891
+ const backupInfo = await this.tryInfo(jsm, backupName);
3892
+ if (backupInfo === null) return false;
3893
+ if (this.isPeerMigrationActive(backupInfo)) return false;
3894
+ const streamInfo = await this.tryInfo(jsm, streamName2);
3895
+ if (streamInfo === null) {
3896
+ this.logger.warn(`Stream ${streamName2}: resuming interrupted migration from ${backupName}`);
3897
+ await jsm.streams.add(desiredConfig);
3898
+ if (backupInfo.state.messages > 0) {
3899
+ await this.restoreFromBackup(jsm, streamName2, desiredConfig, backupName);
3900
+ } else {
3901
+ await jsm.streams.delete(backupName);
3902
+ }
3903
+ return true;
3904
+ }
3905
+ const hasBackupSource = (streamInfo.config.sources ?? []).some((s) => s.name === backupName);
3906
+ if (hasBackupSource) {
3907
+ this.logger.warn(`Stream ${streamName2}: finishing interrupted restore from ${backupName}`);
3908
+ await this.waitForSourceDrained(jsm, streamName2, backupName, backupInfo.state.messages);
3909
+ await jsm.streams.delete(backupName);
3910
+ await jsm.streams.update(streamName2, { ...streamInfo.config, sources: [] });
3911
+ return true;
3912
+ }
3913
+ if (backupInfo.state.messages === 0) {
3914
+ this.logger.warn(`Removing empty migration backup ${backupName}`);
3915
+ await jsm.streams.delete(backupName);
3916
+ return true;
3917
+ }
3918
+ this.logger.warn(
3919
+ `Stream ${streamName2}: restoring ${backupInfo.state.messages} messages from stale ${backupName}`
3920
+ );
3921
+ await this.restoreFromBackup(
3922
+ jsm,
3923
+ streamName2,
3924
+ { ...streamInfo.config, name: streamName2, subjects: streamInfo.config.subjects },
3925
+ backupName
3672
3926
  );
3927
+ return true;
3928
+ }
3929
+ /** Attach the backup as a source, drain it fully, then clean up. */
3930
+ async restoreFromBackup(jsm, streamName2, streamConfig, backupName) {
3931
+ const backupInfo = await jsm.streams.info(backupName);
3932
+ if ((backupInfo.config.sources ?? []).length > 0) {
3933
+ await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
3934
+ }
3935
+ await jsm.streams.update(streamName2, { ...streamConfig, sources: [{ name: backupName }] });
3936
+ await this.waitForSourceDrained(jsm, streamName2, backupName, backupInfo.state.messages);
3937
+ await jsm.streams.delete(backupName);
3938
+ await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
3673
3939
  }
3674
- async waitForSourcing(jsm, streamName2, expectedCount) {
3940
+ /**
3941
+ * Wait until `sourceName` is fully drained into `streamName`. Lag-based, so
3942
+ * concurrent live publishes to the target cannot fake completion the way a
3943
+ * bare message-count comparison could. A freshly attached source reports
3944
+ * `lag: 0, active: -1` before its first sync — `active >= 0` filters that
3945
+ * false positive out (verified against NATS 2.12.6).
3946
+ */
3947
+ async waitForSourceDrained(jsm, streamName2, sourceName, minimumMessages) {
3675
3948
  const deadline = Date.now() + this.sourcingTimeoutMs;
3676
3949
  while (Date.now() < deadline) {
3677
3950
  const info = await jsm.streams.info(streamName2);
3678
- if (info.state.messages >= expectedCount) return;
3951
+ const source = (info.sources ?? []).find((s) => s.name === sourceName);
3952
+ if (source !== void 0 && source.active >= 0 && source.lag === 0 && info.state.messages >= minimumMessages) {
3953
+ return;
3954
+ }
3679
3955
  await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
3680
3956
  }
3681
3957
  throw new Error(
3682
- `Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
3958
+ `Stream sourcing timeout: ${sourceName} has not drained into ${streamName2} within ${this.sourcingTimeoutMs / 1e3}s. The backup is preserved; restoration resumes on the next startup.`
3959
+ );
3960
+ }
3961
+ /**
3962
+ * A backup already present when migrate() begins belongs to another
3963
+ * instance migrating right now (rolling deploy) — wait for it to finish.
3964
+ * Stale leftovers are handled by recoverInterrupted() before migrate() runs,
3965
+ * so a timeout here means something is genuinely stuck.
3966
+ *
3967
+ * @returns true when a peer's backup was observed and cleared.
3968
+ */
3969
+ async waitOutPeerMigration(jsm, backupName) {
3970
+ if (await this.tryInfo(jsm, backupName) === null) return false;
3971
+ this.logger.warn(
3972
+ `Migration backup ${backupName} exists \u2014 another instance appears to be migrating; waiting`
3973
+ );
3974
+ const deadline = Date.now() + this.peerWaitMs;
3975
+ while (Date.now() < deadline) {
3976
+ if (await this.tryInfo(jsm, backupName) === null) return true;
3977
+ await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS * 5));
3978
+ }
3979
+ throw new Error(
3980
+ `Migration backup ${backupName} did not clear within ${this.peerWaitMs / 1e3}s. If no other instance is migrating, recover or remove the backup manually.`
3683
3981
  );
3684
3982
  }
3685
- async cleanupOrphanedBackup(jsm, backupName) {
3983
+ /** Failure before the original was deleted: undo the quiesce, drop our backup. */
3984
+ async rollbackBeforeDelete(jsm, streamName2, originalInfo, backupName) {
3686
3985
  try {
3687
- await jsm.streams.info(backupName);
3688
- this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
3689
- await jsm.streams.delete(backupName);
3986
+ await jsm.streams.update(streamName2, { ...originalInfo.config });
3987
+ const backupInfo = await this.tryInfo(jsm, backupName);
3988
+ if (backupInfo !== null) {
3989
+ await jsm.streams.delete(backupName);
3990
+ }
3991
+ } catch (rollbackErr) {
3992
+ this.logger.error(
3993
+ `Rollback of ${streamName2} after a failed migration also failed \u2014 the stream may be left quiesced:`,
3994
+ rollbackErr
3995
+ );
3996
+ }
3997
+ }
3998
+ isPeerMigrationActive(backupInfo) {
3999
+ const startedAt = backupInfo.config.metadata?.[MIGRATION_STARTED_AT_KEY];
4000
+ if (!startedAt) return false;
4001
+ const startedMs = Date.parse(startedAt);
4002
+ if (Number.isNaN(startedMs)) return false;
4003
+ return Date.now() - startedMs < ACTIVE_MIGRATION_GRACE_MS;
4004
+ }
4005
+ async tryInfo(jsm, name) {
4006
+ try {
4007
+ return await jsm.streams.info(name);
3690
4008
  } catch (err) {
3691
- if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3692
- return;
4009
+ if (err instanceof import_jetstream18.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4010
+ return null;
3693
4011
  }
3694
4012
  throw err;
3695
4013
  }
@@ -3720,6 +4038,15 @@ var StreamProvider = class {
3720
4038
  */
3721
4039
  async ensureStreams(kinds) {
3722
4040
  const jsm = await this.connection.getJetStreamManager();
4041
+ const reservations = kinds.map((kind) => this.buildReservation(kind, this.buildConfig(kind)));
4042
+ if (this.options.dlq) {
4043
+ reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
4044
+ }
4045
+ this.logger.log(`
4046
+ ${formatProvisioningSummary(this.options.name, reservations)}`);
4047
+ if (this.options.provisioning?.preflightStorageCheck) {
4048
+ await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
4049
+ }
3723
4050
  await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
3724
4051
  if (this.options.dlq) {
3725
4052
  await this.ensureDlqStream(jsm);
@@ -3756,6 +4083,7 @@ var StreamProvider = class {
3756
4083
  /** Ensure a single stream exists, creating or updating as needed. */
3757
4084
  async ensureStream(jsm, kind) {
3758
4085
  const config = this.buildConfig(kind);
4086
+ const ctx = this.errorContext(kind, config);
3759
4087
  return withProvisioningSpan(
3760
4088
  this.otel,
3761
4089
  {
@@ -3763,17 +4091,21 @@ var StreamProvider = class {
3763
4091
  endpoint: this.otelEndpoint,
3764
4092
  entity: "stream",
3765
4093
  name: config.name,
3766
- action: "ensure"
4094
+ action: "ensure",
4095
+ maxBytes: ctx.maxBytes,
4096
+ numReplicas: ctx.numReplicas,
4097
+ reservation: ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0
3767
4098
  },
3768
4099
  async () => {
3769
4100
  this.logger.log(`Ensuring stream: ${config.name}`);
4101
+ await this.migration.recoverInterrupted(jsm, config.name, config);
3770
4102
  try {
3771
4103
  const currentInfo = await jsm.streams.info(config.name);
3772
- return await this.handleExistingStream(jsm, currentInfo, config);
4104
+ return await this.handleExistingStream(jsm, currentInfo, config, ctx);
3773
4105
  } catch (err) {
3774
- if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4106
+ if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3775
4107
  this.logger.log(`Creating stream: ${config.name}`);
3776
- return await jsm.streams.add(config);
4108
+ return await this.runStreamOp(ctx, () => jsm.streams.add(config));
3777
4109
  }
3778
4110
  throw err;
3779
4111
  }
@@ -3783,6 +4115,7 @@ var StreamProvider = class {
3783
4115
  /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
3784
4116
  async ensureDlqStream(jsm) {
3785
4117
  const config = this.buildDlqConfig();
4118
+ const ctx = this.errorContext("dlq", config);
3786
4119
  return withProvisioningSpan(
3787
4120
  this.otel,
3788
4121
  {
@@ -3790,24 +4123,30 @@ var StreamProvider = class {
3790
4123
  endpoint: this.otelEndpoint,
3791
4124
  entity: "stream",
3792
4125
  name: config.name,
3793
- action: "ensure"
4126
+ action: "ensure",
4127
+ maxBytes: ctx.maxBytes,
4128
+ numReplicas: ctx.numReplicas,
4129
+ reservation: ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0
3794
4130
  },
3795
4131
  async () => {
3796
4132
  this.logger.log(`Ensuring DLQ stream: ${config.name}`);
3797
4133
  try {
3798
4134
  const currentInfo = await jsm.streams.info(config.name);
3799
- return await this.handleExistingStream(jsm, currentInfo, config);
4135
+ return await this.handleExistingStream(jsm, currentInfo, config, ctx);
3800
4136
  } catch (err) {
3801
- if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4137
+ if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3802
4138
  this.logger.log(`Creating DLQ stream: ${config.name}`);
3803
- return await jsm.streams.add(config);
4139
+ return await this.runStreamOp(ctx, () => jsm.streams.add(config));
3804
4140
  }
3805
4141
  throw err;
3806
4142
  }
3807
4143
  }
3808
4144
  );
3809
4145
  }
3810
- async handleExistingStream(jsm, currentInfo, config) {
4146
+ async handleExistingStream(jsm, currentInfo, config, ctx) {
4147
+ if (this.isSharedStream(config.name)) {
4148
+ config.subjects = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
4149
+ }
3811
4150
  const diff = compareStreamConfig(currentInfo.config, config);
3812
4151
  if (!diff.hasChanges) {
3813
4152
  this.logger.debug(`Stream ${config.name}: no config changes`);
@@ -3822,7 +4161,7 @@ var StreamProvider = class {
3822
4161
  }
3823
4162
  if (!diff.hasImmutableChanges) {
3824
4163
  this.logger.debug(`Stream exists, updating: ${config.name}`);
3825
- return await jsm.streams.update(config.name, config);
4164
+ return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, config));
3826
4165
  }
3827
4166
  if (!this.options.allowDestructiveMigration) {
3828
4167
  this.logger.warn(
@@ -3830,10 +4169,15 @@ var StreamProvider = class {
3830
4169
  );
3831
4170
  if (diff.hasMutableChanges) {
3832
4171
  const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
3833
- return await jsm.streams.update(config.name, mutableConfig);
4172
+ return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, mutableConfig));
3834
4173
  }
3835
4174
  return currentInfo;
3836
4175
  }
4176
+ if (this.isSharedStream(config.name)) {
4177
+ throw new Error(
4178
+ `Stream ${config.name} is shared across services and cannot be destructively migrated: recreating it would delete every other service's durable broadcast consumers and replay retained history to them. Coordinate a manual migration instead.`
4179
+ );
4180
+ }
3837
4181
  await withMigrationSpan(
3838
4182
  this.otel,
3839
4183
  {
@@ -3872,11 +4216,47 @@ var StreamProvider = class {
3872
4216
  }
3873
4217
  }
3874
4218
  }
4219
+ buildReservation(kind, config) {
4220
+ const mb = config.max_bytes;
4221
+ return {
4222
+ kind,
4223
+ name: config.name,
4224
+ storage: config.storage ?? import_jetstream19.StorageType.File,
4225
+ numReplicas: config.num_replicas ?? 1,
4226
+ maxBytes: mb !== void 0 && mb >= 0 ? mb : 0,
4227
+ // NATS uses -1 for unlimited
4228
+ maxAge: config.max_age ?? 0,
4229
+ retention: config.retention ?? import_jetstream19.RetentionPolicy.Limits
4230
+ };
4231
+ }
4232
+ errorContext(kind, config) {
4233
+ return {
4234
+ entity: "stream",
4235
+ name: config.name,
4236
+ kind,
4237
+ maxBytes: config.max_bytes,
4238
+ numReplicas: config.num_replicas ?? 1
4239
+ };
4240
+ }
4241
+ async runStreamOp(ctx, op) {
4242
+ try {
4243
+ return await op();
4244
+ } catch (err) {
4245
+ if (err instanceof import_jetstream19.JetStreamApiError) {
4246
+ throw mapProvisioningError(err, ctx);
4247
+ }
4248
+ throw err;
4249
+ }
4250
+ }
4251
+ /** The broadcast stream is global — every service in the cluster shares it. */
4252
+ isSharedStream(name) {
4253
+ return name === this.getStreamName("broadcast" /* Broadcast */);
4254
+ }
3875
4255
  /** Build the full stream config by merging defaults with user overrides. */
3876
4256
  buildConfig(kind) {
3877
4257
  const name = this.getStreamName(kind);
3878
4258
  const subjects = this.getSubjects(kind);
3879
- const description = `JetStream ${kind} stream for ${this.options.name}`;
4259
+ const description = kind === "broadcast" /* Broadcast */ ? "JetStream broadcast stream (shared across services)" : `JetStream ${kind} stream for ${this.options.name}`;
3880
4260
  const defaults = this.getDefaults(kind);
3881
4261
  const overrides = this.getOverrides(kind);
3882
4262
  return {
@@ -3962,7 +4342,7 @@ var StreamProvider = class {
3962
4342
 
3963
4343
  // src/server/infrastructure/consumer.provider.ts
3964
4344
  var import_common15 = require("@nestjs/common");
3965
- var import_jetstream19 = require("@nats-io/jetstream");
4345
+ var import_jetstream21 = require("@nats-io/jetstream");
3966
4346
  var ConsumerProvider = class {
3967
4347
  constructor(options, connection, streamProvider, patternRegistry) {
3968
4348
  this.options = options;
@@ -4018,15 +4398,16 @@ var ConsumerProvider = class {
4018
4398
  },
4019
4399
  async () => {
4020
4400
  this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
4401
+ const ctx = { entity: "consumer", name, kind };
4021
4402
  try {
4022
4403
  await jsm.consumers.info(stream, name);
4023
4404
  this.logger.debug(`Consumer exists, updating: ${name}`);
4024
- return await jsm.consumers.update(stream, name, config);
4405
+ return await this.runConsumerOp(ctx, () => jsm.consumers.update(stream, name, config));
4025
4406
  } catch (err) {
4026
- if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4407
+ if (!(err instanceof import_jetstream21.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4027
4408
  throw err;
4028
4409
  }
4029
- return await this.createConsumer(jsm, stream, name, config);
4410
+ return await this.createConsumer(jsm, stream, name, kind, config);
4030
4411
  }
4031
4412
  }
4032
4413
  );
@@ -4063,10 +4444,10 @@ var ConsumerProvider = class {
4063
4444
  try {
4064
4445
  return await jsm.consumers.info(stream, name);
4065
4446
  } catch (err) {
4066
- if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4447
+ if (!(err instanceof import_jetstream21.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4067
4448
  throw err;
4068
4449
  }
4069
- return await this.createConsumer(jsm, stream, name, config);
4450
+ return await this.createConsumer(jsm, stream, name, kind, config);
4070
4451
  }
4071
4452
  }
4072
4453
  );
@@ -4084,7 +4465,7 @@ var ConsumerProvider = class {
4084
4465
  `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
4085
4466
  );
4086
4467
  } catch (err) {
4087
- if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4468
+ if (err instanceof import_jetstream21.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4088
4469
  return;
4089
4470
  }
4090
4471
  throw err;
@@ -4093,18 +4474,32 @@ var ConsumerProvider = class {
4093
4474
  /**
4094
4475
  * Create a consumer, handling the race where another pod creates it first.
4095
4476
  */
4096
- async createConsumer(jsm, stream, name, config) {
4477
+ async createConsumer(jsm, stream, name, kind, config) {
4097
4478
  this.logger.log(`Creating consumer: ${name}`);
4479
+ const ctx = { entity: "consumer", name, kind };
4098
4480
  try {
4099
4481
  return await jsm.consumers.add(stream, config);
4100
4482
  } catch (addErr) {
4101
- if (addErr instanceof import_jetstream19.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
4483
+ if (addErr instanceof import_jetstream21.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
4102
4484
  this.logger.debug(`Consumer ${name} created by another pod, using existing`);
4103
4485
  return await jsm.consumers.info(stream, name);
4104
4486
  }
4487
+ if (addErr instanceof import_jetstream21.JetStreamApiError) {
4488
+ throw mapProvisioningError(addErr, ctx);
4489
+ }
4105
4490
  throw addErr;
4106
4491
  }
4107
4492
  }
4493
+ async runConsumerOp(ctx, op) {
4494
+ try {
4495
+ return await op();
4496
+ } catch (err) {
4497
+ if (err instanceof import_jetstream21.JetStreamApiError) {
4498
+ throw mapProvisioningError(err, ctx);
4499
+ }
4500
+ throw err;
4501
+ }
4502
+ }
4108
4503
  /** Build consumer config by merging defaults with user overrides. */
4109
4504
  // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
4110
4505
  buildConfig(kind) {
@@ -4186,7 +4581,7 @@ var ConsumerProvider = class {
4186
4581
 
4187
4582
  // src/server/infrastructure/message.provider.ts
4188
4583
  var import_common16 = require("@nestjs/common");
4189
- var import_jetstream21 = require("@nats-io/jetstream");
4584
+ var import_jetstream23 = require("@nats-io/jetstream");
4190
4585
  var import_rxjs3 = require("rxjs");
4191
4586
  var MessageProvider = class {
4192
4587
  constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
@@ -4247,7 +4642,7 @@ var MessageProvider = class {
4247
4642
  */
4248
4643
  async startOrdered(streamName2, filterSubjects, orderedConfig) {
4249
4644
  const consumerOpts = { filter_subjects: filterSubjects };
4250
- if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream21.DeliverPolicy.All) {
4645
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream23.DeliverPolicy.All) {
4251
4646
  consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
4252
4647
  }
4253
4648
  if (orderedConfig?.optStartSeq !== void 0) {
@@ -4556,6 +4951,7 @@ var MetadataProvider = class {
4556
4951
  // src/server/routing/event.router.ts
4557
4952
  var import_common18 = require("@nestjs/common");
4558
4953
  var import_transport_node4 = require("@nats-io/transport-node");
4954
+ var DLQ_PUBLISH_ATTEMPTS = 3;
4559
4955
  var eventConsumeKindFor = (kind) => {
4560
4956
  if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
4561
4957
  if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
@@ -4642,33 +5038,80 @@ var EventRouter = class {
4642
5038
  return msg.info.deliveryCount >= maxDeliver;
4643
5039
  };
4644
5040
  const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
4645
- const settleSuccess = (msg, ctx) => {
4646
- if (ctx.shouldTerminate) msg.term(ctx.terminateReason);
4647
- else if (ctx.shouldRetry) msg.nak(ctx.retryDelay);
4648
- else msg.ack();
5041
+ const settleSuccess = (msg, ctx, data) => {
5042
+ if (ctx.shouldTerminate) {
5043
+ settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
5044
+ msg.term(ctx.terminateReason);
5045
+ });
5046
+ return void 0;
5047
+ }
5048
+ if (ctx.shouldRetry) {
5049
+ if (handleDeadLetter !== null && isDeadLetter(msg)) {
5050
+ return handleDeadLetter(
5051
+ msg,
5052
+ data,
5053
+ new Error("Retry requested on the final delivery attempt")
5054
+ );
5055
+ }
5056
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5057
+ msg.nak(ctx.retryDelay);
5058
+ });
5059
+ return void 0;
5060
+ }
5061
+ settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
5062
+ msg.ack();
5063
+ });
5064
+ return void 0;
4649
5065
  };
4650
5066
  const settleFailure = async (msg, data, err) => {
4651
5067
  if (handleDeadLetter !== null && isDeadLetter(msg)) {
4652
5068
  await handleDeadLetter(msg, data, err);
4653
5069
  return;
4654
5070
  }
4655
- msg.nak();
5071
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5072
+ msg.nak();
5073
+ });
5074
+ };
5075
+ const captureUnroutable = (capture, msg, err) => {
5076
+ let data;
5077
+ try {
5078
+ data = codec.decode(msg.data);
5079
+ } catch {
5080
+ data = void 0;
5081
+ }
5082
+ return capture(msg, data, err).catch((captureErr) => {
5083
+ logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
5084
+ });
4656
5085
  };
4657
5086
  const resolveEvent = (msg) => {
4658
5087
  const subject = msg.subject;
4659
5088
  try {
4660
5089
  const handler = patternRegistry.getHandler(subject);
4661
5090
  if (!handler) {
4662
- msg.term(`No handler for event: ${subject}`);
4663
5091
  logger5.error(`No handler for subject: ${subject}`);
5092
+ if (handleDeadLetter !== null) {
5093
+ return captureUnroutable(
5094
+ handleDeadLetter,
5095
+ msg,
5096
+ new Error(`No handler for event: ${subject}`)
5097
+ );
5098
+ }
5099
+ msg.term(`No handler for event: ${subject}`);
4664
5100
  return null;
4665
5101
  }
4666
5102
  let data;
4667
5103
  try {
4668
5104
  data = codec.decode(msg.data);
4669
5105
  } catch (err) {
4670
- msg.term("Decode error");
4671
5106
  logger5.error(`Decode error for ${subject}:`, err);
5107
+ if (handleDeadLetter !== null) {
5108
+ return captureUnroutable(
5109
+ handleDeadLetter,
5110
+ msg,
5111
+ new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
5112
+ );
5113
+ }
5114
+ msg.term("Decode error");
4672
5115
  return null;
4673
5116
  }
4674
5117
  eventBus.emitMessageRouted(subject, "event" /* Event */);
@@ -4691,6 +5134,7 @@ var EventRouter = class {
4691
5134
  const handleSafe = (msg) => {
4692
5135
  const resolved = resolveEvent(msg);
4693
5136
  if (resolved === null) return void 0;
5137
+ if (isPromiseLike2(resolved)) return resolved;
4694
5138
  const { handler, data } = resolved;
4695
5139
  const ctx = new RpcContext([msg]);
4696
5140
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
@@ -4723,16 +5167,24 @@ var EventRouter = class {
4723
5167
  });
4724
5168
  }
4725
5169
  if (!isPromiseLike2(pending)) {
4726
- settleSuccess(msg, ctx);
5170
+ const settled = settleSuccess(msg, ctx, data);
4727
5171
  reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4728
- if (stopAckExtension !== null) stopAckExtension();
4729
- return void 0;
5172
+ if (settled === void 0) {
5173
+ if (stopAckExtension !== null) stopAckExtension();
5174
+ return void 0;
5175
+ }
5176
+ return settled.finally(() => {
5177
+ if (stopAckExtension !== null) stopAckExtension();
5178
+ });
4730
5179
  }
4731
5180
  return pending.then(
4732
- () => {
4733
- settleSuccess(msg, ctx);
4734
- reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4735
- if (stopAckExtension !== null) stopAckExtension();
5181
+ async () => {
5182
+ try {
5183
+ await settleSuccess(msg, ctx, data);
5184
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5185
+ } finally {
5186
+ if (stopAckExtension !== null) stopAckExtension();
5187
+ }
4736
5188
  },
4737
5189
  async (err) => {
4738
5190
  eventBus.emit(
@@ -4826,14 +5278,28 @@ var EventRouter = class {
4826
5278
  active--;
4827
5279
  drainBacklog();
4828
5280
  };
5281
+ const routeSafely = (msg) => {
5282
+ try {
5283
+ return route(msg);
5284
+ } catch (err) {
5285
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5286
+ return void 0;
5287
+ }
5288
+ };
5289
+ const trackAsync = (result, msg) => {
5290
+ void result.catch((err) => {
5291
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5292
+ }).finally(onAsyncDone);
5293
+ };
4829
5294
  const drainBacklog = () => {
4830
5295
  while (active < maxActive) {
4831
5296
  const next = backlog.shift();
4832
5297
  if (next === void 0) return;
5298
+ next.stopAckExtension?.();
4833
5299
  active++;
4834
- const result = route(next);
5300
+ const result = routeSafely(next.msg);
4835
5301
  if (result !== void 0) {
4836
- void result.finally(onAsyncDone);
5302
+ trackAsync(result, next.msg);
4837
5303
  } else {
4838
5304
  active--;
4839
5305
  }
@@ -4843,7 +5309,10 @@ var EventRouter = class {
4843
5309
  const subscription = stream$.subscribe({
4844
5310
  next: (msg) => {
4845
5311
  if (active >= maxActive) {
4846
- backlog.push(msg);
5312
+ backlog.push({
5313
+ msg,
5314
+ stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5315
+ });
4847
5316
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
4848
5317
  backlogWarned = true;
4849
5318
  logger5.warn(
@@ -4853,9 +5322,9 @@ var EventRouter = class {
4853
5322
  return;
4854
5323
  }
4855
5324
  active++;
4856
- const result = route(msg);
5325
+ const result = routeSafely(msg);
4857
5326
  if (result !== void 0) {
4858
- void result.finally(onAsyncDone);
5327
+ trackAsync(result, msg);
4859
5328
  } else {
4860
5329
  active--;
4861
5330
  if (backlog.length > 0) drainBacklog();
@@ -4865,6 +5334,12 @@ var EventRouter = class {
4865
5334
  logger5.error(`Stream error in ${kind} router`, err);
4866
5335
  }
4867
5336
  });
5337
+ subscription.add(() => {
5338
+ for (const queued of backlog) {
5339
+ queued.stopAckExtension?.();
5340
+ }
5341
+ backlog.length = 0;
5342
+ });
4868
5343
  this.subscriptions.push(subscription);
4869
5344
  }
4870
5345
  getConcurrency(kind) {
@@ -4879,25 +5354,78 @@ var EventRouter = class {
4879
5354
  }
4880
5355
  /**
4881
5356
  * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
4882
- * success or `nak` on hook failure so NATS retries on the next delivery
4883
- * cycle. Used when DLQ stream isn't configured, or when publishing to it
4884
- * failed and we still have to surface the message somewhere observable.
5357
+ * success. On failure the message is nak'd to release it, but the server
5358
+ * never redelivers past `max_deliver` it stays in the stream for manual
5359
+ * recovery. Used when the DLQ stream isn't configured, or when publishing
5360
+ * to it failed and we still have to surface the message somewhere.
4885
5361
  */
4886
5362
  async fallbackToOnDeadLetterCallback(info, msg) {
4887
- if (!this.deadLetterConfig) {
4888
- msg.term("Dead letter config unavailable");
5363
+ const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
5364
+ if (!onDeadLetter) {
5365
+ this.logger.error(
5366
+ `Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
5367
+ );
5368
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5369
+ msg.nak();
5370
+ });
4889
5371
  return;
4890
5372
  }
4891
5373
  try {
4892
- await this.deadLetterConfig.onDeadLetter(info);
4893
- msg.term("Dead letter processed via fallback callback");
5374
+ await onDeadLetter(info);
5375
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5376
+ msg.term("Dead letter processed via fallback callback");
5377
+ });
4894
5378
  } catch (hookErr) {
4895
5379
  this.logger.error(
4896
- `Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
5380
+ `Fallback onDeadLetter callback failed for ${msg.subject} \u2014 the message stays in the stream and will not be redelivered (max_deliver exhausted); recover it manually:`,
4897
5381
  hookErr
4898
5382
  );
4899
- msg.nak();
5383
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5384
+ msg.nak();
5385
+ });
5386
+ }
5387
+ }
5388
+ /**
5389
+ * Copy the original message headers for the DLQ republish, dropping NATS
5390
+ * server control headers: a copied Nats-TTL expires the DLQ entry (or gets
5391
+ * the publish rejected when the DLQ stream has no allow_msg_ttl), a copied
5392
+ * Nats-Msg-Id collides with the DLQ dedup window.
5393
+ */
5394
+ buildDlqHeaders(msg) {
5395
+ const hdrs = (0, import_transport_node4.headers)();
5396
+ if (!msg.headers) return hdrs;
5397
+ for (const [k, v] of msg.headers) {
5398
+ if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
5399
+ for (const val of v) {
5400
+ hdrs.append(k, val);
5401
+ }
5402
+ }
5403
+ return hdrs;
5404
+ }
5405
+ /**
5406
+ * Attempt the DLQ publish up to {@link DLQ_PUBLISH_ATTEMPTS} times.
5407
+ *
5408
+ * Past `max_deliver` the server never redelivers, so an in-process retry is
5409
+ * the only second chance a dead letter gets. There is no artificial delay
5410
+ * between attempts: when the broker is unreachable each publish already
5411
+ * spends its own request timeout, which spaces the attempts naturally.
5412
+ */
5413
+ async publishToDlqWithRetry(connection, subject, data, headers2) {
5414
+ let lastErr;
5415
+ for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
5416
+ try {
5417
+ await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
5418
+ return;
5419
+ } catch (err) {
5420
+ lastErr = err;
5421
+ if (attempt < DLQ_PUBLISH_ATTEMPTS) {
5422
+ this.logger.warn(
5423
+ `DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
5424
+ );
5425
+ }
5426
+ }
4900
5427
  }
5428
+ throw lastErr;
4901
5429
  }
4902
5430
  /**
4903
5431
  * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
@@ -4917,14 +5445,7 @@ var EventRouter = class {
4917
5445
  return;
4918
5446
  }
4919
5447
  const destinationSubject = dlqStreamName(serviceName);
4920
- const hdrs = (0, import_transport_node4.headers)();
4921
- if (msg.headers) {
4922
- for (const [k, v] of msg.headers) {
4923
- for (const val of v) {
4924
- hdrs.append(k, val);
4925
- }
4926
- }
4927
- }
5448
+ const hdrs = this.buildDlqHeaders(msg);
4928
5449
  let reason = String(error);
4929
5450
  if (error instanceof Error) {
4930
5451
  reason = error.message;
@@ -4937,8 +5458,7 @@ var EventRouter = class {
4937
5458
  hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
4938
5459
  hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
4939
5460
  try {
4940
- const js = this.connection.getJetStreamClient();
4941
- await js.publish(destinationSubject, msg.data, { headers: hdrs });
5461
+ await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
4942
5462
  this.logger.log(`Message sent to DLQ: ${msg.subject}`);
4943
5463
  if (this.deadLetterConfig?.onDeadLetter) {
4944
5464
  try {
@@ -4950,7 +5470,9 @@ var EventRouter = class {
4950
5470
  );
4951
5471
  }
4952
5472
  }
4953
- msg.term("Moved to DLQ stream");
5473
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5474
+ msg.term("Moved to DLQ stream");
5475
+ });
4954
5476
  } catch (publishErr) {
4955
5477
  this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
4956
5478
  await this.fallbackToOnDeadLetterCallback(info, msg);
@@ -5144,7 +5666,9 @@ var RpcRouter = class {
5144
5666
  `rpc-handler:${subject}`
5145
5667
  );
5146
5668
  publishErrorReply(replyTo, correlationId, subject, err);
5147
- msg.term(`Handler error: ${subject}`);
5669
+ settleQuietly(logger5, `Failed to term ${subject}:`, () => {
5670
+ msg.term(`Handler error: ${subject}`);
5671
+ });
5148
5672
  };
5149
5673
  const abortController = new AbortController();
5150
5674
  let pending;
@@ -5172,7 +5696,9 @@ var RpcRouter = class {
5172
5696
  }
5173
5697
  if (!isPromiseLike2(pending)) {
5174
5698
  if (stopAckExtension !== null) stopAckExtension();
5175
- msg.ack();
5699
+ settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
5700
+ msg.ack();
5701
+ });
5176
5702
  publishReply(replyTo, correlationId, pending);
5177
5703
  reportHandlerCompleted(msg, startedAt, "success");
5178
5704
  return void 0;
@@ -5184,7 +5710,9 @@ var RpcRouter = class {
5184
5710
  if (stopAckExtension !== null) stopAckExtension();
5185
5711
  abortController.abort();
5186
5712
  emitRpcTimeout(subject, correlationId);
5187
- msg.term("Handler timeout");
5713
+ settleQuietly(logger5, `Failed to term ${subject}:`, () => {
5714
+ msg.term("Handler timeout");
5715
+ });
5188
5716
  reportHandlerCompleted(msg, startedAt, "terminated");
5189
5717
  }, timeout);
5190
5718
  return pending.then(
@@ -5193,7 +5721,9 @@ var RpcRouter = class {
5193
5721
  settled = true;
5194
5722
  clearTimeout(timeoutId);
5195
5723
  if (stopAckExtension !== null) stopAckExtension();
5196
- msg.ack();
5724
+ settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
5725
+ msg.ack();
5726
+ });
5197
5727
  publishReply(replyTo, correlationId, result);
5198
5728
  reportHandlerCompleted(msg, startedAt, "success");
5199
5729
  },
@@ -5215,14 +5745,28 @@ var RpcRouter = class {
5215
5745
  active--;
5216
5746
  drainBacklog();
5217
5747
  };
5748
+ const routeSafely = (msg) => {
5749
+ try {
5750
+ return handleSafe(msg);
5751
+ } catch (err) {
5752
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5753
+ return void 0;
5754
+ }
5755
+ };
5756
+ const trackAsync = (result, msg) => {
5757
+ void result.catch((err) => {
5758
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5759
+ }).finally(onAsyncDone);
5760
+ };
5218
5761
  const drainBacklog = () => {
5219
5762
  while (active < maxActive) {
5220
5763
  const next = backlog.shift();
5221
5764
  if (next === void 0) return;
5765
+ next.stopAckExtension?.();
5222
5766
  active++;
5223
- const result = handleSafe(next);
5767
+ const result = routeSafely(next.msg);
5224
5768
  if (result !== void 0) {
5225
- void result.finally(onAsyncDone);
5769
+ trackAsync(result, next.msg);
5226
5770
  } else {
5227
5771
  active--;
5228
5772
  }
@@ -5232,7 +5776,10 @@ var RpcRouter = class {
5232
5776
  this.subscription = this.messageProvider.commands$.subscribe({
5233
5777
  next: (msg) => {
5234
5778
  if (active >= maxActive) {
5235
- backlog.push(msg);
5779
+ backlog.push({
5780
+ msg,
5781
+ stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5782
+ });
5236
5783
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
5237
5784
  backlogWarned = true;
5238
5785
  logger5.warn(
@@ -5242,9 +5789,9 @@ var RpcRouter = class {
5242
5789
  return;
5243
5790
  }
5244
5791
  active++;
5245
- const result = handleSafe(msg);
5792
+ const result = routeSafely(msg);
5246
5793
  if (result !== void 0) {
5247
- void result.finally(onAsyncDone);
5794
+ trackAsync(result, msg);
5248
5795
  } else {
5249
5796
  active--;
5250
5797
  if (backlog.length > 0) drainBacklog();
@@ -5254,6 +5801,12 @@ var RpcRouter = class {
5254
5801
  logger5.error("Stream error in RPC router", err);
5255
5802
  }
5256
5803
  });
5804
+ this.subscription.add(() => {
5805
+ for (const queued of backlog) {
5806
+ queued.stopAckExtension?.();
5807
+ }
5808
+ backlog.length = 0;
5809
+ });
5257
5810
  }
5258
5811
  /** Stop routing and unsubscribe. */
5259
5812
  destroy() {
@@ -5536,7 +6089,7 @@ var JetstreamModule = class {
5536
6089
  ],
5537
6090
  useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
5538
6091
  if (options.consumer === false) return null;
5539
- const deadLetterConfig = options.onDeadLetter ? {
6092
+ const deadLetterConfig = options.onDeadLetter || options.dlq ? {
5540
6093
  maxDeliverByStream: /* @__PURE__ */ new Map(),
5541
6094
  onDeadLetter: options.onDeadLetter
5542
6095
  } : void 0;
@@ -5737,6 +6290,7 @@ JetstreamModule = __decorateClass([
5737
6290
  JetstreamHeader,
5738
6291
  JetstreamHealthIndicator,
5739
6292
  JetstreamModule,
6293
+ JetstreamProvisioningError,
5740
6294
  JetstreamRecord,
5741
6295
  JetstreamRecordBuilder,
5742
6296
  JetstreamStrategy,