@cloudbitmaps/core 0.18.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{chunk-ML6AT53O.js → chunk-TNWTIAON.js} +50 -9
  2. package/dist/chunk-TNWTIAON.js.map +7 -0
  3. package/dist/core/audit.d.ts +13 -3
  4. package/dist/core/bit-route.d.ts +5 -5
  5. package/dist/core/blob.d.ts +4 -1
  6. package/dist/core/budget.d.ts +7 -2
  7. package/dist/core/combine-feed.d.ts +3 -7
  8. package/dist/core/combine-many.d.ts +22 -29
  9. package/dist/core/consistency.d.ts +0 -1
  10. package/dist/core/cost.d.ts +13 -2
  11. package/dist/core/crbm/reader.d.ts +4 -3
  12. package/dist/core/crbm-storage-source.d.ts +86 -13
  13. package/dist/core/discover.d.ts +3 -5
  14. package/dist/core/engine.d.ts +15 -0
  15. package/dist/core/erase-id.d.ts +7 -5
  16. package/dist/core/erasure.d.ts +22 -2
  17. package/dist/core/errors.d.ts +10 -2
  18. package/dist/core/load-input.d.ts +1 -2
  19. package/dist/core/load.d.ts +48 -3
  20. package/dist/core/metrics.d.ts +19 -7
  21. package/dist/core/ports.d.ts +21 -62
  22. package/dist/core/retention-sweep.d.ts +9 -4
  23. package/dist/core/retention.d.ts +17 -0
  24. package/dist/core/retry.d.ts +6 -0
  25. package/dist/driver-kit.d.ts +1 -1
  26. package/dist/driver-kit.js +8 -101
  27. package/dist/driver-kit.js.map +2 -2
  28. package/dist/drivers/_shared/object-registry.d.ts +1 -19
  29. package/dist/drivers/_shared/registry.d.ts +5 -2
  30. package/dist/drivers/localfs/fs-util.d.ts +7 -0
  31. package/dist/drivers/localfs/registry.d.ts +1 -0
  32. package/dist/index.d.ts +5 -9
  33. package/dist/index.js +864 -600
  34. package/dist/index.js.map +4 -4
  35. package/package.json +1 -1
  36. package/dist/chunk-ML6AT53O.js.map +0 -7
  37. package/dist/core/combine-held.d.ts +0 -43
  38. package/dist/core/registry-reap.d.ts +0 -22
