@horizon-republic/nestjs-jetstream 2.11.0 → 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";
@@ -512,6 +517,8 @@ var resolveCaptureBody = (option) => {
512
517
  };
513
518
  };
514
519
  var resolveOtelOptions = (options = {}) => {
520
+ if (options === true) options = {};
521
+ if (options === false) options = { enabled: false };
515
522
  return {
516
523
  enabled: options.enabled ?? true,
517
524
  traces: expandTracesOption(options.traces),
@@ -591,7 +598,7 @@ var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ct
591
598
 
592
599
  // src/otel/tracer.ts
593
600
  var import_api2 = require("@opentelemetry/api");
594
- var PACKAGE_VERSION = true ? "2.11.0" : "0.0.0";
601
+ var PACKAGE_VERSION = true ? "2.12.0" : "0.0.0";
595
602
  var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
596
603
 
597
604
  // src/otel/carrier.ts
@@ -1180,7 +1187,10 @@ var withProvisioningSpan = (config, ctx, op) => wrapInfra(
1180
1187
  {
1181
1188
  [ATTR_JETSTREAM_PROVISIONING_ENTITY]: ctx.entity,
1182
1189
  [ATTR_JETSTREAM_PROVISIONING_ACTION]: ctx.action,
1183
- [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
1184
1194
  },
1185
1195
  op
1186
1196
  );
@@ -1356,7 +1366,13 @@ var JetstreamRecordBuilder = class {
1356
1366
  * lockstep. `RESERVED_HEADERS` is defined as an all-lowercase set.
1357
1367
  */
1358
1368
  validateHeaderKey(key) {
1359
- 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)) {
1360
1376
  throw new Error(
1361
1377
  `Header "${key}" is reserved by the JetStream transport and cannot be set manually. Reserved headers: ${[...RESERVED_HEADERS].join(", ")}`
1362
1378
  );
@@ -1502,13 +1518,18 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1502
1518
  async dispatchEvent(packet) {
1503
1519
  if (!this.readyForPublish) await this.connect();
1504
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
+ }
1505
1527
  const eventSubject = this.buildEventSubject(packet.pattern);
1506
1528
  const publishSubject = schedule ? this.buildScheduleSubject(eventSubject) : eventSubject;
1507
1529
  const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
1508
1530
  const encoded = this.codec.encode(data);
1509
1531
  const effectiveMsgId = messageId ?? import_nuid.nuid.next();
1510
1532
  const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
1511
- const publishKind = detectEventKind(packet.pattern);
1512
1533
  const declaredPattern = declaredEventPattern(packet.pattern);
1513
1534
  const streamKind = eventStreamKind(publishKind);
1514
1535
  const startedAt = performance.now();
@@ -1540,10 +1561,10 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1540
1561
  const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1541
1562
  headers: msgHeaders,
1542
1563
  msgID: effectiveMsgId,
1543
- ttl,
1544
1564
  schedule: {
1545
1565
  specification: schedule.at,
1546
- target: eventSubject
1566
+ target: eventSubject,
1567
+ ttl
1547
1568
  }
1548
1569
  });
1549
1570
  warnIfDuplicate("scheduled", ack2);
@@ -1733,13 +1754,18 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1733
1754
  });
1734
1755
  return;
1735
1756
  }
