@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.js CHANGED
@@ -144,7 +144,7 @@ var DEFAULT_ORDERED_STREAM_CONFIG = {
144
144
  };
145
145
  var DEFAULT_DLQ_STREAM_CONFIG = {
146
146
  ...baseStreamConfig,
147
- retention: RetentionPolicy.Workqueue,
147
+ retention: RetentionPolicy.Limits,
148
148
  allow_rollup_hdrs: false,
149
149
  max_consumers: 100,
150
150
  max_msg_size: 10 * MB,
@@ -208,6 +208,7 @@ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
208
208
  "x-reply-to" /* ReplyTo */,
209
209
  "x-error" /* Error */
210
210
  ]);
211
+ var NATS_CONTROL_HEADER_PREFIX = "nats-";
211
212
  var internalName = (name) => `${name}__microservice`;
212
213
  var buildSubject = (serviceName, kind, pattern) => `${internalName(serviceName)}.${kind}.${pattern}`;
213
214
  var buildBroadcastSubject = (pattern) => `broadcast.${pattern}`;
@@ -260,6 +261,9 @@ var ATTR_JETSTREAM_RPC_REPLY_ERROR_CODE = "jetstream.rpc.reply.error.code";
260
261
  var ATTR_JETSTREAM_PROVISIONING_ENTITY = "jetstream.provisioning.entity";
261
262
  var ATTR_JETSTREAM_PROVISIONING_ACTION = "jetstream.provisioning.action";
262
263
  var ATTR_JETSTREAM_PROVISIONING_NAME = "jetstream.provisioning.name";
264
+ var ATTR_JETSTREAM_PROVISIONING_MAX_BYTES = "jetstream.provisioning.max_bytes";
265
+ var ATTR_JETSTREAM_PROVISIONING_NUM_REPLICAS = "jetstream.provisioning.num_replicas";
266
+ var ATTR_JETSTREAM_PROVISIONING_RESERVATION = "jetstream.provisioning.reservation_bytes";
263
267
  var ATTR_JETSTREAM_SELF_HEALING_REASON = "jetstream.self_healing.reason";
264
268
  var ATTR_JETSTREAM_MIGRATION_REASON = "jetstream.migration.reason";
265
269
  var ATTR_JETSTREAM_DEAD_LETTER_REASON = "jetstream.dead_letter.reason";
