@horizon-republic/nestjs-jetstream 2.11.1 → 2.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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.1" : "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,190 @@ 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
+ * Finish a migration a previous process left unfinished; a backup fresh
3884
+ * enough to belong to a live peer migration is left alone.
3885
+ */
3886
+ async recoverInterrupted(jsm, streamName2, desiredConfig) {
3887
+ const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
3888
+ const backupInfo = await this.tryInfo(jsm, backupName);
3889
+ if (backupInfo === null) return false;
3890
+ if (this.isPeerMigrationActive(backupInfo)) return false;
3891
+ const streamInfo = await this.tryInfo(jsm, streamName2);
3892
+ if (streamInfo === null) {
3893
+ this.logger.warn(`Stream ${streamName2}: resuming interrupted migration from ${backupName}`);
3894
+ await jsm.streams.add(desiredConfig);
3895
+ if (backupInfo.state.messages > 0) {
3896
+ await this.restoreFromBackup(jsm, streamName2, desiredConfig, backupName);
3897
+ } else {
3898
+ await jsm.streams.delete(backupName);
3899
+ }
3900
+ return true;
3901
+ }
3902
+ const hasBackupSource = (streamInfo.config.sources ?? []).some((s) => s.name === backupName);
3903
+ if (hasBackupSource) {
3904
+ this.logger.warn(`Stream ${streamName2}: finishing interrupted restore from ${backupName}`);
3905
+ await this.waitForSourceDrained(jsm, streamName2, backupName, backupInfo.state.messages);
3906
+ await jsm.streams.delete(backupName);
3907
+ await jsm.streams.update(streamName2, { ...streamInfo.config, sources: [] });
3908
+ return true;
3909
+ }
3910
+ if (backupInfo.state.messages === 0) {
3911
+ this.logger.warn(`Removing empty migration backup ${backupName}`);
3912
+ await jsm.streams.delete(backupName);
3913
+ return true;
3914
+ }
3915
+ this.logger.warn(
3916
+ `Stream ${streamName2}: restoring ${backupInfo.state.messages} messages from stale ${backupName}`
3917
+ );
3918
+ await this.restoreFromBackup(
3919
+ jsm,
3920
+ streamName2,
3921
+ { ...streamInfo.config, name: streamName2, subjects: streamInfo.config.subjects },
3922
+ backupName
3672
3923
  );
3924
+ return true;
3925
+ }
3926
+ /** Attach the backup as a source, drain it fully, then clean up. */
3927
+ async restoreFromBackup(jsm, streamName2, streamConfig, backupName) {
3928
+ const backupInfo = await jsm.streams.info(backupName);
3929
+ if ((backupInfo.config.sources ?? []).length > 0) {
3930
+ await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
3931
+ }
3932
+ await jsm.streams.update(streamName2, { ...streamConfig, sources: [{ name: backupName }] });
3933
+ await this.waitForSourceDrained(jsm, streamName2, backupName, backupInfo.state.messages);
3934
+ await jsm.streams.delete(backupName);
3935
+ await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
3673
3936
  }
