@cloudbitmaps/core 0.19.1 → 0.21.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 (81) hide show
  1. package/README.md +85 -0
  2. package/dist/{chunk-RSLVTRJY.js → chunk-IGLIZBR7.js} +982 -281
  3. package/dist/chunk-IGLIZBR7.js.map +7 -0
  4. package/dist/core/absent-operands.d.ts +8 -0
  5. package/dist/core/audit.d.ts +67 -4
  6. package/dist/core/bit-route.d.ts +2 -1
  7. package/dist/core/blob.d.ts +12 -1
  8. package/dist/core/bulk-load.d.ts +148 -0
  9. package/dist/core/cas-retry.d.ts +27 -0
  10. package/dist/core/check.d.ts +22 -0
  11. package/dist/core/chunk-checks.d.ts +6 -1
  12. package/dist/core/chunk-window.d.ts +11 -4
  13. package/dist/core/combine-expr.d.ts +1 -3
  14. package/dist/core/combine-feed.d.ts +2 -16
  15. package/dist/core/combine-many.d.ts +21 -15
  16. package/dist/core/concurrency.d.ts +8 -0
  17. package/dist/core/consistency.d.ts +76 -18
  18. package/dist/core/crbm/fingerprint.d.ts +23 -0
  19. package/dist/core/crbm/format.d.ts +2 -5
  20. package/dist/core/crbm/frame.d.ts +10 -0
  21. package/dist/core/crbm/plan-reads.d.ts +9 -3
  22. package/dist/core/crbm/reader.d.ts +51 -12
  23. package/dist/core/crbm/varint.d.ts +2 -1
  24. package/dist/core/crbm/writer.d.ts +5 -1
  25. package/dist/core/crbm-open.d.ts +154 -0
  26. package/dist/core/crbm-storage-source.d.ts +243 -490
  27. package/dist/core/crbm-write.d.ts +75 -0
  28. package/dist/core/crypto.d.ts +6 -1
  29. package/dist/core/engine.d.ts +239 -47
  30. package/dist/core/erase-id.d.ts +97 -27
  31. package/dist/core/erasure.d.ts +4 -3
  32. package/dist/core/errors.d.ts +8 -2
  33. package/dist/core/generation-gc.d.ts +131 -16
  34. package/dist/core/generation-list.d.ts +11 -0
  35. package/dist/core/id-window.d.ts +46 -0
  36. package/dist/core/invalidation-scope.d.ts +77 -0
  37. package/dist/core/known-generations-shape.d.ts +13 -0
  38. package/dist/core/known-generations.d.ts +147 -0
  39. package/dist/core/leases.d.ts +9 -1
  40. package/dist/core/ledger-seed.d.ts +17 -0
  41. package/dist/core/load.d.ts +17 -36
  42. package/dist/core/lru.d.ts +2 -15
  43. package/dist/core/metrics.d.ts +9 -7
  44. package/dist/core/order.d.ts +2 -0
  45. package/dist/core/pinned-storage-source.d.ts +7 -7
  46. package/dist/core/pointer-id.d.ts +19 -0
  47. package/dist/core/ports.d.ts +210 -45
  48. package/dist/core/publish.d.ts +207 -0
  49. package/dist/core/reader-defaults.d.ts +14 -3
  50. package/dist/core/registry-scan.d.ts +0 -2
  51. package/dist/core/retention-sweep.d.ts +69 -65
  52. package/dist/core/retention.d.ts +1 -1
  53. package/dist/core/retry.d.ts +12 -19
  54. package/dist/core/rollback.d.ts +62 -32
  55. package/dist/core/summary.d.ts +74 -25
  56. package/dist/core/token.d.ts +13 -8
  57. package/dist/core/validate.d.ts +2 -0
  58. package/dist/driver-kit.d.ts +6 -1
  59. package/dist/driver-kit.js +90 -10
  60. package/dist/driver-kit.js.map +3 -3
  61. package/dist/drivers/_shared/bytes.d.ts +6 -0
  62. package/dist/drivers/_shared/faults.d.ts +15 -0
  63. package/dist/drivers/_shared/object-keys.d.ts +18 -0
  64. package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
  65. package/dist/drivers/_shared/object-registry.d.ts +3 -1
  66. package/dist/drivers/_shared/options.d.ts +19 -0
  67. package/dist/drivers/_shared/registry.d.ts +31 -19
  68. package/dist/drivers/localfs/exact-case.d.ts +0 -3
  69. package/dist/drivers/localfs/fs-util.d.ts +22 -0
  70. package/dist/drivers/localfs/registry.d.ts +10 -4
  71. package/dist/drivers/localfs/storage.d.ts +16 -8
  72. package/dist/drivers/memory.d.ts +14 -9
  73. package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
  74. package/dist/export/index.d.ts +16 -0
  75. package/dist/index.d.ts +2 -5
  76. package/dist/index.js +4303 -2938
  77. package/dist/index.js.map +4 -4
  78. package/package.json +1 -1
  79. package/dist/chunk-RSLVTRJY.js.map +0 -7
  80. package/dist/core/cost.d.ts +0 -351
  81. package/dist/core/kept-generations.d.ts +0 -40
@@ -11,9 +11,11 @@
11
11
  *
12
12
  * The rewrite is the same generation without one id, so it keeps everything else: the new object carries the source's
13
13
  * metadata as it is, and the row's summary of it, built from what was written, counts one id fewer and holds the same
