@cloudbitmaps/core 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -36
- package/dist/{chunk-BUPDGZRR.js → chunk-5OMC5U7U.js} +87 -40
- package/dist/chunk-5OMC5U7U.js.map +7 -0
- package/dist/core/audit.d.ts +15 -14
- package/dist/core/budget.d.ts +5 -3
- package/dist/core/codec.d.ts +18 -10
- package/dist/core/consistency.d.ts +3 -3
- package/dist/core/cooperative.d.ts +9 -9
- package/dist/core/cost.d.ts +167 -15
- package/dist/core/crbm/format.d.ts +7 -9
- package/dist/core/crbm/reader.d.ts +18 -1
- package/dist/core/crbm/writer.d.ts +2 -2
- package/dist/core/crbm-storage-source.d.ts +150 -60
- package/dist/core/determinism.d.ts +1 -1
- package/dist/core/discover.d.ts +7 -5
- package/dist/core/due-index.d.ts +3 -5
- package/dist/core/engine.d.ts +55 -13
- package/dist/core/erase-id.d.ts +76 -41
- package/dist/core/erasure.d.ts +22 -16
- package/dist/core/errors.d.ts +13 -20
- package/dist/core/generation-gc.d.ts +9 -5
- package/dist/core/keys.d.ts +2 -2
- package/dist/core/load.d.ts +28 -9
- package/dist/core/metrics.d.ts +8 -4
- package/dist/core/name-codec.d.ts +21 -31
- package/dist/core/pinned-storage-source.d.ts +29 -8
- package/dist/core/ports.d.ts +142 -59
- package/dist/core/reader-defaults.d.ts +18 -0
- package/dist/core/registry-scan.d.ts +15 -6
- package/dist/core/retention-sweep.d.ts +26 -18
- package/dist/core/retry.d.ts +8 -4
- package/dist/core/rollback.d.ts +11 -7
- package/dist/core/validate.d.ts +27 -3
- package/dist/driver-kit.d.ts +2 -3
- package/dist/driver-kit.js +13 -52
- package/dist/driver-kit.js.map +3 -3
- package/dist/drivers/_shared/keys.d.ts +4 -5
- package/dist/drivers/_shared/object-registry.d.ts +10 -5
- package/dist/drivers/_shared/registry.d.ts +9 -5
- package/dist/drivers/backends.d.ts +10 -16
- package/dist/drivers/localfs/fs-util.d.ts +3 -2
- package/dist/drivers/localfs/registry.d.ts +4 -4
- package/dist/drivers/memory.d.ts +2 -10
- package/dist/drivers/retry/retrying-chunk-source.d.ts +59 -0
- package/dist/export/index.d.ts +7 -6
- package/dist/index.d.ts +15 -24
- package/dist/index.js +1014 -373
- package/dist/index.js.map +4 -4
- package/dist/testing/fuzz-core.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-BUPDGZRR.js.map +0 -7
- package/dist/drivers/_shared/aws-errors.d.ts +0 -18
- package/dist/drivers/retry/retrying-drivers.d.ts +0 -90
package/dist/core/erase-id.d.ts
CHANGED
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
*
|
|
5
5
|
* There is no per-id delete on an immutable object, and there is no mutable tier to hold a tombstone, so erasure
|
|
6
6
|
* is what every other write in this library is: a new generation. The current generation is streamed chunk by
|
|
7
|
-
* chunk through the ascending writer — every chunk
|
|
8
|
-
* that bit cleared — then published fenced on the generation it streamed, and that generation is collected immediately
|
|
7
|
+
* chunk through the ascending writer — every chunk decoded, range-checked and re-encoded, the one holding the id
|
|
8
|
+
* with that bit cleared — then published fenced on the generation it streamed, and that generation is collected immediately
|
|
9
9
|
* (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Constant memory: one chunk in
|
|
10
10
|
* flight, never the whole segment.
|
|
11
11
|
*
|
|
12
|
+
* **`erased: true` is a claim about every generation of the segment, not only the one it replaced.** A rollback
|
|
13
|
+
* leaves generations above the pointer that were once current and can be made current again, so a holder can sit
|
|
14
|
+
* above the pointer as well as below it, and there can be several. Before it reports `erased: true` the call
|
|
15
|
+
* therefore lists the bucket once more and reads every generation it has not already seen to be clean; any that
|
|
16
|
+
* still holds the id makes it report or throw instead. In the ordinary case that list returns one generation, the
|
|
17
|
+
* current one, and costs one `list`.
|
|
18
|
+
*
|
|
12
19
|
* What it does NOT reach: backups, replicas and noncurrent object versions hold the old object until their own
|
|
13
20
|
* lifecycle removes it. For an at-rest guarantee that survives those, use encryption plus crypto-shred
|
|
14
21
|
* (`destroySegment`), which is per segment — a per-id shred is infeasible because one DEK covers the whole segment.
|
|
@@ -39,10 +46,12 @@
|
|
|
39
46
|
* signal of that state, so a faulting re-read rethrows it rather than replacing it with a transient-looking
|
|
40
47
|
* registry error.
|
|
41
48
|
*
|
|
42
|
-
* **
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
49
|
+
* **A refused rewrite deletes its own object when it would outlive the winner above the pointer.** `putImmutable`
|
|
50
|
+
* commits atomically, so a rewrite whose stream throws leaves no object at all. One that completed is either
|
|
51
|
+
* below the winner's pointer, where the winner's collection takes it, or above it: the case where this call took
|
|
52
|
+
* its number after the winner's object was already in the bucket. Up there nothing collects it, and it was derived
|
|
53
|
+
* from a generation the winner has replaced, so when the winner was another erasure it still holds the id that
|
|
54
|
+
* erasure has just reported gone. That object is deleted before this call returns — see `discardRefused`.
|
|
46
55
|
*
|
|
47
56
|
* Without that fence the failure was silent and severe, and both halves were reproduced: `nextGeneration` picks a
|
|
48
57
|
* number above everything in the bucket, so a forward-only publish here always won — discarding a concurrent
|
|
@@ -50,9 +59,12 @@
|
|
|
50
59
|
* generation put the first one's id back and collected the generation that had evidenced its removal. A false
|
|
51
60
|
* Art. 17 receipt is the worst output this module can produce.
|
|
52
61
|
*
|
|
53
|
-
*
|
|
54
|
-
* physical deletion on return. **Do not re-load the id while erasing it**: a load that lands after this rewrite
|
|
62
|
+
* A live read still fetching from the collected generation heals forward to the rewrite, and a pin of it fails with
|
|
63
|
+
* `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
|
|
55
64
|
* carries whatever its source held, and the library cannot know that source was meant to exclude the id.
|
|
65
|
+
* A load already in flight is the same case: it writes its object above the pointer before it publishes, and an
|
|
66
|
+
* erasure that finds the id in that object deletes it as it does any holder there, so a load that then publishes
|
|
67
|
+
* leaves the pointer naming a missing object.
|
|
56
68
|
*/
|
|
57
69
|
import { type IAuditSink } from './audit.js';
|
|
58
70
|
import type { CodecInterface } from './codec.js';
|
|
@@ -74,9 +86,9 @@ export interface EraseIdDeps {
|
|
|
74
86
|
* -bound AAD. Rewriting an encrypted segment without a keystore throws {@link KeyUnavailableError}.
|
|
75
87
|
*/
|
|
76
88
|
readonly keystore?: IKeystore;
|
|
77
|
-
/** When true, refuse to rewrite a **cleartext** segment (the same guard the read path and
|
|
89
|
+
/** When true, refuse to rewrite a **cleartext** segment (the same guard the read path and a load offer). */
|
|
78
90
|
readonly requireEncryption?: boolean;
|
|
79
|
-
/** Supplying a clock that can yield makes the rewrite cooperative
|
|
91
|
+
/** Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. */
|
|
80
92
|
readonly clock?: Yielder;
|
|
81
93
|
/** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. */
|
|
82
94
|
readonly maxBitmapBytes?: number;
|
|
@@ -85,57 +97,80 @@ export interface EraseIdResult {
|
|
|
85
97
|
readonly segment: string;
|
|
86
98
|
readonly namespace?: string;
|
|
87
99
|
/**
|
|
88
|
-
* True iff the id **was**
|
|
89
|
-
* the
|
|
100
|
+
* True iff the id **was** in the segment — in its current generation, or in any other generation still in the
|
|
101
|
+
* bucket, above the pointer or below it — and **no generation of the segment holds it now**. The current
|
|
102
|
+
* generation is one without it, every generation that held it has been deleted (see `collected`), and the call
|
|
103
|
+
* listed the bucket and read what was left before saying so. This is the field an erasure ledger records.
|
|
90
104
|
*/
|
|
91
105
|
readonly erased: boolean;
|
|
92
106
|
/**
|
|
93
|
-
* Why
|
|
94
|
-
* tombstone — already unreadable), `'no-generation'` (a row with no Storage data yet), `'not-member'` (
|
|
95
|
-
* in the
|
|
107
|
+
* Why the id was not erased, when `erased` is false. `'absent'` (no registry row), `'destroyed'` (a crypto-shred
|
|
108
|
+
* tombstone — already unreadable), `'no-generation'` (a row with no Storage data yet), `'not-member'` (no
|
|
109
|
+
* generation in the bucket holds the id — the common case across a fleet scan), or `'superseded'`.
|
|
96
110
|
*
|
|
97
111
|
* **`'superseded'` means this call did not erase the id, not that the id is still there.** Another writer — a
|
|
98
|
-
* load,
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
112
|
+
* load, another erasure, or a rollback — moved the pointer off the generation this call read, so what it was
|
|
113
|
+
* doing no longer follows from what is current: a rewrite derived from that generation is not a valid successor
|
|
114
|
+
* to the new one, and a generation it meant to delete above the pointer may be the one the pointer now names.
|
|
115
|
+
* Re-run against the new generation: if the id is still present it is erased then; if the racing writer was
|
|
116
|
+
* another erasure of the *same* id, the re-run reports `'not-member'` because it is already gone. Either way the
|
|
117
|
+
* re-run settles it, which is why it is the documented action for this reason and for no other.
|
|
103
118
|
*
|
|
104
|
-
* The first
|
|
105
|
-
*
|
|
106
|
-
*
|
|
119
|
+
* The first three reasons are also returned when the row changes *underneath* a call already in flight, not
|
|
120
|
+
* only when it is read up front — a concurrent `dropSegment` gives `'destroyed'`, a retention sweep that purges
|
|
121
|
+
* the row gives `'absent'`. A caller branching on `reason` never has to care which it was.
|
|
107
122
|
*/
|
|
108
123
|
readonly reason?: 'absent' | 'destroyed' | 'no-generation' | 'not-member' | 'superseded';
|
|
109
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* The generation the id was found in (present whenever one was read) — the newest of them, when the current
|
|
126
|
+
* generation did not hold it and several others did.
|
|
127
|
+
*/
|
|
110
128
|
readonly fromGeneration?: number;
|
|
111
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* The generation written without the id (present whenever one was written, even if `superseded`). A refused
|
|
131
|
+
* rewrite whose object sits above the winner's pointer deletes that object again before it returns.
|
|
132
|
+
*/
|
|
112
133
|
readonly generation?: number;
|
|
113
134
|
/**
|
|
114
|
-
* Generations deleted
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* non-empty `collected` as evidence and an empty one as "someone else got there", never as a failure.
|
|
135
|
+
* Generations this call deleted. On a rewrite that is normally `[fromGeneration]`, plus any older orphans. When
|
|
136
|
+
* the current generation did not hold the id, it is every generation below the pointer (`keep: 0` takes them
|
|
137
|
+
* together) and each one above the pointer that held the id; the ones above it that did not hold the id stay,
|
|
138
|
+
* because they are an operator's rollback targets. Empty when nothing was deleted.
|
|
119
139
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
140
|
+
* **It is what THIS call deleted, not the proof that the id is gone** — those differ: a concurrent collector can
|
|
141
|
+
* take a generation holding the id first, and then `erased: true` is returned without it in `collected`, because
|
|
142
|
+
* the claim is about the bucket rather than about who emptied it. Treat a non-empty `collected` as evidence and
|
|
143
|
+
* a missing holder as "someone else got there", never as a failure.
|
|
124
144
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
145
|
+
* A call that cannot establish the claim **throws** `WriteConflictError` or the underlying fault rather than
|
|
146
|
+
* report `erased: true` over bytes that are still there. Four ways that happens: a Storage `delete` fault; a
|
|
147
|
+
* collection pass that could not prove the segment was still the same one (which an ordinary retirement landing
|
|
148
|
+
* mid-call is enough to cause); the same refusal on the path where the current generation did not hold the id
|
|
149
|
+
* and nothing was published at all; and a generation still holding the id when the bucket is listed at the end
|
|
150
|
+
* — one an operator rolled the pointer onto while a rewrite was collecting, say.
|
|
128
151
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
152
|
+
* **Re-run it**, and read what the re-run says rather than assuming it finished the job. The re-run looks for
|
|
153
|
+
* the id in every generation in the bucket, not only the current one, and there are three outcomes:
|
|
154
|
+
*
|
|
155
|
+
* - `erased: true` naming the generation it found the id in — the ordinary case, and the one that gives you
|
|
156
|
+
* the receipt the failed call could not;
|
|
131
157
|
* - `'not-member'` — a racing collector took that generation first. The bit is gone, but **no run reports a
|
|
132
158
|
* receipt for it**, so keep the failed call's error alongside your ledger if you need the audit trail;
|
|
133
159
|
* - `'absent'` — the segment's registry row is gone, so it is no longer a segment at all and
|
|
134
|
-
* `store.eraseSubject` will not even scan it. Anything left in the bucket is an **orphan**:
|
|
135
|
-
* `
|
|
160
|
+
* `store.eraseSubject` will not even scan it. Anything left in the bucket is an **orphan**: list it with
|
|
161
|
+
* `listGenerations` (`store.generations`) and delete it with `dropSegment` (`store.dropSegment`), which both
|
|
162
|
+
* read the bucket as well as the row. `checkConsistency()` and the generation collection start from the row, so
|
|
163
|
+
* neither reaches a segment that has none.
|
|
136
164
|
*/
|
|
137
165
|
readonly collected: readonly number[];
|
|
138
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Remove `id` from `ref`: rewrite its current generation without it, and delete every other generation that holds
|
|
169
|
+
* it. See the module note for the contract.
|
|
170
|
+
*
|
|
171
|
+
* Emits one `segment.rewrite` audit event at the publish (before the superseded generation is collected), so the
|
|
172
|
+
* compliance record exists the moment the generation without the id is authoritative.
|
|
173
|
+
*/
|
|
139
174
|
export declare function eraseIdFromSegment(ref: SegmentRef, id: number, deps: EraseIdDeps, options?: {
|
|
140
175
|
audit?: IAuditSink;
|
|
141
176
|
}): Promise<EraseIdResult>;
|
package/dist/core/erasure.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* segment/namespace). A `destroyed` segment reads as empty (its DEK is gone). Only works on an **encrypted**
|
|
9
9
|
* segment; a cleartext segment has no key to shred.
|
|
10
10
|
*
|
|
11
|
-
* The tombstone is also the fence every writer respects:
|
|
11
|
+
* The tombstone is also the fence every writer respects: a load refuses
|
|
12
12
|
* a `destroyed` row, so a load racing an erasure cannot resurrect the segment. A single id's erasure is a different
|
|
13
13
|
* operation — `eraseIdFromSegment` rewrites the generation without it.
|
|
14
14
|
*/
|
|
@@ -54,11 +54,18 @@ export declare function destroySegment(ref: SegmentRef, deps: EraseDeps, options
|
|
|
54
54
|
/**
|
|
55
55
|
* Crypto-shred every segment in a namespace. **Irreversible.** `confirmNamespace` must equal `namespace`.
|
|
56
56
|
* Returns a per-segment result (skips cleartext segments unless `allowCleartext`).
|
|
57
|
+
*
|
|
58
|
+
* **Bounded.** It lists the whole namespace before it destroys anything, and holds the listing resident, so the
|
|
59
|
+
* listing is capped at `maxScanSegments` (default {@link DEFAULT_MAX_SCAN_SEGMENTS}, 250,000), the ceiling every
|
|
60
|
+
* other fleet scan keeps. A namespace over it throws `BudgetExceededError` **before any segment is destroyed**,
|
|
61
|
+
* never part-way through; raise `maxScanSegments` when the namespace really is that large and the memory is there.
|
|
62
|
+
* A `maxScanSegments` that is not a finite number >= 1 throws `ValidationError`, again before anything is destroyed.
|
|
57
63
|
*/
|
|
58
64
|
export declare function eraseNamespace(namespace: string, deps: EraseDeps, options: {
|
|
59
65
|
confirmNamespace: string;
|
|
60
66
|
allowCleartext?: boolean;
|
|
61
67
|
audit?: IAuditSink;
|
|
68
|
+
maxScanSegments?: number;
|
|
62
69
|
}): Promise<{
|
|
63
70
|
destroyed: DestroyResult[];
|
|
64
71
|
}>;
|
|
@@ -82,12 +89,12 @@ export interface DropResult {
|
|
|
82
89
|
* Generations still present in Storage when the sweep gave up, ascending. **Empty is the normal outcome** — a
|
|
83
90
|
* non-empty value means the storage was NOT fully reclaimed and the drop should be re-run.
|
|
84
91
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
92
|
+
* Without it, a drop would return `dropped: true` with a populated `generationsDeleted` and no `reason` even
|
|
93
|
+
* when an object holding the **complete set** had just been left in the bucket by a writer that was already
|
|
94
|
+
* mid-write when the tombstone landed (its publish is refused, but the object survives). For a cleartext
|
|
95
|
+
* segment those bytes are readable — and the collection only takes a destroyed segment's generations
|
|
96
|
+
* when something runs it. That result would look like a clean drop, and an operator would get no signal to
|
|
97
|
+
* re-run.
|
|
91
98
|
*/
|
|
92
99
|
readonly generationsRemaining: readonly number[];
|
|
93
100
|
/**
|
|
@@ -136,9 +143,8 @@ export interface DropResult {
|
|
|
136
143
|
* - `dropSegment` **removes the storage**. It works on a cleartext segment, and on an encrypted one it *also*
|
|
137
144
|
* drops the DEKs, so it is a strict superset there.
|
|
138
145
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* points at them. That is exactly the `missing-storage-generation` state the DR runbook says not to serve traffic
|
|
146
|
+
* It is the supported way to delete a segment and stop paying for it. The obvious workaround — an object-store
|
|
147
|
+
* lifecycle rule on the key prefix — deletes the bytes while the registry still points at them. That is exactly the `missing-storage-generation` state the DR runbook says not to serve traffic
|
|
142
148
|
* on, and it surfaces *intermittently*, because a read consults the cache before Storage: cached chunks answer
|
|
143
149
|
* correctly and evicted ones throw. The whole value of this function is that the ordering below cannot be got
|
|
144
150
|
* wrong by a caller.
|
|
@@ -146,8 +152,8 @@ export interface DropResult {
|
|
|
146
152
|
* **THE ORDER IS THE CONTRACT — registry first, then Storage.**
|
|
147
153
|
*
|
|
148
154
|
* 1. **Registry first.** After the tombstone nothing resolves a generation for this segment, so no reader can
|
|
149
|
-
* reach for bytes that are about to disappear, and no writer can publish onto it (
|
|
150
|
-
*
|
|
155
|
+
* reach for bytes that are about to disappear, and no writer can publish onto it (a
|
|
156
|
+
* load refuses a `destroyed` row).
|
|
151
157
|
* 2. **Storage second, best-effort, and re-swept.** Once the pointer is a tombstone the segment resolves as empty,
|
|
152
158
|
* so a failure part-way through leaves **orphaned bytes, not a wrong answer.** Orphans cost money and are
|
|
153
159
|
* cleaned up by re-running; a torn pointer costs correctness and is not self-healing. Given the choice, leak
|
|
@@ -163,10 +169,10 @@ export interface DropResult {
|
|
|
163
169
|
* cache for a window. A fresh store, or any reader that had not touched the segment, sees empty at once.
|
|
164
170
|
*
|
|
165
171
|
* That window is bounded by `cache.genTtlMs` (default 2 s) **only for a reader whose storage source has both a clock
|
|
166
|
-
* and a registry and a positive TTL** — expiry needs all three. A source built without a clock, or
|
|
167
|
-
* `cache.genTtlMs: 0
|
|
168
|
-
* cache-LRU hit never reaches Storage, such a reader
|
|
169
|
-
* be restarted. This is the *same* caching that makes the delete-bytes-first ordering fail intermittently rather
|
|
172
|
+
* and a registry and a positive TTL** — expiry needs all three. A source built without a clock or a registry, or
|
|
173
|
+
* with `cache.genTtlMs: 0`, has no timed refresh: it notices the drop only when a read has to fetch from a deleted
|
|
174
|
+
* generation or its reader cache evicts the segment. Because a cache-LRU hit never reaches Storage, such a reader
|
|
175
|
+
* can answer `true` for a dropped segment **indefinitely**, and must be invalidated or restarted. This is the *same* caching that makes the delete-bytes-first ordering fail intermittently rather
|
|
170
176
|
* than loudly — it cuts both ways.
|
|
171
177
|
*
|
|
172
178
|
* `confirmSegment` must equal `ref.segment`, matching `destroySegment`/`eraseNamespace`. For an automated
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Typed errors — callers learn *why* something failed, never by parsing strings. Retry is the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Typed errors — callers learn *why* something failed, never by parsing strings. Retry is not the engine's job: a
|
|
3
|
+
* driver classifies its backend's failures into this vocabulary, and the store's read wrapper retries the
|
|
4
|
+
* transient ones through `withRetry`.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Registry-symbol brands, so an error stays classifiable when the class object is not shared.
|
|
@@ -36,16 +36,16 @@ export declare class WriteConflictError extends CloudRoaringError {
|
|
|
36
36
|
export declare class IntegrityError extends CloudRoaringError {
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* A requested object/row does not exist. Part of the driver error vocabulary
|
|
40
|
-
* the
|
|
41
|
-
* `null` instead.
|
|
39
|
+
* A requested object/row does not exist. Part of the driver error vocabulary: every storage driver this
|
|
40
|
+
* library ships, the in-memory one included, throws it for a read of a generation object that is not there.
|
|
41
|
+
* A registry `get` of a row that is not there returns `null` instead.
|
|
42
42
|
*/
|
|
43
43
|
export declare class NotFoundError extends CloudRoaringError {
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* This build/configuration cannot perform the requested operation, though nothing is malformed. Two uses:
|
|
47
|
-
* (1) **format** — the bytes are well-formed but unreadable here (an unknown `.crbm` major version
|
|
48
|
-
*
|
|
47
|
+
* (1) **format** — the bytes are well-formed but unreadable here (an unknown `.crbm` major version) —
|
|
48
|
+
* distinct from `IntegrityError` (corruption); and (2)
|
|
49
49
|
* **store configuration** — an operation this store's wiring doesn't support (e.g. a lifecycle helper like
|
|
50
50
|
* `eraseSubject`/`retireExpired` called on a store built without a storage backend). Raised at
|
|
51
51
|
* operation time, before any mutation.
|
|
@@ -79,8 +79,9 @@ export declare class BudgetExceededError extends CloudRoaringError {
|
|
|
79
79
|
export declare class KeyUnavailableError extends CloudRoaringError {
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* A **transient** infrastructure fault
|
|
83
|
-
* a
|
|
82
|
+
* A **transient** infrastructure fault — throttling, a 5xx, a dropped connection, a client-side request
|
|
83
|
+
* timeout. Retrying a read that failed this way is safe; a write that failed this way may still have landed, so
|
|
84
|
+
* its caller re-runs the call rather than replaying the request. Drivers classify their backend's retryable faults and raise this (the
|
|
84
85
|
* SDK-specific knowledge stays in the SDK-specific driver); the retry layer (`core/retry`) retries **only**
|
|
85
86
|
* this class, never a deterministic error like {@link ValidationError}, {@link IntegrityError},
|
|
86
87
|
* {@link NotFoundError}, or {@link WriteConflictError} (retrying those is pointless or wrong). The original
|
|
@@ -91,25 +92,17 @@ export declare class KeyUnavailableError extends CloudRoaringError {
|
|
|
91
92
|
* to log; if you serialize the whole error *chain*, be aware you're including that metadata.
|
|
92
93
|
*/
|
|
93
94
|
export declare class TransientError extends CloudRoaringError {
|
|
94
|
-
/** A second brand so
|
|
95
|
+
/** A second brand so a transient fault is classifiable cross-bundle. */
|
|
95
96
|
readonly [TRANSIENT_BRAND]: true;
|
|
96
97
|
constructor(message: string, options?: {
|
|
97
98
|
cause?: unknown;
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
|
-
/**
|
|
101
|
-
* A single attempt exceeded its time budget. Subclass of {@link TransientError} so the retry layer treats a
|
|
102
|
-
* timeout as retryable by default — a stalled request often succeeds on a fresh connection. Raised by a
|
|
103
|
-
* driver whose injected client reports a request timeout — setting one on your injected client is the
|
|
104
|
-
* recommended way to bound a hang.
|
|
105
|
-
*/
|
|
106
|
-
export declare class TimeoutError extends TransientError {
|
|
107
|
-
}
|
|
108
101
|
/** Any error thrown by CloudRoaring (any tier, any bundle). */
|
|
109
102
|
export declare function isCloudRoaringError(err: unknown): err is CloudRoaringError;
|
|
110
103
|
/** An OCC conditional write/delete lost the race — retry the read-modify-write, don't fail. */
|
|
111
104
|
export declare function isWriteConflictError(err: unknown): err is WriteConflictError;
|
|
112
|
-
/** A retryable transient infrastructure fault
|
|
105
|
+
/** A retryable transient infrastructure fault. The retry layer keys on this. */
|
|
113
106
|
export declare function isTransientError(err: unknown): err is TransientError;
|
|
114
107
|
/** A requested object/row does not exist. */
|
|
115
108
|
export declare function isNotFoundError(err: unknown): err is NotFoundError;
|
|
@@ -30,11 +30,14 @@ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): P
|
|
|
30
30
|
* a pin taken while it was still active re-checks the status on every open — and nothing else in the library
|
|
31
31
|
* would ever collect them, so without this those objects are billed forever.
|
|
32
32
|
*
|
|
33
|
-
* **Throws {@link WriteConflictError}
|
|
34
|
-
* the
|
|
35
|
-
*
|
|
33
|
+
* **Throws {@link WriteConflictError} when it cannot act on the row it read**: the row is gone when it is re-read,
|
|
34
|
+
* after the listing or before a delete; on an active segment, the pointer has fallen below the cutoff before a
|
|
35
|
+
* delete; on a `destroyed` segment, the row's token has changed. Not to be confused with the empty array it still
|
|
36
|
+
* returns when there was genuinely nothing to collect (no row at all, or no pointer yet), which is why an empty
|
|
37
|
+
* array is not a receipt. The row is read before the listing and acted on after it, so a name that was purged and
|
|
36
38
|
* re-created in that window is a *different* segment wearing the same name, and its live object must not be
|
|
37
|
-
* collected on the strength of the old row
|
|
39
|
+
* collected on the strength of the old row: on an active segment the lower of the two pointers is what keeps it
|
|
40
|
+
* safe, and on a `destroyed` one the token. Refusing is safe — the objects are not going anywhere and the next
|
|
38
41
|
* pass reads a consistent row — but it has to be **distinguishable** from "there was nothing to collect", which
|
|
39
42
|
* an empty array is not: `eraseIdFromSegment` reads the returned list as the physical half of its erasure
|
|
40
43
|
* receipt, and would otherwise report `erased: true` over bytes still in the bucket. Re-run it.
|
|
@@ -45,7 +48,8 @@ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): P
|
|
|
45
48
|
* residual, and this is what eventually collects it from the retention sweep.
|
|
46
49
|
*
|
|
47
50
|
* `keep: 0` is how a subject-erasure rewrite makes a bit **physically** gone on return: the generation that held
|
|
48
|
-
* it is collected the moment the rewrite is current. A
|
|
51
|
+
* it is collected the moment the rewrite is current. A live read still fetching from it heals forward to the
|
|
52
|
+
* rewrite, and a pin of it (`seg.pin()`) fails with `NotFoundError` for any chunk it has yet to read.
|
|
49
53
|
*/
|
|
50
54
|
export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDeps, options?: {
|
|
51
55
|
keep?: number;
|
package/dist/core/keys.d.ts
CHANGED
|
@@ -3,14 +3,14 @@ export declare function segmentKey(ref: SegmentRef): string;
|
|
|
3
3
|
export declare function segmentPrefix(ref: SegmentRef): string;
|
|
4
4
|
export declare function chunkRefKey(ref: ChunkRef): string;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Cache key for a chunk scoped to a specific **version** of the segment — {@link chunkRefKey} plus that
|
|
7
7
|
* version (space-delimited, injection-proof exactly as above — the name parts are already encoded). A publish bumps the version, so it naturally
|
|
8
8
|
* misses the cache instead of serving a stale superseded chunk, and the superseded entries age out under the
|
|
9
9
|
* LRU ceiling (no active purge needed).
|
|
10
10
|
*
|
|
11
11
|
* `version` is whatever the Storage source reports as identifying the bytes a read will see — its
|
|
12
12
|
* `currentVersion` where it has one, otherwise the bare generation number. **A generation number alone is not
|
|
13
|
-
* an identity**:
|
|
13
|
+
* an identity**: a load's generation number restarts at 0 once a registry row is purged and the bucket emptied, so a
|
|
14
14
|
* retired-and-re-created name serves different data at the same `currentGen`, and a key built on the number
|
|
15
15
|
* hands the new incarnation the old one's decoded chunks.
|
|
16
16
|
*/
|
package/dist/core/load.d.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* `loadSegment` — the loaded store's primary write path, as one call.
|
|
3
3
|
*
|
|
4
4
|
* A load is always the same four steps: take the next generation number, write one immutable object, move the
|
|
5
|
-
* pointer, collect what the move superseded.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* pointer, collect what the move superseded. Inside the library each is its own internal function, and the
|
|
6
|
+
* step a hand-composed load leaves out is the last one, so a store that composed it would accumulate superseded
|
|
7
|
+
* generations it keeps paying for.
|
|
8
|
+
* Composing it here, once, is the point.
|
|
8
9
|
*
|
|
9
10
|
* The other reason it is one call is the **guard**. A load REPLACES a segment: whatever the stream contains is
|
|
10
11
|
* what the segment contains afterwards. That makes an upstream query returning fewer rows than usual — or none —
|
|
@@ -14,10 +15,12 @@
|
|
|
14
15
|
*
|
|
15
16
|
* Refusing therefore has to clean up after itself. The object is already durable at that point, and it sits
|
|
16
17
|
* ABOVE `currentGen`, where generation collection deliberately never looks (it deletes strictly below the
|
|
17
|
-
* pointer)
|
|
18
|
+
* pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged or
|
|
19
|
+
* gone. Once another write has changed the row, the object's number may name a re-created segment's live object,
|
|
20
|
+
* so it stays, and collection takes it like any other generation once one above it is current.
|
|
18
21
|
*/
|
|
19
22
|
import { type IAuditSink } from './audit.js';
|
|
20
|
-
import type
|
|
23
|
+
import { type CodecInterface } from './codec.js';
|
|
21
24
|
import type { Clock } from './determinism.js';
|
|
22
25
|
import type { IKeystore } from './crypto.js';
|
|
23
26
|
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
@@ -59,19 +62,35 @@ export interface LoadOptions {
|
|
|
59
62
|
readonly allowEmpty?: boolean;
|
|
60
63
|
readonly guard?: LoadGuard;
|
|
61
64
|
/**
|
|
62
|
-
* Generations to keep below the new pointer as a grace window (default 1).
|
|
65
|
+
* Generations to keep below the new pointer as a grace window (default 1). A non-negative integer: anything
|
|
66
|
+
* else (negative, fractional, `NaN`, infinite) throws `ValidationError` before anything is written. Keep at
|
|
67
|
+
* least one: a read that is
|
|
63
68
|
* still fetching chunks from the just-superseded generation would otherwise have it deleted out from under it
|
|
64
|
-
* and pay a re-resolve. A wider window costs storage and
|
|
69
|
+
* and pay a re-resolve. A wider window costs storage, and it is what a pinned handle needs: a pin is never
|
|
70
|
+
* re-resolved, so a chunk a pinned read has not fetched fails with `NotFoundError` once its generation is
|
|
71
|
+
* collected. Keep at least one generation for every one that can be written above the pinned one while your
|
|
72
|
+
* longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
|
|
73
|
+
* guide.
|
|
65
74
|
*/
|
|
66
75
|
readonly keep?: number;
|
|
67
76
|
readonly audit?: IAuditSink;
|
|
68
77
|
}
|
|
69
78
|
/** Why a load did not become current. */
|
|
70
79
|
export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained'
|
|
71
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Another writer got there first. Either another load wrote the same generation number first, so the write-once
|
|
82
|
+
* put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
|
|
83
|
+
* writing: another load published, a retention change, a rollback or an erasure wrote the row, or the row was
|
|
84
|
+
* deleted. Once another write has changed the row, the object stays in the bucket, because its generation number
|
|
85
|
+
* may by then name another incarnation's live object; once the row is gone, the object is deleted.
|
|
86
|
+
*/
|
|
72
87
|
| 'superseded';
|
|
73
88
|
export interface LoadResult {
|
|
74
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* The generation written. Present even when refused: a refusal deletes that object while the segment's row is
|
|
91
|
+
* unchanged or gone, and leaves it in the bucket once another write has changed the row. A load that lost its
|
|
92
|
+
* generation number to another wrote nothing, and reports `size: 0`.
|
|
93
|
+
*/
|
|
75
94
|
readonly generation: number;
|
|
76
95
|
/** Whether this generation is now the segment's current one. */
|
|
77
96
|
readonly published: boolean;
|
package/dist/core/metrics.d.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* Two caveats for sink authors: `onEvent` runs **synchronously on the I/O path**, so keep it cheap and
|
|
12
12
|
* non-blocking (offload batching/network to your own async queue); and `segment`/`namespace` are
|
|
13
13
|
* caller-controlled strings that may be PII and are **unbounded-cardinality** — do not map them to
|
|
14
|
-
* per-series metric labels unless they're known low-cardinality and PII-free. See the
|
|
15
|
-
*
|
|
14
|
+
* per-series metric labels unless they're known low-cardinality and PII-free. See the observability
|
|
15
|
+
* guide.
|
|
16
16
|
*/
|
|
17
17
|
/** The segment operations that emit an `op` latency event (timed with the injected clock, at the facade). */
|
|
18
18
|
export type MetricOpName = 'has' | 'count' | 'intersectInto' | 'unionInto' | 'andNotInto';
|
|
@@ -46,8 +46,7 @@ export type MetricEvent = {
|
|
|
46
46
|
} | {
|
|
47
47
|
readonly kind: 'intersect';
|
|
48
48
|
/**
|
|
49
|
-
* Which chunk-aligned combine this was. **Optional
|
|
50
|
-
* `'intersect'`, which is all this event reported before `union`/`andNot` existed.
|
|
49
|
+
* Which chunk-aligned combine this was. **Optional** — absent means `'intersect'`.
|
|
51
50
|
*
|
|
52
51
|
* For `'union'` over its *include* operands alone, `skippedChunks` is 0 by construction — union reads
|
|
53
52
|
* every chunk of every operand, so there is nothing to prune. It can still be non-zero when a union
|
|
@@ -69,6 +68,11 @@ export type MetricEvent = {
|
|
|
69
68
|
/**
|
|
70
69
|
* **Distinct** chunk-keys pruned — never fetched (the chunk-skipping saving). Counts distinct keys,
|
|
71
70
|
* not per-operand GETs, so it under-states the true GET saving when 3+ operands partially overlap.
|
|
71
|
+
*
|
|
72
|
+
* On a read bounded by `after` / `through`, this and `fetchedChunks` count only the keys inside the range:
|
|
73
|
+
* the keys outside it are never considered, so they are not counted as pruned either. Both count the keys the
|
|
74
|
+
* call would fetch if read to the end, so a page that stops early reports every key up to `through`, or to the
|
|
75
|
+
* end of the segment when `through` is left out.
|
|
72
76
|
*/
|
|
73
77
|
readonly skippedChunks: number;
|
|
74
78
|
} | {
|
|
@@ -5,15 +5,12 @@
|
|
|
5
5
|
* `node:` builtin — and because `validate.ts` needs it to measure a name against the key budget. Core may
|
|
6
6
|
* not import a driver (invariant 7), and a driver may import core, so this is the only layer both can see.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* It also fixes a hazard the allowlist *permitted*: `con`, `nul` and `com1` are Windows device names that
|
|
15
|
-
* validated cleanly and failed only on a user's machine. A grammar can be both too narrow and too permissive
|
|
16
|
-
* at once, which is the argument against having one.
|
|
8
|
+
* A name is **any non-empty string**, and each physical boundary escapes what *it* cannot take literally. An
|
|
9
|
+
* allowlist of characters would reject names for the storage layer's convenience, and the storage layer's
|
|
10
|
+
* convenience is the library's job, not the caller's. It would also let through the names that are actually
|
|
11
|
+
* dangerous: `con`, `nul` and `com1` are Windows device names made only of letters and digits, which pass any
|
|
12
|
+
* allowlist and fail only on a user's machine. A grammar can be both too narrow and too permissive at once,
|
|
13
|
+
* which is the argument against having one.
|
|
17
14
|
*
|
|
18
15
|
* Percent-encoding, because it is the one escape everybody already reads. `%` escapes itself as `%25` and is
|
|
19
16
|
* encoded **first**, which is what makes the transform injective: every `%` in an encoded string starts an
|
|
@@ -22,36 +19,30 @@
|
|
|
22
19
|
* There are two alphabets, because the boundaries differ:
|
|
23
20
|
*
|
|
24
21
|
* - **Object keys** (S3, GCS, Azure Blob) take almost anything. They need `/` escaped so a name cannot
|
|
25
|
-
* invent hierarchy or break a key parser that splits on it,
|
|
26
|
-
*
|
|
22
|
+
* invent hierarchy or break a key parser that splits on it, and control characters because they are not
|
|
23
|
+
* legal in the XML an S3 LIST returns. Every other character outside `[A-Za-z0-9._:-]` is escaped as well.
|
|
27
24
|
* - **Filesystem paths** need all of that plus `:` (an NTFS alternate-data-stream separator, where a write can
|
|
28
25
|
* *succeed* while `readdir` never lists the result), plus three hazards that are about the component as a
|
|
29
26
|
* whole rather than its characters — see {@link encodeNameForPath}.
|
|
30
27
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* would change what `encodeNameForKey` emits for a name containing either — silently moving those segments
|
|
35
|
-
* to a key nothing looks up. A cheap two-character reservation is the right price for not migrating a
|
|
36
|
-
* bucket; the property test below pins it.
|
|
28
|
+
* **The key alphabet is fixed for good.** Adding a character to it would change what `encodeNameForKey` emits
|
|
29
|
+
* for every name containing that character, silently moving those segments to keys nothing looks up. A test pins
|
|
30
|
+
* it, one printable ASCII character at a time.
|
|
37
31
|
*
|
|
38
|
-
* **
|
|
39
|
-
*
|
|
40
|
-
* file is ever edited, and a property test asserts it over
|
|
32
|
+
* **A plain name encodes to itself on the OBJECT-KEY alphabet** — one made only of `[A-Za-z0-9._:-]` that
|
|
33
|
+
* starts with a letter or digit — so S3, GCS and Azure keys read as the names that made them. That is the
|
|
34
|
+
* property to preserve if this file is ever edited, and a property test asserts it over every plain name.
|
|
41
35
|
*
|
|
42
|
-
* The **path** alphabet
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* Windows — but a 0.9 store holding one will not find it after the upgrade, because the readers' round-trip
|
|
46
|
-
* guard skips a spelling the driver would never have written. See the upgrade note in `CHANGELOG.md`.
|
|
36
|
+
* The **path** alphabet escapes more. Besides `:`, two classes of plain name are escaped on a path: a Windows
|
|
37
|
+
* device-name stem (`con`, `nul`, `com1`, `con.backup`) and a trailing dot (`a.`), since both silently alias
|
|
38
|
+
* or fail on Windows.
|
|
47
39
|
*/
|
|
48
40
|
/**
|
|
49
41
|
* Encode a name for use inside an **object key** (S3, GCS, Azure Blob).
|
|
50
42
|
*
|
|
51
|
-
* Leaves
|
|
52
|
-
* including `/` (which would invent hierarchy and break a parser
|
|
53
|
-
*
|
|
54
|
-
* legal in S3's XML responses).
|
|
43
|
+
* Leaves `[A-Za-z0-9._:-]` literal, except a leading `_` and a name that is only `.` or `..`, so a plain name
|
|
44
|
+
* reads as itself, and escapes everything else — including `/` (which would invent hierarchy and break a parser
|
|
45
|
+
* that splits on it) and control characters (not legal in S3's XML responses).
|
|
55
46
|
*/
|
|
56
47
|
export declare function encodeNameForKey(name: string): string;
|
|
57
48
|
/** Inverse of {@link encodeNameForKey}. */
|
|
@@ -66,8 +57,7 @@ export declare function decodeNameFromKey(encoded: string): string;
|
|
|
66
57
|
* 1. **`.` and `..`** are traversal. `join(root, '..')` leaves the storage root.
|
|
67
58
|
* 2. **Windows reserved device names** — `CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`, `LPT1`–`LPT9`, case
|
|
68
59
|
* insensitive and *with or without an extension*, so `con.0.crbm` is reserved too. Opening one addresses a
|
|
69
|
-
* device rather than a file
|
|
70
|
-
* cleanly and broke only on a user's Windows box.
|
|
60
|
+
* device rather than a file, so `store.segment('con')` would break only on a user's Windows box.
|
|
71
61
|
* 3. **A trailing `.` or space**, which Windows silently strips — so `a.` and `a` would become the same
|
|
72
62
|
* directory, quietly merging two segments.
|
|
73
63
|
*
|