@@ -59,8 +59,9 @@ export interface LoadDeps {
59
59
  /**
60
60
  * What a plausible result looks like, so an implausible one is refused instead of published.
61
61
  *
62
- * Both bounds are compared against the **current** generation's cardinality, read from the `.crbm` index rather
63
- * than its payload — one object-header read, and only when a bound is actually set.
62
+ * `minCardinality` judges the new generation alone. `minRetained` and `maxGrowth` judge it against the **current**
63
+ * generation's cardinality, which the load takes from the registry row's summary of it when the row has a usable one,
64
+ * and otherwise from the `.crbm` index (one object-header read, never the payload), and only when a bound needs it.
64
65
  */
65
66
  export interface LoadGuard {
66
67
  /** Refuse a generation with fewer than this many ids. Use for "this segment is never legitimately tiny". */
@@ -76,7 +77,25 @@ export interface LoadGuard {
76
77
  * permissive, which is where an inversion would eventually be written and not noticed.
77
78
  */
78
79
  readonly minRetained?: number;
80
+ /**
81
+ * Refuse a generation larger than this multiple of the current one — `1.5` means "at most half as large again".
82
+ * The ceiling to `minRetained`'s floor: a source that lands duplicated or joined wrong grows a segment as quietly as
83
+ * a partial one shrinks it. A shrinking load is never refused by this bound, and neither is the first load of a
84
+ * segment nor one onto an empty segment, which have no size to multiply. `1` means "never grow".
85
+ *
86
+ * `0` means no bound, as on the two fields above. Setting it makes the load read the current size and fence its
87
+ * publish on it, as `minRetained` does, `allowEmpty: true` or not. The comparison is in floating point, so do not
88
+ * rely on the exact boundary: `100 × 1.15` is `114.99999999999999`.
89
+ */
90
+ readonly maxGrowth?: number;
79
91
  }
92
+ /**
93
+ * Check a guard's bounds before any round trip, and copy them: the one check `load` and each output of
94
+ * `materializeMany` share. The copy is what the load judges, so a guard object the caller reuses or changes while the
95
+ * call runs cannot change the bound it is judged by. `where` prefixes each field's name in the message (`''`, or
96
+ * `'outputs[3].'`).
97
+ */
98
+ export declare function checkedGuard(guard: LoadGuard | undefined, where: string): LoadGuard | undefined;
80
99
  export interface LoadOptions {
81
100
  /**
82
101
  * Publish an empty generation over a non-empty one. Off by default: an empty result is far more often an
@@ -119,8 +138,34 @@ export interface LoadOptions {
119
138
  readonly metadata?: GenerationMetadata;
120
139
  readonly audit?: IAuditSink;
121
140
  }
141
+ /** The bounds a guard can refuse a generation for: every {@link LoadRefusal} but a lost race. */
142
+ export type GuardRefusal = Exclude<LoadRefusal, 'superseded'>;
143
+ /**
144
+ * The bound a generation of `cardinality` ids breaks over a current one of `before` (`null`: none, or not read), in the
145
+ * order a load checks them, or `undefined`. Pure: the one judgement a load and a dry run share.
146
+ */
147
+ export declare function guardRefusal(cardinality: number, before: number | null, guard: LoadGuard | undefined, allowEmpty: boolean | undefined): GuardRefusal | undefined;
148
+ /** What a dry run learns about one write: the size it would replace, and the bound that would refuse it. */
149
+ export interface LoadJudgement {
150
+ /**
151
+ * The current generation's size: `null` when there is none or its object is gone, and, where no bound needs the size,
152
+ * when its object could not be read, as the load would not read it. Always asked for.
153
+ */
154
+ readonly cardinalityBefore: number | null;
155
+ /** The bound a load of this size would be refused for now, absent when it would publish. */
156
+ readonly wouldRefuse?: GuardRefusal;
157
+ /** The requests the judgement made: the row, and the object's index when the row had no usable summary. */
158
+ readonly reads: number;
159
+ }
160
+ /**
161
+ * Judge a write of `cardinality` ids into `ref` as a load would, and write nothing: read the row and the current size
162
+ * as the load reads them, and apply the same bounds. A `destroyed` segment throws the `ValidationError` a load's write
163
+ * would, and an encrypted one with no keystore the `KeyUnavailableError`. What it says holds for now only: a load
164
+ * made later is judged against the segment as it is then.
165
+ */
166
+ export declare function judgeLoad(ref: SegmentRef, cardinality: number, deps: LoadDeps, options?: Pick<LoadOptions, 'allowEmpty' | 'guard'>): Promise<LoadJudgement>;
122
167
  /** Why a load did not become current. */
123
- export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained'
168
+ export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained' | 'max-growth'
124
169
  /**
125
170
  * Another writer got there first. Either another load wrote the same generation number first, so the write-once
126
171
  * put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
@@ -14,8 +14,15 @@
14
14
  * per-series metric labels unless they're known low-cardinality and PII-free. See the observability
15
15
  * guide.
16
16
  */
17
- /** The segment operations that emit an `op` latency event (timed with the injected clock, at the facade). */
18
- export type MetricOpName = 'has' | 'count' | 'intersectInto' | 'unionInto' | 'andNotInto';
17
+ /**
18
+ * The operations that emit an `op` latency event (timed with the injected clock, at the facade). `materializeMany` is
19
+ * timed from its first request, the pins, to its last publish: a call its input checks refuse emits none.
20
+ *
21
+ * A name added here stops two kinds of code compiling: a sink that switches on `name` with an exhaustive `never` check
22
+ * (add a case, or use a `default` branch, which needs no change), and a `Record<MetricOpName, …>` built by hand, a
23
+ * `MetricsSnapshot` literal among them (add the key).
24
+ */
25
+ export type MetricOpName = 'has' | 'count' | 'intersectInto' | 'unionInto' | 'andNotInto' | 'materializeMany';
19
26
  /**
20
27
  * One observability event. A discriminated union on `kind` — new variants can be added over time without
21
28
  * breaking consumers (they switch on the kinds they care about). `namespace` is the segment's namespace
@@ -27,10 +34,11 @@ export type MetricEvent = {
27
34
  readonly namespace?: string;
28
35
  readonly segment: string;
29
36
  /**
30
- * One event per request for chunks: a range request of a combine or `iterate`, which carries every chunk the read
31
- * needs from a stretch of the object, or the one chunk of a read made on its own, however many callers were waiting
32
- * on it. Bytes returned: the range's, the gaps between its chunks included (0 if a single chunk was absent — a GET
33
- * still happened).
37
+ * One event per request for chunks: a range request of a combine, `iterate` or `materializeMany`, which carries
38
+ * every chunk the read needs from a stretch of the object, or the one chunk of a read made on its own, however
39
+ * many callers were waiting on it. A `materializeMany` range serves every output of its group, so its events
40
+ * count the requests the call sent, not requests per output. Bytes returned: the range's, the gaps between its
41
+ * chunks included (0 if a single chunk was absent — a GET still happened).
34
42
  */
35
43
  readonly bytes: number;
36
44
  /**
@@ -45,7 +53,7 @@ export type MetricEvent = {
45
53
  * no cached chunk. A combine or `iterate` looks up every chunk it needs when it opens its streams. A caller that
46
54
  * missed while another caller's read of the same chunk was open waits on that read and adds no `storage.get`, and a
47
55
  * range carries several chunks, so the misses can outnumber the `storage.get` events; the `storage.get` count is
48
- * the number of requests for chunks.
56
+ * the number of requests for chunks. `materializeMany` never looks up the cache, so it emits none.
49
57
  */
50
58
  readonly kind: 'cache';
51
59
  readonly hit: boolean;
@@ -57,6 +65,10 @@ export type MetricEvent = {
57
65
  readonly attempt: number;
58
66
  readonly delayMs: number;
59
67
  } | {
68
+ /**
69
+ * One per chunk-aligned combine of one operator. `materializeMany` emits none: an output is an expression over
70
+ * several operators, and the call's `stats.chunks.pruned` counts the index keys it did not read.
71
+ */
60
72
  readonly kind: 'intersect';
61
73
  /**
62
74
  * Which chunk-aligned combine this was. **Optional** — absent means `'intersect'`.
@@ -10,7 +10,10 @@
10
10
  import type { BlobSink } from './blob.js';
11
11
  import type { WrappedDek } from './crypto.js';
12
12
  import type { IMetricsSink } from './metrics.js';
13
- /** Opaque optimistic-concurrency token — unique per write, compared by equality only. */
13
+ /**
14
+ * Opaque optimistic-concurrency token — unique per write with overwhelming probability, since it carries random parts
15
+ * (see {@link RegistryRecord.token}), and compared by equality only.
16
+ */
14
17
  export type Token = string;
15
18
  /**
16
19
  * The address of one segment: a `namespace` and a `segment` name, both any non-empty string of at most 256
@@ -214,8 +217,14 @@ export interface StorageCaps {
214
217
  * shipped driver to them:
215
218
  *
216
219
  * - **`putImmutable` is write-once and reports a collision as {@link WriteConflictError}.** It never overwrites a
217
- * stored object. `load` reads that error as a lost race for the generation number (`superseded`), so a driver
218
- * that threw anything else for an existing key would surface as a failed load.
220
+ * stored object, two concurrent writes of one key included: exactly one lands. `load` reads that error as a lost
221
+ * race for the generation number (`superseded`), so a driver that threw anything else for an existing key would
222
+ * surface as a failed load. A write whose writer fails stores nothing, and the key can be written after.
223
+ * - **Keys keep every name apart.** One segment in two namespaces, two names an encoding could fold into one (`a/b`
224
+ * and `a_b`), and a name that extends another (`s` and `s.1`) are separate objects, each listed and deleted alone;
225
+ * generations are listed and deleted by number, never by a prefix of one.
226
+ * - **A bucket or container that does not exist is not a missing object**: it fails with an error that is neither
227
+ * {@link NotFoundError}, which would read as an empty segment, nor a {@link TransientError}, which a retry cannot fix.
219
228
  * - **A missing object makes `getRange` and `getTail` throw {@link NotFoundError}** — never an empty or short
220
229
  * result. Heal-forward (a read whose generation a sweep collected re-resolves), the erasure's holder probe and
221
230
  * its verify of the rewrite, and a pin's replaced-object check all branch on that one error. (A zero-length
@@ -234,6 +243,8 @@ export interface StorageCaps {
234
243
  * holding the id, `dropSegment`'s `generationsRemaining`, the retention sweep's check that a tombstone's storage
235
244
  * is gone, and rollback's post-move check prove a deletion or a presence by listing, so a listing that lags
236
245
  * reports an erasure as incomplete or a stale object as gone.
246
+ * - **`list` yields every generation, however many pages the service splits it into.** One that stops after its
247
+ * first page drops generations from every collection and every check above.
237
248
  * - **Never replay a conditional write without telling the replay apart.** A `putImmutable` that lands and then
238
249
  * loses its response, sent again, finds its own object and would report a collision. Either send each write
239
250
  * once, with the backend client's retry off for that request, or recognise your own write when the
@@ -486,47 +497,6 @@ export interface RegCaps {
486
497
  */
487
498
  readonly conditionalDelete?: boolean;
488
499
  }
489
- /** What {@link IRegistryDriver.reapLegacyTombstones} is asked to do. */
490
- export interface ReapLegacyTombstonesOptions {
491
- /** Only rows in this namespace; every namespace, the library's bookkeeping namespaces included, when absent. */
492
- readonly namespace?: string;
493
- /** Count what would be removed and remove nothing. */
494
- readonly dryRun: boolean;
495
- /** The most rows to remove (or, in a dry run, to count as removable) in this call. */
496
- readonly limit: number;
497
- }
498
- /** What a reap of the tombstones a registry holds found. Every count is of rows, not requests. */
499
- export interface ReapLegacyTombstonesResult {
500
- /** The call was a dry run: nothing was removed, and `wouldReap` says what a real run removes. */
501
- readonly dryRun: boolean;
502
- /** Rows read. Rows the `limit` stopped the run before reading are not counted. */
503
- readonly examined: number;
504
- /** Rows removed. Always 0 in a dry run. */
505
- readonly reaped: number;
506
- /** Rows a dry run found removable. Always 0 in a real run. */
507
- readonly wouldReap: number;
508
- /**
509
- * The `limit` was spent and the listing had more keys: rows were left unread, and they may or may not be removable,
510
- * so run again. A run is not resumable: each lists and reads from the start, so a dry run with a `limit` reports the
511
- * same first rows every time, and `wouldReap` is a total only when this is `false`.
512
- */
513
- readonly limited: boolean;
514
- /** Rows read and left, by why. */
515
- readonly skipped: {
516
- /** A live (`active`, not deleted) row. */
517
- readonly live: number;
518
- /** A `destroyed` row that is not `deleted`: a crypto-shred's or a drop's tombstone, kept as the attestation of an erasure. */
519
- readonly destroyed: number;
520
- /**
521
- * A deleted row that carries an incarnation id, written by a 0.12 or later release. On a registry that removes rows,
522
- * `delete` and the sweep remove such a row themselves; one left while the registry could not remove rows (GCS by
523
- * default, S3 on a custom endpoint) stays, and this call, which refuses on such a registry, does not remove it.
524
- */
525
- readonly incarnated: number;
526
- /** A row that changed, or was already gone (a 404), between its read and its delete, so the delete (fenced on the version read) refused. */
527
- readonly raced: number;
528
- };
529
- }
530
500
  /**
531
501
  * Refuse, with `UnsupportedError`, a write that would write an object before its row when the registry reports it
532
502
  * cannot write a row (see {@link RegCaps.canWrite}). Called before the write's first request.
@@ -623,8 +593,8 @@ export declare function brandAsBackend<T extends {
623
593
  export declare function isStorageBackend(value: unknown): value is StorageBackend;
624
594
  /**
625
595
  * Per-segment registry — the authoritative source of `currentGen`, the discovery index, and the wrapped-DEK
626
- * holder. One row per segment under OCC: a never-reused, equality-compared {@link Token} (ABA-safe across
627
- * delete→recreate).
596
+ * holder. One row per segment under OCC: an equality-compared {@link Token} that is never reused, with overwhelming
597
+ * probability (ABA-safe across delete→recreate).
628
598
  *
629
599
  * **What a driver must do** — callers rely on each of these, and the registry conformance suite holds every
630
600
  * shipped driver to them:
@@ -643,7 +613,11 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
643
613
  * **With `expected` it is fenced**: it lands only while the row still carries that token, and otherwise throws
644
614
  * {@link WriteConflictError} and leaves the row.
645
615
  * - **Reads are strongly consistent** (`RegCaps.strongRead`), and `list` yields every existing row,
646
- * `destroyed` tombstones included, with every field (see {@link IRegistryDriver.list}).
616
+ * `destroyed` tombstones included, with every field (see {@link IRegistryDriver.list}), however many pages the
617
+ * service splits the listing into.
618
+ * - **A bucket or container that does not exist is not an absent row**: a read, a listing and a write fail with an
619
+ * error that is neither {@link NotFoundError} nor a {@link TransientError}, and a read never answers `null`, which would
620
+ * read as a segment with no generation.
647
621
  * - **Never replay a conditional write without telling the replay apart.** A `create` or `compareAndSwap`
648
622
  * that lands and then loses its response, sent again, finds its own write and would report a conflict.
649
623
  * Either send each write once, with the backend client's retry off for that request, or recognise your own
@@ -701,20 +675,5 @@ export interface IRegistryDriver {
701
675
  * created after the decision. A third-party driver that ignores `expected` keeps the unfenced behaviour.
702
676
  */
703
677
  delete(ref: SegmentRef, expected?: Token): Promise<void>;
704
- /**
705
- * Optional: remove the `deleted: true` rows with no incarnation id, which a release before 0.12 left, or a 0.12 or
706
- * later release left by deleting a row born before 0.12, so that no later full `list` reads them. Only a registry that
707
- * keeps such rows in its backend implements it; the object registries do.
708
- *
709
- * It removes a row only if it is `deleted: true` with no incarnation id (whatever its `status`), and only by a delete
710
- * the backend applies under the version it read (see {@link RegCaps.conditionalDelete}), so a `create` that lands over
711
- * the row first wins. A registry that cannot delete under that precondition throws `CapabilityError` before its first
712
- * request. It never touches a live row, a `destroyed` row that is not `deleted`, or a deleted row with an incarnation
713
- * id, so it does not remove every row a full `list` reads. If `conditionalDelete` was set on a backend that ignores the
714
- * precondition, the delete is unfenced.
715
- *
716
- * Not resumable: every call lists and reads from the start (R reads for R rows, whatever the `limit`).
717
- */
718
- reapLegacyTombstones?(options: ReapLegacyTombstonesOptions): Promise<ReapLegacyTombstonesResult>;
719
678
  }
720
679
  export {};
@@ -177,7 +177,8 @@ export type RetireEntry = {
177
177
  * expiring is the silence that costs a retention commitment.
178
178
  * `'limit'` — eligible, but this cycle's `limit` was already spent. Re-run to continue.
179
179
  * `'tombstone-not-empty'` — a tombstone whose Storage generations are not gone even after a GC
180
- * attempt, so its row is kept: the row is what keeps the segment reachable by the generation collection and
180
+ * attempt, so its row is kept, and, unless the collection lost a race to a write of the row, a refused purge
181
+ * (see {@link RetireExpiredResult.purgeFaults}): the row is what keeps the segment reachable by the generation collection and
181
182
  * refused by every writer. Several causes, all self-healing: the storage really could not be reclaimed,
182
183
  * the collection *declined* because the row changed underneath it (`WriteConflictError`, which this
183
184
  * sweep swallows deliberately), or this was a `dryRun`, which reports the reason without attempting the
@@ -193,7 +194,10 @@ export type RetireEntry = {
193
194
  export interface RetireExpiredResult {
194
195
  /** Registry rows enumerated. */
195
196
  readonly scanned: number;
196
- /** Rows whose policy said "expired" — including any the `limit` deferred. */
197
+ /**
198
+ * Rows whose policy said "expired", counted until the `limit` stopped the scan: the first row it deferred is counted
199
+ * and the rows after it are not read, so with `limited: true` this is a lower bound.
200
+ */
197
201
  readonly eligible: number;
198
202
  /**
199
203
  * Segments **actually retired**. Zero under `dryRun` — see `wouldRetire`. Kept honest because this is the field
@@ -208,8 +212,9 @@ export interface RetireExpiredResult {
208
212
  /** True when `limit` cut the cycle short — **more segments are still eligible**. Re-run. */
209
213
  readonly limited: boolean;
210
214
  /**
211
- * Deletes this sweep attempted that the registry **refused for a reason other than a lost race**: a tombstone's
212
- * purge, or the removal of a due-index pointer. A policy that denies delete, an Azure blob with a snapshot, or any
215
+ * Deletes this sweep attempted that were **refused for a reason other than a lost race**: a tombstone's purge
216
+ * (including one whose objects were still there after a collection that raised nothing), or the removal of a
217
+ * due-index pointer. A policy that denies delete, an Azure blob with a snapshot, or any
213
218
  * raw provider error is one; a write that landed between the sweep's read and its fenced delete is not (that is
214
219
  * `failed: contended` in the ledger). Each leaves its row or pointer in place, so a purge that keeps failing never
215
220
  * frees the name.
@@ -44,6 +44,14 @@ export interface SetRetentionResult {
44
44
  */
45
45
  readonly indexed: boolean;
46
46
  }
47
+ /**
48
+ * The key of the retention sweep's mark on a tombstone it wrote, which is what lets a later sweep purge the row. Only the
49
+ * sweep's own tombstone write sets it, and every other tombstone write removes it ({@link withoutSweepMark}), so a
50
+ * restored or hand-edited row cannot hand the sweep a tombstone it did not write.
51
+ */
52
+ export declare const SWEEP_MARK = "retiredBySweepAt";
53
+ /** `meta` without the sweep's mark, or `undefined` when it carries none, so a write that changes nothing is not made. */
54
+ export declare function withoutSweepMark(meta: GovernanceMeta | undefined): GovernanceMeta | undefined | null;
47
55
  /**
48
56
  * Read the policy off a registry row's `retention` metadata. Returns `null` when there is no policy, and
49
57
  * `'invalid'` when the key is present but unusable (wrong type, or below the epoch-seconds floor).
@@ -56,6 +64,15 @@ export interface SetRetentionResult {
56
64
  export declare function readRetentionPolicy(meta: GovernanceMeta | undefined): RetentionPolicy | null | 'invalid';
57
65
  /** The stored policy for one segment, or `null` if it has none (and `'invalid'` for a malformed one). */
58
66
  export declare function getSegmentRetention(ref: SegmentRef, deps: RetentionDeps): Promise<RetentionPolicy | null | 'invalid'>;
67
+ /**
68
+ * Record when a segment becomes eligible for retirement, creating its registry row if it has none.
69
+ *
70
+ * Idempotent and safe to re-run: writing the same instant twice is a no-op in effect. Other keys already in the
71
+ * row's `retention` metadata are preserved — this owns one key, not the whole object.
72
+ *
73
+ * Refuses a `destroyed` segment: a tombstone has nothing left to retire, and putting a policy on one would make
74
+ * a sweep repeatedly "retire" bytes that are already gone.
75
+ */
59
76
  export declare function setSegmentRetention(ref: SegmentRef, deps: RetentionDeps, policy: RetentionPolicy): Promise<SetRetentionResult>;
60
77
  /**
61
78
  * Remove a segment's expiry so no sweep will retire it. Returns whether a policy was actually removed (`false`
@@ -75,6 +75,12 @@ export type ReadRetry = RetryDeps & {
75
75
  * The write itself is never passed here.
76
76
  */
77
77
  export declare function retryRead<T>(op: () => Promise<T>, retry: ReadRetry | undefined): Promise<T>;
78
+ /**
79
+ * `policy`, checked when it is wired rather than at the first retry: a delay or factor that is not a finite number, a
80
+ * negative delay, a factor below 1 or a jitter mode it does not know would make the backoff `NaN`, which a timer reads
81
+ * as 1 ms, so bounded backoff silently became a hot retry loop.
82
+ */
83
+ export declare function checkedRetryPolicy(policy: RetryPolicy): RetryPolicy;
78
84
  /**
79
85
  * Run `op`, retrying transient failures per `policy`. Resolves with `op`'s result, or rejects with the last
80
86
  * error once attempts are exhausted (or immediately for a non-retryable error). The thrown error is always
@@ -1,7 +1,7 @@
1
1
  export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, } from './core/ports.js';
2
2
  export { brandAsBackend, STORAGE_BACKEND } from './core/ports.js';
3
3
  export type { IMetricsSink, MetricEvent } from './core/metrics.js';
4
- export { IntegrityError, isNotFoundError, isValidationError, isWriteConflictError, NotFoundError, TransientError, ValidationError, WriteConflictError, } from './core/errors.js';
4
+ export { IntegrityError, isIntegrityError, isNotFoundError, isTransientError, isValidationError, isWriteConflictError, NotFoundError, TransientError, ValidationError, WriteConflictError, } from './core/errors.js';
5
5
  export { validateSegmentRef } from './core/validate.js';
6
6
  export type { BlobSink } from './core/blob.js';
7
7
  export { MAX_ROW_BYTES, ObjectStoreRegistry, ObjectVersionRaced, } from './drivers/_shared/object-registry.js';
@@ -1,5 +1,4 @@
1
1
  import {
2
- CapabilityError,
3
2
  DEFAULT_NAMESPACE,
4
3
  IntegrityError,
5
4
  NotFoundError,
@@ -14,7 +13,9 @@ import {
14
13
  encodeNameForPath,
15
14
  entropyIsAvailable,
16
15
  incarnationOf,
16
+ isIntegrityError,
17
17
  isNotFoundError,
18
+ isTransientError,
18
19
  isValidationError,
19
20
  isWriteConflictError,
20
21
  mapWithConcurrency,
@@ -29,7 +30,7 @@ import {
29
30
  validateRegistryPatch,
30
31
  validateSegmentRef,
31
32
  webCryptoEntropy
32
- } from "./chunk-ML6AT53O.js";
33
+ } from "./chunk-TNWTIAON.js";
33
34
 
34
35
  // src/drivers/_shared/object-registry-keys.ts
35
36
  var REGISTRY_SUFFIX = ".reg";
@@ -205,6 +206,7 @@ var ObjectStoreRegistry = class {
205
206
  return this.decodeRow(key, seen);
206
207
  }
207
208
  async *list(namespace) {
209
+ if (namespace !== void 0) validateSegmentRef({ segment: "x", namespace });
208
210
  const prefix = registryListPrefix(this.prefix, namespace);
209
211
  let page = [];
210
212
  const flush = async function* (keys) {
@@ -258,104 +260,6 @@ var ObjectStoreRegistry = class {
258
260
  }
259
261
  throw new WriteConflictError(`registry delete: contention deleting "${ref.segment}" \u2014 retry`);
260
262
  }
261
- /**
262
- * Remove the `deleted: true` rows with no incarnation id (the envelopes a release before 0.12 left), or count them.
263
- * One listing, a read per key and a conditional delete per envelope, so a row that a create wrote over since it was
264
- * read keeps its new version and the delete is refused. Refuses with {@link CapabilityError}, before any request,
265
- * where the store's delete is not known to apply its precondition.
266
- *
267
- * An object that cannot be read or parsed stops the run with the error that names its key, and removes nothing
268
- * more: it may be a newer release's row. Rows removed before the stop stay removed, and a re-run is safe.
269
- *
270
- * Not resumable: it lists and reads from the start on every call. A delete refused as a conflict, including one for a
271
- * row already gone, is counted as `raced`.
272
- */
273
- async reapLegacyTombstones(options) {
274
- await this.store.resolveCapabilities?.();
275
- const remove = this.removesRows() ? this.store.delete : void 0;
276
- if (remove === void 0) {
277
- throw new CapabilityError(
278
- `reapRegistryTombstones: this registry's delete is not conditional (capabilities().conditionalDelete is false), so a removal could take a row a create has just written over the envelope. Nothing was requested`
279
- );
280
- }
281
- const { dryRun, limit } = options;
282
- const counts = {
283
- examined: 0,
284
- reaped: 0,
285
- wouldReap: 0,
286
- live: 0,
287
- destroyed: 0,
288
- incarnated: 0,
289
- raced: 0
290
- };
291
- let limited = false;
292
- const removable = () => counts.reaped + counts.wouldReap;
293
- const handle = async (keys) => {
294
- const rows = await mapWithConcurrency(
295
- keys,
296
- LIST_READ_CONCURRENCY,
297
- (key) => this.readRow(key).then((row) => ({ key, row }))
298
- );
299
- const found = [];
300
- for (const { key, row } of rows) {
301
- if (row === null) continue;
302
- counts.examined += 1;
303
- const { deleted, record } = row.env;
304
- if (!deleted) {
305
- if (record.status === "destroyed") counts.destroyed += 1;
306
- else counts.live += 1;
307
- } else if (incarnationOf(record.token) !== void 0) {
308
- counts.incarnated += 1;
309
- } else if (removable() + found.length >= limit) {
310
- limited = true;
311
- } else {
312
- found.push({ key, version: row.version });
313
- }
314
- }
315
- if (dryRun) {
316
- counts.wouldReap += found.length;
317
- return;
318
- }
319
- await mapWithConcurrency(found, LIST_READ_CONCURRENCY, async ({ key, version }) => {
320
- try {
321
- await remove.call(this.store, key, { version });
322
- counts.reaped += 1;
323
- } catch (err) {
324
- if (!isWriteConflictError(err)) throw err;
325
- counts.raced += 1;
326
- }
327
- });
328
- };
329
- let page = [];
330
- for await (const key of this.store.listKeys(
331
- registryListPrefix(this.prefix, options.namespace)
332
- )) {
333
- if (parseRegistryKey(this.prefix, key) === null) continue;
334
- if (removable() >= limit) {
335
- limited = true;
336
- break;
337
- }
338
- page.push(key);
339
- if (page.length >= LIST_READ_CONCURRENCY) {
340
- await handle(page);
341
- page = [];
342
- }
343
- }
344
- if (page.length > 0) await handle(page);
345
- return {
346
- dryRun,
347
- examined: counts.examined,
348
- reaped: counts.reaped,
349
- wouldReap: counts.wouldReap,
350
- limited,
351
- skipped: {
352
- live: counts.live,
353
- destroyed: counts.destroyed,
354
- incarnated: counts.incarnated,
355
- raced: counts.raced
356
- }
357
- };
358
- }
359
263
  /**
360
264
  * Read + parse a registry object, returning its envelope and version fence, or null if absent.
361
265
  *
@@ -380,7 +284,8 @@ var ObjectStoreRegistry = class {
380
284
  `registry object has no version fence (needed for compare-and-swap): ${objectKey}`
381
285
  );
382
286
  }
383
- const env = parseRegistryEnvelope(new TextDecoder().decode(row.bytes), objectKey);
287
+ const stored = parseRegistryKey(this.prefix, objectKey) ?? void 0;
288
+ const env = parseRegistryEnvelope(new TextDecoder().decode(row.bytes), objectKey, stored);
384
289
  return { env, bytes: row.bytes, version: row.version };
385
290
  }
386
291
  /** {@link ObjectRegistryStore.read}, retrying the bounded number of times a mid-read overwrite allows. */
@@ -418,7 +323,9 @@ export {
418
323
  brandAsBackend,
419
324
  encodeNameForKey,
420
325
  encodeNameForPath,
326
+ isIntegrityError,
421
327
  isNotFoundError,
328
+ isTransientError,
422
329
  isValidationError,
423
330
  isWriteConflictError,
424
331
  namespaceKeyPart,