3674
- async waitForSourcing(jsm, streamName2, expectedCount) {
3937
+ /**
3938
+ * Lag-based drain check — live publishes cannot fake completion. A fresh
3939
+ * source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
3940
+ * hence the active guard.
3941
+ */
3942
+ async waitForSourceDrained(jsm, streamName2, sourceName, minimumMessages) {
3675
3943
  const deadline = Date.now() + this.sourcingTimeoutMs;
3676
3944
  while (Date.now() < deadline) {
3677
3945
  const info = await jsm.streams.info(streamName2);
3678
- if (info.state.messages >= expectedCount) return;
3946
+ const source = (info.sources ?? []).find((s) => s.name === sourceName);
3947
+ if (source !== void 0 && source.active >= 0 && source.lag === 0 && info.state.messages >= minimumMessages) {
3948
+ return;
3949
+ }
3679
3950
  await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
3680
3951
  }
3681
3952
  throw new Error(
3682
- `Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
3953
+ `Stream sourcing timeout: ${sourceName} has not drained into ${streamName2} within ${this.sourcingTimeoutMs / 1e3}s. The backup is preserved; restoration resumes on the next startup.`
3683
3954
  );
3684
3955
  }
3685
- async cleanupOrphanedBackup(jsm, backupName) {
3956
+ /**
3957
+ * A backup present at migrate() start is a live peer migration — wait it
3958
+ * out. Stale leftovers were already handled by recoverInterrupted().
3959
+ */
3960
+ async waitOutPeerMigration(jsm, backupName) {
3961
+ if (await this.tryInfo(jsm, backupName) === null) return false;
3962
+ this.logger.warn(
3963
+ `Migration backup ${backupName} exists \u2014 another instance appears to be migrating; waiting`
3964
+ );
3965
+ const deadline = Date.now() + this.peerWaitMs;
3966
+ while (Date.now() < deadline) {
3967
+ if (await this.tryInfo(jsm, backupName) === null) return true;
3968
+ await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS * 5));
3969
+ }
3970
+ throw new Error(
3971
+ `Migration backup ${backupName} did not clear within ${this.peerWaitMs / 1e3}s. If no other instance is migrating, recover or remove the backup manually.`
3972
+ );
3973
+ }
3974
+ /** Failure before the original was deleted: undo the quiesce, drop our backup. */
3975
+ async rollbackBeforeDelete(jsm, streamName2, originalInfo, backupName) {
3686
3976
  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);
3977
+ await jsm.streams.update(streamName2, { ...originalInfo.config });
3978
+ const backupInfo = await this.tryInfo(jsm, backupName);
3979
+ if (backupInfo !== null) {
3980
+ await jsm.streams.delete(backupName);
3981
+ }
3982
+ } catch (rollbackErr) {
3983
+ this.logger.error(
3984
+ `Rollback of ${streamName2} after a failed migration also failed \u2014 the stream may be left quiesced:`,
3985
+ rollbackErr
3986
+ );
3987
+ }
3988
+ }
3989
+ isPeerMigrationActive(backupInfo) {
3990
+ const startedAt = backupInfo.config.metadata?.[MIGRATION_STARTED_AT_KEY];
3991
+ if (!startedAt) return false;
3992
+ const startedMs = Date.parse(startedAt);
3993
+ if (Number.isNaN(startedMs)) return false;
3994
+ return Date.now() - startedMs < ACTIVE_MIGRATION_GRACE_MS;
3995
+ }
3996
+ async tryInfo(jsm, name) {
3997
+ try {
3998
+ return await jsm.streams.info(name);
3690
3999
  } catch (err) {
3691
- if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3692
- return;
4000
+ if (err instanceof import_jetstream18.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4001
+ return null;
3693
4002
  }
3694
4003
  throw err;
3695
4004
  }
@@ -3697,6 +4006,17 @@ var StreamMigration = class {
3697
4006
  };
3698
4007
 
3699
4008
  // src/server/infrastructure/stream.provider.ts
4009
+ var subjectCovers = (broad, narrow) => {
4010
+ if (broad === narrow) return false;
4011
+ const broadTokens = broad.split(".");
4012
+ const narrowTokens = narrow.split(".");
4013
+ for (let i = 0; i < broadTokens.length; i += 1) {
4014
+ if (broadTokens[i] === ">") return i < narrowTokens.length;
4015
+ if (i >= narrowTokens.length || narrowTokens[i] === ">") return false;
4016
+ if (broadTokens[i] !== "*" && broadTokens[i] !== narrowTokens[i]) return false;
4017
+ }
4018
+ return broadTokens.length === narrowTokens.length;
4019
+ };
3700
4020
  var StreamProvider = class {
3701
4021
  constructor(options, connection) {
3702
4022
  this.options = options;
@@ -3720,6 +4040,15 @@ var StreamProvider = class {
3720
4040
  */
3721
4041
  async ensureStreams(kinds) {
3722
4042
  const jsm = await this.connection.getJetStreamManager();
4043
+ const reservations = kinds.map((kind) => this.buildReservation(kind, this.buildConfig(kind)));
4044
+ if (this.options.dlq) {
4045
+ reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
4046
+ }
4047
+ this.logger.log(`
4048
+ ${formatProvisioningSummary(this.options.name, reservations)}`);
4049
+ if (this.options.provisioning?.preflightStorageCheck) {
4050
+ await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
4051
+ }
3723
4052
  await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
3724
4053
  if (this.options.dlq) {
3725
4054
  await this.ensureDlqStream(jsm);
@@ -3742,13 +4071,8 @@ var StreamProvider = class {
3742
4071
  }
3743
4072
  case "cmd" /* Command */:
3744
4073
  return [`${name}.${"cmd" /* Command */}.>`];
3745
- case "broadcast" /* Broadcast */: {
3746
- const subjects = ["broadcast.>"];
3747
- if (this.isSchedulingEnabled(kind)) {
3748
- subjects.push("broadcast._sch.>");
3749
- }
3750
- return subjects;
3751
- }
4074
+ case "broadcast" /* Broadcast */:
4075
+ return ["broadcast.>"];
3752
4076
  case "ordered" /* Ordered */:
3753
4077
  return [`${name}.${"ordered" /* Ordered */}.>`];
3754
4078
  }
@@ -3756,6 +4080,7 @@ var StreamProvider = class {
3756
4080
  /** Ensure a single stream exists, creating or updating as needed. */
3757
4081
  async ensureStream(jsm, kind) {
3758
4082
  const config = this.buildConfig(kind);
4083
+ const ctx = this.errorContext(kind, config);
3759
4084
  return withProvisioningSpan(
3760
4085
  this.otel,
3761
4086
  {
@@ -3763,17 +4088,21 @@ var StreamProvider = class {
3763
4088
  endpoint: this.otelEndpoint,
3764
4089
  entity: "stream",
3765
4090
  name: config.name,
3766
- action: "ensure"
4091
+ action: "ensure",
4092
+ maxBytes: ctx.maxBytes,
4093
+ numReplicas: ctx.numReplicas,
4094
+ reservation: ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0
3767
4095
  },
3768
4096
  async () => {
3769
4097
  this.logger.log(`Ensuring stream: ${config.name}`);
4098
+ await this.migration.recoverInterrupted(jsm, config.name, config);
3770
4099
  try {
3771
4100
  const currentInfo = await jsm.streams.info(config.name);
3772
- return await this.handleExistingStream(jsm, currentInfo, config);
4101
+ return await this.handleExistingStream(jsm, currentInfo, config, ctx);
3773
4102
  } catch (err) {
3774
- if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4103
+ if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3775
4104
  this.logger.log(`Creating stream: ${config.name}`);
3776
- return await jsm.streams.add(config);
4105
+ return await this.runStreamOp(ctx, () => jsm.streams.add(config));
3777
4106
  }
3778
4107
  throw err;
3779
4108
  }
@@ -3783,6 +4112,7 @@ var StreamProvider = class {
3783
4112
  /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
3784
4113
  async ensureDlqStream(jsm) {
3785
4114
  const config = this.buildDlqConfig();
4115
+ const ctx = this.errorContext("dlq", config);
3786
4116
  return withProvisioningSpan(
3787
4117
  this.otel,
3788
4118
  {
@@ -3790,24 +4120,31 @@ var StreamProvider = class {
3790
4120
  endpoint: this.otelEndpoint,
3791
4121
  entity: "stream",
3792
4122
  name: config.name,
3793
- action: "ensure"
4123
+ action: "ensure",
4124
+ maxBytes: ctx.maxBytes,
4125
+ numReplicas: ctx.numReplicas,
4126
+ reservation: ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0
3794
4127
  },
3795
4128
  async () => {
3796
4129
  this.logger.log(`Ensuring DLQ stream: ${config.name}`);
3797
4130
  try {
3798
4131
  const currentInfo = await jsm.streams.info(config.name);
3799
- return await this.handleExistingStream(jsm, currentInfo, config);
4132
+ return await this.handleExistingStream(jsm, currentInfo, config, ctx);
3800
4133
  } catch (err) {
3801
- if (err instanceof import_jetstream17.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4134
+ if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3802
4135
  this.logger.log(`Creating DLQ stream: ${config.name}`);
3803
- return await jsm.streams.add(config);
4136
+ return await this.runStreamOp(ctx, () => jsm.streams.add(config));
3804
4137
  }
3805
4138
  throw err;
3806
4139
  }
3807
4140
  }
3808
4141
  );
3809
4142
  }
3810
- async handleExistingStream(jsm, currentInfo, config) {
4143
+ async handleExistingStream(jsm, currentInfo, config, ctx) {
4144
+ if (this.isSharedStream(config.name)) {
4145
+ const merged = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
4146
+ config.subjects = merged.filter((s) => !merged.some((other) => subjectCovers(other, s)));
4147
+ }
3811
4148
  const diff = compareStreamConfig(currentInfo.config, config);
3812
4149
  if (!diff.hasChanges) {
3813
4150
  this.logger.debug(`Stream ${config.name}: no config changes`);
@@ -3822,7 +4159,7 @@ var StreamProvider = class {
3822
4159
  }
3823
4160
  if (!diff.hasImmutableChanges) {
3824
4161
  this.logger.debug(`Stream exists, updating: ${config.name}`);
3825
- return await jsm.streams.update(config.name, config);
4162
+ return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, config));
3826
4163
  }
3827
4164
  if (!this.options.allowDestructiveMigration) {
3828
4165
  this.logger.warn(
@@ -3830,10 +4167,15 @@ var StreamProvider = class {
3830
4167
  );
3831
4168
  if (diff.hasMutableChanges) {
3832
4169
  const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
3833
- return await jsm.streams.update(config.name, mutableConfig);
4170
+ return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, mutableConfig));
3834
4171
  }
3835
4172
  return currentInfo;
3836
4173
  }
4174
+ if (this.isSharedStream(config.name)) {
4175
+ throw new Error(
4176
+ `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.`
4177
+ );
4178
+ }
3837
4179
  await withMigrationSpan(
3838
4180
  this.otel,
3839
4181
  {
@@ -3872,11 +4214,47 @@ var StreamProvider = class {
3872
4214
  }
3873
4215
  }
3874
4216
  }
4217
+ buildReservation(kind, config) {
4218
+ const mb = config.max_bytes;
4219
+ return {
4220
+ kind,
4221
+ name: config.name,
4222
+ storage: config.storage ?? import_jetstream19.StorageType.File,
4223
+ numReplicas: config.num_replicas ?? 1,
4224
+ maxBytes: mb !== void 0 && mb >= 0 ? mb : 0,
4225
+ // NATS uses -1 for unlimited
4226
+ maxAge: config.max_age ?? 0,
4227
+ retention: config.retention ?? import_jetstream19.RetentionPolicy.Limits
4228
+ };
4229
+ }
4230
+ errorContext(kind, config) {
4231
+ return {
4232
+ entity: "stream",
4233
+ name: config.name,
4234
+ kind,
4235
+ maxBytes: config.max_bytes,
4236
+ numReplicas: config.num_replicas ?? 1
4237
+ };
4238
+ }
4239
+ async runStreamOp(ctx, op) {
4240
+ try {
4241
+ return await op();
4242
+ } catch (err) {
4243
+ if (err instanceof import_jetstream19.JetStreamApiError) {
4244
+ throw mapProvisioningError(err, ctx);
4245
+ }
4246
+ throw err;
4247
+ }
4248
+ }
4249
+ /** The broadcast stream is global — every service in the cluster shares it. */
4250
+ isSharedStream(name) {
4251
+ return name === this.getStreamName("broadcast" /* Broadcast */);
4252
+ }
3875
4253
  /** Build the full stream config by merging defaults with user overrides. */
3876
4254
  buildConfig(kind) {
3877
4255
  const name = this.getStreamName(kind);
3878
4256
  const subjects = this.getSubjects(kind);
3879
- const description = `JetStream ${kind} stream for ${this.options.name}`;
4257
+ const description = kind === "broadcast" /* Broadcast */ ? "JetStream broadcast stream (shared across services)" : `JetStream ${kind} stream for ${this.options.name}`;
3880
4258
  const defaults = this.getDefaults(kind);
3881
4259
  const overrides = this.getOverrides(kind);
3882
4260
  return {
@@ -3962,7 +4340,7 @@ var StreamProvider = class {
3962
4340
 
3963
4341
  // src/server/infrastructure/consumer.provider.ts
3964
4342
  var import_common15 = require("@nestjs/common");
3965
- var import_jetstream19 = require("@nats-io/jetstream");
4343
+ var import_jetstream21 = require("@nats-io/jetstream");
3966
4344
  var ConsumerProvider = class {
3967
4345
  constructor(options, connection, streamProvider, patternRegistry) {
3968
4346
  this.options = options;
@@ -4018,15 +4396,16 @@ var ConsumerProvider = class {
4018
4396
  },
4019
4397
  async () => {
4020
4398
  this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
4399
+ const ctx = { entity: "consumer", name, kind };
4021
4400
  try {
4022
4401
  await jsm.consumers.info(stream, name);
4023
4402
  this.logger.debug(`Consumer exists, updating: ${name}`);
4024
- return await jsm.consumers.update(stream, name, config);
4403
+ return await this.runConsumerOp(ctx, () => jsm.consumers.update(stream, name, config));
4025
4404
  } catch (err) {
4026
- if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4405
+ if (!(err instanceof import_jetstream21.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4027
4406
  throw err;
4028
4407
  }
4029
- return await this.createConsumer(jsm, stream, name, config);
4408
+ return await this.createConsumer(jsm, stream, name, kind, config);
4030
4409
  }
4031
4410
  }
4032
4411
  );
@@ -4063,10 +4442,10 @@ var ConsumerProvider = class {
4063
4442
  try {
4064
4443
  return await jsm.consumers.info(stream, name);
4065
4444
  } catch (err) {
4066
- if (!(err instanceof import_jetstream19.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4445
+ if (!(err instanceof import_jetstream21.JetStreamApiError) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4067
4446
  throw err;
4068
4447
  }
4069
- return await this.createConsumer(jsm, stream, name, config);
4448
+ return await this.createConsumer(jsm, stream, name, kind, config);
4070
4449
  }
4071
4450
  }
4072
4451
  );
@@ -4084,7 +4463,7 @@ var ConsumerProvider = class {
4084
4463
  `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
4085
4464
  );
4086
4465
  } catch (err) {
4087
- if (err instanceof import_jetstream19.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4466
+ if (err instanceof import_jetstream21.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4088
4467
  return;
4089
4468
  }
4090
4469
  throw err;
@@ -4093,18 +4472,32 @@ var ConsumerProvider = class {
4093
4472
  /**
4094
4473
  * Create a consumer, handling the race where another pod creates it first.
4095
4474
  */
4096
- async createConsumer(jsm, stream, name, config) {
4475
+ async createConsumer(jsm, stream, name, kind, config) {
4097
4476
  this.logger.log(`Creating consumer: ${name}`);
4477
+ const ctx = { entity: "consumer", name, kind };
4098
4478
  try {
4099
4479
  return await jsm.consumers.add(stream, config);
4100
4480
  } catch (addErr) {
4101
- if (addErr instanceof import_jetstream19.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
4481
+ if (addErr instanceof import_jetstream21.JetStreamApiError && addErr.apiError().err_code === 10148 /* ConsumerAlreadyExists */) {
4102
4482
  this.logger.debug(`Consumer ${name} created by another pod, using existing`);
4103
4483
  return await jsm.consumers.info(stream, name);
4104
4484
  }
4485
+ if (addErr instanceof import_jetstream21.JetStreamApiError) {
4486
+ throw mapProvisioningError(addErr, ctx);
4487
+ }
4105
4488
  throw addErr;
4106
4489
  }
4107
4490
  }
4491
+ async runConsumerOp(ctx, op) {
4492
+ try {
4493
+ return await op();
4494
+ } catch (err) {
4495
+ if (err instanceof import_jetstream21.JetStreamApiError) {
4496
+ throw mapProvisioningError(err, ctx);
4497
+ }
4498
+ throw err;
4499
+ }
4500
+ }
4108
4501
  /** Build consumer config by merging defaults with user overrides. */
4109
4502
  // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
4110
4503
  buildConfig(kind) {
@@ -4186,7 +4579,7 @@ var ConsumerProvider = class {
4186
4579
 
4187
4580
  // src/server/infrastructure/message.provider.ts
4188
4581
  var import_common16 = require("@nestjs/common");
4189
- var import_jetstream21 = require("@nats-io/jetstream");
4582
+ var import_jetstream23 = require("@nats-io/jetstream");
4190
4583
  var import_rxjs3 = require("rxjs");
4191
4584
  var MessageProvider = class {
4192
4585
  constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
@@ -4247,7 +4640,7 @@ var MessageProvider = class {
4247
4640
  */
4248
4641
  async startOrdered(streamName2, filterSubjects, orderedConfig) {
4249
4642
  const consumerOpts = { filter_subjects: filterSubjects };
4250
- if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream21.DeliverPolicy.All) {
4643
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream23.DeliverPolicy.All) {
4251
4644
  consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
4252
4645
  }
4253
4646
  if (orderedConfig?.optStartSeq !== void 0) {
@@ -4556,6 +4949,7 @@ var MetadataProvider = class {
4556
4949
  // src/server/routing/event.router.ts
4557
4950
  var import_common18 = require("@nestjs/common");
4558
4951
  var import_transport_node4 = require("@nats-io/transport-node");
4952
+ var DLQ_PUBLISH_ATTEMPTS = 3;
4559
4953
  var eventConsumeKindFor = (kind) => {
4560
4954
  if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
4561
4955
  if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
@@ -4642,33 +5036,80 @@ var EventRouter = class {
4642
5036
  return msg.info.deliveryCount >= maxDeliver;
4643
5037
  };
4644
5038
  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();
5039
+ const settleSuccess = (msg, ctx, data) => {
5040
+ if (ctx.shouldTerminate) {
5041
+ settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
5042
+ msg.term(ctx.terminateReason);
5043
+ });
5044
+ return void 0;
5045
+ }
5046
+ if (ctx.shouldRetry) {
5047
+ if (handleDeadLetter !== null && isDeadLetter(msg)) {
5048
+ return handleDeadLetter(
5049
+ msg,
5050
+ data,
5051
+ new Error("Retry requested on the final delivery attempt")
5052
+ );
5053
+ }
5054
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5055
+ msg.nak(ctx.retryDelay);
5056
+ });
5057
+ return void 0;
5058
+ }
5059
+ settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
5060
+ msg.ack();
5061
+ });
5062
+ return void 0;
4649
5063
  };
4650
5064
  const settleFailure = async (msg, data, err) => {
4651
5065
  if (handleDeadLetter !== null && isDeadLetter(msg)) {
4652
5066
  await handleDeadLetter(msg, data, err);
4653
5067
  return;
4654
5068
  }
4655
- msg.nak();
5069
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5070
+ msg.nak();
5071
+ });
5072
+ };
5073
+ const captureUnroutable = (capture, msg, err) => {
5074
+ let data;
5075
+ try {
5076
+ data = codec.decode(msg.data);
5077
+ } catch {
5078
+ data = void 0;
5079
+ }
5080
+ return capture(msg, data, err).catch((captureErr) => {
5081
+ logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
5082
+ });
4656
5083
  };
4657
5084
  const resolveEvent = (msg) => {
4658
5085
  const subject = msg.subject;
4659
5086
  try {
4660
5087
  const handler = patternRegistry.getHandler(subject);
4661
5088
  if (!handler) {
4662
- msg.term(`No handler for event: ${subject}`);
4663
5089
  logger5.error(`No handler for subject: ${subject}`);
5090
+ if (handleDeadLetter !== null) {
5091
+ return captureUnroutable(
5092
+ handleDeadLetter,
5093
+ msg,
5094
+ new Error(`No handler for event: ${subject}`)
5095
+ );
5096
+ }
5097
+ msg.term(`No handler for event: ${subject}`);
4664
5098
  return null;
4665
5099
  }
4666
5100
  let data;
4667
5101
  try {
4668
5102
  data = codec.decode(msg.data);
4669
5103
  } catch (err) {
4670
- msg.term("Decode error");
4671
5104
  logger5.error(`Decode error for ${subject}:`, err);
5105
+ if (handleDeadLetter !== null) {
5106
+ return captureUnroutable(
5107
+ handleDeadLetter,
5108
+ msg,
5109
+ new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
5110
+ );
5111
+ }
5112
+ msg.term("Decode error");
4672
5113
  return null;
4673
5114
  }
4674
5115
  eventBus.emitMessageRouted(subject, "event" /* Event */);
@@ -4691,6 +5132,7 @@ var EventRouter = class {
4691
5132
  const handleSafe = (msg) => {
4692
5133
  const resolved = resolveEvent(msg);
4693
5134
  if (resolved === null) return void 0;
5135
+ if (isPromiseLike2(resolved)) return resolved;
4694
5136
  const { handler, data } = resolved;
4695
5137
  const ctx = new RpcContext([msg]);
4696
5138
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
@@ -4723,16 +5165,24 @@ var EventRouter = class {
4723
5165
  });
4724
5166
  }
4725
5167
  if (!isPromiseLike2(pending)) {
4726
- settleSuccess(msg, ctx);
5168
+ const settled = settleSuccess(msg, ctx, data);
4727
5169
  reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4728
- if (stopAckExtension !== null) stopAckExtension();
4729
- return void 0;
5170
+ if (settled === void 0) {
5171
+ if (stopAckExtension !== null) stopAckExtension();
5172
+ return void 0;
5173
+ }
5174
+ return settled.finally(() => {
5175
+ if (stopAckExtension !== null) stopAckExtension();
5176
+ });
4730
5177
  }
4731
5178
  return pending.then(
4732
- () => {
4733
- settleSuccess(msg, ctx);
4734
- reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4735
- if (stopAckExtension !== null) stopAckExtension();
5179
+ async () => {
5180
+ try {
5181
+ await settleSuccess(msg, ctx, data);
5182
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5183
+ } finally {
5184
+ if (stopAckExtension !== null) stopAckExtension();
5185
+ }
4736
5186
  },
4737
5187
  async (err) => {
4738
5188
  eventBus.emit(
@@ -4826,14 +5276,28 @@ var EventRouter = class {
4826
5276
  active--;
4827
5277
  drainBacklog();
4828
5278
  };
5279
+ const routeSafely = (msg) => {
5280
+ try {
5281
+ return route(msg);
5282
+ } catch (err) {
5283
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5284
+ return void 0;
5285
+ }
5286
+ };
5287
+ const trackAsync = (result, msg) => {
5288
+ void result.catch((err) => {
5289
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5290
+ }).finally(onAsyncDone);
5291
+ };
4829
5292
  const drainBacklog = () => {
4830
5293
  while (active < maxActive) {
4831
5294
  const next = backlog.shift();
4832
5295
  if (next === void 0) return;
5296
+ next.stopAckExtension?.();
4833
5297
  active++;
4834
- const result = route(next);
5298
+ const result = routeSafely(next.msg);
4835
5299
  if (result !== void 0) {
4836
- void result.finally(onAsyncDone);
5300
+ trackAsync(result, next.msg);
4837
5301
  } else {
4838
5302
  active--;
4839
5303
  }
@@ -4843,7 +5307,10 @@ var EventRouter = class {
4843
5307
  const subscription = stream$.subscribe({
4844
5308
  next: (msg) => {
4845
5309
  if (active >= maxActive) {
4846
- backlog.push(msg);
5310
+ backlog.push({
5311
+ msg,
5312
+ stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5313
+ });
4847
5314
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
4848
5315
  backlogWarned = true;
4849
5316
  logger5.warn(
@@ -4853,9 +5320,9 @@ var EventRouter = class {
4853
5320
  return;
4854
5321
  }
4855
5322
  active++;
4856
- const result = route(msg);
5323
+ const result = routeSafely(msg);
4857
5324
  if (result !== void 0) {
4858
- void result.finally(onAsyncDone);
5325
+ trackAsync(result, msg);
4859
5326
  } else {
4860
5327
  active--;
4861
5328
  if (backlog.length > 0) drainBacklog();
@@ -4865,6 +5332,12 @@ var EventRouter = class {
4865
5332
  logger5.error(`Stream error in ${kind} router`, err);
4866
5333
  }
4867
5334
  });
5335
+ subscription.add(() => {
5336
+ for (const queued of backlog) {
5337
+ queued.stopAckExtension?.();
5338
+ }
5339
+ backlog.length = 0;
5340
+ });
4868
5341
  this.subscriptions.push(subscription);
4869
5342
  }
4870
5343
  getConcurrency(kind) {
@@ -4878,27 +5351,72 @@ var EventRouter = class {
4878
5351
  return void 0;
4879
5352
  }
4880
5353
  /**
4881
- * 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.
5354
+ * Last resort: invoke onDeadLetter, then term on success. On failure the
5355
+ * message is nak'd never redelivered past max_deliver, but preserved.
4885
5356
  */
4886
5357
  async fallbackToOnDeadLetterCallback(info, msg) {
4887
- if (!this.deadLetterConfig) {
4888
- msg.term("Dead letter config unavailable");
5358
+ const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
5359
+ if (!onDeadLetter) {
5360
+ this.logger.error(
5361
+ `Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
5362
+ );
5363
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5364
+ msg.nak();
5365
+ });
4889
5366
  return;
4890
5367
  }
4891
5368
  try {
4892
- await this.deadLetterConfig.onDeadLetter(info);
4893
- msg.term("Dead letter processed via fallback callback");
5369
+ await onDeadLetter(info);
5370
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5371
+ msg.term("Dead letter processed via fallback callback");
5372
+ });
4894
5373
  } catch (hookErr) {
4895
5374
  this.logger.error(
4896
- `Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
5375
+ `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
5376
  hookErr
4898
5377
  );
4899
- msg.nak();
5378
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5379
+ msg.nak();
5380
+ });
4900
5381
  }
4901
5382
  }
5383
+ /**
5384
+ * Copy headers for the DLQ republish, dropping NATS control headers — a
5385
+ * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
5386
+ */
5387
+ buildDlqHeaders(msg) {
5388
+ const hdrs = (0, import_transport_node4.headers)();
5389
+ if (!msg.headers) return hdrs;
5390
+ for (const [k, v] of msg.headers) {
5391
+ if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
5392
+ for (const val of v) {
5393
+ hdrs.append(k, val);
5394
+ }
5395
+ }
5396
+ return hdrs;
5397
+ }
5398
+ /**
5399
+ * Past max_deliver the server never redelivers, so these in-process attempts
5400
+ * are the only second chance a dead letter gets. No artificial delay — an
5401
+ * unreachable broker already spaces attempts via its own request timeout.
5402
+ */
5403
+ async publishToDlqWithRetry(connection, subject, data, headers2) {
5404
+ let lastErr;
5405
+ for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
5406
+ try {
5407
+ await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
5408
+ return;
5409
+ } catch (err) {
5410
+ lastErr = err;
5411
+ if (attempt < DLQ_PUBLISH_ATTEMPTS) {
5412
+ this.logger.warn(
5413
+ `DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
5414
+ );
5415
+ }
5416
+ }
5417
+ }
5418
+ throw lastErr;
5419
+ }
4902
5420
  /**
4903
5421
  * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
4904
5422
  *
@@ -4917,14 +5435,7 @@ var EventRouter = class {
4917
5435
  return;
4918
5436
  }
4919
5437
  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
- }
5438
+ const hdrs = this.buildDlqHeaders(msg);
4928
5439
  let reason = String(error);
4929
5440
  if (error instanceof Error) {
4930
5441
  reason = error.message;
@@ -4937,8 +5448,7 @@ var EventRouter = class {
4937
5448
  hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
4938
5449
  hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
4939
5450
  try {
4940
- const js = this.connection.getJetStreamClient();
4941
- await js.publish(destinationSubject, msg.data, { headers: hdrs });
5451
+ await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
4942
5452
  this.logger.log(`Message sent to DLQ: ${msg.subject}`);
4943
5453
  if (this.deadLetterConfig?.onDeadLetter) {
4944
5454
  try {
@@ -4950,7 +5460,9 @@ var EventRouter = class {
4950
5460
  );
4951
5461
  }
4952
5462
  }
4953
- msg.term("Moved to DLQ stream");
5463
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5464
+ msg.term("Moved to DLQ stream");
5465
+ });
4954
5466
  } catch (publishErr) {
4955
5467
  this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
4956
5468
  await this.fallbackToOnDeadLetterCallback(info, msg);
@@ -5144,7 +5656,9 @@ var RpcRouter = class {
5144
5656
  `rpc-handler:${subject}`
5145
5657
  );
5146
5658
  publishErrorReply(replyTo, correlationId, subject, err);
5147
- msg.term(`Handler error: ${subject}`);
5659
+ settleQuietly(logger5, `Failed to term ${subject}:`, () => {
5660
+ msg.term(`Handler error: ${subject}`);
5661
+ });
5148
5662
  };
5149
5663
  const abortController = new AbortController();
5150
5664
  let pending;
@@ -5172,7 +5686,9 @@ var RpcRouter = class {
5172
5686
  }
5173
5687
  if (!isPromiseLike2(pending)) {
5174
5688
  if (stopAckExtension !== null) stopAckExtension();
5175
- msg.ack();
5689
+ settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
5690
+ msg.ack();
5691
+ });
5176
5692
  publishReply(replyTo, correlationId, pending);
5177
5693
  reportHandlerCompleted(msg, startedAt, "success");
5178
5694
  return void 0;
@@ -5184,7 +5700,9 @@ var RpcRouter = class {
5184
5700
  if (stopAckExtension !== null) stopAckExtension();
5185
5701
  abortController.abort();
5186
5702
  emitRpcTimeout(subject, correlationId);
5187
- msg.term("Handler timeout");
5703
+ settleQuietly(logger5, `Failed to term ${subject}:`, () => {
5704
+ msg.term("Handler timeout");
5705
+ });
5188
5706
  reportHandlerCompleted(msg, startedAt, "terminated");
5189
5707
  }, timeout);
5190
5708
  return pending.then(
@@ -5193,7 +5711,9 @@ var RpcRouter = class {
5193
5711
  settled = true;
5194
5712
  clearTimeout(timeoutId);
5195
5713
  if (stopAckExtension !== null) stopAckExtension();
5196
- msg.ack();
5714
+ settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
5715
+ msg.ack();
5716
+ });
5197
5717
  publishReply(replyTo, correlationId, result);
5198
5718
  reportHandlerCompleted(msg, startedAt, "success");
5199
5719
  },
@@ -5215,14 +5735,28 @@ var RpcRouter = class {
5215
5735
  active--;
5216
5736
  drainBacklog();
5217
5737
  };
5738
+ const routeSafely = (msg) => {
5739
+ try {
5740
+ return handleSafe(msg);
5741
+ } catch (err) {
5742
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5743
+ return void 0;
5744
+ }
5745
+ };
5746
+ const trackAsync = (result, msg) => {
5747
+ void result.catch((err) => {
5748
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5749
+ }).finally(onAsyncDone);
5750
+ };
5218
5751
  const drainBacklog = () => {
5219
5752
  while (active < maxActive) {
5220
5753
  const next = backlog.shift();
5221
5754
  if (next === void 0) return;
5755
+ next.stopAckExtension?.();
5222
5756
  active++;
5223
- const result = handleSafe(next);
5757
+ const result = routeSafely(next.msg);
5224
5758
  if (result !== void 0) {
5225
- void result.finally(onAsyncDone);
5759
+ trackAsync(result, next.msg);
5226
5760
  } else {
5227
5761
  active--;
5228
5762
  }
@@ -5232,7 +5766,10 @@ var RpcRouter = class {
5232
5766
  this.subscription = this.messageProvider.commands$.subscribe({
5233
5767
  next: (msg) => {
5234
5768
  if (active >= maxActive) {
5235
- backlog.push(msg);
5769
+ backlog.push({
5770
+ msg,
5771
+ stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5772
+ });
5236
5773
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
5237
5774
  backlogWarned = true;
5238
5775
  logger5.warn(
@@ -5242,9 +5779,9 @@ var RpcRouter = class {
5242
5779
  return;
5243
5780
  }
5244
5781
  active++;
5245
- const result = handleSafe(msg);
5782
+ const result = routeSafely(msg);
5246
5783
  if (result !== void 0) {
5247
- void result.finally(onAsyncDone);
5784
+ trackAsync(result, msg);
5248
5785
  } else {
5249
5786
  active--;
5250
5787
  if (backlog.length > 0) drainBacklog();
@@ -5254,6 +5791,12 @@ var RpcRouter = class {
5254
5791
  logger5.error("Stream error in RPC router", err);
5255
5792
  }
5256
5793
  });
5794
+ this.subscription.add(() => {
5795
+ for (const queued of backlog) {
5796
+ queued.stopAckExtension?.();
5797
+ }
5798
+ backlog.length = 0;
5799
+ });
5257
5800
  }
5258
5801
  /** Stop routing and unsubscribe. */
5259
5802
  destroy() {
@@ -5536,7 +6079,7 @@ var JetstreamModule = class {
5536
6079
  ],
5537
6080
  useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
5538
6081
  if (options.consumer === false) return null;
5539
- const deadLetterConfig = options.onDeadLetter ? {
6082
+ const deadLetterConfig = options.onDeadLetter || options.dlq ? {
5540
6083
  maxDeliverByStream: /* @__PURE__ */ new Map(),
5541
6084
  onDeadLetter: options.onDeadLetter
5542
6085
  } : void 0;
@@ -5737,6 +6280,7 @@ JetstreamModule = __decorateClass([
5737
6280
  JetstreamHeader,
5738
6281
  JetstreamHealthIndicator,
5739
6282
  JetstreamModule,
6283
+ JetstreamProvisioningError,
5740
6284
  JetstreamRecord,
5741
6285
  JetstreamRecordBuilder,
5742
6286
  JetstreamStrategy,