pg-boss 12.33.3 → 12.33.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAKD,QAAA,MAAM,mBAAmB,uQAaf,CAAA;AA2EV,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAgBD,iBAAS,mBAAmB,CAAE,KAAK,EAAE,MAAM,UAE1C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAgDhD;AAOD,iBAAS,eAAe,CAAE,IAAI,EAAE,GAAG,EAAE,EAAE,MAAc,EAAE;;CAAK,GAAG,KAAK,CAAC,OAAO,CA4D3E;AAED,iBAAS,mBAAmB,CAAE,MAAM,EAAE,GAAG,QAOxC;AAED,iBAAS,gBAAgB,CAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,QA2D/C;AA2GD,iBAAS,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG;IAClD,OAAO,EAAE,KAAK,CAAC,mBAAmB,CAAA;IAClC,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;CACjC,CAgCA;AAqBD,iBAAS,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAQlD;AAED,iBAAS,SAAS,CAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC,0BAA0B,CA0B9F;AA8GD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAiC9C;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AA+LD,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EAClB,CAAA"}
1
+ {"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAKD,QAAA,MAAM,mBAAmB,uQAaf,CAAA;AA2EV,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAiBD,iBAAS,mBAAmB,CAAE,KAAK,EAAE,MAAM,UAE1C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAgDhD;AAOD,iBAAS,eAAe,CAAE,IAAI,EAAE,GAAG,EAAE,EAAE,MAAc,EAAE;;CAAK,GAAG,KAAK,CAAC,OAAO,CA4D3E;AAED,iBAAS,mBAAmB,CAAE,MAAM,EAAE,GAAG,QAOxC;AAED,iBAAS,gBAAgB,CAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,QA2D/C;AA2GD,iBAAS,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG;IAClD,OAAO,EAAE,KAAK,CAAC,mBAAmB,CAAA;IAClC,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;CACjC,CAgCA;AAqBD,iBAAS,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAQlD;AAED,iBAAS,SAAS,CAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC,0BAA0B,CA0B9F;AA8GD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAiC9C;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AA+LD,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EAClB,CAAA"}
package/dist/attorney.js CHANGED
@@ -41,10 +41,10 @@ const BACKEND_PROFILES = {
41
41
  noAddColumnBackfill: true,
42
42
  noListenNotify: true,
43
43
  // Online DDL runs as a schema-change job, not the PG CONCURRENTLY path, and
44
- // pg_stat_progress_create_index isn't available — so BAM can't use liveness-based reclaim.
44
+ // pg_stat_progress_create_index isn't available, so BAM can't use liveness-based reclaim.
45
45
  noIndexProgressView: true,
46
- // REINDEX is rejected in either form — "CockroachDB does not require reindexing" with
47
- // CONCURRENTLY, "unimplemented: this syntax" without — and the bloat check itself cannot run:
46
+ // REINDEX is rejected in either form, "CockroachDB does not require reindexing" with
47
+ // CONCURRENTLY, "unimplemented: this syntax" without, and the bloat check itself cannot run:
48
48
  // there is no pg_relation_size(), and reltuples / relpages is an "unsupported binary
49
49
  // operator: <float4> / <int4>".
50
50
  noReindex: true,
@@ -74,7 +74,7 @@ const BACKEND_PROFILES = {
74
74
  // No noIndexProgressView: pg-boss keeps its tables coordinator-local (it never calls
75
75
  // create_distributed_table), so CREATE INDEX CONCURRENTLY runs against ordinary local Postgres tables
76
76
  // on the coordinator, where pg_stat_progress_create_index is accurate and liveness-based reclaim is
77
- // valid. This holds ONLY while the tables stay coordinator-local — if they are ever distributed, the
77
+ // valid. This holds ONLY while the tables stay coordinator-local. If they are ever distributed, the
78
78
  // coordinator's progress view would misread in-flight worker builds as dead and BAM could double-build.
79
79
  citus: { kind: 'distributed', flags: {} },
80
80
  pglite: { kind: 'embedded', flags: {} }
@@ -103,8 +103,9 @@ function validateQueueArgs(config = {}) {
103
103
  const ISO_DATE = /^\d{4}-\d{2}-\d{2}/;
104
104
  // Matched against the remainder past YYYY-MM-DD, and deliberately a whitelist of the zone-less
105
105
  // spellings rather than a test for a missing zone: a trailing '-01' would read as an offset, and
106
- // forms Postgres resolves on its own — a named zone ('... UTC', '... America/New_York'), a
107
- // single-digit offset ('+5:30') — must be left exactly as they are rather than pinned to UTC.
106
+ // forms Postgres resolves on its own must be left exactly as they are rather than pinned to UTC.
107
+ // Those forms are a named zone ('... UTC', '... America/New_York') and a single-digit offset
108
+ // ('+5:30').
108
109
  const ZONELESS_TIME = /^(?:[T ]\d{2}(?::\d{2}(?::\d{2}(?:[.,]\d+)?)?)?)?$/;
109
110
  function pinZonelessDateTime(value) {
110
111
  return ISO_DATE.test(value) && ZONELESS_TIME.test(value.slice(10)) ? value + 'Z' : value;
@@ -172,14 +173,14 @@ function checkUpdateArgs(args, { upsert = false } = {}) {
172
173
  assert(typeof options === 'object', 'options should be an object');
173
174
  const { id, singletonKey, match } = options;
174
175
  // Both update() and upsert() target by exactly one of id or singletonKey. (upsert() may also
175
- // require a singletonKey at runtime on key_strict_fifo queues — enforced in the manager, which
176
- // knows the policy — because an insert-on-miss there needs a key.)
176
+ // require a singletonKey at runtime on key_strict_fifo queues, enforced in the manager, which
177
+ // knows the policy, because an insert-on-miss there needs a key.)
177
178
  assert((!!id) !== (!!singletonKey), `${verb} requires exactly one of id or singletonKey`);
178
179
  assert(!(id && match !== undefined), 'match is only valid when targeting jobs by singletonKey');
179
180
  assert(match === undefined || JOB_MATCH_STRATEGIES.includes(match), `match must be one of: ${JOB_MATCH_STRATEGIES.join(', ')}`);
180
181
  assert(!('priority' in options) || (Number.isInteger(options.priority)), 'priority must be an integer');
181
182
  if ('startAfter' in options) {
182
- // Unlike send(), update() must honor a numeric startAfter of 0 (or negative) — the caller is
183
+ // Unlike send(), update() must honor a numeric startAfter of 0 (or negative). The caller is
183
184
  // explicitly pulling a deferred job forward to now. The send-style `+startAfter > 0` guard
184
185
  // coerced 0 to undefined, which JSON.stringify then dropped, silently making the edit a no-op.
185
186
  // Any finite number is passed through as a seconds interval ('0' -> now()); a string is kept.
@@ -442,7 +443,7 @@ function validateWarningConfig(config) {
442
443
  assert(!('warningRetentionDays' in config) || config.warningRetentionDays <= POLICY.MAX_RETENTION_DAYS, `configuration assert: warningRetentionDays cannot exceed ${POLICY.MAX_RETENTION_DAYS} days`);
443
444
  }
444
445
  // Expands config.backend into the internal compatibility flags. The flags are derived
445
- // solely from the backend profile — they are not part of the public input, so a
446
+ // solely from the backend profile. They are not part of the public input, so a
446
447
  // deployment can't end up with an inconsistent combination.
447
448
  function resolveBackend(config) {
448
449
  const backend = ('backend' in config) ? config.backend : 'postgres';
@@ -582,7 +583,7 @@ function applyPollingInterval(config) {
582
583
  : 2000;
583
584
  assert(!('notifyPollingIntervalSeconds' in config) || config.notifyPollingIntervalSeconds >= POLICY.MIN_POLLING_INTERVAL_MS / 1000, `configuration assert: notifyPollingIntervalSeconds must be at least every ${POLICY.MIN_POLLING_INTERVAL_MS}ms`);
584
585
  // Relaxed backstop poll used only while NOTIFY is active for the queue; falls back to
585
- // pollingInterval when notify is unavailable. It must never be smaller than the base poll —
586
+ // pollingInterval when notify is unavailable. It must never be smaller than the base poll,
586
587
  // that would make a notify-active queue poll more aggressively than an idle one, the opposite
587
588
  // of the intent. When explicit, reject a value below the base; when defaulted, floor it at the
588
589
  // base so bumping pollingIntervalSeconds past 30s can't silently leave notify smaller.
@@ -629,7 +630,7 @@ function validateReindexConfig(config) {
629
630
  assert(maxIndexBytes === undefined || (Number.isInteger(maxIndexBytes) && maxIndexBytes > 0), 'configuration assert: reindex.maxIndexBytes must be an integer > 0');
630
631
  // force belongs to an explicit supervise() call, not to a background timer that would then
631
632
  // rebuild every job index on every interval.
632
- assert(force === undefined || force === false, 'configuration assert: reindex.force cannot be set in constructor options — pass it to supervise()');
633
+ assert(force === undefined || force === false, 'configuration assert: reindex.force cannot be set in constructor options. Pass it to supervise()');
633
634
  }
634
635
  assert(!('reindexIntervalSeconds' in config) || config.reindexIntervalSeconds >= 1, 'configuration assert: reindexIntervalSeconds must be at least every second');
635
636
  config.reindexIntervalSeconds = config.reindexIntervalSeconds || POLICY.MAX_EXPIRATION_HOURS * 60 * 60;
package/dist/bam.js CHANGED
@@ -101,7 +101,7 @@ class Bam extends EventEmitter {
101
101
  });
102
102
  try {
103
103
  let alreadyBuilt = false;
104
- // A re-attempted command (a stale in_progress reclaim, or a retry of a prior 'failed' — including
104
+ // A re-attempted command (a stale in_progress reclaim, or a retry of a prior 'failed', including
105
105
  // failed rows left by older releases) needs the catalog consulted before the command is re-run,
106
106
  // because the row keeps the text it was enqueued with and that text may not be idempotent.
107
107
  // Probe indisvalid on every backend: pg_index is readable everywhere, and both outcomes matter.
@@ -112,7 +112,7 @@ class Bam extends EventEmitter {
112
112
  if (rows[0]?.invalid) {
113
113
  // An interrupted or failed CREATE INDEX CONCURRENTLY left an INVALID stub. Drop it
114
114
  // (best-effort, IF EXISTS) so the re-run rebuilds cleanly instead of the command's own
115
- // IF NOT EXISTS skipping over a broken index forever. Only on the liveness path —
115
+ // IF NOT EXISTS skipping over a broken index forever. Only on the liveness path,
116
116
  // CockroachDB/YugabyteDB roll interrupted builds back, so there is nothing to heal and
117
117
  // DROP ... CONCURRENTLY isn't their model.
118
118
  if (!this.#config.noIndexProgressView) {
@@ -127,7 +127,7 @@ class Bam extends EventEmitter {
127
127
  // oldest index commands (job_i7, job_i8) were queued without IF NOT EXISTS, so re-running
128
128
  // one against its own valid index fails with "already exists" and the failed row is retried
129
129
  // forever. Short-circuit to marking the row done instead. This half is deliberately NOT
130
- // gated on the liveness path — the timeout-only backends need it most, because their stale
130
+ // gated on the liveness path. The timeout-only backends need it most, because their stale
131
131
  // window is BAM_STALE_SECONDS (24 hours) rather than the grace window.
132
132
  alreadyBuilt = true;
133
133
  }
package/dist/boss.d.ts CHANGED
@@ -15,7 +15,7 @@ declare class Boss extends EventEmitter implements types.EventsMixin {
15
15
  /**
16
16
  * The REINDEX statements this instance would run, for installations where it cannot run them
17
17
  * itself. Unlike the background pass this applies no ownership filter and no size cap unless one
18
- * is passed — the commands are for an operator, who may run them as a different role.
18
+ * is passed. The commands are for an operator, who may run them as a different role.
19
19
  */
20
20
  getReindexCommands(options?: types.ReindexOptions): Promise<string[]>;
21
21
  }
package/dist/boss.js CHANGED
@@ -68,7 +68,7 @@ function describeXminHolder(source, row) {
68
68
  return `${who} (${where}) has held a transaction open ${open}`;
69
69
  }
70
70
  // SQLSTATE 25001 (active_sql_transaction): "REINDEX CONCURRENTLY cannot run inside a transaction
71
- // block". Raised when a user-supplied adapter wraps executeSql in a transaction — a property of the
71
+ // block". Raised when a user-supplied adapter wraps executeSql in a transaction. A property of the
72
72
  // adapter, not of this pass, so it disables rebuilds for the life of the instance rather than
73
73
  // retrying every interval.
74
74
  const IN_TRANSACTION_ERROR = '25001';
@@ -86,7 +86,7 @@ class Boss extends EventEmitter {
86
86
  // keeps running; only the DDL is abandoned.
87
87
  #reindexUnavailable = null;
88
88
  // Warn once per bloated index rather than on every pass. An index leaves the set as soon as it
89
- // stops qualifying — rebuilt, dropped, or refilled — so a later episode warns again.
89
+ // stops qualifying (rebuilt, dropped, or refilled) so a later episode warns again.
90
90
  #warnedBloat = new Set();
91
91
  // Local rate limit for passes that only report bloat, which deliberately leave the shared interval
92
92
  // claim to whichever instance can act on it.
@@ -229,7 +229,7 @@ class Boss extends EventEmitter {
229
229
  // Ensure today's/tomorrow's partitions exist before any insertQueueStats below. Retention
230
230
  // (#maintainWarnings/#maintainQueueStats) runs at the tail. Both live here, in the public
231
231
  // supervise() path that also performs the writes, rather than in the timer-only #onSupervise
232
- // wrapper — so manual supervise() callers (instances run with the built-in supervisor disabled)
232
+ // wrapper, so manual supervise() callers (instances run with the built-in supervisor disabled)
233
233
  // get partitions provisioned and old data pruned, not just job retention.
234
234
  if (this.#config.persistQueueStats && !this.#config.noTablePartitioning && !this.#stopping) {
235
235
  await this.#ensureQueueStatsPartitions();
@@ -574,7 +574,7 @@ class Boss extends EventEmitter {
574
574
  holder: describeXminHolder(holder.source, horizon.row),
575
575
  holderClass: XMIN_HOLDERS[holder.source],
576
576
  // Null unless the holder is a backend this role could read a row for. `self` says whether it
577
- // shares this connection's application_name — pg-boss pinning its own horizon and an
577
+ // shares this connection's application_name. A pg-boss instance pinning its own horizon and an
578
578
  // external reporting tool pinning it have opposite fixes.
579
579
  holderPid: backend?.pid ?? null,
580
580
  holderApplicationName: backend?.applicationName ?? null,
@@ -598,10 +598,10 @@ class Boss extends EventEmitter {
598
598
  });
599
599
  }
600
600
  /**
601
- * The widest holder in the row. Every source the query returns has already been qualified — the
601
+ * The widest holder in the row. Every source the query returns has already been qualified. The
602
602
  * backends column is filtered server-side to transactions that predate the failed vacuum, and a
603
603
  * slot, standby or prepared transaction advertises an xmin only while something is genuinely
604
- * stuck — so this is a straight maximum. The horizon is pinned to the oldest of them, which makes
604
+ * stuck, so this is a straight maximum. The horizon is pinned to the oldest of them, which makes
605
605
  * the widest the one worth naming.
606
606
  */
607
607
  #attributeXminHorizon(row) {
@@ -665,7 +665,7 @@ class Boss extends EventEmitter {
665
665
  }
666
666
  return readable;
667
667
  }
668
- // DDL runs outside the slow-query timer. A REINDEX is expected to take seconds — routing it
668
+ // DDL runs outside the slow-query timer. A REINDEX is expected to take seconds, routing it
669
669
  // through #executeQuery would emit a bogus slow_query warning on every rebuild.
670
670
  async #executeDdl(sql) {
671
671
  return unwrapSQLResult(await this.#db.executeSql(sql));
@@ -686,7 +686,7 @@ class Boss extends EventEmitter {
686
686
  * Detection still runs where the rebuild cannot: a role that does not own the indexes, an adapter
687
687
  * that wraps queries in a transaction, and `reindex: false` all still produce the `index_bloat`
688
688
  * warning and can act on getReindexCommands(). The one exception is a backend that stores data
689
- * outside PostgreSQL's heap — see the noReindex gate below.
689
+ * outside PostgreSQL's heap. See the noReindex gate below.
690
690
  */
691
691
  async #reindex(tables, options) {
692
692
  if (this.#stopping)
@@ -696,7 +696,7 @@ class Boss extends EventEmitter {
696
696
  // rejects `reltuples / relpages` outright ("unsupported binary operator: <float4> / <int4>"),
697
697
  // so running the check would throw once per interval; YugabyteDB answers but reports relpages
698
698
  // and pg_relation_size as 0 for every relation, so nothing could ever match. Both store data in
699
- // an LSM that compacts on its own, and both reject REINDEX in either form — CockroachDB with
699
+ // an LSM that compacts on its own, and both reject REINDEX in either form. CockroachDB with
700
700
  // the hint "CockroachDB does not require reindexing."
701
701
  if (this.#config.noReindex)
702
702
  return;
@@ -706,7 +706,7 @@ class Boss extends EventEmitter {
706
706
  // An explicit force is a request to run now; everything else waits for an interval.
707
707
  //
708
708
  // Which interval depends on whether this instance can do the work. The shared claim exists so
709
- // exactly one instance in the cluster rebuilds per window — an instance that is only ever going
709
+ // exactly one instance in the cluster rebuilds per window. An instance that is only ever going
710
710
  // to report bloat has no business taking it, or a peer configured to rebuild would find the
711
711
  // window gone and skip the rebuild for a whole day. Detection-only passes throttle themselves
712
712
  // locally instead, on the same interval.
@@ -753,7 +753,7 @@ class Boss extends EventEmitter {
753
753
  if (code === IN_TRANSACTION_ERROR) {
754
754
  this.#reindexUnavailable = err.message;
755
755
  failed.set(target.name, this.#reindexUnavailable);
756
- // Every remaining index would fail identically — the transaction wrapper is a property of
756
+ // Every remaining index would fail identically. The transaction wrapper is a property of
757
757
  // the adapter, not of this index.
758
758
  break;
759
759
  }
@@ -789,7 +789,7 @@ class Boss extends EventEmitter {
789
789
  continue;
790
790
  // Ownership first: an index the role cannot touch was never a candidate, so it has no entry in
791
791
  // `failed` no matter why the pass stopped. #reindexUnavailable comes next and covers the
792
- // indexes the 25001 giveup skipped without attempting — they are neither failed nor rebuilt,
792
+ // indexes the 25001 giveup skipped without attempting. They are neither failed nor rebuilt,
793
793
  // and reporting a size cap they are nowhere near would point at the wrong knob.
794
794
  const reason = failed.get(index.name) ??
795
795
  (!index.owned
@@ -805,11 +805,11 @@ class Boss extends EventEmitter {
805
805
  /**
806
806
  * The REINDEX statements this instance would run, for installations where it cannot run them
807
807
  * itself. Unlike the background pass this applies no ownership filter and no size cap unless one
808
- * is passed — the commands are for an operator, who may run them as a different role.
808
+ * is passed. The commands are for an operator, who may run them as a different role.
809
809
  */
810
810
  async getReindexCommands(options) {
811
811
  // The catalog query reads pg_class.relpages and pg_relation_size(), which the heap-less engines
812
- // either reject outright or answer with zeroes — same gate as #reindex, and there is nothing to
812
+ // either reject outright or answer with zeroes, same gate as #reindex, and there is nothing to
813
813
  // rebuild on them anyway.
814
814
  if (this.#config.noReindex)
815
815
  return [];
@@ -25,7 +25,8 @@ import type { Clock } from './types.ts';
25
25
  * is nothing for a skew correction to correct, which is why the fix is in when the attempt is made
26
26
  * rather than in what it is measured against.
27
27
  *
28
- * `anchor()` is what a pass calls once its claim has been stamped. A pass that returns without ever
28
+ * `anchor()` is what a pass calls once its claim has been stamped, and it takes a wait of its own
29
+ * for the pass that knows when the row it was refused by comes due. A pass that returns without ever
29
30
  * reaching its claim - stopped, already working, an error on the way in - is re-armed from the end
30
31
  * of the callback instead, so the chain cannot die on a path that never anchored it.
31
32
  */
@@ -36,9 +37,15 @@ export declare class ClaimTimer {
36
37
  stop(): void;
37
38
  /**
38
39
  * Re-anchors the next attempt to now, because the claim this timer drives has just been stamped.
39
- * Called whether the claim was won or lost: the winner needs its next attempt to fall after its
40
- * own stamp, and a loser's phase is its own business either way.
40
+ *
41
+ * Called whether the claim was won or lost. A winner needs its next attempt to fall an interval
42
+ * after its own stamp. A loser has two phases it could take, and which one it takes is not a
43
+ * matter of taste: measured from its own failure it comes back an interval later, which is up to
44
+ * a whole interval after the row is next due, and that is the deployment's spacing the moment the
45
+ * instance holding the claim stops. A caller that knows when the row comes due passes that wait
46
+ * in `ms` instead. See Timekeeper.onCron(), which is the one claim where the difference costs
47
+ * work rather than latency.
41
48
  */
42
- anchor(): void;
49
+ anchor(ms?: number): void;
43
50
  }
44
51
  //# sourceMappingURL=claimTimer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"claimTimer.d.ts","sourceRoot":"","sources":["../src/claimTimer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAc,MAAM,YAAY,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,UAAU;;gBASR,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC;IAMnE,KAAK,IAAK,IAAI;IAOd,IAAI,IAAK,IAAI;IAKb;;;;OAIG;IACH,MAAM,IAAK,IAAI;CAwChB"}
1
+ {"version":3,"file":"claimTimer.d.ts","sourceRoot":"","sources":["../src/claimTimer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAc,MAAM,YAAY,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,UAAU;;gBASR,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC;IAMnE,KAAK,IAAK,IAAI;IAOd,IAAI,IAAK,IAAI;IAKb;;;;;;;;;;OAUG;IACH,MAAM,CAAE,EAAE,GAAE,MAAiB,GAAG,IAAI;CAwCrC"}
@@ -24,7 +24,8 @@
24
24
  * is nothing for a skew correction to correct, which is why the fix is in when the attempt is made
25
25
  * rather than in what it is measured against.
26
26
  *
27
- * `anchor()` is what a pass calls once its claim has been stamped. A pass that returns without ever
27
+ * `anchor()` is what a pass calls once its claim has been stamped, and it takes a wait of its own
28
+ * for the pass that knows when the row it was refused by comes due. A pass that returns without ever
28
29
  * reaching its claim - stopped, already working, an error on the way in - is re-armed from the end
29
30
  * of the callback instead, so the chain cannot die on a path that never anchored it.
30
31
  */
@@ -52,20 +53,26 @@ export class ClaimTimer {
52
53
  }
53
54
  /**
54
55
  * Re-anchors the next attempt to now, because the claim this timer drives has just been stamped.
55
- * Called whether the claim was won or lost: the winner needs its next attempt to fall after its
56
- * own stamp, and a loser's phase is its own business either way.
56
+ *
57
+ * Called whether the claim was won or lost. A winner needs its next attempt to fall an interval
58
+ * after its own stamp. A loser has two phases it could take, and which one it takes is not a
59
+ * matter of taste: measured from its own failure it comes back an interval later, which is up to
60
+ * a whole interval after the row is next due, and that is the deployment's spacing the moment the
61
+ * instance holding the claim stops. A caller that knows when the row comes due passes that wait
62
+ * in `ms` instead. See Timekeeper.onCron(), which is the one claim where the difference costs
63
+ * work rather than latency.
57
64
  */
58
- anchor() {
65
+ anchor(ms = this.#ms) {
59
66
  if (this.#stopped)
60
67
  return;
61
68
  this.#anchored = true;
62
- this.#arm();
69
+ this.#arm(ms);
63
70
  }
64
- #arm() {
71
+ #arm(ms = this.#ms) {
65
72
  this.#disarm();
66
73
  if (this.#stopped)
67
74
  return;
68
- this.#handle = this.#clock.setTimeout(() => { this.#run(); }, this.#ms);
75
+ this.#handle = this.#clock.setTimeout(() => { this.#run(); }, ms);
69
76
  }
70
77
  #disarm() {
71
78
  if (this.#handle !== undefined) {
package/dist/cli.js CHANGED
@@ -193,7 +193,7 @@ async function createDb(config) {
193
193
  return db;
194
194
  }
195
195
  // Like getConnectionConfig, but returns null instead of exiting when no connection is
196
- // configured — used by commands (e.g. `plans`) where a connection is optional.
196
+ // configured, used by commands (e.g. `plans`) where a connection is optional.
197
197
  function tryGetConnectionConfig(args) {
198
198
  const fileConfig = loadConfigFile(args.config);
199
199
  const hasConnection = args.connectionString || process.env.PGBOSS_DATABASE_URL || fileConfig.connectionString ||
@@ -303,7 +303,7 @@ async function cmdMigrate(args) {
303
303
  }
304
304
  // Render from the DB's actual version so the printed SQL is exactly what `migrate` would run.
305
305
  // Offline (or not yet installed) we can't know it, so fall back to the oldest supported starting
306
- // version — the full chain — instead of a bogus "from 0" that fails on non-idempotent steps.
306
+ // version (the full chain) instead of a bogus "from 0" that fails on non-idempotent steps.
307
307
  const fromVersion = version ?? migrationStore.getMinVersion(schema);
308
308
  const sql = migrationStore.migrate(schema, fromVersion, migrationStore.getAllForConfig(config), config.noAdvisoryLocks, { inlineAsync: true, partitionTables });
309
309
  console.log(`-- SQL to migrate pg-boss from version ${fromVersion} to ${schemaVersion}:`);
@@ -382,7 +382,7 @@ async function cmdDoctor(args) {
382
382
  }
383
383
  console.log(`Schema "${schema}" version ${version} (latest: ${schemaVersion})`);
384
384
  if (version < schemaVersion) {
385
- console.log(`⚠ Migrations pending: ${schemaVersion - version} — run "pg-boss migrate" before trusting drift results`);
385
+ console.log(`⚠ Migrations pending: ${schemaVersion - version}. Run "pg-boss migrate" before trusting drift results`);
386
386
  }
387
387
  // Reuse the same drift scan the boss.detectSchemaDrift() API runs, rather than duplicating the
388
388
  // catalog queries + computeSchemaDrift wiring here (the two copies had already drifted apart and
@@ -404,7 +404,7 @@ async function cmdDoctor(args) {
404
404
  await contractor.detectClockOverride();
405
405
  if (override) {
406
406
  console.log('\njob_now() carries a TestClock override, left behind by a test run that was');
407
- console.log('killed before releasing its clock. Restoring it — make sure no instance is');
407
+ console.log('killed before releasing its clock. Restoring it, so make sure no instance is');
408
408
  console.log('holding a live TestClock against this schema.');
409
409
  await contractor.restoreClockFunction();
410
410
  console.log(' restored.');
@@ -416,7 +416,7 @@ async function cmdDoctor(args) {
416
416
  }
417
417
  }
418
418
  if (report.building.length) {
419
- console.log(`\nBuilding (async index build in progress — not yet drift) (${report.building.length}):`);
419
+ console.log(`\nBuilding (async index build in progress, not yet drift) (${report.building.length}):`);
420
420
  for (const i of report.building)
421
421
  console.log(` ${i.table}.${i.name}`);
422
422
  }
@@ -429,19 +429,19 @@ async function cmdDoctor(args) {
429
429
  console.log(`\n⚠ INDEX BLOAT (rebuild with "pg-boss reindex") (${bloated.length}):`);
430
430
  for (const i of bloated) {
431
431
  const mb = Math.round(Number(i.bytes) / 1024 / 1024);
432
- console.log(` ${i.table}.${i.name} — ${mb} MB across ${i.pages} pages, ~${i.entries} live entries`);
432
+ console.log(` ${i.table}.${i.name}: ${mb} MB across ${i.pages} pages, ~${i.entries} live entries`);
433
433
  if (!i.owned)
434
434
  console.log(' note: the connected role does not own this index and cannot reindex it');
435
435
  }
436
436
  }
437
437
  }
438
438
  catch {
439
- // Best effort — a role without catalog visibility should not fail the whole drift report.
439
+ // Best effort. A role without catalog visibility should not fail the whole drift report.
440
440
  }
441
- // Extra indexes are informational (a stale pg-boss index or a user-added one) — a warning, not
441
+ // Extra indexes are informational (a stale pg-boss index or a user-added one). A warning, not
442
442
  // drift. Printed regardless of overall status; it never changes the exit code.
443
443
  if (report.extraIndexes.length) {
444
- console.log(`\n⚠ EXTRA INDEXES (present on a managed table but not expected — harmless) (${report.extraIndexes.length}):`);
444
+ console.log(`\n⚠ EXTRA INDEXES (present on a managed table but not expected, harmless) (${report.extraIndexes.length}):`);
445
445
  for (const i of report.extraIndexes)
446
446
  console.log(` ${i.table}.${i.name}`);
447
447
  }
@@ -465,9 +465,10 @@ async function cmdDoctor(args) {
465
465
  }
466
466
  }
467
467
  if (report.invalid.length) {
468
- // The definition is correct — the index is just invalid (interrupted build) — so show the DDL to
469
- // drop and rebuild it, not an expected-vs-actual comparison (they would be identical).
470
- console.log(`\nINVALID (present but marked invalid — drop and rebuild) (${report.invalid.length}):`);
468
+ // The definition is correct, since an invalid index is an interrupted build rather than a
469
+ // wrong shape. So show the DDL to drop and rebuild it, not an expected-vs-actual comparison,
470
+ // which would be identical.
471
+ console.log(`\nINVALID (present but marked invalid, drop and rebuild) (${report.invalid.length}):`);
471
472
  for (const i of report.invalid) {
472
473
  console.log(` ${i.table}.${i.name}`);
473
474
  if (i.definition)
@@ -596,7 +597,7 @@ async function cmdReindex(args) {
596
597
  // The check reads pg_class.relpages and pg_relation_size(). CockroachDB has neither (it
597
598
  // rejects `reltuples / relpages` as an unsupported binary operator) and YugabyteDB reports
598
599
  // zeroes for every relation. --backend catches those up front (above); this stays for a target
599
- // that was not declared — say what happened instead of surfacing a raw catalog error.
600
+ // that was not declared, say what happened instead of surfacing a raw catalog error.
600
601
  console.error(`Could not read index statistics from schema "${schema}": ${err.message}`);
601
602
  console.error('The bloat check reads pg_class.relpages and pg_relation_size(), which CockroachDB and YugabyteDB do not provide.');
602
603
  process.exitCode = 1;
@@ -625,7 +626,7 @@ async function cmdReindex(args) {
625
626
  }
626
627
  for (const index of rows) {
627
628
  const mb = Math.round(Number(index.bytes) / 1024 / 1024);
628
- console.log(` ${index.table}.${index.name} — ${mb} MB across ${index.pages} pages, ~${index.entries} live entries`);
629
+ console.log(` ${index.table}.${index.name}: ${mb} MB across ${index.pages} pages, ~${index.entries} live entries`);
629
630
  }
630
631
  // Executing is different from printing: REINDEX needs ownership, so an index this role cannot
631
632
  // touch is reported up front rather than attempted for the sake of the server's error message.
@@ -140,7 +140,7 @@ class Contractor {
140
140
  // Function-body and enum drift are best-effort: pg_get_functiondef is unsupported on some backends
141
141
  // (CockroachDB), so a failure here SKIPS the function check rather than aborting the whole scan.
142
142
  // `functionsSupported` must be tracked separately from an empty result: an empty `liveFunctions`
143
- // means "query failed / unsupported", which is NOT the same as "no functions found" — feeding
143
+ // means "query failed / unsupported", which is NOT the same as "no functions found", feeding
144
144
  // `live: []` to the drift check would report every expected function as missing and flip `ok` to
145
145
  // false on every CockroachDB scan. So the check is gated (passed `undefined`) when the query throws.
146
146
  let liveFunctions = [];
@@ -162,7 +162,7 @@ class Contractor {
162
162
  }
163
163
  // Table presence is read from a catalog-only query independent of the column diff below. The column
164
164
  // query uses pg_get_expr (unsupported on some backends) and is best-effort; if it throws, the column
165
- // check is skipped — but table presence must NOT collapse to "everything missing", so it comes from
165
+ // check is skipped, but table presence must NOT collapse to "everything missing", so it comes from
166
166
  // its own pg_class probe. pg_class is available everywhere, so this rarely throws; if it somehow
167
167
  // does, fall back to the columns-derived set rather than aborting the scan.
168
168
  let liveTables = null;
package/dist/db.js CHANGED
@@ -114,7 +114,7 @@ class Db extends EventEmitter {
114
114
  const keepAliveInitialDelay = this.config.notifyKeepAliveInitialDelayMs ?? DEFAULT_LISTEN_KEEP_ALIVE_INITIAL_DELAY_MS;
115
115
  // Only self-heal once the listener has been established at least once. If the INITIAL connect
116
116
  // fails, the rejection propagates to the caller (Notifier.start), which falls back to
117
- // polling-only and discards this subscription's close handle — so a reconnect scheduled from
117
+ // polling-only and discards this subscription's close handle, so a reconnect scheduled from
118
118
  // the client 'error' handler would be an untracked connection nothing can close, keeping the
119
119
  // event loop alive and delivering notifications into a stopped manager.
120
120
  let established = false;
@@ -198,7 +198,7 @@ class Db extends EventEmitter {
198
198
  });
199
199
  // Track the client before connecting so close() can tear down a connect still in flight
200
200
  // (e.g. shutdown during a reconnect). If connect or LISTEN then rejects, the catch ends
201
- // it and rethrows — without that, a LISTEN that fails after connect() succeeded would
201
+ // it and rethrows. Without that, a LISTEN that fails after connect() succeeded would
202
202
  // leak an open connection. The reconnect .catch below reschedules on failure; an initial
203
203
  // failure propagates to the caller.
204
204
  client = next;
package/dist/drifter.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { resolveSchemaName } from "./tools.js";
2
2
  const SINGLE_QUOTE_REGEX = /'/g;
3
- // Extracts the first balanced parenthesised group from a CREATE INDEX statement — the key-column
3
+ // Extracts the first balanced parenthesised group from a CREATE INDEX statement. The key-column
4
4
  // list. Stops at the matching close paren, so a trailing INCLUDE(...) or WHERE(...) is excluded and
5
5
  // an inner COALESCE(...) is kept. Works on both hand-written DDL and pg_get_indexdef output (whose
6
6
  // leading `USING btree (` opens the same first group). The INCLUDE payload is not ignored, just
7
- // compared separately — see extractIndexIncludeList.
7
+ // compared separately. See extractIndexIncludeList.
8
8
  function extractIndexKeyList(ddl) {
9
9
  const open = ddl.indexOf('(');
10
10
  if (open === -1)
@@ -20,11 +20,11 @@ function extractIndexKeyList(ddl) {
20
20
  }
21
21
  // Strips SQL type casts, including schema-qualified enum casts (Postgres renders a job_state literal
22
22
  // as `'active'::pgboss.job_state` and a text literal as `''::text`). Run before whitespace removal is
23
- // fine — casts never contain spaces.
23
+ // fine, casts never contain spaces.
24
24
  const CAST_REGEX = /::(?:[a-z_][a-z0-9_$]*\.)?[a-z_][a-z0-9_$]*(?:\[\])?/g;
25
25
  // Normalises a key-column list so an expected list and a pg_get_indexdef list compare equal when they
26
26
  // mean the same thing: lower-cased, quotes and whitespace stripped, and type casts removed. Column
27
- // ORDER is preserved — an index on (a, b) must not normalise equal to (b, a), which is exactly the
27
+ // ORDER is preserved. An index on (a, b) must not normalise equal to (b, a), which is exactly the
28
28
  // index-ordinal significance the drift check needs. Parens are kept so COALESCE(...) survives.
29
29
  function normalizeKeyList(keyList) {
30
30
  return keyList
@@ -76,13 +76,13 @@ export function indexIncludeRaw(ddl) {
76
76
  const list = extractIndexIncludeList(ddl);
77
77
  return list === null ? '' : list.replace(CAST_REGEX, '').replace(/\s+/g, ' ').trim();
78
78
  }
79
- // Everything after the top-level WHERE — the partial-index predicate — or '' for a non-partial index.
79
+ // Everything after the top-level WHERE (the partial-index predicate) or '' for a non-partial index.
80
80
  function extractPredicate(ddl) {
81
81
  const m = ddl.match(/\bWHERE\b/i);
82
82
  return m ? ddl.slice(m.index + m[0].length) : '';
83
83
  }
84
- // Normalises a predicate for comparison. Both sides originate from pg_get_indexdef — expected from the
85
- // manifest, live from the catalog — so both carry pg's canonical form; this only undoes the cosmetic
84
+ // Normalises a predicate for comparison. Both sides originate from pg_get_indexdef, expected from the
85
+ // manifest, live from the catalog, so both carry pg's canonical form; this only undoes the cosmetic
86
86
  // differences pg can vary between two equal predicates:
87
87
  // - casts added to every literal (`'active'::pgboss.job_state`, `'x'::text`) → stripped
88
88
  // - `IN (a, b)` rendered as `= ANY (ARRAY[a, b])` → folded back to `IN (a, b)`
@@ -130,7 +130,7 @@ function outerParensWrapWhole(s) {
130
130
  return false;
131
131
  }
132
132
  // Removes the redundant outer parentheses pg_get_indexdef wraps a whole predicate in. Only the
133
- // outermost pair is stripped — inner grouping (which may be meaningful) is left intact.
133
+ // outermost pair is stripped, inner grouping (which may be meaningful) is left intact.
134
134
  function stripOuterParens(s) {
135
135
  let out = s.trim();
136
136
  while (outerParensWrapWhole(out)) {
@@ -151,12 +151,12 @@ export function displayIndexDefinition(def) {
151
151
  const predicate = stripOuterParens(cleaned.slice(m.index + m[0].length).trim());
152
152
  return `${head} ${predicate}`;
153
153
  }
154
- // Extracts the body of a function definition — the text between the outer dollar-quote tags. Works on
154
+ // Extracts the body of a function definition. The text between the outer dollar-quote tags. Works on
155
155
  // both pg-boss's own `CREATE FUNCTION ... AS $$ … $$` and Postgres's `pg_get_functiondef` output
156
156
  // (which wraps the body in `$function$ … $function$`). Postgres stores prosrc verbatim, so the two
157
157
  // bodies are byte-identical for an un-drifted function, modulo the dollar-quote tag name. Nested
158
158
  // dollar quotes with a different tag (pg-boss uses `$cmd$`) live inside the body and are preserved.
159
- // Returns '' when no dollar-quoted body is found — an un-diffable definition is skipped, not flagged.
159
+ // Returns '' when no dollar-quoted body is found. An un-diffable definition is skipped, not flagged.
160
160
  export function extractFunctionBody(def) {
161
161
  const open = def.match(/\$[A-Za-z0-9_]*\$/);
162
162
  if (!open)
@@ -168,7 +168,7 @@ export function extractFunctionBody(def) {
168
168
  }
169
169
  // Normalises a function body so a hand-written definition and pg_get_functiondef's stored copy compare
170
170
  // equal despite cosmetic reindentation: runs of whitespace collapse to a single space and the ends are
171
- // trimmed. Case is preserved — a changed string literal or identifier is real drift.
171
+ // trimmed. Case is preserved. A changed string literal or identifier is real drift.
172
172
  export function normalizeFunctionBody(body) {
173
173
  return body.replace(/\s+/g, ' ').trim();
174
174
  }
@@ -211,7 +211,7 @@ export function getSchemaFunctions(schema) {
211
211
  `;
212
212
  }
213
213
  // The ordered value list of an enum type, for the enum-definition diff. ORDER BY enumsortorder
214
- // preserves declaration order — reordering the enum is itself drift (the numeric base type makes
214
+ // preserves declaration order, reordering the enum is itself drift (the numeric base type makes
215
215
  // created < retry < … < failed load-bearing for state comparisons).
216
216
  export function getEnumDefinition(schema, typeName = 'job_state') {
217
217
  return `
@@ -244,7 +244,7 @@ export function getSchemaColumns(schema) {
244
244
  }
245
245
  // Every ordinary ('r') or partitioned ('p') table in the schema, catalog-only (pg_class), for the
246
246
  // table-presence check. Deliberately independent of getSchemaColumns: that query uses pg_get_expr,
247
- // which is unsupported on some backends and is wrapped in a best-effort try/catch — deriving table
247
+ // which is unsupported on some backends and is wrapped in a best-effort try/catch, deriving table
248
248
  // presence from its (possibly empty-on-failure) result would false-report every managed table as
249
249
  // missing whenever the column query throws. pg_class is available everywhere, so table presence stays
250
250
  // reliable even when the column diff is skipped.
@@ -296,7 +296,7 @@ function computeFunctionDrift(expected, live) {
296
296
  return { missingFunctions, mismatchedFunctions };
297
297
  }
298
298
  // Column presence / default / type / nullability diff per managed table. A table with no live columns
299
- // is skipped (it does not exist — table presence is reported separately), so a missing table never
299
+ // is skipped (it does not exist, table presence is reported separately), so a missing table never
300
300
  // floods the report with every column. Default, type, and nullability drift are only checked for
301
301
  // expected tables that carry the corresponding `defaults`/`types` maps (the fixed managed tables); the
302
302
  // job/partition tables omit them and are name-only.
@@ -331,7 +331,7 @@ export function computeColumnDrift(expected, live) {
331
331
  }
332
332
  if (types && types[col]) {
333
333
  // Both sides are the canonical format_type() form (expected from the manifest, actual from the
334
- // live catalog), so a direct comparison suffices — no alias folding needed.
334
+ // live catalog), so a direct comparison suffices. No alias folding needed.
335
335
  const actualType = liveCol.type ?? '';
336
336
  if (types[col].type !== actualType) {
337
337
  typeMismatches.push({ column: col, expected: types[col].type, actual: actualType });
@@ -374,7 +374,7 @@ export function computeConstraintDrift(expected, live) {
374
374
  }
375
375
  return drift;
376
376
  }
377
- // Enum diff: ordered value-set comparison. An absent enum (empty actual — pre-enum schema or a backend
377
+ // Enum diff: ordered value-set comparison. An absent enum (empty actual, pre-enum schema or a backend
378
378
  // without enums) is not treated as drift. Order is significant; the numeric base type relies on it.
379
379
  function computeEnumDrift(name, expected, actual) {
380
380
  if (actual.length === 0)
@@ -384,13 +384,13 @@ function computeEnumDrift(name, expected, actual) {
384
384
  }
385
385
  // Presence + index definition diff: which managed indexes exist, plus (for present, valid ones) a
386
386
  // key-column-order and partial-predicate comparison. Ordinals are treated asymmetrically, the same
387
- // convention test/pgSchemaHelper.ts encodes — index column ORDER is significant here (an index on
387
+ // convention test/pgSchemaHelper.ts encodes, index column ORDER is significant here (an index on
388
388
  // (a, b) differs from (b, a)); the table-column diff instead normalises ordinal position away.
389
389
  //
390
390
  // Generic engine: it carries no pg-boss knowledge. Every dimension is one optional `{ expected, live }`
391
- // entry — none is privileged. `indexes.building` names async builds still in progress (pulled out of
391
+ // entry, none is privileged. `indexes.building` names async builds still in progress (pulled out of
392
392
  // "missing"); `tables.expected` is the managed table set that scopes the extra-index warning. Each
393
- // dimension is best-effort — callers omit the ones a backend can't support.
393
+ // dimension is best-effort, callers omit the ones a backend can't support.
394
394
  export function computeSchemaDrift(opts = {}) {
395
395
  const expectedIndexes = opts.indexes?.expected ?? [];
396
396
  const liveIndexes = opts.indexes?.live ?? [];
@@ -412,7 +412,7 @@ export function computeSchemaDrift(opts = {}) {
412
412
  else if (idx.keys && found.def) {
413
413
  // Definition-diff: a present, valid index whose key columns/order or predicate differ from the
414
414
  // expected shape. Comparison is on the normalised forms (order-significant, format-insensitive),
415
- // but the report carries the readable raw text. Only when the key list parses — an unparseable
415
+ // but the report carries the readable raw text. Only when the key list parses. An unparseable
416
416
  // def is skipped, not falsely flagged. (An empty normalised key list means we could not parse the
417
417
  // def; '' is never a real key list, whereas an empty predicate is legitimate for a non-partial
418
418
  // index.)
@@ -436,7 +436,7 @@ export function computeSchemaDrift(opts = {}) {
436
436
  }
437
437
  }
438
438
  // Extra indexes: standalone (non-constraint-backing) indexes on a managed table that the expected set
439
- // does not account for — a stale pg-boss index or one a user added. These are informational (an extra
439
+ // does not account for. A stale pg-boss index or one a user added. These are informational (an extra
440
440
  // index is harmless), so they are surfaced as a warning and do NOT flip `ok`. Scoped to managed tables
441
441
  // so a user's indexes on their own tables in the schema are never reported.
442
442
  const managedTables = new Set(opts.tables?.expected ?? expectedIndexes.map(i => i.table));