@horizon-republic/nestjs-jetstream 2.12.0 → 2.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -598,7 +598,7 @@ var extractContext = (ctx, carrier, getter) => import_api.propagation.extract(ct
598
598
 
599
599
  // src/otel/tracer.ts
600
600
  var import_api2 = require("@opentelemetry/api");
601
- var PACKAGE_VERSION = true ? "2.12.0" : "0.0.0";
601
+ var PACKAGE_VERSION = true ? "2.12.1" : "0.0.0";
602
602
  var getTracer = () => import_api2.trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
603
603
 
604
604
  // src/otel/carrier.ts
@@ -3880,11 +3880,8 @@ var StreamMigration = class {
3880
3880
  );
3881
3881
  }
3882
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.
3883
+ * Finish a migration a previous process left unfinished; a backup fresh
3884
+ * enough to belong to a live peer migration is left alone.
3888
3885
  */
3889
3886
  async recoverInterrupted(jsm, streamName2, desiredConfig) {
3890
3887
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
@@ -3938,11 +3935,9 @@ var StreamMigration = class {
3938
3935
  await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
3939
3936
  }
3940
3937
  /**
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).
3938
+ * Lag-based drain check live publishes cannot fake completion. A fresh
3939
+ * source reports lag 0 / active -1 before its first sync (NATS 2.12.6),
3940
+ * hence the active guard.
3946
3941
  */
3947
3942
  async waitForSourceDrained(jsm, streamName2, sourceName, minimumMessages) {
3948
3943
  const deadline = Date.now() + this.sourcingTimeoutMs;
@@ -3959,12 +3954,8 @@ var StreamMigration = class {
3959
3954
  );
3960
3955
  }
3961
3956
  /**
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.
3957
+ * A backup present at migrate() start is a live peer migration — wait it
3958
+ * out. Stale leftovers were already handled by recoverInterrupted().
3968
3959
  */
3969
3960
  async waitOutPeerMigration(jsm, backupName) {
3970
3961
  if (await this.tryInfo(jsm, backupName) === null) return false;
@@ -4015,6 +4006,17 @@ var StreamMigration = class {
4015
4006
  };
4016
4007
 
4017
4008
  // src/server/infrastructure/stream.provider.ts
4009
+ var subjectCovers = (broad, narrow) => {
4010
+ if (broad === narrow) return false;
4011
+ const broadTokens = broad.split(".");
4012
+ const narrowTokens = narrow.split(".");
4013
+ for (let i = 0; i < broadTokens.length; i += 1) {
4014
+ if (broadTokens[i] === ">") return i < narrowTokens.length;
4015
+ if (i >= narrowTokens.length || narrowTokens[i] === ">") return false;
4016
+ if (broadTokens[i] !== "*" && broadTokens[i] !== narrowTokens[i]) return false;
4017
+ }
4018
+ return broadTokens.length === narrowTokens.length;
4019
+ };
4018
4020
  var StreamProvider = class {
4019
4021
  constructor(options, connection) {
4020
4022
  this.options = options;
@@ -4069,13 +4071,8 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4069
4071
  }
4070
4072
  case "cmd" /* Command */:
4071
4073
  return [`${name}.${"cmd" /* Command */}.>`];
4072
- case "broadcast" /* Broadcast */: {
4073
- const subjects = ["broadcast.>"];
4074
- if (this.isSchedulingEnabled(kind)) {
4075
- subjects.push("broadcast._sch.>");
4076
- }
4077
- return subjects;
4078
- }
4074
+ case "broadcast" /* Broadcast */:
4075
+ return ["broadcast.>"];
4079
4076
  case "ordered" /* Ordered */:
4080
4077
  return [`${name}.${"ordered" /* Ordered */}.>`];
4081
4078
  }
@@ -4145,7 +4142,8 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4145
4142
  }
4146
4143
  async handleExistingStream(jsm, currentInfo, config, ctx) {
4147
4144
  if (this.isSharedStream(config.name)) {
4148
- config.subjects = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
4145
+ const merged = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
4146
+ config.subjects = merged.filter((s) => !merged.some((other) => subjectCovers(other, s)));
4149
4147
  }
4150
4148
  const diff = compareStreamConfig(currentInfo.config, config);
4151
4149
  if (!diff.hasChanges) {
@@ -5353,11 +5351,8 @@ var EventRouter = class {
5353
5351
  return void 0;
5354
5352
  }
5355
5353
  /**
5356
- * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
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.
5354
+ * Last resort: invoke onDeadLetter, then term on success. On failure the
5355
+ * message is nak'd never redelivered past max_deliver, but preserved.
5361
5356
  */
5362
5357
  async fallbackToOnDeadLetterCallback(info, msg) {
5363
5358
  const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
@@ -5386,10 +5381,8 @@ var EventRouter = class {
5386
5381
  }
5387
5382
  }
5388
5383
  /**
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.
5384
+ * Copy headers for the DLQ republish, dropping NATS control headers — a
5385
+ * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
5393
5386
  */
5394
5387
  buildDlqHeaders(msg) {
5395
5388
  const hdrs = (0, import_transport_node4.headers)();
@@ -5403,12 +5396,9 @@ var EventRouter = class {
5403
5396
  return hdrs;
5404
5397
  }
5405
5398
  /**
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.
5399
+ * Past max_deliver the server never redelivers, so these in-process attempts
5400
+ * are the only second chance a dead letter gets. No artificial delay — an
5401
+ * unreachable broker already spaces attempts via its own request timeout.
5412
5402
  */
5413
5403
  async publishToDlqWithRetry(connection, subject, data, headers2) {
5414
5404
  let lastErr;
package/dist/index.d.cts CHANGED
@@ -1532,27 +1532,19 @@ declare class EventRouter {
1532
1532
  private getConcurrency;
1533
1533
  private getAckExtensionConfig;
1534
1534
  /**
1535
- * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
1536
- * success. On failure the message is nak'd to release it, but the server
1537
- * never redelivers past `max_deliver` — it stays in the stream for manual
1538
- * recovery. Used when the DLQ stream isn't configured, or when publishing
1539
- * to it failed and we still have to surface the message somewhere.
1535
+ * Last resort: invoke onDeadLetter, then term on success. On failure the
1536
+ * message is nak'd never redelivered past max_deliver, but preserved.
1540
1537
  */
1541
1538
  private fallbackToOnDeadLetterCallback;
1542
1539
  /**
1543
- * Copy the original message headers for the DLQ republish, dropping NATS
1544
- * server control headers: a copied Nats-TTL expires the DLQ entry (or gets
1545
- * the publish rejected when the DLQ stream has no allow_msg_ttl), a copied
1546
- * Nats-Msg-Id collides with the DLQ dedup window.
1540
+ * Copy headers for the DLQ republish, dropping NATS control headers — a
1541
+ * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
1547
1542
  */
1548
1543
  private buildDlqHeaders;
1549
1544
  /**
1550
- * Attempt the DLQ publish up to {@link DLQ_PUBLISH_ATTEMPTS} times.
1551
- *
1552
- * Past `max_deliver` the server never redelivers, so an in-process retry is
1553
- * the only second chance a dead letter gets. There is no artificial delay
1554
- * between attempts: when the broker is unreachable each publish already
1555
- * spends its own request timeout, which spaces the attempts naturally.
1545
+ * Past max_deliver the server never redelivers, so these in-process attempts
1546
+ * are the only second chance a dead letter gets. No artificial delay — an
1547
+ * unreachable broker already spaces attempts via its own request timeout.
1556
1548
  */
1557
1549
  private publishToDlqWithRetry;
1558
1550
  /**
package/dist/index.d.ts CHANGED
@@ -1532,27 +1532,19 @@ declare class EventRouter {
1532
1532
  private getConcurrency;
1533
1533
  private getAckExtensionConfig;
1534
1534
  /**
1535
- * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
1536
- * success. On failure the message is nak'd to release it, but the server
1537
- * never redelivers past `max_deliver` — it stays in the stream for manual
1538
- * recovery. Used when the DLQ stream isn't configured, or when publishing
1539
- * to it failed and we still have to surface the message somewhere.
1535
+ * Last resort: invoke onDeadLetter, then term on success. On failure the
1536
+ * message is nak'd never redelivered past max_deliver, but preserved.
1540
1537
  */
1541
1538
  private fallbackToOnDeadLetterCallback;
1542
1539
  /**
1543
- * Copy the original message headers for the DLQ republish, dropping NATS
1544
- * server control headers: a copied Nats-TTL expires the DLQ entry (or gets
1545
- * the publish rejected when the DLQ stream has no allow_msg_ttl), a copied
1546
- * Nats-Msg-Id collides with the DLQ dedup window.
1540
+ * Copy headers for the DLQ republish, dropping NATS control headers — a
1541
+ * copied Nats-TTL would expire the DLQ entry, Nats-Msg-Id trips dedup.
1547
1542
  */
1548
1543
  private buildDlqHeaders;
1549
1544
  /**
1550
- * Attempt the DLQ publish up to {@link DLQ_PUBLISH_ATTEMPTS} times.
1551
- *
1552
- * Past `max_deliver` the server never redelivers, so an in-process retry is
1553
- * the only second chance a dead letter gets. There is no artificial delay
1554
- * between attempts: when the broker is unreachable each publish already
1555
- * spends its own request timeout, which spaces the attempts naturally.
1545
+ * Past max_deliver the server never redelivers, so these in-process attempts
1546
+ * are the only second chance a dead letter gets. No artificial delay — an
1547
+ * unreachable broker already spaces attempts via its own request timeout.
1556
1548
  */
1557
1549
  private publishToDlqWithRetry;
1558
1550
  /**
package/dist/index.js CHANGED
@@ -533,7 +533,7 @@ var extractContext = (ctx, carrier, getter) => propagation.extract(ctx, carrier,
533
533
 
534
534
  // src/otel/tracer.ts
535
535
  import { trace } from "@opentelemetry/api";
536
- var PACKAGE_VERSION = true ? "2.12.0" : "0.0.0";
536
+ var PACKAGE_VERSION = true ? "2.12.1" : "0.0.0";
537
537
  var getTracer = () => trace.getTracer(TRACER_NAME, PACKAGE_VERSION);
538
538
 
539
539
  // src/otel/carrier.ts
@@ -3842,11 +3842,8 @@ var StreamMigration = class {
3842
3842
  );
3843
3843
  }
3844
3844
  /**
3845
- * Detect and finish a migration that a previous process left unfinished.
3846
- * Safe against concurrent instances: a backup fresh enough to belong to a
3847
- * live migration is left alone.
3848
- *
3849
- * @returns true when recovery work was performed.
3845
+ * Finish a migration a previous process left unfinished; a backup fresh
3846
+ * enough to belong to a live peer migration is left alone.
3850
3847
  */
3851
3848
  async recoverInterrupted(jsm, streamName2, desiredConfig) {
3852
3849
  const backupName = `${streamName2}${MIGRATION_BACKUP_SUFFIX}`;
@@ -3900,11 +3897,9 @@ var StreamMigration = class {
3900
3897
  await jsm.streams.update(streamName2, { ...streamConfig, sources: [] });
3901
3898
  }
3902
3899
  /**
3903
- * Wait until `sourceName` is fully drained into `streamName`. Lag-based, so
3904
- * concurrent live publishes to the target cannot fake completion the way a
3905
- * bare message-count comparison could. A freshly attached source reports
3906
- * `lag: 0, active: -1` before its first sync — `active >= 0` filters that
3907
- * false positive out (verified against NATS 2.12.6).
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.
3908
3903
  */
3909
3904
  async waitForSourceDrained(jsm, streamName2, sourceName, minimumMessages) {
3910
3905
  const deadline = Date.now() + this.sourcingTimeoutMs;
@@ -3921,12 +3916,8 @@ var StreamMigration = class {
3921
3916
  );
3922
3917
  }
3923
3918
  /**
3924
- * A backup already present when migrate() begins belongs to another
3925
- * instance migrating right now (rolling deploy) wait for it to finish.
3926
- * Stale leftovers are handled by recoverInterrupted() before migrate() runs,
3927
- * so a timeout here means something is genuinely stuck.
3928
- *
3929
- * @returns true when a peer's backup was observed and cleared.
3919
+ * A backup present at migrate() start is a live peer migration — wait it
3920
+ * out. Stale leftovers were already handled by recoverInterrupted().
3930
3921
  */
3931
3922
  async waitOutPeerMigration(jsm, backupName) {
3932
3923
  if (await this.tryInfo(jsm, backupName) === null) return false;
@@ -3977,6 +3968,17 @@ var StreamMigration = class {
3977
3968
  };
3978
3969
 
3979
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
+ };
3980
3982
  var StreamProvider = class {
3981
3983
  constructor(options, connection) {
3982
3984
  this.options = options;
@@ -4031,13 +4033,8 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4031
4033
  }
4032
4034
  case "cmd" /* Command */:
4033
4035
  return [`${name}.${"cmd" /* Command */}.>`];
4034
- case "broadcast" /* Broadcast */: {
4035
- const subjects = ["broadcast.>"];
4036
- if (this.isSchedulingEnabled(kind)) {
4037
- subjects.push("broadcast._sch.>");
4038
- }
4039
- return subjects;
4040
- }
4036
+ case "broadcast" /* Broadcast */:
4037
+ return ["broadcast.>"];
4041
4038
  case "ordered" /* Ordered */:
4042
4039
  return [`${name}.${"ordered" /* Ordered */}.>`];
4043
4040
  }
@@ -4107,7 +4104,8 @@ ${formatProvisioningSummary(this.options.name, reservations)}`);
4107
4104
  }
4108
4105
  async handleExistingStream(jsm, currentInfo, config, ctx) {
4109
4106
  if (this.isSharedStream(config.name)) {
4110
- config.subjects = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
4107
+ const merged = [.../* @__PURE__ */ new Set([...config.subjects, ...currentInfo.config.subjects])];
4108
+ config.subjects = merged.filter((s) => !merged.some((other) => subjectCovers(other, s)));
4111
4109
  }
4112
4110
  const diff = compareStreamConfig(currentInfo.config, config);
4113
4111
  if (!diff.hasChanges) {
@@ -5324,11 +5322,8 @@ var EventRouter = class {
5324
5322
  return void 0;
5325
5323
  }
5326
5324
  /**
5327
- * Last-resort path for a dead letter: invoke `onDeadLetter`, then `term` on
5328
- * success. On failure the message is nak'd to release it, but the server
5329
- * never redelivers past `max_deliver` — it stays in the stream for manual
5330
- * recovery. Used when the DLQ stream isn't configured, or when publishing
5331
- * to it failed and we still have to surface the message somewhere.
5325
+ * Last resort: invoke onDeadLetter, then term on success. On failure the
5326
+ * message is nak'd never redelivered past max_deliver, but preserved.
5332
5327
  */
5333
5328
  async fallbackToOnDeadLetterCallback(info, msg) {
5334
5329
  const onDeadLetter = this.deadLetterConfig?.onDeadLetter;
@@ -5357,10 +5352,8 @@ var EventRouter = class {
5357
5352
  }
5358
5353
  }
5359
5354
  /**
5360
- * Copy the original message headers for the DLQ republish, dropping NATS
5361
- * server control headers: a copied Nats-TTL expires the DLQ entry (or gets
5362
- * the publish rejected when the DLQ stream has no allow_msg_ttl), a copied
5363
- * Nats-Msg-Id collides with the DLQ dedup window.
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.
5364
5357
  */
5365
5358
  buildDlqHeaders(msg) {
5366
5359
  const hdrs = natsHeaders3();
@@ -5374,12 +5367,9 @@ var EventRouter = class {
5374
5367
  return hdrs;
5375
5368
  }
5376
5369
  /**
5377
- * Attempt the DLQ publish up to {@link DLQ_PUBLISH_ATTEMPTS} times.
5378
- *
5379
- * Past `max_deliver` the server never redelivers, so an in-process retry is
5380
- * the only second chance a dead letter gets. There is no artificial delay
5381
- * between attempts: when the broker is unreachable each publish already
5382
- * spends its own request timeout, which spaces the attempts naturally.
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.
5383
5373
  */
5384
5374
  async publishToDlqWithRetry(connection, subject, data, headers2) {
5385
5375
  let lastErr;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@horizon-republic/nestjs-jetstream",
3
- "version": "2.12.0",
3
+ "version": "2.12.1",
4
4
  "description": "A NestJS transport for NATS with JetStream events, broadcast fan-out, and Core/JetStream RPC.",
5
5
  "repository": {
6
6
  "type": "git",