14
- * metadata. Metadata is not scanned for the id. On an encrypted segment, a source object with no metadata block whose
15
- * row's sealed summary of it has metadata is rewritten with the row's: the block's presence is not authenticated, so
16
- * the authenticated copy decides, and the erasure still goes through.
14
+ * metadata. Metadata is not scanned for the id. A source object whose metadata block was stripped is another object
15
+ * than the row's summary names: its size is not the one the summary records, so the fingerprint check refuses it
16
+ * before the erasure reads any of its ids. Only one forged to match that fingerprint gets past, and on an encrypted
17
+ * segment it is rewritten with the metadata of the row's sealed summary: the block's presence is not authenticated, so
18
+ * the authenticated copy decides.
17
19
  *
18
20
  * **`erased: true` is a claim about every generation of the segment, not only the one it replaced.** A rollback
19
21
  * leaves generations above the pointer that were once current and can be made current again, so a holder can sit
@@ -48,16 +50,22 @@
48
50
  * Which reason comes from **re-reading the row**, not from assuming a supersession: a moved pointer is
49
51
  * `'superseded'`, a tombstoned row `'destroyed'`, a purged row `'absent'`, a row with no pointer
50
52
  * `'no-generation'`. A pointer still on `from` means the object it names is genuinely gone — the forbidden
51
- * `missing-storage-generation` state — and that throws, because no re-run fixes it. That `NotFoundError` is the only
52
- * signal of that state, so a faulting re-read rethrows it rather than replacing it with a transient-looking
53
- * registry error.
53
+ * `missing-storage-generation` state — or is another object than the row's summary names by its fingerprint, and that
54
+ * throws, because no re-run fixes either. That `NotFoundError` is the only signal of those states, so a faulting
55
+ * re-read rethrows it rather than replacing it with a transient-looking registry error.
54
56
  *
55
57
  * **A refused rewrite deletes its own object when it would outlive the winner above the pointer.** `putImmutable`
56
58
  * commits atomically, so a rewrite whose stream throws leaves no object at all. One that completed is either
57
59
  * below the winner's pointer, where the winner's collection takes it, or above it: the case where this call took
58
60
  * its number after the winner's object was already in the bucket. Up there nothing collects it, and it was derived
59
61
  * from a generation the winner has replaced, so when the winner was another erasure it still holds the id that