@@ -529,7 +533,7 @@ var extractContext = (ctx, carrier, getter) => propagation.extract(ctx, carrier,
529
533
 
530
534
  // src/otel/tracer.ts
531
535
  import { trace } from "@opentelemetry/api";
532
- var PACKAGE_VERSION = true ? "2.11.1" : "0.0.0";
536
+ var PACKAGE_VERSION = true ? "2.12.1" : "0.0.0";
533
537
  var getTracer = () => trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
534
538
 
535
539
  // src/otel/carrier.ts
@@ -1129,7 +1133,10 @@ var withProvisioningSpan = (config, ctx, op) => wrapInfra(
1129
1133
  {
1130
1134
  [ATTR_JETSTREAM_PROVISIONING_ENTITY]: ctx.entity,
1131
1135
  [ATTR_JETSTREAM_PROVISIONING_ACTION]: ctx.action,
1132
- [ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name
1136
+ [ATTR_JETSTREAM_PROVISIONING_NAME]: ctx.name,
1137
+ [ATTR_JETSTREAM_PROVISIONING_MAX_BYTES]: ctx.maxBytes,
1138
+ [ATTR_JETSTREAM_PROVISIONING_NUM_REPLICAS]: ctx.numReplicas,
1139
+ [ATTR_JETSTREAM_PROVISIONING_RESERVATION]: ctx.reservation
1133
1140
  },
1134
1141
  op
1135
1142
  );
@@ -1305,7 +1312,13 @@ var JetstreamRecordBuilder = class {
1305
1312
  * lockstep. `RESERVED_HEADERS` is defined as an all-lowercase set.
1306
1313
  */
1307
1314
  validateHeaderKey(key) {
1308
- if (RESERVED_HEADERS.has(key.toLowerCase())) {
1315
+ const normalized = key.toLowerCase();
1316
+ if (normalized.startsWith(NATS_CONTROL_HEADER_PREFIX)) {
1317
+ throw new Error(
1318
+ `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.`
1319
+ );
1320
+ }
1321
+ if (RESERVED_HEADERS.has(normalized)) {
1309
1322
  throw new Error(
1310
1323
  `Header "${key}" is reserved by the JetStream transport and cannot be set manually. Reserved headers: ${[...RESERVED_HEADERS].join(", ")}`
1311
1324
  );
@@ -1451,13 +1464,18 @@ var JetstreamClient = class extends ClientProxy {
1451
1464
  async dispatchEvent(packet) {
1452
1465
  if (!this.readyForPublish) await this.connect();
1453
1466
  const { data, hdrs, messageId, schedule, ttl } = this.extractRecordData(packet.data);
1467
+ const publishKind = detectEventKind(packet.pattern);
1468
+ if (schedule && publishKind === "ordered" /* Ordered */) {
1469
+ throw new Error(
1470
+ `scheduleAt() is not supported for ordered events (pattern: ${packet.pattern}). Scheduled delivery is available for workqueue events and broadcasts.`
1471
+ );
1472
+ }
1454
1473
  const eventSubject = this.buildEventSubject(packet.pattern);
1455
1474
  const publishSubject = schedule ? this.buildScheduleSubject(eventSubject) : eventSubject;
1456
1475
  const msgHeaders = this.buildHeaders(hdrs, { subject: eventSubject });
1457
1476
  const encoded = this.codec.encode(data);
1458
1477
  const effectiveMsgId = messageId ?? nuid.next();
1459
1478
  const record = packet.data instanceof JetstreamRecord ? packet.data : new JetstreamRecord(data, /* @__PURE__ */ new Map());
1460
- const publishKind = detectEventKind(packet.pattern);
1461
1479
  const declaredPattern = declaredEventPattern(packet.pattern);
1462
1480
  const streamKind = eventStreamKind(publishKind);
1463
1481
  const startedAt = performance.now();
@@ -1489,10 +1507,10 @@ var JetstreamClient = class extends ClientProxy {
1489
1507
  const ack2 = await this.connection.getJetStreamClient().publish(publishSubject, encoded, {
1490
1508
  headers: msgHeaders,
1491
1509
  msgID: effectiveMsgId,
1492
- ttl,
1493
1510
  schedule: {
1494
1511
  specification: schedule.at,
1495
- target: eventSubject
1512
+ target: eventSubject,
1513
+ ttl
1496
1514
  }
1497
1515
  });
1498
1516
  warnIfDuplicate("scheduled", ack2);
@@ -1682,13 +1700,18 @@ var JetstreamClient = class extends ClientProxy {
1682
1700
  });
1683
1701
  return;
1684
1702
  }
1685
- await context6.with(
1703
+ const ack = await context6.with(
1686
1704
  spanHandle.activeContext,
1687
1705
  () => this.connection.getJetStreamClient().publish(subject, encoded, {
1688
1706
  headers: hdrs,
1689
1707
  msgID: messageId ?? nuid.next()
1690
1708
  })
1691
1709
  );
1710
+ if (ack.duplicate) {
1711
+ throw new Error(
1712
+ `Duplicate RPC publish for ${subject}: the messageId was already used within the stream dedup window, so the reply belongs to the original request`
1713
+ );
1714
+ }
1692
1715
  this.reportPublished(declaredPattern, "cmd" /* Command */, startedAt, "success");
1693
1716
  } catch (err) {
1694
1717
  const existingTimeout = this.pendingTimeouts.get(correlationId);
@@ -1858,13 +1881,17 @@ var JetstreamClient = class extends ClientProxy {
1858
1881
  * uses a separate `_sch` namespace that is NOT matched by any consumer filter.
1859
1882
  * NATS holds the message and publishes it to the target subject after the delay.
1860
1883
  *
1884
+ * A unique per-message suffix is appended because the server stores schedules
1885
+ * as rollup messages — one active schedule per subject (ADR-51). Without it,
1886
+ * concurrent schedules of the same pattern would silently replace each other.
1887
+ *
1861
1888
  * Examples:
1862
- * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder`
1863
- * - `broadcast.config.updated` → `broadcast._sch.config.updated`
1889
+ * - `{svc}__microservice.ev.order.reminder` → `{svc}__microservice._sch.order.reminder.<nuid>`
1890
+ * - `broadcast.config.updated` → `broadcast._sch.config.updated.<nuid>`
1864
1891
  */
1865
1892
  buildScheduleSubject(eventSubject) {
1866
1893
  if (eventSubject.startsWith("broadcast.")) {
1867
- return eventSubject.replace("broadcast.", "broadcast._sch.");
1894
+ return `${eventSubject.replace("broadcast.", "broadcast._sch.")}.${nuid.next()}`;
1868
1895
  }
1869
1896
  const targetPrefix = `${internalName(this.targetName)}.`;
1870
1897
  if (!eventSubject.startsWith(targetPrefix)) {
@@ -1876,7 +1903,7 @@ var JetstreamClient = class extends ClientProxy {
1876
1903
  throw new Error(`Event subject missing pattern segment: ${eventSubject}`);
1877
1904
  }
1878
1905
  const pattern = withoutPrefix.slice(dotIndex + 1);
1879
- return `${targetPrefix}_sch.${pattern}`;
1906
+ return `${targetPrefix}_sch.${pattern}.${nuid.next()}`;
1880
1907
  }
1881
1908
  };
1882
1909
 
@@ -1888,6 +1915,7 @@ var JsonCodec = class {
1888
1915
  return encoder.encode(JSON.stringify(data));
1889
1916
  }
1890
1917
  decode(data) {
1918
+ if (data.length === 0) return void 0;
1891
1919
  return JSON.parse(decoder.decode(data));
1892
1920
  }
1893
1921
  };
@@ -1901,6 +1929,7 @@ var MsgpackCodec = class {
1901
1929
  return this.packr.pack(data);
1902
1930
  }
1903
1931
  decode(data) {
1932
+ if (data.length === 0) return void 0;
1904
1933
  return this.packr.unpack(data);
1905
1934
  }
1906
1935
  };
@@ -2981,43 +3010,11 @@ var JetstreamStrategy = class extends Server {
2981
3010
  * Called by NestJS when `connectMicroservice()` is used, or internally by the module.
2982
3011
  */
2983
3012
  async listen(callback) {
2984
- if (this.started) {
2985
- this.logger.warn("listen() called more than once \u2014 ignoring");
2986
- return;
2987
- }
2988
- this.started = true;
2989
- this.patternRegistry.registerHandlers(this.getHandlers());
2990
- const { streams: streamKinds, durableConsumers: durableKinds } = this.resolveRequiredKinds();
2991
- if (streamKinds.length > 0) {
2992
- await this.streamProvider.ensureStreams(streamKinds);
2993
- if (durableKinds.length > 0) {
2994
- const consumers = await this.consumerProvider.ensureConsumers(durableKinds);
2995
- this.populateAckWaitMap(consumers);
2996
- this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
2997
- this.messageProvider.start(consumers);
2998
- }
2999
- if (this.patternRegistry.hasOrderedHandlers()) {
3000
- const orderedStreamName = this.streamProvider.getStreamName("ordered" /* Ordered */);
3001
- await this.messageProvider.startOrdered(
3002
- orderedStreamName,
3003
- this.patternRegistry.getOrderedSubjects(),
3004
- this.options.ordered
3005
- );
3006
- }
3007
- if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
3008
- this.eventRouter.start();
3009
- }
3010
- if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3011
- await this.rpcRouter.start();
3012
- }
3013
- }
3014
- if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3015
- await this.coreRpcServer.start();
3016
- }
3017
- if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
3018
- await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
3013
+ try {
3014
+ await this.doListen(callback);
3015
+ } catch (err) {
3016
+ callback(err);
3019
3017
  }
3020
- callback();
3021
3018
  }
3022
3019
  /** Stop all consumers, routers, subscriptions, and metadata heartbeat. Called during shutdown. */
3023
3020
  close() {
@@ -3076,6 +3073,33 @@ var JetstreamStrategy = class extends Server {
3076
3073
  getPatternRegistry() {
3077
3074
  return this.patternRegistry;
3078
3075
  }
3076
+ async doListen(callback) {
3077
+ if (this.started) {
3078
+ this.logger.warn("listen() called more than once \u2014 ignoring");
3079
+ return;
3080
+ }
3081
+ this.started = true;
3082
+ this.patternRegistry.registerHandlers(this.getHandlers());
3083
+ const { streams: streamKinds, durableConsumers: durableKinds } = this.resolveRequiredKinds();
3084
+ if (streamKinds.length > 0) {
3085
+ await this.streamProvider.ensureStreams(streamKinds);
3086
+ let consumers = null;
3087
+ if (durableKinds.length > 0) {
3088
+ consumers = await this.consumerProvider.ensureConsumers(durableKinds);
3089
+ this.populateAckWaitMap(consumers);
3090
+ this.eventRouter.updateMaxDeliverMap(this.buildMaxDeliverMap(consumers));
3091
+ }
3092
+ await this.startRouters();
3093
+ await this.startConsumption(consumers);
3094
+ }
3095
+ if (isCoreRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3096
+ await this.coreRpcServer.start();
3097
+ }
3098
+ if (this.metadataProvider && this.patternRegistry.hasMetadata()) {
3099
+ await this.metadataProvider.publish(this.patternRegistry.getMetadataEntries());
3100
+ }
3101
+ callback();
3102
+ }
3079
3103
  /** Determine which streams and durable consumers are needed. */
3080
3104
  resolveRequiredKinds() {
3081
3105
  const streams = [];
@@ -3097,7 +3121,29 @@ var JetstreamStrategy = class extends Server {
3097
3121
  }
3098
3122
  return { streams, durableConsumers };
3099
3123
  }
3100
- /** Populate the shared ack_wait map from actual NATS consumer configs. */
3124
+ /** Subscribe the event and RPC routers to the message subjects. */
3125
+ async startRouters() {
3126
+ if (this.patternRegistry.hasEventHandlers() || this.patternRegistry.hasBroadcastHandlers() || this.patternRegistry.hasOrderedHandlers()) {
3127
+ this.eventRouter.start();
3128
+ }
3129
+ if (isJetStreamRpcMode(this.options.rpc) && this.patternRegistry.hasRpcHandlers()) {
3130
+ await this.rpcRouter.start();
3131
+ }
3132
+ }
3133
+ /** Begin durable and ordered consumption; routers must already be subscribed. */
3134
+ async startConsumption(consumers) {
3135
+ if (consumers !== null) {
3136
+ this.messageProvider.start(consumers);
3137
+ }
3138
+ if (this.patternRegistry.hasOrderedHandlers()) {
3139
+ const orderedStreamName = this.streamProvider.getStreamName("ordered" /* Ordered */);
3140
+ await this.messageProvider.startOrdered(
3141
+ orderedStreamName,
3142
+ this.patternRegistry.getOrderedSubjects(),
3143
+ this.options.ordered
3144
+ );
3145
+ }
3146
+ }
3101
3147
  populateAckWaitMap(consumers) {
3102
3148
  for (const [kind, info] of consumers) {
3103
3149
  if (info.config.ack_wait) {
@@ -3336,6 +3382,15 @@ var serializeError = (err) => {
3336
3382
  return err;
3337
3383
  };
3338
3384
 
3385
+ // src/utils/settle-quietly.ts
3386
+ var settleQuietly = (logger5, label, action) => {
3387
+ try {
3388
+ action();
3389
+ } catch (err) {
3390
+ logger5.error(label, err);
3391
+ }
3392
+ };
3393
+
3339
3394
  // src/utils/unwrap-result.ts
3340
3395
  import { isObservable } from "rxjs";
3341
3396
  var unwrapResult = (result) => {
@@ -3501,16 +3556,168 @@ var CoreRpcServer = class {
3501
3556
 
3502
3557
  // src/server/infrastructure/stream.provider.ts
3503
3558
  import { Logger as Logger13 } from "@nestjs/common";
3504
- import { JetStreamApiError as JetStreamApiError2 } from "@nats-io/jetstream";
3559
+ import {
3560
+ JetStreamApiError as JetStreamApiError2,
3561
+ RetentionPolicy as RetentionPolicy2,
3562
+ StorageType as StorageType4
3563
+ } from "@nats-io/jetstream";
3505
3564
 
3506
3565
  // src/server/infrastructure/nats-error-codes.ts
3507
3566
  var NatsErrorCode = /* @__PURE__ */ ((NatsErrorCode2) => {
3508
3567
  NatsErrorCode2[NatsErrorCode2["ConsumerNotFound"] = 10014] = "ConsumerNotFound";
3509
3568
  NatsErrorCode2[NatsErrorCode2["ConsumerAlreadyExists"] = 10148] = "ConsumerAlreadyExists";
3510
3569
  NatsErrorCode2[NatsErrorCode2["StreamNotFound"] = 10059] = "StreamNotFound";
3570
+ NatsErrorCode2[NatsErrorCode2["StorageResourcesExceeded"] = 10047] = "StorageResourcesExceeded";
3571
+ NatsErrorCode2[NatsErrorCode2["NoSuitablePeers"] = 10005] = "NoSuitablePeers";
3511
3572
  return NatsErrorCode2;
3512
3573
  })(NatsErrorCode || {});
3513
3574
 
3575
+ // src/server/infrastructure/provisioning-budget.ts
3576
+ import { StorageType as StorageType2 } from "@nats-io/jetstream";
3577
+ var GIB = 1024 ** 3;
3578
+ var fmt = (bytes) => `${(bytes / GIB).toFixed(2)} GiB`;
3579
+ var resolveTierBudget = (info, replicas) => {
3580
+ const tier = info.tiers?.[`R${replicas}`];
3581
+ const limits = tier?.limits ?? info.limits;
3582
+ return {
3583
+ maxStorage: limits?.max_storage ?? 0,
3584
+ reserved: tier?.reserved_storage ?? info.reserved_storage ?? 0,
3585
+ tiered: tier !== void 0
3586
+ };
3587
+ };
3588
+ var groupByReplicas = (reservations) => {
3589
+ const groups = /* @__PURE__ */ new Map();
3590
+ for (const r of reservations) {
3591
+ if (r.storage !== StorageType2.File) continue;
3592
+ const prev = groups.get(r.numReplicas) ?? 0;
3593
+ groups.set(r.numReplicas, prev + r.maxBytes * r.numReplicas);
3594
+ }
3595
+ return groups;
3596
+ };
3597
+ var assertStorageBudget = async (jsm, serviceName, reservations, logger5) => {
3598
+ try {
3599
+ const info = await jsm.getAccountInfo();
3600
+ const groups = groupByReplicas(reservations);
3601
+ let limitNotSetWarned = false;
3602
+ let okReserved = 0;
3603
+ let anyWarned = false;
3604
+ for (const [replicas, incremental] of groups) {
3605
+ const { maxStorage, reserved, tiered } = resolveTierBudget(info, replicas);
3606
+ const tierNote = tiered ? ` (tier R${replicas})` : "";
3607
+ if (maxStorage <= 0) {
3608
+ if (!limitNotSetWarned) {
3609
+ limitNotSetWarned = true;
3610
+ logger5.warn(
3611
+ `Storage preflight for "${serviceName}": account file-storage limit not set (max_storage=${maxStorage}); the server max_file_store cannot be verified from the client.`
3612
+ );
3613
+ }
3614
+ continue;
3615
+ }
3616
+ const remaining = maxStorage - reserved;
3617
+ if (incremental > remaining) {
3618
+ anyWarned = true;
3619
+ logger5.warn(
3620
+ `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.`
3621
+ );
3622
+ continue;
3623
+ }
3624
+ okReserved += incremental;
3625
+ }
3626
+ if (!anyWarned && !limitNotSetWarned && okReserved > 0) {
3627
+ logger5.log(
3628
+ `Storage preflight for "${serviceName}" OK: reserving ~${fmt(okReserved)} across file-backed streams within account limits.`
3629
+ );
3630
+ }
3631
+ } catch (err) {
3632
+ logger5.debug(`Storage preflight skipped \u2014 account info unavailable: ${String(err)}`);
3633
+ }
3634
+ };
3635
+
3636
+ // src/server/infrastructure/provisioning-error.ts
3637
+ var REMEDIATION = {
3638
+ [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.",
3639
+ [10005 /* NoSuitablePeers */]: "Fewer healthy peers than `num_replicas`, or no peer has enough reserved storage headroom. Reduce replicas or add/repair cluster nodes."
3640
+ };
3641
+ var GENERIC_REMEDIATION = "Inspect the NATS server logs and JetStream account limits for the underlying cause.";
3642
+ var JetstreamProvisioningError = class _JetstreamProvisioningError extends Error {
3643
+ entity;
3644
+ target;
3645
+ kind;
3646
+ errCode;
3647
+ errDescription;
3648
+ remediation;
3649
+ maxBytes;
3650
+ numReplicas;
3651
+ reservation;
3652
+ constructor(fields) {
3653
+ const reservationNote = fields.reservation !== void 0 ? ` reservation=${fields.reservation}B (max_bytes=${fields.maxBytes}B \xD7 replicas=${fields.numReplicas}).` : "";
3654
+ super(
3655
+ `JetStream ${fields.entity} provisioning failed for "${fields.target}" (kind=${fields.kind}): ${fields.errDescription} [err_code=${fields.errCode}].${reservationNote} ${fields.remediation}`,
3656
+ { cause: fields.cause }
3657
+ );
3658
+ this.name = "JetstreamProvisioningError";
3659
+ this.entity = fields.entity;
3660
+ this.target = fields.target;
3661
+ this.kind = fields.kind;
3662
+ this.errCode = fields.errCode;
3663
+ this.errDescription = fields.errDescription;
3664
+ this.remediation = fields.remediation;
3665
+ this.maxBytes = fields.maxBytes;
3666
+ this.numReplicas = fields.numReplicas;
3667
+ this.reservation = fields.reservation;
3668
+ Object.setPrototypeOf(this, _JetstreamProvisioningError.prototype);
3669
+ }
3670
+ };
3671
+ var mapProvisioningError = (err, ctx) => {
3672
+ const api = err.apiError();
3673
+ const remediation = REMEDIATION[api.err_code] ?? GENERIC_REMEDIATION;
3674
+ const reservation = ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0;
3675
+ return new JetstreamProvisioningError({
3676
+ entity: ctx.entity,
3677
+ target: ctx.name,
3678
+ kind: ctx.kind,
3679
+ errCode: api.err_code,
3680
+ errDescription: api.description,
3681
+ remediation,
3682
+ maxBytes: ctx.maxBytes,
3683
+ numReplicas: ctx.numReplicas,
3684
+ reservation,
3685
+ cause: err
3686
+ });
3687
+ };
3688
+
3689
+ // src/server/infrastructure/provisioning-summary.ts
3690
+ import { StorageType as StorageType3 } from "@nats-io/jetstream";
3691
+ var GIB2 = 1024 ** 3;
3692
+ var NANOS_PER_SECOND = 1e9;
3693
+ var NANOS_PER_HOUR = 3600 * NANOS_PER_SECOND;
3694
+ var NANOS_PER_DAY = 86400 * NANOS_PER_SECOND;
3695
+ var formatBytes = (bytes) => {
3696
+ if (bytes <= 0) return "0 B";
3697
+ return `${(bytes / GIB2).toFixed(2)} GiB`;
3698
+ };
3699
+ var formatAge = (nanos) => {
3700
+ if (nanos <= 0) return "unlimited";
3701
+ if (nanos >= NANOS_PER_DAY) return `${(nanos / NANOS_PER_DAY).toFixed(1)}d`;
3702
+ if (nanos >= NANOS_PER_HOUR) return `${(nanos / NANOS_PER_HOUR).toFixed(1)}h`;
3703
+ return `${(nanos / NANOS_PER_SECOND).toFixed(0)}s`;
3704
+ };
3705
+ var formatProvisioningSummary = (serviceName, reservations) => {
3706
+ const lines = [`Provisioning ${reservations.length} stream(s) for "${serviceName}":`];
3707
+ let totalFileMaxBytes = 0;
3708
+ for (const r of reservations) {
3709
+ if (r.storage === StorageType3.File) totalFileMaxBytes += r.maxBytes;
3710
+ const clusterReservation = r.maxBytes * r.numReplicas;
3711
+ lines.push(
3712
+ ` \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)}`
3713
+ );
3714
+ }
3715
+ lines.push(
3716
+ ` \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.`
3717
+ );
3718
+ return lines.join("\n");
3719
+ };
3720
+
3514
3721
  // src/server/infrastructure/stream-config-diff.ts
3515
3722
  var TRANSPORT_CONTROLLED_PROPERTIES = /* @__PURE__ */ new Set([
3516
3723
  "retention"
@@ -3568,85 +3775,192 @@ var isEqual = (a, b) => {
3568
3775
 
3569
3776
  // src/server/infrastructure/stream-migration.ts
3570
3777
  import { Logger as Logger12 } from "@nestjs/common";
3571
- import { JetStreamApiError } from "@nats-io/jetstream";
3778
+ import {
3779
+ JetStreamApiError
3780
+ } from "@nats-io/jetstream";
3572
3781
  var MIGRATION_BACKUP_SUFFIX = "__migration_backup";
3573
3782
  var DEFAULT_SOURCING_TIMEOUT_MS = 3e4;
3574
3783
  var SOURCING_POLL_INTERVAL_MS = 100;
3784
+ var DEFAULT_PEER_WAIT_MS = 6e4;
3785
+ var ACTIVE_MIGRATION_GRACE_MS = 9e4;
3786
+ var MIGRATION_STARTED_AT_KEY = "nestjs-jetstream-migration-started-at";
3575
3787
  var StreamMigration = class {
3576
- constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS) {
3788
+ constructor(sourcingTimeoutMs = DEFAULT_SOURCING_TIMEOUT_MS, peerWaitMs = DEFAULT_PEER_WAIT_MS) {
3577
3789
  this.sourcingTimeoutMs = sourcingTimeoutMs;
3790
+ this.peerWaitMs = peerWaitMs;
3578
3791
  }
3579
3792
  logger = new Logger12("Jetstream:Stream");
3580
3793
  async migrate(jsm, streamName2, newConfig) {
3581
3794
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
3582
3795
  const startTime = Date.now();
3796
+ const peerFinished = await this.waitOutPeerMigration(jsm, backupName);
3583
3797
  const currentInfo = await jsm.streams.info(streamName2);
3584
- await this.cleanupOrphanedBackup(jsm, backupName);
3585
- const messageCount = currentInfo.state.messages;
3798
+ if (peerFinished && !compareStreamConfig(currentInfo.config, newConfig).hasImmutableChanges) {
3799
+ this.logger.log(`Stream ${streamName2}: migration completed by another instance`);
3800
+ await jsm.streams.update(streamName2, newConfig);
3801
+ return;
3802
+ }
3586
3803
  this.logger.log(`Stream ${streamName2}: destructive migration started`);
3587
3804
  let originalDeleted = false;
3805
+ let drainedCount = 0;
3588
3806
  try {
3589
- if (messageCount > 0) {
3590
- this.logger.log(` Phase 1/4: Backing up ${messageCount} messages \u2192 ${backupName}`);
3807
+ this.logger.log(` Phase 1/4: Quiescing ${streamName2} (publishes rejected during migration)`);
3808
+ await jsm.streams.update(streamName2, { ...currentInfo.config, subjects: [] });
3809
+ drainedCount = (await jsm.streams.info(streamName2)).state.messages;
3810
+ if (drainedCount > 0) {
3811
+ this.logger.log(` Phase 2/4: Backing up ${drainedCount} messages \u2192 ${backupName}`);
3591
3812
  await jsm.streams.add({
3592
3813
  ...currentInfo.config,
3593
3814
  name: backupName,
3594
3815
  subjects: [],
3595
- sources: [{ name: streamName2 }]
3816
+ sources: [{ name: streamName2 }],
3817
+ metadata: { [MIGRATION_STARTED_AT_KEY]: (/* @__PURE__ */ new Date()).toISOString() }
3596
3818
  });
3597
- await this.waitForSourcing(jsm, backupName, messageCount);
3819
+ await this.waitForSourceDrained(jsm, backupName, streamName2, drainedCount);
3598
3820
  }
3599
- this.logger.log(` Phase 2/4: Deleting old stream`);
3821
+ this.logger.log(` Phase 3/4: Recreating ${streamName2} with the new config`);
3600
3822
  await jsm.streams.delete(streamName2);
3601
3823
  originalDeleted = true;
3602
- this.logger.log(` Phase 3/4: Creating stream with new config`);
3603
3824
  await jsm.streams.add(newConfig);
3604
- if (messageCount > 0) {
3605
- const backupInfo = await jsm.streams.info(backupName);
3606
- await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
3607
- this.logger.log(` Phase 4/4: Restoring ${messageCount} messages from backup`);
3608
- await jsm.streams.update(streamName2, {
3609
- ...newConfig,
3610
- sources: [{ name: backupName }]
3611
- });
3612
- await this.waitForSourcing(jsm, streamName2, messageCount);
3613
- await jsm.streams.update(streamName2, { ...newConfig, sources: [] });
3614
- await jsm.streams.delete(backupName);
3825
+ if (drainedCount > 0) {
3826
+ this.logger.log(` Phase 4/4: Restoring ${drainedCount} messages from backup`);
3827
+ await this.restoreFromBackup(jsm, streamName2, newConfig, backupName);
3615
3828
  }
3616
3829
  } catch (err) {
3617
- if (originalDeleted && messageCount > 0) {
3830
+ if (originalDeleted) {
3618
3831
  this.logger.error(
3619
- `Migration failed after deleting original stream. Backup ${backupName} preserved for manual recovery.`
3832
+ `Migration of ${streamName2} failed after the original was deleted. Backup ${backupName} preserved \u2014 restoration resumes on the next startup.`
3620
3833
  );
3621
3834
  } else {
3622
- await this.cleanupOrphanedBackup(jsm, backupName);
3835
+ await this.rollbackBeforeDelete(jsm, streamName2, currentInfo, backupName);
3623
3836
  }
3624
3837
  throw err;
3625
3838
  }
3626
3839
  const durationMs = Date.now() - startTime;
3627
3840
  this.logger.log(
3628
- `Stream ${streamName2}: migration complete (${messageCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
3841
+ `Stream ${streamName2}: migration complete (${drainedCount} messages preserved, took ${(durationMs / 1e3).toFixed(1)}s)`
3842
+ );
3843
+ }
3844
+ /**
3845
+ * Finish a migration a previous process left unfinished; a backup fresh
3846
+ * enough to belong to a live peer migration is left alone.
3847
+ */
3848
+ async recoverInterrupted(jsm, streamName2, desiredConfig) {
3849
+ const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
3850
+ const backupInfo = await this.tryInfo(jsm, backupName);
3851
+ if (backupInfo === null) return false;
3852
+ if (this.isPeerMigrationActive(backupInfo)) return false;
3853
+ const streamInfo = await this.tryInfo(jsm, streamName2);
3854
+ if (streamInfo === null) {
3855
+ this.logger.warn(`Stream ${streamName2}: resuming interrupted migration from ${backupName}`);
3856
+ await jsm.streams.add(desiredConfig);
3857
+ if (backupInfo.state.messages > 0) {
3858
+ await this.restoreFromBackup(jsm, streamName2, desiredConfig, backupName);
3859
+ } else {
3860
+ await jsm.streams.delete(backupName);
3861
+ }
3862
+ return true;
3863
+ }
3864
+ const hasBackupSource = (streamInfo.config.sources ?? []).some((s) => s.name === backupName);
3865
+ if (hasBackupSource) {
3866
+ this.logger.warn(`Stream ${streamName2}: finishing interrupted restore from ${backupName}`);
3867
+ await this.waitForSourceDrained(jsm, streamName2, backupName, backupInfo.state.messages);
3868
+ await jsm.streams.delete(backupName);
3869
+ await jsm.streams.update(streamName2, { ...streamInfo.config, sources: [] });
3870
+ return true;
3871
+ }
3872
+ if (backupInfo.state.messages === 0) {
3873
+ this.logger.warn(`Removing empty migration backup ${backupName}`);
3874
+ await jsm.streams.delete(backupName);
3875
+ return true;
3876
+ }
3877
+ this.logger.warn(
3878
+ `Stream ${streamName2}: restoring ${backupInfo.state.messages} messages from stale ${backupName}`
3879
+ );
3880
+ await this.restoreFromBackup(
3881
+ jsm,
3882
+ streamName2,
3883
+ { ...streamInfo.config, name: streamName2, subjects: streamInfo.config.subjects },
3884
+ backupName
3629
3885
  );
3886
+ return true;
3630
3887
  }
3631
- async waitForSourcing(jsm, streamName2, expectedCount) {
3888
+ /** Attach the backup as a source, drain it fully, then clean up. */
3889
+ async restoreFromBackup(jsm, streamName2, streamConfig, backupName) {
3890
+ const backupInfo = await jsm.streams.info(backupName);
3891
+ if ((backupInfo.config.sources ?? []).length > 0) {
3892
+ await jsm.streams.update(backupName, { ...backupInfo.config, sources: [] });
3893
+ }
3894
+ await jsm.streams.update(streamName2, { ...streamConfig, sources: [{ name: backupName }] });
3895
+ await this.waitForSourceDrained(jsm, streamName2, backupName, backupInfo.state.messages);
3896
+ await jsm.streams.delete(backupName);
3897
+ await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
3898
+ }
3899
+ /**
3900
+ * Lag-based drain check — live publishes cannot fake completion. A fresh
3901
+ * source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
3902
+ * hence the active guard.
3903
+ */
3904
+ async waitForSourceDrained(jsm, streamName2, sourceName, minimumMessages) {
3632
3905
  const deadline = Date.now() + this.sourcingTimeoutMs;
3633
3906
  while (Date.now() < deadline) {
3634
3907
  const info = await jsm.streams.info(streamName2);
3635
- if (info.state.messages >= expectedCount) return;
3908
+ const source = (info.sources ?? []).find((s) => s.name === sourceName);
3909
+ if (source !== void 0 && source.active >= 0 && source.lag === 0 && info.state.messages >= minimumMessages) {
3910
+ return;
3911
+ }
3636
3912
  await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS));
3637
3913
  }
3638
3914
  throw new Error(
3639
- `Stream sourcing timeout: ${streamName2} has not reached ${expectedCount} messages within ${this.sourcingTimeoutMs / 1e3}s`
3915
+ `Stream sourcing timeout: ${sourceName} has not drained into ${streamName2} within ${this.sourcingTimeoutMs / 1e3}s. The backup is preserved; restoration resumes on the next startup.`
3916
+ );
3917
+ }
3918
+ /**
3919
+ * A backup present at migrate() start is a live peer migration — wait it
3920
+ * out. Stale leftovers were already handled by recoverInterrupted().
3921
+ */
3922
+ async waitOutPeerMigration(jsm, backupName) {
3923
+ if (await this.tryInfo(jsm, backupName) === null) return false;
3924
+ this.logger.warn(
3925
+ `Migration backup ${backupName} exists \u2014 another instance appears to be migrating; waiting`
3926
+ );
3927
+ const deadline = Date.now() + this.peerWaitMs;
3928
+ while (Date.now() < deadline) {
3929
+ if (await this.tryInfo(jsm, backupName) === null) return true;
3930
+ await new Promise((r) => setTimeout(r, SOURCING_POLL_INTERVAL_MS * 5));
3931
+ }
3932
+ throw new Error(
3933
+ `Migration backup ${backupName} did not clear within ${this.peerWaitMs / 1e3}s. If no other instance is migrating, recover or remove the backup manually.`
3640
3934
  );
3641
3935
  }
3642
- async cleanupOrphanedBackup(jsm, backupName) {
3936
+ /** Failure before the original was deleted: undo the quiesce, drop our backup. */
3937
+ async rollbackBeforeDelete(jsm, streamName2, originalInfo, backupName) {
3643
3938
  try {
3644
- await jsm.streams.info(backupName);
3645
- this.logger.warn(`Found orphaned migration backup stream: ${backupName}, cleaning up`);
3646
- await jsm.streams.delete(backupName);
3939
+ await jsm.streams.update(streamName2, { ...originalInfo.config });
3940
+ const backupInfo = await this.tryInfo(jsm, backupName);
3941
+ if (backupInfo !== null) {
3942
+ await jsm.streams.delete(backupName);
3943
+ }
3944
+ } catch (rollbackErr) {
3945
+ this.logger.error(
3946
+ `Rollback of ${streamName2} after a failed migration also failed \u2014 the stream may be left quiesced:`,
3947
+ rollbackErr
3948
+ );
3949
+ }
3950
+ }
3951
+ isPeerMigrationActive(backupInfo) {
3952
+ const startedAt = backupInfo.config.metadata?.[MIGRATION_STARTED_AT_KEY];
3953
+ if (!startedAt) return false;
3954
+ const startedMs = Date.parse(startedAt);
3955
+ if (Number.isNaN(startedMs)) return false;
3956
+ return Date.now() - startedMs < ACTIVE_MIGRATION_GRACE_MS;
3957
+ }
3958
+ async tryInfo(jsm, name) {
3959
+ try {
3960
+ return await jsm.streams.info(name);
3647
3961
  } catch (err) {
3648
3962
  if (err instanceof JetStreamApiError && err.apiError().err_code === 10059 /* StreamNotFound */) {
3649
- return;
3963
+ return null;
3650
3964
  }
3651
3965
  throw err;
3652
3966
  }
@@ -3654,6 +3968,17 @@ var StreamMigration = class {
3654
3968
  };
3655
3969
 
3656
3970
  // src/server/infrastructure/stream.provider.ts
3971
+ var subjectCovers = (broad, narrow) => {
3972
+ if (broad === narrow) return false;
3973
+ const broadTokens = broad.split(".");
3974
+ const narrowTokens = narrow.split(".");
3975
+ for (let i = 0; i < broadTokens.length; i += 1) {
3976
+ if (broadTokens[i] === ">") return i < narrowTokens.length;
3977
+ if (i >= narrowTokens.length || narrowTokens[i] === ">") return false;
3978
+ if (broadTokens[i] !== "*" && broadTokens[i] !== narrowTokens[i]) return false;
3979
+ }
3980
+ return broadTokens.length === narrowTokens.length;
3981
+ };
3657
3982
  var StreamProvider = class {
3658
3983
  constructor(options, connection) {
3659
3984
  this.options = options;
@@ -3677,6 +4002,15 @@ var StreamProvider = class {
3677
4002
  */
3678
4003
  async ensureStreams(kinds) {
3679
4004
  const jsm = await this.connection.getJetStreamManager();
4005
+ const reservations = kinds.map((kind) => this.buildReservation(kind, this.buildConfig(kind)));
4006
+ if (this.options.dlq) {
4007
+ reservations.push(this.buildReservation("dlq", this.buildDlqConfig()));
4008
+ }
4009
+ this.logger.log(`
4010
+ ${formatProvisioningSummary(this.options.name, reservations)}`);
4011
+ if (this.options.provisioning?.preflightStorageCheck) {
4012
+ await assertStorageBudget(jsm, this.options.name, reservations, this.logger);
4013
+ }
3680
4014
  await Promise.all(kinds.map((kind) => this.ensureStream(jsm, kind)));
3681
4015
  if (this.options.dlq) {
3682
4016
  await this.ensureDlqStream(jsm);
@@ -3699,13 +4033,8 @@ var StreamProvider = class {
3699
4033
  }
3700
4034
  case "cmd" /* Command */:
3701
4035
  return [`${name}.${"cmd" /* Command */}.>`];
3702
- case "broadcast" /* Broadcast */: {
3703
- const subjects = ["broadcast.>"];
3704
- if (this.isSchedulingEnabled(kind)) {
3705
- subjects.push("broadcast._sch.>");
3706
- }
3707
- return subjects;
3708
- }
4036
+ case "broadcast" /* Broadcast */:
4037
+ return ["broadcast.>"];
3709
4038
  case "ordered" /* Ordered */:
3710
4039
  return [`${name}.${"ordered" /* Ordered */}.>`];
3711
4040
  }
@@ -3713,6 +4042,7 @@ var StreamProvider = class {
3713
4042
  /** Ensure a single stream exists, creating or updating as needed. */
3714
4043
  async ensureStream(jsm, kind) {
3715
4044
  const config = this.buildConfig(kind);
4045
+ const ctx = this.errorContext(kind, config);
3716
4046
  return withProvisioningSpan(
3717
4047
  this.otel,
3718
4048
  {
@@ -3720,17 +4050,21 @@ var StreamProvider = class {
3720
4050
  endpoint: this.otelEndpoint,
3721
4051
  entity: "stream",
3722
4052
  name: config.name,
3723
- action: "ensure"
4053
+ action: "ensure",
4054
+ maxBytes: ctx.maxBytes,
4055
+ numReplicas: ctx.numReplicas,
4056
+ reservation: ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0
3724
4057
  },
3725
4058
  async () => {
3726
4059
  this.logger.log(`Ensuring stream: ${config.name}`);
4060
+ await this.migration.recoverInterrupted(jsm, config.name, config);
3727
4061
  try {
3728
4062
  const currentInfo = await jsm.streams.info(config.name);
3729
- return await this.handleExistingStream(jsm, currentInfo, config);
4063
+ return await this.handleExistingStream(jsm, currentInfo, config, ctx);
3730
4064
  } catch (err) {
3731
4065
  if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
3732
4066
  this.logger.log(`Creating stream: ${config.name}`);
3733
- return await jsm.streams.add(config);
4067
+ return await this.runStreamOp(ctx, () => jsm.streams.add(config));
3734
4068
  }
3735
4069
  throw err;
3736
4070
  }
@@ -3740,6 +4074,7 @@ var StreamProvider = class {
3740
4074
  /** Ensure a dead-letter queue stream exists, creating or updating as needed. */
3741
4075
  async ensureDlqStream(jsm) {
3742
4076
  const config = this.buildDlqConfig();
4077
+ const ctx = this.errorContext("dlq", config);
3743
4078
  return withProvisioningSpan(
3744
4079
  this.otel,
3745
4080
  {
@@ -3747,24 +4082,31 @@ var StreamProvider = class {
3747
4082
  endpoint: this.otelEndpoint,
3748
4083
  entity: "stream",
3749
4084
  name: config.name,
3750
- action: "ensure"
4085
+ action: "ensure",
4086
+ maxBytes: ctx.maxBytes,
4087
+ numReplicas: ctx.numReplicas,
4088
+ reservation: ctx.maxBytes !== void 0 && ctx.numReplicas !== void 0 ? ctx.maxBytes * ctx.numReplicas : void 0
3751
4089
  },
3752
4090
  async () => {
3753
4091
  this.logger.log(`Ensuring DLQ stream: ${config.name}`);
3754
4092
  try {
3755
4093
  const currentInfo = await jsm.streams.info(config.name);
3756
- return await this.handleExistingStream(jsm, currentInfo, config);
4094
+ return await this.handleExistingStream(jsm, currentInfo, config, ctx);
3757
4095
  } catch (err) {
3758
4096
  if (err instanceof JetStreamApiError2 && err.apiError().err_code === 10059 /* StreamNotFound */) {
3759
4097
  this.logger.log(`Creating DLQ stream: ${config.name}`);
3760
- return await jsm.streams.add(config);
4098
+ return await this.runStreamOp(ctx, () => jsm.streams.add(config));
3761
4099
  }
3762
4100
  throw err;
3763
4101
  }
3764
4102
  }
3765
4103
  );
3766
4104
  }
3767
- async handleExistingStream(jsm, currentInfo, config) {
4105
+ async handleExistingStream(jsm, currentInfo, config, ctx) {
4106
+ if (this.isSharedStream(config.name)) {
4107
+ const merged = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
4108
+ config.subjects = merged.filter((s) => !merged.some((other) => subjectCovers(other, s)));
4109
+ }
3768
4110
  const diff = compareStreamConfig(currentInfo.config, config);
3769
4111
  if (!diff.hasChanges) {
3770
4112
  this.logger.debug(`Stream ${config.name}: no config changes`);
@@ -3779,7 +4121,7 @@ var StreamProvider = class {
3779
4121
  }
3780
4122
  if (!diff.hasImmutableChanges) {
3781
4123
  this.logger.debug(`Stream exists, updating: ${config.name}`);
3782
- return await jsm.streams.update(config.name, config);
4124
+ return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, config));
3783
4125
  }
3784
4126
  if (!this.options.allowDestructiveMigration) {
3785
4127
  this.logger.warn(
@@ -3787,10 +4129,15 @@ var StreamProvider = class {
3787
4129
  );
3788
4130
  if (diff.hasMutableChanges) {
3789
4131
  const mutableConfig = this.buildMutableOnlyConfig(config, currentInfo.config, diff);
3790
- return await jsm.streams.update(config.name, mutableConfig);
4132
+ return await this.runStreamOp(ctx, () => jsm.streams.update(config.name, mutableConfig));
3791
4133
  }
3792
4134
  return currentInfo;
3793
4135
  }
4136
+ if (this.isSharedStream(config.name)) {
4137
+ throw new Error(
4138
+ `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.`
4139
+ );
4140
+ }
3794
4141
  await withMigrationSpan(
3795
4142
  this.otel,
3796
4143
  {
@@ -3829,11 +4176,47 @@ var StreamProvider = class {
3829
4176
  }
3830
4177
  }
3831
4178
  }
4179
+ buildReservation(kind, config) {
4180
+ const mb = config.max_bytes;
4181
+ return {
4182
+ kind,
4183
+ name: config.name,
4184
+ storage: config.storage ?? StorageType4.File,
4185
+ numReplicas: config.num_replicas ?? 1,
4186
+ maxBytes: mb !== void 0 && mb >= 0 ? mb : 0,
4187
+ // NATS uses -1 for unlimited
4188
+ maxAge: config.max_age ?? 0,
4189
+ retention: config.retention ?? RetentionPolicy2.Limits
4190
+ };
4191
+ }
4192
+ errorContext(kind, config) {
4193
+ return {
4194
+ entity: "stream",
4195
+ name: config.name,
4196
+ kind,
4197
+ maxBytes: config.max_bytes,
4198
+ numReplicas: config.num_replicas ?? 1
4199
+ };
4200
+ }
4201
+ async runStreamOp(ctx, op) {
4202
+ try {
4203
+ return await op();
4204
+ } catch (err) {
4205
+ if (err instanceof JetStreamApiError2) {
4206
+ throw mapProvisioningError(err, ctx);
4207
+ }
4208
+ throw err;
4209
+ }
4210
+ }
4211
+ /** The broadcast stream is global — every service in the cluster shares it. */
4212
+ isSharedStream(name) {
4213
+ return name === this.getStreamName("broadcast" /* Broadcast */);
4214
+ }
3832
4215
  /** Build the full stream config by merging defaults with user overrides. */
3833
4216
  buildConfig(kind) {
3834
4217
  const name = this.getStreamName(kind);
3835
4218
  const subjects = this.getSubjects(kind);
3836
- const description = `JetStream ${kind} stream for ${this.options.name}`;
4219
+ const description = kind === "broadcast" /* Broadcast */ ? "JetStream broadcast stream (shared across services)" : `JetStream ${kind} stream for ${this.options.name}`;
3837
4220
  const defaults = this.getDefaults(kind);
3838
4221
  const overrides = this.getOverrides(kind);
3839
4222
  return {
@@ -3975,15 +4358,16 @@ var ConsumerProvider = class {
3975
4358
  },
3976
4359
  async () => {
3977
4360
  this.logger.log(`Ensuring consumer: ${name} on stream: ${stream}`);
4361
+ const ctx = { entity: "consumer", name, kind };
3978
4362
  try {
3979
4363
  await jsm.consumers.info(stream, name);
3980
4364
  this.logger.debug(`Consumer exists, updating: ${name}`);
3981
- return await jsm.consumers.update(stream, name, config);
4365
+ return await this.runConsumerOp(ctx, () => jsm.consumers.update(stream, name, config));
3982
4366
  } catch (err) {
3983
4367
  if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
3984
4368
  throw err;
3985
4369
  }
3986
- return await this.createConsumer(jsm, stream, name, config);
4370
+ return await this.createConsumer(jsm, stream, name, kind, config);
3987
4371
  }
3988
4372
  }
3989
4373
  );
@@ -4023,7 +4407,7 @@ var ConsumerProvider = class {
4023
4407
  if (!(err instanceof JetStreamApiError3) || err.apiError().err_code !== 10014 /* ConsumerNotFound */) {
4024
4408
  throw err;
4025
4409
  }
4026
- return await this.createConsumer(jsm, stream, name, config);
4410
+ return await this.createConsumer(jsm, stream, name, kind, config);
4027
4411
  }
4028
4412
  }
4029
4413
  );
@@ -4050,8 +4434,9 @@ var ConsumerProvider = class {
4050
4434
  /**
4051
4435
  * Create a consumer, handling the race where another pod creates it first.
4052
4436
  */
4053
- async createConsumer(jsm, stream, name, config) {
4437
+ async createConsumer(jsm, stream, name, kind, config) {
4054
4438
  this.logger.log(`Creating consumer: ${name}`);
4439
+ const ctx = { entity: "consumer", name, kind };
4055
4440
  try {
4056
4441
  return await jsm.consumers.add(stream, config);
4057
4442
  } catch (addErr) {
@@ -4059,9 +4444,22 @@ var ConsumerProvider = class {
4059
4444
  this.logger.debug(`Consumer ${name} created by another pod, using existing`);
4060
4445
  return await jsm.consumers.info(stream, name);
4061
4446
  }
4447
+ if (addErr instanceof JetStreamApiError3) {
4448
+ throw mapProvisioningError(addErr, ctx);
4449
+ }
4062
4450
  throw addErr;
4063
4451
  }
4064
4452
  }
4453
+ async runConsumerOp(ctx, op) {
4454
+ try {
4455
+ return await op();
4456
+ } catch (err) {
4457
+ if (err instanceof JetStreamApiError3) {
4458
+ throw mapProvisioningError(err, ctx);
4459
+ }
4460
+ throw err;
4461
+ }
4462
+ }
4065
4463
  /** Build consumer config by merging defaults with user overrides. */
4066
4464
  // eslint-disable-next-line @typescript-eslint/naming-convention -- NATS API uses snake_case
4067
4465
  buildConfig(kind) {
@@ -4522,6 +4920,7 @@ var MetadataProvider = class {
4522
4920
  // src/server/routing/event.router.ts
4523
4921
  import { Logger as Logger17 } from "@nestjs/common";
4524
4922
  import { headers as natsHeaders3 } from "@nats-io/transport-node";
4923
+ var DLQ_PUBLISH_ATTEMPTS = 3;
4525
4924
  var eventConsumeKindFor = (kind) => {
4526
4925
  if (kind === "broadcast" /* Broadcast */) return "broadcast" /* Broadcast */;
4527
4926
  if (kind === "ordered" /* Ordered */) return "ordered" /* Ordered */;
@@ -4608,33 +5007,80 @@ var EventRouter = class {
4608
5007
  return msg.info.deliveryCount >= maxDeliver;
4609
5008
  };
4610
5009
  const handleDeadLetter = hasDlqCheck ? (msg, data, err) => this.handleDeadLetter(msg, data, err) : null;
4611
- const settleSuccess = (msg, ctx) => {
4612
- if (ctx.shouldTerminate) msg.term(ctx.terminateReason);
4613
- else if (ctx.shouldRetry) msg.nak(ctx.retryDelay);
4614
- else msg.ack();
5010
+ const settleSuccess = (msg, ctx, data) => {
5011
+ if (ctx.shouldTerminate) {
5012
+ settleQuietly(logger5, `Failed to term ${msg.subject}:`, () => {
5013
+ msg.term(ctx.terminateReason);
5014
+ });
5015
+ return void 0;
5016
+ }
5017
+ if (ctx.shouldRetry) {
5018
+ if (handleDeadLetter !== null && isDeadLetter(msg)) {
5019
+ return handleDeadLetter(
5020
+ msg,
5021
+ data,
5022
+ new Error("Retry requested on the final delivery attempt")
5023
+ );
5024
+ }
5025
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5026
+ msg.nak(ctx.retryDelay);
5027
+ });
5028
+ return void 0;
5029
+ }
5030
+ settleQuietly(logger5, `Failed to ack ${msg.subject}:`, () => {
5031
+ msg.ack();
5032
+ });
5033
+ return void 0;
4615
5034
  };
4616
5035
  const settleFailure = async (msg, data, err) => {
4617
5036
  if (handleDeadLetter !== null && isDeadLetter(msg)) {
4618
5037
  await handleDeadLetter(msg, data, err);
4619
5038
  return;
4620
5039
  }
4621
- msg.nak();
5040
+ settleQuietly(logger5, `Failed to nak ${msg.subject}:`, () => {
5041
+ msg.nak();
5042
+ });
5043
+ };
5044
+ const captureUnroutable = (capture, msg, err) => {
5045
+ let data;
5046
+ try {
5047
+ data = codec.decode(msg.data);
5048
+ } catch {
5049
+ data = void 0;
5050
+ }
5051
+ return capture(msg, data, err).catch((captureErr) => {
5052
+ logger5.error(`Dead-letter capture failed for unroutable ${msg.subject}:`, captureErr);
5053
+ });
4622
5054
  };
4623
5055
  const resolveEvent = (msg) => {
4624
5056
  const subject = msg.subject;
4625
5057
  try {
4626
5058
  const handler = patternRegistry.getHandler(subject);
4627
5059
  if (!handler) {
4628
- msg.term(`No handler for event: ${subject}`);
4629
5060
  logger5.error(`No handler for subject: ${subject}`);
5061
+ if (handleDeadLetter !== null) {
5062
+ return captureUnroutable(
5063
+ handleDeadLetter,
5064
+ msg,
5065
+ new Error(`No handler for event: ${subject}`)
5066
+ );
5067
+ }
5068
+ msg.term(`No handler for event: ${subject}`);
4630
5069
  return null;
4631
5070
  }
4632
5071
  let data;
4633
5072
  try {
4634
5073
  data = codec.decode(msg.data);
4635
5074
  } catch (err) {
4636
- msg.term("Decode error");
4637
5075
  logger5.error(`Decode error for ${subject}:`, err);
5076
+ if (handleDeadLetter !== null) {
5077
+ return captureUnroutable(
5078
+ handleDeadLetter,
5079
+ msg,
5080
+ new Error(`Decode error: ${err instanceof Error ? err.message : String(err)}`)
5081
+ );
5082
+ }
5083
+ msg.term("Decode error");
4638
5084
  return null;
4639
5085
  }
4640
5086
  eventBus.emitMessageRouted(subject, "event" /* Event */);
@@ -4657,6 +5103,7 @@ var EventRouter = class {
4657
5103
  const handleSafe = (msg) => {
4658
5104
  const resolved = resolveEvent(msg);
4659
5105
  if (resolved === null) return void 0;
5106
+ if (isPromiseLike2(resolved)) return resolved;
4660
5107
  const { handler, data } = resolved;
4661
5108
  const ctx = new RpcContext([msg]);
4662
5109
  const stopAckExtension = hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null;
@@ -4689,16 +5136,24 @@ var EventRouter = class {
4689
5136
  });
4690
5137
  }
4691
5138
  if (!isPromiseLike2(pending)) {
4692
- settleSuccess(msg, ctx);
5139
+ const settled = settleSuccess(msg, ctx, data);
4693
5140
  reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4694
- if (stopAckExtension !== null) stopAckExtension();
4695
- return void 0;
5141
+ if (settled === void 0) {
5142
+ if (stopAckExtension !== null) stopAckExtension();
5143
+ return void 0;
5144
+ }
5145
+ return settled.finally(() => {
5146
+ if (stopAckExtension !== null) stopAckExtension();
5147
+ });
4696
5148
  }
4697
5149
  return pending.then(
4698
- () => {
4699
- settleSuccess(msg, ctx);
4700
- reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
4701
- if (stopAckExtension !== null) stopAckExtension();
5150
+ async () => {
5151
+ try {
5152
+ await settleSuccess(msg, ctx, data);
5153
+ reportHandlerCompleted(msg, startedAt, statusForContext(ctx));
5154
+ } finally {
5155
+ if (stopAckExtension !== null) stopAckExtension();
5156
+ }
4702
5157
  },
4703
5158
  async (err) => {
4704
5159
  eventBus.emit(
@@ -4792,14 +5247,28 @@ var EventRouter = class {
4792
5247
  active--;
4793
5248
  drainBacklog();
4794
5249
  };
5250
+ const routeSafely = (msg) => {
5251
+ try {
5252
+ return route(msg);
5253
+ } catch (err) {
5254
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5255
+ return void 0;
5256
+ }
5257
+ };
5258
+ const trackAsync = (result, msg) => {
5259
+ void result.catch((err) => {
5260
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5261
+ }).finally(onAsyncDone);
5262
+ };
4795
5263
  const drainBacklog = () => {
4796
5264
  while (active < maxActive) {
4797
5265
  const next = backlog.shift();
4798
5266
  if (next === void 0) return;
5267
+ next.stopAckExtension?.();
4799
5268
  active++;
4800
- const result = route(next);
5269
+ const result = routeSafely(next.msg);
4801
5270
  if (result !== void 0) {
4802
- void result.finally(onAsyncDone);
5271
+ trackAsync(result, next.msg);
4803
5272
  } else {
4804
5273
  active--;
4805
5274
  }
@@ -4809,7 +5278,10 @@ var EventRouter = class {
4809
5278
  const subscription = stream$.subscribe({
4810
5279
  next: (msg) => {
4811
5280
  if (active >= maxActive) {
4812
- backlog.push(msg);
5281
+ backlog.push({
5282
+ msg,
5283
+ stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5284
+ });
4813
5285
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
4814
5286
  backlogWarned = true;
4815
5287
  logger5.warn(
@@ -4819,9 +5291,9 @@ var EventRouter = class {
4819
5291
  return;
4820
5292
  }
4821
5293
  active++;
4822
- const result = route(msg);
5294
+ const result = routeSafely(msg);
4823
5295
  if (result !== void 0) {
4824
- void result.finally(onAsyncDone);
5296
+ trackAsync(result, msg);
4825
5297
  } else {
4826
5298
  active--;
4827
5299
  if (backlog.length > 0) drainBacklog();
@@ -4831,6 +5303,12 @@ var EventRouter = class {
4831
5303
  logger5.error(`Stream error in ${kind} router`, err);
4832
5304
  }
4833
5305
  });
5306
+ subscription.add(() => {
5307
+ for (const queued of backlog) {
5308
+ queued.stopAckExtension?.();
5309
+ }
5310
+ backlog.length = 0;
5311
+ });
4834
5312
  this.subscriptions.push(subscription);
4835
5313
  }
4836
5314
  getConcurrency(kind) {
@@ -4844,26 +5322,71 @@ var EventRouter = class {
4844
5322
  return void 0;
4845
5323
  }
4846
5324
  /**
4847
- * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
4848
- * success or `nak` on hook failure so NATS retries on the next delivery
4849
- * cycle. Used when DLQ stream isn't configured, or when publishing to it
4850
- * failed and we still have to surface the message somewhere observable.
5325
+ * Last resort: invoke onDeadLetter, then term on success. On failure the
5326
+ * message is nak'd never redelivered past max_deliver, but preserved.
4851
5327
  */
4852
5328
  async fallbackToOnDeadLetterCallback(info, msg) {
4853
- if (!this.deadLetterConfig) {
4854
- msg.term("Dead letter config unavailable");
5329
+ const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
5330
+ if (!onDeadLetter) {
5331
+ this.logger.error(
5332
+ `Dead letter for ${msg.subject} could not be captured (DLQ publish failed, no onDeadLetter callback) \u2014 leaving the message in the stream`
5333
+ );
5334
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5335
+ msg.nak();
5336
+ });
4855
5337
  return;
4856
5338
  }
4857
5339
  try {
4858
- await this.deadLetterConfig.onDeadLetter(info);
4859
- msg.term("Dead letter processed via fallback callback");
5340
+ await onDeadLetter(info);
5341
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5342
+ msg.term("Dead letter processed via fallback callback");
5343
+ });
4860
5344
  } catch (hookErr) {
4861
5345
  this.logger.error(
4862
- `Fallback onDeadLetter callback failed for ${msg.subject}, nak for retry:`,
5346
+ `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:`,
4863
5347
  hookErr
4864
5348
  );
4865
- msg.nak();
5349
+ settleQuietly(this.logger, `Failed to nak ${msg.subject}:`, () => {
5350
+ msg.nak();
5351
+ });
5352
+ }
5353
+ }
5354
+ /**
5355
+ * Copy headers for the DLQ republish, dropping NATS control headers — a
5356
+ * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
5357
+ */
5358
+ buildDlqHeaders(msg) {
5359
+ const hdrs = natsHeaders3();
5360
+ if (!msg.headers) return hdrs;
5361
+ for (const [k, v] of msg.headers) {
5362
+ if (k.toLowerCase().startsWith(NATS_CONTROL_HEADER_PREFIX)) continue;
5363
+ for (const val of v) {
5364
+ hdrs.append(k, val);
5365
+ }
5366
+ }
5367
+ return hdrs;
5368
+ }
5369
+ /**
5370
+ * Past max_deliver the server never redelivers, so these in-process attempts
5371
+ * are the only second chance a dead letter gets. No artificial delay — an
5372
+ * unreachable broker already spaces attempts via its own request timeout.
5373
+ */
5374
+ async publishToDlqWithRetry(connection, subject, data, headers2) {
5375
+ let lastErr;
5376
+ for (let attempt = 1; attempt <= DLQ_PUBLISH_ATTEMPTS; attempt += 1) {
5377
+ try {
5378
+ await connection.getJetStreamClient().publish(subject, data, { headers: headers2 });
5379
+ return;
5380
+ } catch (err) {
5381
+ lastErr = err;
5382
+ if (attempt < DLQ_PUBLISH_ATTEMPTS) {
5383
+ this.logger.warn(
5384
+ `DLQ publish attempt ${attempt}/${DLQ_PUBLISH_ATTEMPTS} failed for ${subject}, retrying`
5385
+ );
5386
+ }
5387
+ }
4866
5388
  }
5389
+ throw lastErr;
4867
5390
  }
4868
5391
  /**
4869
5392
  * Publish a dead letter to the configured Dead-Letter Queue (DLQ) stream.
@@ -4883,14 +5406,7 @@ var EventRouter = class {
4883
5406
  return;
4884
5407
  }
4885
5408
  const destinationSubject = dlqStreamName(serviceName);
4886
- const hdrs = natsHeaders3();
4887
- if (msg.headers) {
4888
- for (const [k, v] of msg.headers) {
4889
- for (const val of v) {
4890
- hdrs.append(k, val);
4891
- }
4892
- }
4893
- }
5409
+ const hdrs = this.buildDlqHeaders(msg);
4894
5410
  let reason = String(error);
4895
5411
  if (error instanceof Error) {
4896
5412
  reason = error.message;
@@ -4903,8 +5419,7 @@ var EventRouter = class {
4903
5419
  hdrs.set("x-failed-at" /* FailedAt */, (/* @__PURE__ */ new Date()).toISOString());
4904
5420
  hdrs.set("x-delivery-count" /* DeliveryCount */, msg.info.deliveryCount.toString());
4905
5421
  try {
4906
- const js = this.connection.getJetStreamClient();
4907
- await js.publish(destinationSubject, msg.data, { headers: hdrs });
5422
+ await this.publishToDlqWithRetry(this.connection, destinationSubject, msg.data, hdrs);
4908
5423
  this.logger.log(`Message sent to DLQ: ${msg.subject}`);
4909
5424
  if (this.deadLetterConfig?.onDeadLetter) {
4910
5425
  try {
@@ -4916,7 +5431,9 @@ var EventRouter = class {
4916
5431
  );
4917
5432
  }
4918
5433
  }
4919
- msg.term("Moved to DLQ stream");
5434
+ settleQuietly(this.logger, `Failed to term ${msg.subject}:`, () => {
5435
+ msg.term("Moved to DLQ stream");
5436
+ });
4920
5437
  } catch (publishErr) {
4921
5438
  this.logger.error(`Failed to publish to DLQ for ${msg.subject}:`, publishErr);
4922
5439
  await this.fallbackToOnDeadLetterCallback(info, msg);
@@ -5110,7 +5627,9 @@ var RpcRouter = class {
5110
5627
  `rpc-handler:${subject}`
5111
5628
  );
5112
5629
  publishErrorReply(replyTo, correlationId, subject, err);
5113
- msg.term(`Handler error: ${subject}`);
5630
+ settleQuietly(logger5, `Failed to term ${subject}:`, () => {
5631
+ msg.term(`Handler error: ${subject}`);
5632
+ });
5114
5633
  };
5115
5634
  const abortController = new AbortController();
5116
5635
  let pending;
@@ -5138,7 +5657,9 @@ var RpcRouter = class {
5138
5657
  }
5139
5658
  if (!isPromiseLike2(pending)) {
5140
5659
  if (stopAckExtension !== null) stopAckExtension();
5141
- msg.ack();
5660
+ settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
5661
+ msg.ack();
5662
+ });
5142
5663
  publishReply(replyTo, correlationId, pending);
5143
5664
  reportHandlerCompleted(msg, startedAt, "success");
5144
5665
  return void 0;
@@ -5150,7 +5671,9 @@ var RpcRouter = class {
5150
5671
  if (stopAckExtension !== null) stopAckExtension();
5151
5672
  abortController.abort();
5152
5673
  emitRpcTimeout(subject, correlationId);
5153
- msg.term("Handler timeout");
5674
+ settleQuietly(logger5, `Failed to term ${subject}:`, () => {
5675
+ msg.term("Handler timeout");
5676
+ });
5154
5677
  reportHandlerCompleted(msg, startedAt, "terminated");
5155
5678
  }, timeout);
5156
5679
  return pending.then(
@@ -5159,7 +5682,9 @@ var RpcRouter = class {
5159
5682
  settled = true;
5160
5683
  clearTimeout(timeoutId);
5161
5684
  if (stopAckExtension !== null) stopAckExtension();
5162
- msg.ack();
5685
+ settleQuietly(logger5, `Failed to ack ${subject}:`, () => {
5686
+ msg.ack();
5687
+ });
5163
5688
  publishReply(replyTo, correlationId, result);
5164
5689
  reportHandlerCompleted(msg, startedAt, "success");
5165
5690
  },
@@ -5181,14 +5706,28 @@ var RpcRouter = class {
5181
5706
  active--;
5182
5707
  drainBacklog();
5183
5708
  };
5709
+ const routeSafely = (msg) => {
5710
+ try {
5711
+ return handleSafe(msg);
5712
+ } catch (err) {
5713
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5714
+ return void 0;
5715
+ }
5716
+ };
5717
+ const trackAsync = (result, msg) => {
5718
+ void result.catch((err) => {
5719
+ logger5.error(`Unexpected routing failure for ${msg.subject}:`, err);
5720
+ }).finally(onAsyncDone);
5721
+ };
5184
5722
  const drainBacklog = () => {
5185
5723
  while (active < maxActive) {
5186
5724
  const next = backlog.shift();
5187
5725
  if (next === void 0) return;
5726
+ next.stopAckExtension?.();
5188
5727
  active++;
5189
- const result = handleSafe(next);
5728
+ const result = routeSafely(next.msg);
5190
5729
  if (result !== void 0) {
5191
- void result.finally(onAsyncDone);
5730
+ trackAsync(result, next.msg);
5192
5731
  } else {
5193
5732
  active--;
5194
5733
  }
@@ -5198,7 +5737,10 @@ var RpcRouter = class {
5198
5737
  this.subscription = this.messageProvider.commands$.subscribe({
5199
5738
  next: (msg) => {
5200
5739
  if (active >= maxActive) {
5201
- backlog.push(msg);
5740
+ backlog.push({
5741
+ msg,
5742
+ stopAckExtension: hasAckExtension ? startAckExtensionTimer(msg, ackExtensionInterval) : null
5743
+ });
5202
5744
  if (!backlogWarned && backlog.length >= backlogWarnThreshold) {
5203
5745
  backlogWarned = true;
5204
5746
  logger5.warn(
@@ -5208,9 +5750,9 @@ var RpcRouter = class {
5208
5750
  return;
5209
5751
  }
5210
5752
  active++;
5211
- const result = handleSafe(msg);
5753
+ const result = routeSafely(msg);
5212
5754
  if (result !== void 0) {
5213
- void result.finally(onAsyncDone);
5755
+ trackAsync(result, msg);
5214
5756
  } else {
5215
5757
  active--;
5216
5758
  if (backlog.length > 0) drainBacklog();
@@ -5220,6 +5762,12 @@ var RpcRouter = class {
5220
5762
  logger5.error("Stream error in RPC router", err);
5221
5763
  }
5222
5764
  });
5765
+ this.subscription.add(() => {
5766
+ for (const queued of backlog) {
5767
+ queued.stopAckExtension?.();
5768
+ }
5769
+ backlog.length = 0;
5770
+ });
5223
5771
  }
5224
5772
  /** Stop routing and unsubscribe. */
5225
5773
  destroy() {
@@ -5502,7 +6050,7 @@ var JetstreamModule = class {
5502
6050
  ],
5503
6051
  useFactory: (options, messageProvider, patternRegistry, codec, eventBus, ackWaitMap, connection) => {
5504
6052
  if (options.consumer === false) return null;
5505
- const deadLetterConfig = options.onDeadLetter ? {
6053
+ const deadLetterConfig = options.onDeadLetter || options.dlq ? {
5506
6054
  maxDeliverByStream: /* @__PURE__ */ new Map(),
5507
6055
  onDeadLetter: options.onDeadLetter
5508
6056
  } : void 0;
@@ -5702,6 +6250,7 @@ export {
5702
6250
  JetstreamHeader,
5703
6251
  JetstreamHealthIndicator,
5704
6252
  JetstreamModule,
6253
+ JetstreamProvisioningError,
5705
6254
  JetstreamRecord,
5706
6255
  JetstreamRecordBuilder,
5707
6256
  JetstreamStrategy,