1736
- await import_api8.context.with(
1757
+ const ack = await import_api8.context.with(
1737
1758
  spanHandle.activeContext,
1738
1759
  () => this.connection.getJetStreamClient().publish(subject, encoded, {
1739
1760
  headers: hdrs,
1740
1761
  msgID: messageId ?? import_nuid.nuid.next()
1741
1762
  })
1742
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
+ }
1743
1769
  this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1744
1770
  } catch (err) {
1745
1771
  const existingTimeout = this.pendingTimeouts.get(correlationId);
@@ -1909,13 +1935,17 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1909
1935
  * uses a separate `_sch` namespace that is NOT matched by any consumer filter.
1910
1936
  * NATS holds the message and publishes it to the target subject after the delay.
1911
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
+ *
1912
1942
  * Examples:
1913
- * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder`
1914
- * - `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>`
1915
1945
  */
1916
1946
  buildScheduleSubject(eventSubject) {
1917
1947
  if (eventSubject.startsWith("broadcast.")) {
1918
- return eventSubject.replace("broadcast.", "broadcast._sch.");
1948
+ return `${eventSubject.replace("broadcast.", "broadcast._sch.")}.${import_nuid.nuid.next()}`;
1919
1949
  }
1920
1950
  const targetPrefix = `${internalName(this.targetName)}.`;
1921
1951
  if (!eventSubject.startsWith(targetPrefix)) {
@@ -1927,7 +1957,7 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
1927
1957
  throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
1928
1958
  }
1929
1959
  const pattern = withoutPrefix.slice(dotIndex + 1);
1930
- return `${targetPrefix}_sch.${pattern}`;
1960
+ return `${targetPrefix}_sch.${pattern}.${import_nuid.nuid.next()}`;
1931
1961
  }
1932
1962
  };
1933
1963
 
@@ -1939,6 +1969,7 @@ var JsonCodec = class {
1939
1969
  return encoder.encode(JSON.stringify(data));
1940
1970
  }
1941
1971
  decode(data) {
1972
+ if (data.length === 0) return void 0;
1942
1973
  return JSON.parse(decoder.decode(data));
1943
1974
  }
1944
1975
  };
@@ -1952,6 +1983,7 @@ var MsgpackCodec = class {
1952
1983
  return this.packr.pack(data);
1953
1984
  }
1954
1985
  decode(data) {
1986
+ if (data.length === 0) return void 0;
1955
1987
  return this.packr.unpack(data);
1956
1988
  }
1957
1989
  };
@@ -2751,6 +2783,7 @@ var JetstreamMetricsService = class {
2751
2783
  activeServers = /* @__PURE__ */ new Set();
2752
2784
  async onApplicationBootstrap() {
2753
2785
  if (this.metrics !== null) return;
2786
+ if (!this.options.metrics || !this.config || !this.promClient) return;
2754
2787
  if (!this.config.register) {
2755
2788
  throw new Error(
2756
2789
  "JetstreamMetricsService requires a prom-client Registry \u2014 none was resolved by JetstreamMetricsModule."
@@ -2776,7 +2809,7 @@ var JetstreamMetricsService = class {
2776
2809
  }
2777
2810
  /** @internal Visible for tests. `0` disables polling. */
2778
2811
  getEffectivePollInterval() {
2779
- return this.config.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
2812
+ return this.config?.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
2780
2813
  }
2781
2814
  /**
2782
2815
  * NATS connects during early bootstrap, before this service subscribes to
@@ -2942,28 +2975,29 @@ var normalizeMetricsConfig = (option, promClient) => {
2942
2975
  };
2943
2976
  };
2944
2977
  var JetstreamMetricsModule = class {
2945
- static forFeature(metricsOption) {
2946
- if (!metricsOption) {
2947
- return { module: JetstreamMetricsModule, providers: [], exports: [] };
2948
- }
2978
+ static forFeature() {
2949
2979
  const promClientProvider = {
2950
2980
  provide: JETSTREAM_METRICS_PROM_CLIENT,
2951
- useFactory: async () => {
2981
+ inject: [JETSTREAM_OPTIONS],
2982
+ useFactory: async (opts) => {
2983
+ if (!opts.metrics) return null;
2952
2984
  const mod = await resolvePromClient();
2953
2985
  return { Counter: mod.Counter, Histogram: mod.Histogram, Gauge: mod.Gauge };
2954
2986
  }
2955
2987
  };
2956
2988
  const configProvider = {
2957
2989
  provide: JETSTREAM_METRICS_CONFIG,
2958
- useFactory: async () => {
2990
+ inject: [JETSTREAM_OPTIONS],
2991
+ useFactory: async (opts) => {
2992
+ if (!opts.metrics) return null;
2959
2993
  const mod = await resolvePromClient();
2960
- return normalizeMetricsConfig(metricsOption, mod);
2994
+ return normalizeMetricsConfig(opts.metrics, mod);
2961
2995
  }
2962
2996
  };
2963
2997
  const registryProvider = {
2964
2998
  provide: JETSTREAM_METRICS_REGISTRY,
2965
2999
  inject: [JETSTREAM_METRICS_CONFIG],
2966
- useFactory: (cfg) => cfg.register
3000
+ useFactory: (cfg) => cfg?.register ?? null
2967
3001
  };
2968
3002
  const serviceProvider = {
2969
3003
  provide: JetstreamMetricsService,
@@ -3020,43 +3054,11 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3020
3054
  * Called by NestJS when `connectMicroservice()` is used, or internally by the module.
3021
3055
  */
3022
3056
  async listen(callback) {
3023
- if (this.started) {
3024
- this.logger.warn("listen() called more than once \u2014 ignoring");
3025
- return;
3026
- }
3027
- this.started = true;
3028
- this.patternRegistry.registerHandlers(this.getHandlers());
3029
- const { streams: streamKinds, durableConsumers: durableKinds } = this.resolveRequiredKinds();
3030
- if (streamKinds.length > 0) {
3031
- await this.streamProvider.ensureStreams(streamKinds);
3032
- if (durableKinds.length > 0) {
3033
- const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
3034
- this.populateAckWaitMap(consumers);
3035
- this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
3036
- this.messageProvider.start(consumers);
3037
- }
3038
- if (this.patternRegistry.hasOrderedHandlers()) {
3039
- const orderedStreamName = this.streamProvider.getStreamName("ordered" /* Ordered */);
3040
- await this.messageProvider.startOrdered(
3041
- orderedStreamName,
3042
- this.patternRegistry.getOrderedSubjects(),
3043
- this.options.ordered
3044
- );
3045
- }
3046
- if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
3047
- this.eventRouter.start();
3048
- }
3049
- if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3050
- await this.rpcRouter.start();
3051
- }
3052
- }
3053
- if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3054
- await this.coreRpcServer.start();
3055
- }
3056
- if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
3057
- await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
3057
+ try {
3058
+ await this.doListen(callback);
3059
+ } catch (err) {
3060
+ callback(err);
3058
3061
  }
3059
- callback();
3060
3062
  }
3061
3063
  /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
3062
3064
  close() {
@@ -3115,6 +3117,33 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3115
3117
  getPatternRegistry() {
3116
3118
  return this.patternRegistry;
3117
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
+ }
3118
3147
  /** Determine which streams and durable consumers are needed. */
3119
3148
  resolveRequiredKinds() {
3120
3149
  const streams = [];
@@ -3136,7 +3165,29 @@ var JetstreamStrategy = class extends import_microservices2.Server {
3136
3165
  }
3137
3166
  return { streams, durableConsumers };
3138
3167
  }
3139
- /** 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
+ }
3140
3191
  populateAckWaitMap(consumers) {
3141
3192
  for (const [kind, info] of consumers) {
3142
3193
  if (info.config.ack_wait) {
@@ -3375,6 +3426,15 @@ var serializeError = (err) => {
3375
3426
  return err;
3376
3427
  };
3377
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
+
3378
3438
  // src/utils/unwrap-result.ts
3379
3439
  var import_rxjs2 = require("rxjs");
3380
3440
  var unwrapResult = (result) => {
@@ -3540,16 +3600,164 @@ var CoreRpcServer = class {
3540
3600
 
3541
3601
  // src/server/infrastructure/stream.provider.ts
3542
3602
  var import_common14 = require("@nestjs/common");
3543
- var import_jetstream17 = require("@nats-io/jetstream");
3603
+ var import_jetstream19 = require("@nats-io/jetstream");
3544
3604
 
3545
3605
  // src/server/infrastructure/nats-error-codes.ts
3546
3606
  var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
3547
3607
  NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
3548
3608
  NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
3549
3609
  NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
3610
+ NatsErrorCode2[NatsErrorCode2["StorageResourcesExceeded"] = 10047] = "StorageResourcesExceeded";
3611
+ NatsErrorCode2[NatsErrorCode2["NoSuitablePeers"] = 10005] = "NoSuitablePeers";
3550
3612
  return NatsErrorCode2;
3551
3613
  })(NatsErrorCode || {});
3552
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
+
3553
3761
  // src/server/infrastructure/stream-config-diff.ts
3554
3762
  var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
3555
3763
  "retention"
@@ -3607,85 +3815,199 @@ var isEqual = (a, b) => {
3607
3815
 
3608
3816
  // src/server/infrastructure/stream-migration.ts
3609
3817
  var import_common13 = require("@nestjs/common");
3610
- var import_jetstream16 = require("@nats-io/jetstream");
3818
+ var import_jetstream18 = require("@nats-io/jetstream");
3611
3819
  var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
3612
3820
  var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
3613
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";
3614
3825
  var StreamMigration = class {
3615
- constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
3826
+ constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS, peerWaitMs = DEFAULT_PEER_WAIT_MS) {
3616
3827
  this.sourcingTimeoutMs = sourcingTimeoutMs;
3828
+ this.peerWaitMs = peerWaitMs;
3617
3829
  }
3618
3830
  logger = new import_common13.Logger("Jetstream:Stream");
3619
3831
  async migrate(jsm, streamName2, newConfig) {
3620
3832
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
3621
3833
  const startTime = Date.now();
3834
+ const peerFinished = await this.waitOutPeerMigration(jsm, backupName);
3622
3835
  const currentInfo = await jsm.streams.info(streamName2);
3623
- await this.cleanupOrphanedBackup(jsm, backupName);
3624
- 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
+ }
3625
3841
  this.logger.log(`Stream ${streamName2}: destructive migration started`);
3626
3842
  let originalDeleted = false;
3843
+ let drainedCount = 0;
3627
3844
  try {
3628
- if (messageCount > 0) {
3629
- 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}`);
3630
3850
  await jsm.streams.add({
3631
3851
  ...currentInfo.config,
3632
3852
  name: backupName,
3633
3853
  subjects: [],
3634
- sources: [{ name: streamName2 }]
3854
+ sources: [{ name: streamName2 }],
3855
+ metadata: { [MIGRATION_STARTED_AT_KEY]: (/* @__PURE__ */ new Date()).toISOString() }
3635
3856
  });