60
- * erasure has just reported gone. That object is deleted before this call returns — see `discardRefused`.
62
+ * erasure has just reported gone. That object is deleted before this call returns, once its footer matches the
63
+ * rewrite's and a renewal of the row has refused every publish fenced on a row read before it, under the version the
64
+ * footer read reported, unless {@link DISCARD_ATTEMPTS} renewals lose to other writes (a load, a rollback, a
65
+ * retention write): then it is kept, and can hold the winner's id, until a later call takes it: any erasure that
66
+ * rewrites the segment (this one's re-run among them, while its id is still in the current generation) numbers above it
67
+ * and its `keep: 0` collection takes it, an erasure of the winner's id deletes it as a holder, and once loads number
68
+ * past it their collection takes it as it takes any orphan — see `discardRefused`.
61
69
  *
62
70
  * Without that fence the failure was silent and severe, and both halves were reproduced: `nextGeneration` picks a
63
71
  * number above everything in the bucket, so a forward-only publish here always won — discarding a concurrent
@@ -68,11 +76,28 @@
68
76
  * A live read still fetching from the collected generation heals forward to the rewrite, and a pin of it fails with
69
77
  * `NotFoundError` for any chunk it has yet to read: that is the documented cost of physical deletion on return. **Do not re-load the id while erasing it**: a load that lands after this rewrite
70
78
  * carries whatever its source held, and the library cannot know that source was meant to exclude the id.
71
- * A load already in flight writes its object above the pointer before it publishes. An erasure that finds the id in that
72
- * object writes the row before deleting it, so the load's publish, fenced on the row it read, is refused
73
- * (`published: false`) and the pointer never names a missing object (see `fenceInFlight`). On a row with no pointer
74
- * the erasure writes nothing to the row, so a first load's object there may still be published; an erasure that finds
75
- * the id in it refuses instead of deleting it.
79
+ * A load already in flight writes its object above the pointer before it publishes, or, as a first load onto a row with
80
+ * no pointer, before there is a pointer at all. An erasure that finds the id in that object renews the row's `pointerId`
81
+ * before deleting it, so the load's publish, fenced on the row it read, is refused (`published: false`) and the pointer
82
+ * never names a missing object (see `fence`). An erasure that finds no holder writes nothing.
83
+ *
84
+ * Before it deletes anything, a call that found a holder takes every generation it will delete out of the row's ledger
85
+ * of known generations, in one write made after its search (the same write as the renewal where there is one). A
86
+ * generation the ledger names is one a collection by a count spares, so the delete comes after the name goes: a
87
+ * rollback that raced the write loses its swap, and a collection that reads the row before a delete spares what the row
88
+ * names. That write is made only when the ledger names something to delete, and it is the
89
+ * one case in which an erasure of an id held only by an older generation writes the row.
90
+ *
91
+ * An object sealed under a key its row does not hold cannot be searched, and no read of the segment can open it: a
92
+ * first load's on a row with no pointer, whose key that load has not published; on a segment with a key, a first
93
+ * load's that lost the race to the one that published, or crashed, under a key it made and never stored; and on a
94
+ * cleartext segment with a pointer, any encrypted object, since a key is made only for a segment's first generation
95
+ * and a publish never adds one to a segment that has generations: a first load's that minted a key and crashed, or
96
+ * lost the race to a cleartext first load. It counts as
97
+ * a holder whatever the id: an erasure of any id that finds one deletes it, under the same renewal of the row and read
98
+ * before the delete, and lists it in `collected`, and a first load still in flight that wrote it is refused. Only a
99
+ * searched object that held the id makes the answer `erased: true`. The current generation is never one of them: one
100
+ * the row's key does not open throws, as every read of the segment does.
76
101
  */
77
102
  import { type IAuditSink } from './audit.js';
78
103
  import type { CodecInterface } from './codec.js';
@@ -81,6 +106,20 @@ import type { Rng } from './determinism.js';
81
106
  import type { IKeystore } from './crypto.js';
82
107
  import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
83
108
  import { type ReadRetry } from './retry.js';
109
+ /**
110
+ * Renewals a refused rewrite's discard makes before it keeps its object: the first, and one more each time a write that
111
+ * is neither a write of leases alone nor another erasure's renewal of the row lands between its read of the row and its
112
+ * renewal. Those two are waited out, up to the limit on lease waits (`LEASE_ONLY_RETRIES`), and cost none, so the
113
+ * losers of one race of erasures all delete their rewrites; a write that gets no answer is sent again within the same
114
+ * renewal.
115
+ */
116
+ export declare const DISCARD_ATTEMPTS = 8;
117
+ /**
118
+ * Writes a call's fence makes before it gives up on a row that keeps changing under it: each one lost to another writer
119
+ * (a load, a rollback, another erasure) that is not a write of the row's leases alone costs one. The call then reports
120
+ * `'superseded'` with nothing deleted.
121
+ */
122
+ export declare const FENCE_ATTEMPTS = 5;
84
123
  /** What {@link eraseIdFromSegment} needs: the objects, the pointer, the codec, and the key material if encrypted. */
85
124
  export interface EraseIdDeps {
86
125
  readonly storage: IStorageDriver;
@@ -98,11 +137,15 @@ export interface EraseIdDeps {
98
137
  readonly keystore?: IKeystore;
99
138
  /** When true, refuse to rewrite a **cleartext** segment (the same guard the read path and a load offer). */
100
139
  readonly requireEncryption?: boolean;
101
- /** Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. */
140
+ /**
141
+ * Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. Its `sleep` is what a registry
142
+ * write that got no answer waits on before a fresh one is sent; with no `sleep`, such a write is not sent again and
143
+ * its `TransientError` is thrown.
144
+ */
102
145
  readonly clock?: Yielder;
103
146
  /**
104
- * The random source that spreads the waits between the rewrite's fresh compare-and-swaps. Absent, the read retry's
105
- * source is used if it has one, and otherwise each wait is its bound.
147
+ * The random source that spreads the waits between the fresh compare-and-swaps of the rewrite and of the row's
148
+ * renewal. Absent, the read retry's source is used if it has one, and otherwise each wait is its bound.
106
149
  */
107
150
  readonly rng?: Rng;
108
151
  /** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. The `.crbm` reader refuses an entry above its own `maxPayloadBytes` (1 MiB, plus 28 bytes when encrypted) at open, so raise that on the chunk source too. */
@@ -111,7 +154,7 @@ export interface EraseIdDeps {
111
154
  * The store's read retry, for the reads the rewrite makes along the way: the generation it rewrites and each of its
112
155
  * chunks, the read-back that verifies the generation it wrote, and any other generation it checks for the id. A
113
156
  * transient fault on one is run again under it rather than failing the erasure. Absent, each read is made once.
114
- * The deletes are not retried, and the rewrite's registry write is settled as a load's is.
157
+ * The deletes are not retried, and the rewrite's registry write and the row's renewal are settled as a load's is.
115
158
  */
116
159
  readonly readRetry?: ReadRetry;
117
160
  }
@@ -127,13 +170,20 @@ export interface EraseIdResult {
127
170
  readonly erased: boolean;
128
171
  /**
129
172
  * Why the id was not erased, when `erased` is false. `'absent'` (no registry row), `'destroyed'` (a crypto-shred
130
- * tombstone — already unreadable), `'no-generation'` (a row with no Storage data yet), `'not-member'` (no
131
- * generation in the bucket holds the id — the common case across a fleet scan), or `'superseded'`.
173
+ * tombstone — already unreadable), `'no-generation'` (a row with no generation yet: its bucket can hold first loads'
174
+ * objects, none of them found to hold the id, and `collected` names any this call deleted unsearched, see below),
175
+ * `'not-member'` (no
176
+ * generation in the bucket holds the id — the common case across a fleet scan — with the same note on `collected`),
177
+ * or `'superseded'`.
132
178
  *
133
179
  * **`'superseded'` means this call did not erase the id, not that the id is still there.** Another writer — a
134
- * load, another erasure, or a rollback — moved the pointer off the generation this call read, so what it was
135
- * doing no longer follows from what is current: a rewrite derived from that generation is not a valid successor
136
- * to the new one, and a generation it meant to delete above the pointer may be the one the pointer now names.
180
+ * load, another erasure, or a rollback — moved the pointer off the generation this call read (on a row with no
181
+ * pointer, any other write of the row before this call renewed it, a `setRetention` included), or replaced an
182
+ * object it meant to delete (another erasure deleted a holder above the pointer, or on a row with no pointer, and a
183
+ * load stored another object under its number, so the storage driver's conditional delete refused this call's,
184
+ * whether or not the pointer moved). What it was doing no longer follows from what is current: a rewrite derived
185
+ * from that generation is not a valid successor to the new one, and a generation it meant to delete may be the one
186
+ * the pointer now names, or another object than the one it read.
137
187
  * Re-run against the new generation: if the id is still present it is erased then; if the racing writer was
138
188
  * another erasure of the *same* id, the re-run reports `'not-member'` because it is already gone. Either way the
139
189
  * re-run settles it, which is why it is the documented action for this reason and for no other.
@@ -151,14 +201,20 @@ export interface EraseIdResult {
151
201
  readonly fromGeneration?: number;
152
202
  /**
153
203
  * The generation written without the id (present whenever one was written, even if `superseded`). A refused
154
- * rewrite whose object sits above the winner's pointer deletes that object again before it returns.
204
+ * rewrite whose object sits above the winner's pointer deletes that object again before it returns, when its footer
205
+ * matches the rewrite's and a renewal of the row refuses every publish fenced on a row read before it.
155
206
  */
156
207
  readonly generation?: number;
157
208
  /**
158
209
  * Generations this call deleted. On a rewrite that is normally `[fromGeneration]`, plus any older orphans. When
159
- * the current generation did not hold the id, it is every generation below the pointer (`keep: 0` takes them
160
- * together) and each one above the pointer that held the id; the ones above it that did not hold the id stay,
161
- * because they are an operator's rollback targets. Empty when nothing was deleted.
210
+ * the current generation did not hold the id and a generation this call searched did, it is every generation below
211
+ * the pointer (`keep: 0` takes them together) and each one above the pointer that held the id; the ones above it
212
+ * that did not hold the id stay, because they are an operator's rollback targets. On a row with no pointer it is
213
+ * each first load's object that held the id. Wherever it found them, it is also each object sealed under a key the
214
+ * row does not hold, which this call cannot search and deletes whatever the id. When those are the only holders, it
215
+ * is only those, deleted one by one, and every generation this call searched stays: `reason` is then
216
+ * `'no-generation'` on a row with no pointer and `'not-member'` on one with a pointer. Empty when nothing was
217
+ * deleted.
162
218
  *
163
219
  * **It is what THIS call deleted, not the proof that the id is gone** — those differ: a concurrent collector can
164
220
  * take a generation holding the id first, and then `erased: true` is returned without it in `collected`, because
@@ -170,7 +226,18 @@ export interface EraseIdResult {
170
226
  * collection pass that could not prove the segment was still the same one (which an ordinary retirement landing
171
227
  * mid-call is enough to cause); the same refusal on the path where the current generation did not hold the id
172
228
  * and nothing was published at all; and a generation still holding the id when the bucket is listed at the end
173
- * — one an operator rolled the pointer onto while a rewrite was collecting, say.
229
+ * — one an operator rolled the pointer onto while a rewrite was collecting, say, or, on a row with no pointer, the
230
+ * object of a first load that read the row before this call renewed it and wrote it after this call listed the
231
+ * bucket. The row write an erasure makes before deleting a holder a load may still publish, or before a refused
232
+ * rewrite discards its own object, when it gets no answer that reading the row can settle, throws the registry's
233
+ * `TransientError`, and that object is not deleted.
234
+ *
235
+ * A rollback that lands between the call's read of the row and one of its deletes, onto the generation being deleted,
236
+ * leaves the pointer on an object that is gone. The call finds it at its next read of the row, or by reading the row
237
+ * once more after its last delete, and **throws `NotFoundError`** rather than report anything: every read of the
238
+ * segment fails until a rollback moves the pointer to a generation still in the bucket, and nothing restores the
239
+ * deleted object. When nothing left in the bucket holds the id, the call has emitted its `segment.collect` event
240
+ * first, so the deletion is on the record. Re-run it after that rollback.
174
241
  *
175
242
  * **Re-run it**, and read what the re-run says rather than assuming it finished the job. The re-run looks for
176
243
  * the id in every generation in the bucket, not only the current one, and there are three outcomes:
@@ -192,7 +259,10 @@ export interface EraseIdResult {
192
259
  * it. See the module note for the contract.
193
260
  *
194
261
  * Emits one `segment.rewrite` audit event at the publish (before the superseded generation is collected), so the
195
- * compliance record exists the moment the generation without the id is authoritative.
262
+ * compliance record exists the moment the generation without the id is authoritative. An erasure that rewrites nothing,
263
+ * because only other generations held the id, emits one `segment.collect` once no generation holds it, and so does one
264
+ * that deleted only objects no read of the segment can open, which names no generation the id was found in. Either
265
+ * event comes before the `NotFoundError` of a rollback that moved the pointer onto a generation the call deleted.
196
266
  */
197
267
  export declare function eraseIdFromSegment(ref: SegmentRef, id: number, deps: EraseIdDeps, options?: {
198
268
  audit?: IAuditSink;
@@ -13,7 +13,7 @@
13
13
  * operation — `eraseIdFromSegment` rewrites the generation without it.
14
14
  */
15
15
  import { type IAuditSink } from './audit.js';
16
- import { type ChurnDeps } from './leases.js';
16
+ import type { ChurnDeps } from './leases.js';
17
17
  import type { IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef } from './ports.js';
18
18
  export interface EraseDeps extends ChurnDeps {
19
19
  readonly registry: IRegistryDriver;
@@ -60,7 +60,7 @@ export declare function destroySegment(ref: SegmentRef, deps: EraseDeps, options
60
60
  * listing is capped at `maxScanSegments` (default {@link DEFAULT_MAX_SCAN_SEGMENTS}, 250,000), the ceiling every
61
61
  * other fleet scan keeps. A namespace over it throws `BudgetExceededError` **before any segment is destroyed**,
62
62
  * never part-way through; raise `maxScanSegments` when the namespace really is that large and the memory is there.
63
- * A `maxScanSegments` that is not a finite number >= 1 throws `ValidationError`, again before anything is destroyed.
63
+ * A `maxScanSegments` that is not a safe whole number of at least 1 throws `ValidationError`, again before anything is destroyed.
64
64
  */
65
65
  export declare function eraseNamespace(namespace: string, deps: EraseDeps, options: {
66
66
  confirmNamespace: string;
@@ -195,7 +195,7 @@ export declare function dropSegment(ref: SegmentRef, deps: DropDeps, options: {
195
195
  * in the shred's own write, so only a tombstone a sweep wrote ever carries a sweep's mark, which is what lets a later
196
196
  * sweep purge it and never a crypto-shred's.
197
197
  */
198
- export interface SweepDrop {
198
+ interface SweepDrop {
199
199
  readonly wanted: (row: RegistryRecord) => boolean;
200
200
  readonly stamp: (retention: RegistryRecord['retention']) => RegistryRecord['retention'];
201
201
  }
@@ -208,3 +208,4 @@ export declare function dropSegmentFor(ref: SegmentRef, deps: DropDeps, options:
208
208
  dryRun?: boolean;
209
209
  audit?: IAuditSink;
210
210
  }, sweep: SweepDrop | undefined): Promise<DropResult | null>;
211
+ export {};
@@ -26,7 +26,11 @@ export declare class CloudRoaringError extends Error {
26
26
  readonly [ERROR_BRAND]: true;
27
27
  constructor(message: string);
28
28
  }
29
- /** Invalid caller input (bad id, segment name, options). Raised before any storage call. */
29
+ /**
30
+ * Invalid input, which the same call fails on again. Most is refused before any storage call (a bad id, segment name
31
+ * or option); some is found by the call itself: a read past the end of an object, or a bucket or container a cloud
32
+ * driver finds does not exist.
33
+ */
30
34
  export declare class ValidationError extends CloudRoaringError {
31
35
  }
32
36
  /** An OCC conditional write/delete lost the race — the row changed since it was read. */
@@ -148,7 +152,9 @@ export declare function isLeaseLimitError(err: unknown): err is LeaseLimitError;
148
152
  export declare function isNotFoundError(err: unknown): err is NotFoundError;
149
153
  /** Corrupt/oversized/failed-checksum bytes from a tier. */
150
154
  export declare function isIntegrityError(err: unknown): err is IntegrityError;
151
- /** Invalid caller input. */
155
+ /**
156
+ * Invalid caller input; also the error a cloud driver raises for a bucket or container it finds does not exist.
157
+ */
152
158
  export declare function isValidationError(err: unknown): err is ValidationError;
153
159
  /** This build or this store's wiring cannot do what was asked, though nothing is malformed. */
154
160
  export declare function isUnsupportedError(err: unknown): err is UnsupportedError;
@@ -1,9 +1,84 @@
1
- import type { GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef, Token } from './ports.js';
1
+ import { NotFoundError } from './errors.js';
2
+ import type { GenKey, IStorageDriver, IRegistryDriver, KnownGeneration, RegistryRecord, SegmentRef, Token } from './ports.js';
2
3
  /** The two ports generation bookkeeping needs: the objects, and the pointer that says which one is current. */
3
- export interface GenerationDeps {
4
+ interface GenerationDeps {
4
5
  readonly storage: IStorageDriver;
5
6
  readonly registry: IRegistryDriver;
7
+ /** What a delete loop waits on before it reads the row again ({@link watchDeletes}). Absent, it does not wait. */
8
+ readonly clock?: {
9
+ readonly sleep?: (ms: number) => Promise<void>;
10
+ };
6
11
  }
12
+ /**
13
+ * The waits, in milliseconds, before each further read of the row when a delete loop finds the pointer on a generation
14
+ * it deleted, with no object under it ({@link watchDeletes}). A rollback that moved the pointer there checks the bucket
15
+ * after its move, and when the check runs after the delete it finds the object gone and puts the pointer back: one
16
+ * listing of the segment's objects (one page while the segment holds under a thousand) and one compare-and-swap, two
17
+ * round trips, three when a lease write lands first. A rollback client farther from the store than the deleting
18
+ * client takes that much longer, so the loop reads the row once at once and, with a clock that can wait, three more
19
+ * times over 1.75 s in all, which covers clients a few hundred milliseconds away. Paid only when a read finds the
20
+ * pointer on a missing generation the loop deleted, never on a pass where nothing raced.
21
+ */
22
+ export declare const DELETE_SETTLE_WAITS_MS: readonly number[];
23
+ /** What a delete loop has deleted, held to every read of the segment's row it makes after a delete. */
24
+ interface DeleteWatch {
25
+ /** Note a delete that landed. */
26
+ deleted(generation: number): void;
27
+ /** Every generation noted so far. */
28
+ readonly gone: ReadonlySet<number>;
29
+ /**
30
+ * `row`, a read of the row made after `held`, the row a delete was decided on, held to what was deleted: the deleted
31
+ * generation it leaves the pointer on, with no object under it once the row is read again, or `undefined`.
32
+ */
33
+ check(held: RegistryRecord, row: RegistryRecord | null): Promise<number | undefined>;
34
+ /** The same for one more read of the row, made only when a delete landed since the last {@link check}. */
35
+ settle(held: RegistryRecord): Promise<number | undefined>;
36
+ }
37
+ /**
38
+ * Hold a delete loop's reads of the row to what it deleted.
39
+ *
40
+ * Every loop that deletes generations reads the row before each delete, and one round trip remains between that read
41
+ * and the delete. A rollback that lands inside it onto the generation being deleted passes its own check (a listing made
42
+ * before the delete) and leaves the pointer on an object that is gone. Nothing can put the object back, so the loop
43
+ * detects it instead: every read of the row it makes after a delete, and one more after its last delete when it made no
44
+ * read since, is held to what it deleted.
45
+ *
46
+ * A read names a deleted generation when its row is active, of the incarnation `held` was read from, and its pointer
47
+ * names one. A tombstone never does, since no rollback moves a `destroyed` row's pointer, and neither does a row purged
48
+ * and made again, whose numbers are its own. Then:
49
+ *
50
+ * - The bucket is asked, with one zero-byte read, a metadata request on every shipped driver. A number can be taken
51
+ * again once its object is deleted (invariant 2): after a rollback below it, a load can number it afresh and
52
+ * publish, and then the object there is that load's, and nothing is missing. Where the storage driver ignores a
53
+ * delete's condition, the loop's own delete can take that object too, and the read finds it missing, which it is.
54
+ * - A missing object is not yet the answer. A rollback whose own check ran after the delete finds the object gone and
55
+ * puts the pointer back, and the loop's read can fall between its move and that undo. So the row is read again, at
56
+ * once and, with a clock that can wait, after each of {@link DELETE_SETTLE_WAITS_MS}, each read judged as the first
57
+ * was, the bucket asked again; the answer is the generation the last of them names, only if every one names a
58
+ * deleted generation with no object under it.
59
+ *
60
+ * A read of the bucket that fails in any other way than "not found" throws: it says nothing about the object.
61
+ */
62
+ export declare function watchDeletes(deps: GenerationDeps, ref: SegmentRef): DeleteWatch;
63
+ /** Who deleted the generation a {@link pointerOnDeletedError} names, which leads its message. */
64
+ type Deleter = {
65
+ readonly kind: 'collection';
66
+ } | {
67
+ readonly kind: 'erasure';
68
+ } | {
69
+ readonly kind: 'load';
70
+ readonly published: number;
71
+ } | {
72
+ readonly kind: 'refused-load';
73
+ readonly refusal: string;
74
+ };
75
+ /**
76
+ * The error for a pointer a delete loop found on a generation it deleted ({@link watchDeletes}). The writer that moved
77
+ * the pointer there is a rollback, or, where the storage driver ignores a delete's condition, a load that took the
78
+ * number again and published. A rollback slower than the loop's further reads can still put the pointer back after
79
+ * them, so the message says to look before acting.
80
+ */
81
+ export declare function pointerOnDeletedError(ref: SegmentRef, deleter: Deleter, generation: number): NotFoundError;
7
82
  /**
8
83
  * The generation number a writer should use for the segment's **next** object: one above the highest generation
9
84
  * the registry points at *or* that is present in Storage — whichever is higher. The erasure rewrite numbers this
@@ -20,7 +95,7 @@ export interface GenerationDeps {
20
95
  */
21
96
  export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): Promise<number>;
22
97
  /** The number a load takes, and whether one existence check proved it free (so nothing is above the pointer). */
23
- export interface LoadNumber {
98
+ interface LoadNumber {
24
99
  readonly generation: number;
25
100
  /**
26
101
  * `true` when the number is `currentGen + 1` and the check found no object holding it. Collection by name rests
@@ -28,6 +103,8 @@ export interface LoadNumber {
28
103
  * the listing would have kept is a name the pass takes. `false` when the listing numbered the load.
29
104
  */
30
105
  readonly checked: boolean;
106
+ /** The generations that listing found, when the load listed: a seed of the ledger reads them instead of listing again. */
107
+ readonly listed?: readonly number[];
31
108
  }
32
109
  /**
33
110
  * The generation number a load takes, from the row it has already read: `currentGen + 1` (0 with no row or no
@@ -46,7 +123,8 @@ export interface LoadNumber {
46
123
  * and collection takes it once a generation above it is current. Like a listing's, the number can be one whose
47
124
  * object was deleted (an erasure removes the generations above a rolled-back pointer that held the id), which is
48
125
  * why nothing identifies a generation by its number alone: the reader cache and the chunk cache key on the number and
49
- * the row's token, checked against the object a reader opens, and a pin on its object's fingerprint.
126
+ * the row's `pointerId`, which every write that takes a number again renews, and an open of a row with a usable
127
+ * summary holds the object to the fingerprint the summary records; a pin keys on its object's fingerprint.
50
128
  */
51
129
  export declare function nextLoadGeneration(ref: SegmentRef, deps: GenerationDeps, row: RegistryRecord | null): Promise<LoadNumber>;
52
130
  /**
@@ -72,6 +150,11 @@ export declare const LIST_COLLECTION_CADENCE = 16;
72
150
  * a pin taken while it was still active re-checks the status on every open — and nothing else in the library
73
151
  * would ever collect them, so without this those objects are billed forever.
74
152
  *
153
+ * **Throws {@link NotFoundError} when a rollback moved the pointer onto a generation it deleted**: the rollback landed
154
+ * between the pass's re-proof of the row and that delete, so the pointer names an object that is gone. The pass finds
155
+ * it at its next re-proof, or, after its last delete, by reading the row once more. A deleted object cannot be
156
+ * restored: the answer is a rollback to a generation still in the bucket.
157
+ *
75
158
  * **Throws {@link WriteConflictError} when it cannot act on the row it read**: the row is gone when it is re-read,
76
159
  * after the listing or before a delete; on an active segment, the pointer has fallen below the cutoff before a
77
160
  * delete; on a `destroyed` segment, the row's token has changed. Not to be confused with the empty array it still
@@ -96,33 +179,52 @@ export declare const LIST_COLLECTION_CADENCE = 16;
96
179
  export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDeps, options?: {
97
180
  keep?: number;
98
181
  }): Promise<number[]>;
182
+ /**
183
+ * {@link gcOrphanGenerations}, reporting a rollback onto a generation it deleted in `missing` instead of throwing it: the
184
+ * pass stopped there. For a caller that records what it deleted before it raises, as an erasure does. With `skipKnown` the
185
+ * pass also spares every generation the row names when it reads the row before a delete (an erasure's passes on an active
186
+ * row, which remove from the row what they mean to delete first).
187
+ */
188
+ export declare function collectBelowPointer(ref: SegmentRef, deps: GenerationDeps, options?: {
189
+ keep?: number;
190
+ skipKnown?: boolean;
191
+ }): Promise<{
192
+ readonly deleted: number[];
193
+ readonly missing?: number;
194
+ readonly knownSpared?: number[];
195
+ }>;
99
196
  /**
100
197
  * What makes a collection spare leased generations: the clock that says whether a lease has ended. Only a load's
101
198
  * collection passes one, and a load with no clock passes none: it cannot tell a live lease from an ended one, so it
102
199
  * reads none. A pass without a guard never reads the row's leases, which is what keeps erasure, shred and drop deleting
103
200
  * what a lease names.
104
201
  */
105
- export interface LeaseGuard {
202
+ interface LeaseGuard {
106
203
  readonly now: () => number;
107
204
  }
108
205
  /** What a load's publish recorded in the row, as `publishGeneration` reports it. */
109
206
  export interface PublishedKept {
110
- /** The list the publish wrote, or `undefined` when it wrote none (the row then does not know which are kept). */
207
+ /** The window the publish recorded, as numbers, or `undefined` when it recorded none (the row then does not know which are kept). */
111
208
  readonly list: readonly number[] | undefined;
209
+ /** The ledger the publish wrote, in the order the generations were made current, when it wrote one. */
210
+ readonly known?: readonly KnownGeneration[];
112
211
  /** The names the publish pushed out of the window, ascending. */
113
212
  readonly evict: readonly number[];
114
213
  /** The token of the row the publish wrote, when it is known: a write settled by reading the row has none. */
115
214
  readonly token: Token | undefined;
116
215
  }
117
216
  /**
118
- * Collect after a load's publish. The row records the generations the load keeps, so the pass deletes by name what the
119
- * publish pushed out of that window ({@link deleteEvicted}) and lists the segment ({@link gcOrphanGenerations}'s
217
+ * Collect after a load's publish. The row's ledger records the generations the load keeps, so the pass deletes by name
218
+ * what the publish pushed out of that window ({@link deleteEvicted}) and lists the segment ({@link gcOrphanGenerations}'s
120
219
  * pass) only when it cannot rely on the row or must catch what a name does not:
121
220
  *
122
- * - the row recorded no list (a `keep` above {@link MAX_KEPT_GENERATIONS}, a row an earlier schema wrote, one a
123
- * rollback or an erasure moved, or a publish whose write was settled by reading the row): the pass keeps the
124
- * newest `keep` generations present below the pointer, and records them with one compare-and-swap, fenced on the
125
- * publish's own token, so the next load collects by name;
221
+ * - the publish recorded no window to collect by name (a `keep` above {@link MAX_KEPT_GENERATIONS}, a publish that
222
+ * seeded its ledger from the bucket, a row with no usable ledger, or a publish whose write was settled by reading the
223
+ * row): the pass keeps `keep` of the generations present below the pointer, the ledger's own most recently current
224
+ * names first and then the newest others by number, or, over a row with no usable ledger, `previous` first and then
225
+ * the newest by number ({@link keptWindow}), and writes nothing: the publish already wrote the ledger, which is what
226
+ * the next load derives its window from. It spares every name the row knows when it re-reads the row before a
227
+ * delete, which a publish or a rollback that landed since can have added to;
126
228
  * - the guard found the current generation's object gone, or, for a `keep` of 1 or more when the guard took the size
127
229
  * from the row's summary and opened nothing, one zero-byte read of it does not find it (`proveCurrent`): the
128
230
  * window then counts a generation that is not there, so the same pass keeps what is;
@@ -134,10 +236,13 @@ export interface PublishedKept {
134
236
  * below it. A load onto a `destroyed` row is refused at its publish and never reaches here; a drop that lands after
135
237
  * the publish leaves this pass collecting the tombstone's generations, and the drop's own sweep takes the rest.
136
238
  *
137
- * The seed's compare-and-swap moves the row's token, so a derived writer in flight on the row (an erasure rewrite)
138
- * meets a lost fence and re-derives: it happens once per row that recorded no list. A `WriteConflictError` from it is
139
- * a lost race, and the load took effect, so it is not raised; the next load records the list. Any other fault is
140
- * raised, as every fault of a collection after a landed publish is.
239
+ * Any fault of the collection is raised, as every fault of a collection after a landed publish is.
240
+ *
241
+ * So is a rollback onto a generation the pass deleted, which the pass finds by holding every read of the row it makes
242
+ * after a delete to what it deleted, and by reading the row once more after its last delete
243
+ * ({@link watchDeletes}): it throws {@link NotFoundError}, saying the load took effect at `generation` and the
244
+ * generation the pointer names is missing. That read is the one request this adds to a load whose pass deleted
245
+ * anything.
141
246
  */
142
247
  export declare function collectAfterLoad(ref: SegmentRef, deps: GenerationDeps, options: {
143
248
  generation: number;
@@ -155,6 +260,11 @@ export declare function collectAfterLoad(ref: SegmentRef, deps: GenerationDeps,
155
260
  proveCurrent?: GenKey;
156
261
  /** Spare the generations the row's live leases hold. Absent, the pass ignores leases. */
157
262
  leases?: LeaseGuard;
263
+ /**
264
+ * The generation the row named as current when the load read it, which its publish superseded. A window kept by
265
+ * listing holds it first, while the pointer is still the load's. Absent: the load found no pointer.
266
+ */
267
+ previous?: number;
158
268
  }): Promise<number[]>;
159
269
  /**
160
270
  * Delete, with no listing, the generations a publish of `generation` pushed out of its row's window, `evict`, taken
@@ -170,9 +280,14 @@ export declare function collectAfterLoad(ref: SegmentRef, deps: GenerationDeps,
170
280
  * from taking a generation that a later publish, after a rollback, put in the window. A pointer that has moved above
171
281
  * `generation` does not stop it: a publish landing in the meantime leaves routine collection working. A fault, a
172
282
  * registry read or a delete that throws, propagates, with the pointer at `generation`.
283
+ *
284
+ * A rollback onto a name it deleted is not a stop: every read of the row after a delete is held to what it deleted, and
285
+ * so is one more read after its last delete when it made none since ({@link watchDeletes}). One that names a
286
+ * deleted generation throws {@link NotFoundError}, saying the load took effect at `generation`.
173
287
  */
174
288
  export declare function deleteEvicted(ref: SegmentRef, deps: GenerationDeps, options: {
175
289
  generation: number;
176
290
  evict: readonly number[];
177
291
  leases?: LeaseGuard;
178
292
  }): Promise<number[]>;
293
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The generations a segment's bucket holds.
3
+ */
4
+ import type { IStorageDriver, SegmentRef } from './ports.js';
5
+ /**
6
+ * The generations the bucket holds for `ref`, from one listing, each once and in ascending order. A listing that spans
7
+ * a purge and a re-create can yield one number twice (the objects are re-created under the numbers just swept), so the
8
+ * duplicates are dropped here rather than counted by each caller. A caller that stops at the first object it wants
9
+ * reads the listing itself.
10
+ */
11
+ export declare function generationsOf(storage: IStorageDriver, ref: SegmentRef): Promise<number[]>;
@@ -0,0 +1,46 @@
1
+ import type { CodecBitmap, CodecInterface } from './codec.js';
2
+ /** A validated range: inclusive id bounds, as the chunk key and the remainder at each end. */
3
+ export interface IdWindow {
4
+ readonly loKey: number;
5
+ readonly loRem: number;
6
+ readonly hiKey: number;
7
+ readonly hiRem: number;
8
+ }
9
+ /** A range as a read is asked for it: each bound optional. */
10
+ interface IdBounds {
11
+ readonly after?: number | undefined;
12
+ readonly through?: number | undefined;
13
+ }
14
+ /**
15
+ * Throws {@link ValidationError} unless `bound` is left out or is an id: an integer from 0 to 4294967295. The message
16
+ * says what is wrong and never carries the value, which is an id.
17
+ */
18
+ export declare function assertIdBound(name: 'after' | 'through', bound: number | undefined): void;
19
+ /**
20
+ * Validate a read's range. `null` when none was asked for, so the read does no extra work; `'empty'` when no id can be
21
+ * in it (`after >= through`, which is a cursor at the end of its window, not an error).
22
+ */
23
+ export declare function windowOf(range: IdBounds | undefined): IdWindow | 'empty' | null;
24
+ /** The window a range with neither bound set describes: every id. */
25
+ export declare const WHOLE_ID_SPACE: IdWindow;
26
+ /**
27
+ * Whether the window cuts inside `chunkKey`: one of its two edge chunks, and only where the bound falls inside the chunk
28
+ * rather than on its boundary. A chunk the window covers whole is read as it is.
29
+ */
30
+ export declare const isEdge: (chunkKey: number, w: IdWindow) => boolean;
31
+ /** The ids of an edge chunk that lie inside the window. The chunk iterates ascending, so it stops at the top. */
32
+ export declare function edgeIds(chunk: CodecBitmap, chunkKey: number, w: IdWindow): Generator<number>;
33
+ /** The ascending `keys` inside the window's chunk span, as a new array: two binary searches, no scan. */
34
+ export declare function keysWithin(keys: readonly number[], w: IdWindow): number[];
35
+ /**
36
+ * The ascending `keys` inside the window's chunk span (every key, for `null`) and above the key `after`: what a read
37
+ * that plans again after taking `after` still reads.
38
+ */
39
+ export declare function keysAbove(keys: readonly number[], w: IdWindow | null, after: number): number[];
40
+ /** The index of the first element of ascending `a` greater than `value`: a binary search. */
41
+ export declare function firstAbove(a: Uint32Array, value: number): number;
42
+ /** The part of an edge chunk's ascending remainders `rem` inside the window, as a view of `rem`. */
43
+ export declare function remaindersWithin(rem: Uint32Array, chunkKey: number, w: IdWindow): Uint32Array;
44
+ /** An edge chunk cut to the window, as a new bitmap: the remainders {@link edgeIds} would yield. */
45
+ export declare function cutToWindow(codec: CodecInterface, chunk: CodecBitmap, chunkKey: number, w: IdWindow): CodecBitmap;
46
+ export {};