3636
- await this.waitForSourcing(jsm, backupName, messageCount);
3857
+ await this.waitForSourceDrained(jsm, backupName, streamName2, drainedCount);
3637
3858
  }
3638
- this.logger.log(` Phase 2/4: Deleting old stream`);
3859
+ this.logger.log(` Phase 3/4: Recreating ${streamName2} with the new config`);
3639
3860
  await jsm.streams.delete(streamName2);
3640
3861
  originalDeleted = true;
3641
- this.logger.log(` Phase 3/4: Creating stream with new config`);
3642
3862
  await jsm.streams.add(newConfig);
3643
- if (messageCount > 0) {
3644
- const backupInfo = await jsm.streams.info(backupName);
3645
- await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
3646
- this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
3647
- await jsm.streams.update(streamName2, {
3648
- ...newConfig,
3649
- sources: [{ name: backupName }]
3650
- });
3651
- await this.waitForSourcing(jsm, streamName2, messageCount);
3652
- await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
3653
- 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);
3654
3866
  }
3655
3867
  } catch (err) {
3656
- if (originalDeleted && messageCount > 0) {
3868
+ if (originalDeleted) {
3657
3869
  this.logger.error(
3658
- `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.`
3659
3871
  );
3660
3872
  } else {
3661
- await this.cleanupOrphanedBackup(jsm, backupName);
3873
+ await this.rollbackBeforeDelete(jsm, streamName2, currentInfo, backupName);
3662
3874
  }
3663
3875
  throw err;
3664
3876
  }
3665
3877
  const durationMs = Date.now() - startTime;
3666
3878
  this.logger.log(
3667
- `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
3668
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: [] });
3669
3939
  }
3670
- 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) {
3671
3948
  const deadline = Date.now() + this.sourcingTimeoutMs;
3672
3949
  while (Date.now() < deadline) {
3673
3950
  const info = await jsm.streams.info(streamName2);
3674
- 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
+ }
3675
3955
  await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
3676
3956
  }
3677
3957
  throw new Error(
3678
- `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.`
3679
3959
  );
3680
3960
  }
3681
- async cleanupOrphanedBackup(jsm, backupName) {
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.`
3981
+ );
3982
+ }
3983
+ /** Failure before the original was deleted: undo the quiesce, drop our backup. */
3984
+ async rollbackBeforeDelete(jsm, streamName2, originalInfo, backupName) {
3682
3985
  try {
3683
- await jsm.streams.info(backupName);
3684
- this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
3685
- 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);
3686
4008
  } catch (err) {
3687
- if (err instanceof import_jetstream16.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3688
- return;
4009
+ if (err instanceof import_jetstream18.JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
4010
+ return null;
3689
4011
  }
3690
4012
  throw err;
3691
4013
  }
@@ -3716,6 +4038,15 @@ var StreamProvider = class {
3716
4038
  */
3717
4039
  async ensureStreams(kinds) {
3718
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
+ }
3719
4050
  await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
3720
4051
  if (this.options.dlq) {
3721
4052
  await this.ensureDlqStream(jsm);
@@ -3752,6 +4083,7 @@ var StreamProvider = class {
3752
4083
  /** Ensure a single stream exists, creating or updating as needed. */
3753
4084
  async ensureStream(jsm, kind) {
3754
4085
  const config = this.buildConfig(kind);
4086
+ const ctx = this.errorContext(kind, config);
3755
4087
  return withProvisioningSpan(
3756
4088
  this.otel,
3757
4089
  {
@@ -3759,17 +4091,21 @@ var StreamProvider = class {
3759
4091
  endpoint: this.otelEndpoint,
3760
4092
  entity: "stream",
3761
4093
  name: config.name,
3762
- 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
3763
4098
  },
3764
4099
  async () => {
3765
4100
  this.logger.log(`Ensuring stream: ${config.name}`);
4101
+ await this.migration.recoverInterrupted(jsm, config.name, config);
3766
4102
  try {
3767
4103
  const currentInfo = await jsm.streams.info(config.name);
3768
- return await this.handleExistingStream(jsm, currentInfo, config);
4104
+ return await this.handleExistingStream(jsm, currentInfo, config, ctx);
3769
4105
  } catch (err) {
3770
- 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 */) {
3771
4107
  this.logger.log(`Creating stream: ${config.name}`);
3772
- return await jsm.streams.add(config);
4108
+ return await this.runStreamOp(ctx, () => jsm.streams.add(config));
3773
4109
  }
3774
4110
  throw err;
3775
4111
  }
@@ -3779,6 +4115,7 @@ var StreamProvider = class {
3779
4115
  /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
3780
4116
  async ensureDlqStream(jsm) {
3781
4117
  const config = this.buildDlqConfig();
4118
+ const ctx = this.errorContext("dlq", config);
3782
4119
  return withProvisioningSpan(
3783
4120
  this.otel,
3784
4121
  {
@@ -3786,24 +4123,30 @@ var StreamProvider = class {
3786
4123
  endpoint: this.otelEndpoint,
3787
4124
  entity: "stream",
3788
4125
  name: config.name,
3789
- 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
3790
4130
  },
3791
4131
  async () => {
3792
4132
  this.logger.log(`Ensuring DLQ stream: ${config.name}`);
3793
4133
  try {
3794
4134
  const currentInfo = await jsm.streams.info(config.name);
3795
- return await this.handleExistingStream(jsm, currentInfo, config);
4135
+ return await this.handleExistingStream(jsm, currentInfo, config, ctx);
3796
4136
  } catch (err) {
3797
- 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 */) {
3798
4138
  this.logger.log(`Creating DLQ stream: ${config.name}`);
3799
- return await jsm.streams.add(config);
4139
+ return await this.runStreamOp(ctx, () => jsm.streams.add(config));
3800
4140
  }
3801
4141
  throw err;
3802
4142
  }
3803
4143
  }
3804
4144
  );
3805
4145
  }
3806
- 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
+ }
3807
4150
  const diff = compareStreamConfig(currentInfo.config, config);
3808
4151
  if (!diff.hasChanges) {
3809
4152
  this.logger.debug(`Stream ${config.name}: no config changes`);
@@ -3818,7 +4161,7 @@ var StreamProvider = class {
3818
4161
  }
3819
4162
  if (!diff.hasImmutableChanges) {
3820
4163
  this.logger.debug(`Stream exists, updating: ${config.name}`);
3821
- return await jsm.streams.update(config.name, config);
4164
+ return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, config));
3822
4165
  }
3823
4166
  if (!this.options.allowDestructiveMigration) {
3824
4167
  this.logger.warn(
@@ -3826,10 +4169,15 @@ var StreamProvider = class {
3826
4169
  );
3827
4170
  if (diff.hasMutableChanges) {
3828
4171
  const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
3829
- return await jsm.streams.update(config.name, mutableConfig);
4172
+ return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, mutableConfig));
3830
4173
  }
3831
4174
  return currentInfo;
3832
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
+ }
3833
4181
  await withMigrationSpan(
3834
4182
  this.otel,
3835
4183
  {
@@ -3868,11 +4216,47 @@ var StreamProvider = class {
3868
4216
  }
3869
4217
  }
3870
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
+ }
3871
4255
  /** Build the full stream config by merging defaults with user overrides. */
3872
4256
  buildConfig(kind) {
3873
4257
  const name = this.getStreamName(kind);
3874
4258
  const subjects = this.getSubjects(kind);
3875
- 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}`;
3876
4260
  const defaults = this.getDefaults(kind);
3877
4261
  const overrides = this.getOverrides(kind);
3878
4262
  return {
@@ -3958,7 +4342,7 @@ var StreamProvider = class {
3958
4342
 
3959
4343
  // src/server/infrastructure/consumer.provider.ts
3960
4344
  var import_common15 = require("@nestjs/common");
3961
- var import_jetstream19 = require("@nats-io/jetstream");
4345
+ var import_jetstream21 = require("@nats-io/jetstream");
3962
4346
  var ConsumerProvider = class {
3963
4347
  constructor(options, connection, streamProvider, patternRegistry) {
3964
4348
  this.options = options;
@@ -4014,15 +4398,16 @@ var ConsumerProvider = class {
4014
4398
  },
4015
4399
  async () => {
4016
4400
  this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
4401
+ const ctx = { entity: "consumer", name, kind };
4017
4402
  try {
4018
4403
  await jsm.consumers.info(stream, name);
4019
4404
  this.logger.debug(`Consumer exists, updating: ${name}`);
4020
- return await jsm.consumers.update(stream, name, config);
4405
+ return await this.runConsumerOp(ctx, () => jsm.consumers.update(stream, name, config));
4021
4406
  } catch (err) {
4022
- 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 */) {
4023
4408
  throw err;
4024
4409
  }
4025
- return await this.createConsumer(jsm, stream, name, config);
4410
+ return await this.createConsumer(jsm, stream, name, kind, config);
4026
4411
  }
4027
4412
  }
4028
4413
  );
@@ -4059,10 +4444,10 @@ var ConsumerProvider = class {
4059
4444
  try {
4060
4445
  return await jsm.consumers.info(stream, name);
4061
4446
  } catch (err) {
4062
- 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 */) {
4063
4448
  throw err;
4064
4449
  }
4065
- return await this.createConsumer(jsm, stream, name, config);
4450
+ return await this.createConsumer(jsm, stream, name, kind, config);
4066
4451
  }
4067
4452
  }
4068
4453
  );
@@ -4080,7 +4465,7 @@ var ConsumerProvider = class {
4080
4465
  `Stream ${stream} is being migrated (backup ${backupName} exists). Waiting for migration to complete before recovering consumer.`
4081
4466
  );
4082
4467
  } catch (err) {
4083
- 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 */) {
4084
4469
  return;
4085
4470
  }
4086
4471
  throw err;
@@ -4089,18 +4474,32 @@ var ConsumerProvider = class {
4089
4474
  /**
4090
4475
  * Create a consumer, handling the race where another pod creates it first.
4091
4476
  */
4092
- async createConsumer(jsm, stream, name, config) {
4477
+ async createConsumer(jsm, stream, name, kind, config) {
4093
4478
  this.logger.log(`Creating consumer: ${name}`);
4479
+ const ctx = { entity: "consumer", name, kind };
4094
4480
  try {
4095
4481
  return await jsm.consumers.add(stream, config);
4096
4482
  } catch (addErr) {
4097
- 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 */) {
4098
4484
  this.logger.debug(`Consumer ${name} created by another pod, using existing`);
4099
4485
  return await jsm.consumers.info(stream, name);
4100
4486
  }
4487
+ if (addErr instanceof import_jetstream21.JetStreamApiError) {
4488
+ throw mapProvisioningError(addErr, ctx);
4489
+ }
4101
4490
  throw addErr;
4102
4491
  }
4103
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
+ }
4104
4503
  /** Build consumer config by merging defaults with user overrides. */
4105
4504
  // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
4106
4505
  buildConfig(kind) {
@@ -4182,7 +4581,7 @@ var ConsumerProvider = class {
4182
4581
 
4183
4582
  // src/server/infrastructure/message.provider.ts
4184
4583
  var import_common16 = require("@nestjs/common");
4185
- var import_jetstream21 = require("@nats-io/jetstream");
4584
+ var import_jetstream23 = require("@nats-io/jetstream");
4186
4585
  var import_rxjs3 = require("rxjs");
4187
4586
  var MessageProvider = class {
4188
4587
  constructor(connection, eventBus, consumeOptionsMap = /* @__PURE__ */ new Map(), consumerRecoveryFn) {
@@ -4243,7 +4642,7 @@ var MessageProvider = class {
4243
4642
  */
4244
4643
  async startOrdered(streamName2, filterSubjects, orderedConfig) {
4245
4644
  const consumerOpts = { filter_subjects: filterSubjects };
4246
- if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream21.DeliverPolicy.All) {
4645
+ if (orderedConfig?.deliverPolicy !== void 0 && orderedConfig.deliverPolicy !== import_jetstream23.DeliverPolicy.All) {
4247
4646
  consumerOpts.deliver_policy = orderedConfig.deliverPolicy;
4248
4647
  }
4249
4648
  if (orderedConfig?.optStartSeq !== void 0) {
@@ -4552,6 +4951,7 @@ var MetadataProvider = class {
4552
4951
  // src/server/routing/event.router.ts
4553
4952
  var import_common18 = require("@nestjs/common");
4554
4953
  var import_transport_node4 = require("@nats-io/transport-node");
4954
+ var DLQ_PUBLISH_ATTEMPTS = 3;
4555
4955
  var eventConsumeKindFor = (kind) => {
4556
4956
  if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
4557
4957
  if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
@@ -4638,33 +5038,80 @@ var EventRouter = class {
4638
5038
  return msg.info.deliveryCount >= maxDeliver;
4639
5039
  };
4640
5040
  const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
4641
- const settleSuccess = (msg, ctx) => {
4642
- if (ctx.shouldTerminate) msg.term(ctx.terminateReason);
4643
- else if (ctx.shouldRetry) msg.nak(ctx.retryDelay);
4644
- 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;
4645
5065
  };
4646
5066
  const settleFailure = async (msg, data, err) => {
4647
5067
  if (handleDeadLetter !== null && isDeadLetter(msg)) {
4648
5068
  await handleDeadLetter(msg, data, err);
4649
5069
  return;
4650
5070
  }
4651
- 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
+ });
4652
5085
  };
4653
5086
  const resolveEvent = (msg) => {
4654
5087
  const subject = msg.subject;
4655
5088
  try {
4656
5089
  const handler = patternRegistry.getHandler(subject);
4657
5090
  if (!handler) {
4658
- msg.term(`No handler for event: ${subject}`);
4659
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}`);
4660
5100
  return null;
4661
5101
  }
4662
5102
  let data;
4663
5103
  try {
4664
5104
  data = codec.decode(msg.data);
4665
5105
  } catch (err) {
4666
- msg.term("Decode error");
4667
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");
4668
5115
  return null;
4669
5116
  }
4670
5117
  eventBus.emitMessageRouted(subject, "event" /* Event */);
@@ -4687,6 +5134,7 @@ var EventRouter = class {
4687
5134
  const handleSafe = (msg) => {
4688
5135
  const resolved = resolveEvent(msg);
4689
5136
  if (resolved === null) return void 0;
5137
+ if (isPromiseLike2(resolved)) return resolved;
4690
5138
  const { handler, data } = resolved;
4691
5139
  const ctx = new RpcContext([msg]);
4692
5140
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
@@ -4719,16 +5167,24 @@ var EventRouter = class {
4719
5167
  });
4720
5168
  }
4721
5169
  if (!isPromiseLike2(pending)) {
4722
- settleSuccess(msg, ctx);
5170
+ const settled = settleSuccess(msg, ctx, data);
4723
5171
  reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4724
- if (stopAckExtension !== null) stopAckExtension();
4725
- 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
+ });
4726
5179
  }
4727
5180
  return pending.then(
4728
- () => {
4729
- settleSuccess(msg, ctx);
4730
- reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4731
- 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
+ }
4732
5188
  },
4733
5189
  async (err) => {
4734
5190
  eventBus.emit(
@@ -4822,14 +5278,28 @@ var EventRouter = class {
4822
5278
  active--;
4823
5279
  drainBacklog();
4824
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
+ };
4825
5294
  const drainBacklog = () => {
4826
5295
  while (active < maxActive) {
4827
5296
  const next = backlog.shift();
4828
5297
  if (next === void 0) return;
5298
+ next.stopAckExtension?.();
4829
5299
  active++;
4830
- const result = route(next);
5300
+ const result = routeSafely(next.msg);
4831
5301
  if (result !== void 0) {
4832
- void result.finally(onAsyncDone);
5302
+ trackAsync(result, next.msg);
4833
5303
  } else {
4834
5304
  active--;
4835
5305
  }
@@ -4839,7 +5309,10 @@ var EventRouter = class {
4839
5309
  const subscription = stream$.subscribe({
4840
5310
  next: (msg) => {
4841
5311
  if (active >= maxActive) {
4842
- backlog.push(msg);
5312
+ backlog.push({
5313
+ msg,
5314
+ stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5315
+ });
4843
5316
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
4844
5317
  backlogWarned = true;
4845
5318
  logger5.warn(
@@ -4849,9 +5322,9 @@ var EventRouter = class {
4849
5322
  return;
4850
5323
  }
4851
5324
  active++;
4852
- const result = route(msg);
5325
+ const result = routeSafely(msg);
4853
5326
  if (result !== void 0) {
4854
- void result.finally(onAsyncDone);
5327
+ trackAsync(result, msg);
4855
5328
  } else {
4856
5329
  active--;
4857
5330
  if (backlog.length > 0) drainBacklog();
@@ -4861,6 +5334,12 @@ var EventRouter = class {
4861
5334
  logger5.error(`Stream error in ${kind} router`, err);
4862
5335
  }
4863
5336
  });
5337
+ subscription.add(() => {
5338
+ for (const queued of backlog) {
5339
+ queued.stopAckExtension?.();
5340
+ }
5341
+ backlog.length = 0;
5342
+ });
4864
5343
  this.subscriptions.push(subscription);
4865
5344
  }
4866
5345
  getConcurrency(kind) {
@@ -4875,25 +5354,78 @@ var EventRouter = class {
4875
5354
  }
4876
5355
  /**
4877
5356
  * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
4878
- * success or `nak` on hook failure so NATS retries on the next delivery
4879
- * cycle. Used when DLQ stream isn't configured, or when publishing to it
4880
- * 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.
4881
5361
  */
4882
5362
  async fallbackToOnDeadLetterCallback(info, msg) {
4883
- if (!this.deadLetterConfig) {
4884
- 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
+ });
4885
5371
  return;
4886
5372
  }
4887
5373
  try {
4888
- await this.deadLetterConfig.onDeadLetter(info);
4889
- 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
+ });
4890
5378
  } catch (hookErr) {
4891
5379
  this.logger.error(
4892
- `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:`,
4893
5381
  hookErr
4894
5382
  );
4895
- 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
+ }
4896
5427
  }
5428
+ throw lastErr;
4897
5429
  }
4898
5430
  /**
4899
5431
  * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
@@ -4913,14 +5445,7 @@ var EventRouter = class {
4913
5445
  return;
4914
5446
  }
4915
5447
  const destinationSubject = dlqStreamName(serviceName);
4916
- const hdrs = (0, import_transport_node4.headers)();
4917
- if (msg.headers) {
4918
- for (const [k, v] of msg.headers) {
4919
- for (const val of v) {
4920
- hdrs.append(k, val);
4921
- }
4922
- }
4923
- }
5448
+ const hdrs = this.buildDlqHeaders(msg);
4924
5449
  let reason = String(error);
4925
5450
  if (error instanceof Error) {
4926
5451
  reason = error.message;
@@ -4933,8 +5458,7 @@ var EventRouter = class {
4933
5458
  hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
4934
5459
  hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
4935
5460
  try {
4936
- const js = this.connection.getJetStreamClient();
4937
- await js.publish(destinationSubject, msg.data, { headers: hdrs });
5461
+ await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
4938
5462
  this.logger.log(`Message sent to DLQ: ${msg.subject}`);
4939
5463
  if (this.deadLetterConfig?.onDeadLetter) {
4940
5464
  try {
@@ -4946,7 +5470,9 @@ var EventRouter = class {
4946
5470
  );
4947
5471
  }
4948
5472
  }
4949
- msg.term("Moved to DLQ stream");
5473
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5474
+ msg.term("Moved to DLQ stream");
5475
+ });
4950
5476
  } catch (publishErr) {
4951
5477
  this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
4952
5478
  await this.fallbackToOnDeadLetterCallback(info, msg);
@@ -5140,7 +5666,9 @@ var RpcRouter = class {
5140
5666
  `rpc-handler:${subject}`
5141
5667
  );
5142
5668
  publishErrorReply(replyTo, correlationId, subject, err);
5143
- msg.term(`Handler error: ${subject}`);
5669
+ settleQuietly(logger5, `Failed to term ${subject}:`, () => {
5670
+ msg.term(`Handler error: ${subject}`);
5671
+ });
5144
5672
  };
5145
5673
  const abortController = new AbortController();
5146
5674
  let pending;
@@ -5168,7 +5696,9 @@ var RpcRouter = class {
5168
5696
  }
5169
5697
  if (!isPromiseLike2(pending)) {
5170
5698
  if (stopAckExtension !== null) stopAckExtension();
5171
- msg.ack();
5699
+ settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
5700
+ msg.ack();
5701
+ });
5172
5702
  publishReply(replyTo, correlationId, pending);
5173
5703
  reportHandlerCompleted(msg, startedAt, "success");
5174
5704
  return void 0;
@@ -5180,7 +5710,9 @@ var RpcRouter = class {
5180
5710
  if (stopAckExtension !== null) stopAckExtension();
5181
5711
  abortController.abort();
5182
5712
  emitRpcTimeout(subject, correlationId);
5183
- msg.term("Handler timeout");
5713
+ settleQuietly(logger5, `Failed to term ${subject}:`, () => {
5714
+ msg.term("Handler timeout");
5715
+ });
5184
5716
  reportHandlerCompleted(msg, startedAt, "terminated");
5185
5717
  }, timeout);
5186
5718
  return pending.then(
@@ -5189,7 +5721,9 @@ var RpcRouter = class {
5189
5721
  settled = true;
5190
5722
  clearTimeout(timeoutId);
5191
5723
  if (stopAckExtension !== null) stopAckExtension();
5192
- msg.ack();
5724
+ settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
5725
+ msg.ack();
5726
+ });
5193
5727
  publishReply(replyTo, correlationId, result);
5194
5728
  reportHandlerCompleted(msg, startedAt, "success");
5195
5729
  },
@@ -5211,14 +5745,28 @@ var RpcRouter = class {
5211
5745
  active--;
5212
5746
  drainBacklog();
5213
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
+ };
5214
5761
  const drainBacklog = () => {
5215
5762
  while (active < maxActive) {
5216
5763
  const next = backlog.shift();
5217
5764
  if (next === void 0) return;
5765
+ next.stopAckExtension?.();
5218
5766
  active++;
5219
- const result = handleSafe(next);
5767
+ const result = routeSafely(next.msg);
5220
5768
  if (result !== void 0) {
5221
- void result.finally(onAsyncDone);
5769
+ trackAsync(result, next.msg);
5222
5770
  } else {
5223
5771
  active--;
5224
5772
  }
@@ -5228,7 +5776,10 @@ var RpcRouter = class {
5228
5776
  this.subscription = this.messageProvider.commands$.subscribe({
5229
5777
  next: (msg) => {
5230
5778
  if (active >= maxActive) {
5231
- backlog.push(msg);
5779
+ backlog.push({
5780
+ msg,
5781
+ stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5782
+ });
5232
5783
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
5233
5784
  backlogWarned = true;
5234
5785
  logger5.warn(
@@ -5238,9 +5789,9 @@ var RpcRouter = class {
5238
5789
  return;
5239
5790
  }
5240
5791
  active++;
5241
- const result = handleSafe(msg);
5792
+ const result = routeSafely(msg);
5242
5793
  if (result !== void 0) {
5243
- void result.finally(onAsyncDone);
5794
+ trackAsync(result, msg);
5244
5795
  } else {
5245
5796
  active--;
5246
5797
  if (backlog.length > 0) drainBacklog();
@@ -5250,6 +5801,12 @@ var RpcRouter = class {
5250
5801
  logger5.error("Stream error in RPC router", err);
5251
5802
  }
5252
5803
  });
5804
+ this.subscription.add(() => {
5805
+ for (const queued of backlog) {
5806
+ queued.stopAckExtension?.();
5807
+ }
5808
+ backlog.length = 0;
5809
+ });
5253
5810
  }
5254
5811
  /** Stop routing and unsubscribe. */
5255
5812
  destroy() {
@@ -5320,7 +5877,7 @@ var JetstreamModule = class {
5320
5877
  return {
5321
5878
  module: JetstreamModule,
5322
5879
  global: true,
5323
- imports: options.metrics ? [JetstreamMetricsModule.forFeature(options.metrics)] : [],
5880
+ imports: [JetstreamMetricsModule.forFeature()],
5324
5881
  providers,
5325
5882
  exports: [
5326
5883
  JETSTREAM_CONNECTION,
@@ -5346,11 +5903,10 @@ var JetstreamModule = class {
5346
5903
  static forRootAsync(asyncOptions) {
5347
5904
  const asyncProviders = this.createAsyncOptionsProvider(asyncOptions);
5348
5905
  const coreProviders = this.createCoreDependentProviders();
5349
- const metricsImports = asyncOptions.metrics ? [JetstreamMetricsModule.forFeature(asyncOptions.metrics)] : [];
5350
5906
  return {
5351
5907
  module: JetstreamModule,
5352
5908
  global: true,
5353
- imports: [...asyncOptions.imports ?? [], ...metricsImports],
5909
+ imports: [...asyncOptions.imports ?? [], JetstreamMetricsModule.forFeature()],
5354
5910
  providers: [...asyncProviders, ...coreProviders],
5355
5911
  exports: [
5356
5912
  JETSTREAM_CONNECTION,
@@ -5533,7 +6089,7 @@ var JetstreamModule = class {
5533
6089
  ],
5534
6090
  useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
5535
6091
  if (options.consumer === false) return null;
5536
- const deadLetterConfig = options.onDeadLetter ? {
6092
+ const deadLetterConfig = options.onDeadLetter || options.dlq ? {
5537
6093
  maxDeliverByStream: /* @__PURE__ */ new Map(),
5538
6094
  onDeadLetter: options.onDeadLetter
5539
6095
  } : void 0;
@@ -5734,6 +6290,7 @@ JetstreamModule = __decorateClass([
5734
6290
  JetstreamHeader,
5735
6291
  JetstreamHealthIndicator,
5736
6292
  JetstreamModule,
6293
+ JetstreamProvisioningError,
5737
6294
  JetstreamRecord,
5738
6295
  JetstreamRecordBuilder,
5739
6296
  JetstreamStrategy,