@cloudbitmaps/core 0.11.2 → 0.13.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 (44) hide show
  1. package/dist/{chunk-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
  2. package/dist/chunk-5W3XS2IU.js.map +7 -0
  3. package/dist/core/audit.d.ts +6 -0
  4. package/dist/core/chunk-window.d.ts +24 -0
  5. package/dist/core/codec.d.ts +33 -1
  6. package/dist/core/consistency.d.ts +21 -2
  7. package/dist/core/cost.d.ts +91 -10
  8. package/dist/core/crbm/format.d.ts +42 -3
  9. package/dist/core/crbm/reader.d.ts +45 -3
  10. package/dist/core/crbm/writer.d.ts +32 -3
  11. package/dist/core/crbm-storage-source.d.ts +204 -23
  12. package/dist/core/crypto.d.ts +11 -7
  13. package/dist/core/determinism.d.ts +7 -0
  14. package/dist/core/engine.d.ts +41 -8
  15. package/dist/core/erase-id.d.ts +23 -3
  16. package/dist/core/erasure.d.ts +8 -4
  17. package/dist/core/generation-gc.d.ts +102 -4
  18. package/dist/core/load-input.d.ts +55 -0
  19. package/dist/core/load.d.ts +65 -10
  20. package/dist/core/metadata.d.ts +42 -0
  21. package/dist/core/metrics.d.ts +7 -1
  22. package/dist/core/pinned-storage-source.d.ts +2 -1
  23. package/dist/core/ports.d.ts +119 -13
  24. package/dist/core/registry-scan.d.ts +6 -0
  25. package/dist/core/retention-sweep.d.ts +47 -2
  26. package/dist/core/retry.d.ts +14 -0
  27. package/dist/core/rollback.d.ts +34 -4
  28. package/dist/core/summary.d.ts +61 -0
  29. package/dist/core/token.d.ts +36 -0
  30. package/dist/core/validate.d.ts +2 -0
  31. package/dist/driver-kit.js +39 -20
  32. package/dist/driver-kit.js.map +2 -2
  33. package/dist/drivers/_shared/entropy.d.ts +5 -0
  34. package/dist/drivers/_shared/object-registry.d.ts +36 -6
  35. package/dist/drivers/_shared/registry.d.ts +47 -19
  36. package/dist/drivers/localfs/registry.d.ts +10 -0
  37. package/dist/drivers/memory.d.ts +9 -3
  38. package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.js +1878 -501
  41. package/dist/index.js.map +4 -4
  42. package/dist/testing/fuzz-core.d.ts +7 -6
  43. package/package.json +1 -1
  44. package/dist/chunk-5OMC5U7U.js.map +0 -7
@@ -33,11 +33,29 @@
33
33
  * once Storage is provably empty for it —
34
34
  * because deleting the row is what makes the name reusable and takes the segment out of reach of
35
35
  * the generation collection.
36
+ *
37
+ * **On a registry that reports `conditionalDelete`, the purge removes the row for good**, so a full scan stops paying a
38
+ * read for every name a namespace ever held: the scan costs what is live and what is inside its grace. The delete is
39
+ * fenced on the token the purge judged and applied by the store only to that version of the row. Each retirement
40
+ * files a pointer in the due index under the day its tombstone's grace ends, so `scan: 'index'` purges as well as
41
+ * retires; nothing on the row records that day, which the purge derives from the stamp. The purge removes every pointer
42
+ * it read to the row, and a pointer that outlives its row anyway (a purge that ran with another grace, a delete that
43
+ * landed and lost its response, a pointer older than an index scan's lookback) is removed by the next scan that reads
44
+ * it: an index scan in its buckets, and an unscoped fleet scan, which reads every pointer, in all of them. Where a
45
+ * delete only rewrites the row as a tombstone, none of this is done: nothing is removed for good, so a pointer would
46
+ * only add a row for every scan to read.
36
47
  */
37
48
  import { type IAuditSink } from './audit.js';
38
49
  import type { DropDeps, DropResult } from './erasure.js';
39
50
  /** Default cap on retirements per sweep — a bounded batch, so a policy mistake costs one batch, not the fleet. */
40
51
  export declare const DEFAULT_RETIRE_LIMIT = 100;
52
+ /**
53
+ * How many purges in a row the registry may refuse in one call before the sweep stops purging for the rest of it. A
54
+ * blanket refusal (a policy that denies delete) costs this many attempts and no more; a refusal particular to one row
55
+ * (a blob with a snapshot, an object under a legal hold) does not hold the purges behind it, since a purge that succeeds
56
+ * starts the count again.
57
+ */
58
+ export declare const MAX_CONSECUTIVE_PURGE_FAULTS = 3;
41
59
  /**
42
60
  * Default delay before a retirement's own tombstone row is purged: 24 h.
43
61
  *
@@ -54,7 +72,11 @@ export interface RetireExpiredOptions {
54
72
  * caller, never from the platform — `store.retireExpired()` fills it in from the store's clock.
55
73
  */
56
74
  readonly now: number;
57
- /** Maximum segments to retire in this cycle (default 100). */
75
+ /**
76
+ * Maximum segments to retire in this cycle (default 100). It also bounds the purges attempted (a purge the registry
77
+ * refuses is not charged; {@link RetireExpiredResult.purgeFaults} says how purging stops after refusals) and the
78
+ * due-index pointers to nothing one call removes.
79
+ */
58
80
  readonly limit?: number;
59
81
  /** Report what would be retired and change nothing. */
60
82
  readonly dryRun?: boolean;
@@ -75,6 +97,11 @@ export interface RetireExpiredOptions {
75
97
  * pointer write failed (`indexed: false`), or whose ref is too long to index, has no pointer — so a
76
98
  * deployment that *only* ever runs `'index'` will never retire those. Run `'fleet'` periodically as the
77
99
  * repair pass. The default is `'fleet'` because it is complete by construction.
100
+ *
101
+ * An index scan purges too, on a registry that reports `conditionalDelete`: each retirement files a pointer to its
102
+ * tombstone under the day the tombstone's grace ends, and the scan reads it back with the expiry pointers. A pointer
103
+ * whose segment is gone is removed once read, by an index scan in the buckets it reads, and by an unscoped `'fleet'`
104
+ * scan (no `namespace`) in every bucket.
78
105
  */
79
106
  readonly scan?: 'fleet' | 'index';
80
107
  /**
@@ -94,7 +121,8 @@ export interface RetireExpiredOptions {
94
121
  */
95
122
  readonly lookbackBuckets?: number;
96
123
  /**
97
- * Whether to delete the tombstone rows this sweep's own past retirements left (default `true`). Set `false` to
124
+ * Whether to delete the tombstone rows this sweep's own past retirements left (default `true`): removed for good on
125
+ * a registry that reports `conditionalDelete`, tombstoned on one that does not. Set `false` to
98
126
  * keep every one of them — the right choice if something outside this library treats the presence of a
99
127
  * `destroyed` row as an attestation. That includes the row of a retirement whose drop found no Storage
100
128
  * generation to delete and left none behind: with the default that row is deleted in the same pass, since it
@@ -179,6 +207,23 @@ export interface RetireExpiredResult {
179
207
  readonly tombstonesPurged: number;
180
208
  /** True when `limit` cut the cycle short — **more segments are still eligible**. Re-run. */
181
209
  readonly limited: boolean;
210
+ /**
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
213
+ * raw provider error is one; a write that landed between the sweep's read and its fenced delete is not (that is
214
+ * `failed: contended` in the ledger). Each leaves its row or pointer in place, so a purge that keeps failing never
215
+ * frees the name.
216
+ *
217
+ * A refused purge is not charged to `limit`, so retirements go on: a tombstone that cannot be purged does not hold
218
+ * the segments behind it past their expiry. **Purging stops for the rest of the call after
219
+ * {@link MAX_CONSECUTIVE_PURGE_FAULTS} (3) refused purges in a row**, and a purge that succeeds starts the count
220
+ * again. A blanket refusal therefore costs three attempts a call, and a refusal particular to one row (a blob with a
221
+ * snapshot, an object under a legal hold) holds nothing behind it. The next call tries again. Check this field; a
222
+ * ledger entry for each is `skipped`, but a caller that reads only `retired` sees none of it.
223
+ */
224
+ readonly purgeFaults: number;
225
+ /** The first of those faults, as a ledger reason (`failed: …` with the provider's message). Absent when there were none. */
226
+ readonly firstPurgeFault?: `failed: ${string}`;
182
227
  readonly dryRun: boolean;
183
228
  /** Per-segment ledger. Inspect it: a `skipped` or failed entry is a segment that still holds data. */
184
229
  readonly entries: readonly RetireEntry[];
@@ -61,6 +61,20 @@ export interface RetryDeps {
61
61
  export declare function backoffDelayMs(policy: RetryPolicy, attempt: number): number;
62
62
  /** Apply the policy's jitter to a computed delay using the injected RNG. */
63
63
  export declare function applyJitter(policy: RetryPolicy, delayMs: number, rng: Rng): number;
64
+ /**
65
+ * The store's read retry, as a write path takes it for the reads it makes along the way: a policy (the default when
66
+ * absent) and what {@link withRetry} needs to run it.
67
+ */
68
+ export type ReadRetry = RetryDeps & {
69
+ readonly policy?: RetryPolicy;
70
+ };
71
+ /**
72
+ * Run a read under `retry`, or once when there is none. For the reads a write makes along the way (a load guard's
73
+ * read of the current generation, an erasure's reads of the generations it rewrites and verifies): a read is safe
74
+ * to repeat, so a transient fault on one is retried under the store's policy rather than failing the whole write.
75
+ * The write itself is never passed here.
76
+ */
77
+ export declare function retryRead<T>(op: () => Promise<T>, retry: ReadRetry | undefined): Promise<T>;
64
78
  /**
65
79
  * Run `op`, retrying transient failures per `policy`. Resolves with `op`'s result, or rejects with the last
66
80
  * error once attempts are exhausted (or immediately for a non-retryable error). The thrown error is always
@@ -17,17 +17,32 @@
17
17
  * Art. 30 record or an incident review wants to find.
18
18
  */
19
19
  import { type IAuditSink } from './audit.js';
20
- import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
20
+ import type { IKeystore } from './crypto.js';
21
+ import type { IStorageDriver, GenerationMetadata, IRegistryDriver, SegmentRef } from './ports.js';
21
22
  /** What the generation helpers need: the objects, and the pointer that says which one is current. */
22
23
  export interface GenerationListDeps {
23
24
  readonly storage: IStorageDriver;
24
25
  readonly registry: IRegistryDriver;
26
+ /**
27
+ * The key of an encrypted segment, so a rollback can read its target's count and metadata and seal them into the
28
+ * row. Without it, a rollback of an encrypted segment still works and leaves the row with no summary of the
29
+ * generation it moves to, which a reader then takes from the object.
30
+ */
31
+ readonly keystore?: IKeystore;
25
32
  }
26
33
  /** One generation present in the bucket. */
27
34
  export interface GenerationEntry {
28
35
  readonly generation: number;
29
36
  /** Whether the registry pointer currently names this generation. */
30
37
  readonly current: boolean;
38
+ /**
39
+ * The current generation's id count, from its registry row, with no read of the object. Present only on the current
40
+ * entry, and only when the row's summary can be used: for an encrypted segment that takes a keystore that opens the
41
+ * segment's key. It is the row's word, not confirmed against the object.
42
+ */
43
+ readonly cardinality?: number;
44
+ /** The metadata the current generation was loaded with, from the same row; absent when it has none. */
45
+ readonly metadata?: GenerationMetadata;
31
46
  }
32
47
  /**
33
48
  * Every generation still present for a segment, ascending, with the current one marked.
@@ -38,7 +53,8 @@ export interface GenerationEntry {
38
53
  *
39
54
  * One registry read and one `list` call, whether or not the segment has a registry row, so it also finds the
40
55
  * objects a purged row left behind. It does not open the objects, so it costs nothing per generation and tells you
41
- * nothing about their contents — read the sizes through the store's own report if you need them.
56
+ * nothing about their contents — read the sizes through the store's own report if you need them. The current entry
57
+ * carries the id count and metadata its row records, from the same read.
42
58
  */
43
59
  export declare function listGenerations(ref: SegmentRef, deps: GenerationListDeps): Promise<GenerationEntry[]>;
44
60
  export interface RollbackResult {
@@ -58,12 +74,26 @@ export interface RollbackResult {
58
74
  * design exists to avoid;
59
75
  * - a **crypto-shredded** segment is refused: every generation of it is unreadable, so a rollback would produce a
60
76
  * segment that resolves and then fails;
77
+ * - a target that is **not what the row says the segment is** is refused with `IntegrityError`: a cleartext object
78
+ * under a row with keys, or an encrypted one under a row with none, either of which every read would then refuse.
79
+ * One tail read of the target decides it, as does a footer that fails its own checks;
61
80
  * - rolling to the generation **already current** is a no-op that reports itself, not an error.
62
81
  *
82
+ * The same tail read opens the target when the segment's key is at hand, and the rollback writes the target's own id
83
+ * count and metadata into the row, in the compare-and-swap that moves the pointer: a reader that sees the target as
84
+ * current sees what describes it. (One tail read, and a range read as well when the target's index is longer than the
85
+ * tail read: a target of very many chunks.) A store with no keystore, or one that cannot open the segment's key for any
86
+ * reason, a keystore that is unreachable or timed out included, still rolls an encrypted segment back and leaves the
87
+ * row with no summary of the target. The undo of a rollback whose target was collected meanwhile puts back the summary
88
+ * the row had, and so does the undo of an `allowForward` rollback whose target was replaced meanwhile: an object above
89
+ * the pointer can be deleted by an erasure and its number taken by a load, between the read and the swap, so after the
90
+ * swap such a target is read once more, and the pointer put back unless it is the object whose summary was written.
91
+ *
63
92
  * What it does **not** do is delete anything. The generations above the new pointer stay in the bucket, which is
64
93
  * what makes the rollback reversible — roll forward again by naming one of them. They are also then *above*
65
- * `currentGen`, where generation collection never looks. They stay until a load numbers above them (collection
66
- * then keeps the newest `keep` of what is below its pointer), `dropSegment` deletes them, or a subject erasure
94
+ * `currentGen`, where generation collection never looks. They stay until loads pass them (the first load whose
95
+ * number one of them holds numbers above them all, and collection then keeps the newest `keep` of what is below its
96
+ * pointer), `dropSegment` deletes them, or a subject erasure
67
97
  * deletes them: all of those present when it rewrites, and only those holding the id when the current generation does not.
68
98
  * Not collecting them is a deliberate trade: an operator who has just undone a bad load should not have the
69
99
  * evidence collected out from under them, while a rollback target that still holds erased data would make the
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A generation's summary on its registry row: how a writer builds it, how a reader opens it, and how it is held
3
+ * against the object it describes.
4
+ *
5
+ * The row carries a cached description of the current generation, `{ generation, cardinality, metadata? }`, written by
6
+ * the same compare-and-swap that moves the pointer. On a cleartext segment the values are in the clear. On an
7
+ * encrypted one the row holds `{ generation, sealed }` instead: the cardinality as a fixed-width little-endian u64,
8
+ * then the metadata's canonical JSON if there is any, sealed under the segment's data key with the summary scope of
9
+ * the associated data, which binds it to its namespace, segment and generation. The count is fixed-width so the
10
+ * ciphertext's length reveals only the metadata's size, never how many digits the count has. Pure: no I/O.
11
+ *
12
+ * The generation's `.crbm` object stays the truth, and the summary is a copy of it. {@link summaryAgrees} is the
13
+ * comparison of one with the other.
14
+ */
15
+ import type { Aead } from './crypto.js';
16
+ import type { ClearRegistrySummary, GenerationMetadata, RegistryRecord, RegistrySummary, SealedRegistrySummary, SegmentRef } from './ports.js';
17
+ /** What a summary says about a generation: its id count, and its metadata when it has any. */
18
+ export interface GenerationDescription {
19
+ readonly cardinality: number;
20
+ readonly metadata?: GenerationMetadata | undefined;
21
+ }
22
+ /** The clear summary of a generation on a cleartext segment. `metadata` is left off when there is none. */
23
+ export declare function clearSummary(generation: number, cardinality: number, metadata?: GenerationMetadata): ClearRegistrySummary;
24
+ /**
25
+ * The sealed summary of a generation on an encrypted segment: the cardinality as a u64, then the metadata's canonical
26
+ * JSON, sealed under `aead` and bound to `ref` and `generation`.
27
+ */
28
+ export declare function sealSummary(aead: Aead, ref: SegmentRef, generation: number, cardinality: number, metadata?: GenerationMetadata): SealedRegistrySummary;
29
+ /**
30
+ * What a sealed summary says, opened with `aead` under the summary scope of `ref` and the summary's own generation.
31
+ * The bytes are untrusted: a blob that is not base64, is the wrong size, fails its authentication (tampered, or moved
32
+ * from another generation's row), or holds a count outside 0 to 2^32 or metadata that breaks the rules, is an
33
+ * {@link IntegrityError}.
34
+ */
35
+ export declare function openSummary(aead: Aead, ref: SegmentRef, summary: SealedRegistrySummary): GenerationDescription;
36
+ /**
37
+ * What a row's summary says of its current generation, or `undefined` when it says nothing a reader may use. It is
38
+ * used only for the generation it names, on an active row, and in the shape the row's keys call for: clear on a row
39
+ * with no wrapped keys, sealed on a row with them. A sealed one is opened with `aead`, and is unusable without it,
40
+ * or when it does not open, so a summary a tamperer moved or rewrote is never believed. Never throws for a bad
41
+ * summary: the row's copy is a cache, and a copy that cannot be used is a copy that is not there.
42
+ */
43
+ export declare function usableSummary(ref: SegmentRef, row: RegistryRecord, aead: Aead | undefined): GenerationDescription | undefined;
44
+ /**
45
+ * Whether a summary's description of a generation is what its object holds: the same id count and the same metadata,
46
+ * compared in canonical form, so key order never matters. A summary with metadata over an object with none, and the
47
+ * reverse, disagree: whether an encrypted object's metadata block is present is not authenticated (stripping it needs
48
+ * no key), so the row's sealed copy is what a missing block is held against.
49
+ */
50
+ export declare function summaryAgrees(summary: GenerationDescription, object: GenerationDescription): boolean;
51
+ /**
52
+ * The metadata an erasure carries into its rewrite of a generation: the object's own, except that an object with none
53
+ * whose row's authenticated summary of the same generation has some gets the row's. Whether an encrypted object has a
54
+ * metadata block is not authenticated, so a block stripped from one reads as none, and the sealed summary on the row is
55
+ * the one copy that says there was a block; carrying it keeps the rewrite from erasing the record of what it dropped.
56
+ * Pass `authenticated` only for a summary that was opened (a clear one has no more authority than the block it sits
57
+ * beside). Never refuses: a rewrite over a stripped block is still an erasure.
58
+ */
59
+ export declare function metadataToCarry(object: GenerationMetadata | undefined, authenticated: GenerationDescription | undefined): GenerationMetadata | undefined;
60
+ /** The summary of a generation of `ref`, clear or sealed under `aead` as the segment's keys call for. */
61
+ export declare function summaryOf(ref: SegmentRef, generation: number, description: GenerationDescription, aead: Aead | undefined): RegistrySummary;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The registry token's incarnation: the part of a token that says which incarnation of a name a row is.
3
+ *
4
+ * A shipped registry gives a row it creates a token `<incarnation>.<counter>.<write>`: 32 lowercase hex digits drawn
5
+ * from injected entropy when the row is created, a decimal counter, and 16 hex digits drawn for each write. Every write
6
+ * of the row keeps the incarnation, and a name created again draws a new one, so two incarnations are told apart by it
7
+ * even when their rows were created in the same millisecond or one is gone. A row a 0.11 build wrote has no incarnation
8
+ * for as long as it lives (`"7"`, then `"8.<write>"`), and a registry of someone else's issues tokens in any form.
9
+ *
10
+ * This lives in `core/` so the publish can read it, and the registry drivers, which build the tokens, import it from
11
+ * here: the form has one definition.
12
+ */
13
+ import type { Token } from './ports.js';
14
+ /** An incarnation-form token: the incarnation id, the counter and a write part, `.`-separated, in lowercase hex. */
15
+ export declare const INCARNATION_TOKEN: RegExp;
16
+ /**
17
+ * The incarnation id in a token, or `undefined` for one that has none: a schema-1-born row's, or any token another
18
+ * driver issues. Two tokens with the same incarnation are writes of one incarnation of a row.
19
+ */
20
+ export declare function incarnationOf(token: Token): string | undefined;
21
+ /**
22
+ * Whether two reads of one name's row are one incarnation of it.
23
+ *
24
+ * - Both tokens carry an incarnation id: the ids decide, and the clock stamps do not enter.
25
+ * - Only one does: different incarnations. A name created again is always given an incarnation id, and a write never
26
+ * takes one away, so a row without an id never becomes a row with one, nor the reverse.
27
+ * - Neither does (a row 0.11 wrote, or a registry of someone else's): there is no id to compare, and the creation stamp
28
+ * is the only signal left. It is a clock reading, so a caller that acts on this answer proves its own object too.
29
+ */
30
+ export declare function sameIncarnation(a: {
31
+ readonly token: Token;
32
+ readonly createdAt: number;
33
+ }, b: {
34
+ readonly token: Token;
35
+ readonly createdAt: number;
36
+ }): boolean;
@@ -7,6 +7,8 @@ import type { ChunkRef, SegmentRef } from './ports.js';
7
7
  * a namespace and a segment at the ceiling.
8
8
  */
9
9
  export declare const MAX_NAME_LENGTH = 256;
10
+ /** Whether `value` is well-formed UTF-16: no unpaired surrogate, so it survives a round trip through UTF-8. */
11
+ export declare function isWellFormedString(value: string): boolean;
10
12
  /**
11
13
  * How long a name is once storage has it — the longer of the two encodings.
12
14
  *
@@ -11,20 +11,24 @@ import {
11
11
  decodeNameFromKey,
12
12
  encodeNameForKey,
13
13
  encodeNameForPath,
14
+ entropyIsAvailable,
15
+ incarnationOf,
14
16
  isNotFoundError,
15
17
  isValidationError,
16
18
  isWriteConflictError,
17
19
  mapWithConcurrency,
18
20
  namespaceKeyPart,
19
21
  namespacePathPart,
22
+ newIncarnationToken,
23
+ nextRegistryToken,
20
24
  parseRegistryEnvelope,
21
25
  recordFromNew,
22
- registryCounterOf,
23
26
  serializeRegistryEnvelope,
24
27
  validateNewRegistryRecord,
25
28
  validateRegistryPatch,
26
- validateSegmentRef
27
- } from "./chunk-5OMC5U7U.js";
29
+ validateSegmentRef,
30
+ webCryptoEntropy
31
+ } from "./chunk-5W3XS2IU.js";
28
32
 
29
33
  // src/drivers/_shared/object-registry-keys.ts
30
34
  var REGISTRY_SUFFIX = ".reg";
@@ -108,47 +112,59 @@ var ObjectVersionRaced = class extends Error {
108
112
  }
109
113
  };
110
114
  var ObjectStoreRegistry = class {
111
- constructor(store, prefix, now) {
115
+ /**
116
+ * `entropy` draws the random parts of every token: a new row's incarnation id, and each write's own part. It
117
+ * defaults to the platform's Web Crypto. Inject one only to make a test replayable: a seeded source in production
118
+ * gives every process the same ids.
119
+ */
120
+ constructor(store, prefix, now, entropy = webCryptoEntropy) {
112
121
  this.store = store;
113
122
  this.prefix = prefix;
114
123
  this.now = now;
124
+ this.entropy = entropy;
115
125
  }
116
126
  store;
117
127
  prefix;
118
128
  now;
129
+ entropy;
119
130
  capabilities() {
120
- return { strongRead: true };
131
+ const conditionalDelete = this.removesRows();
132
+ return entropyIsAvailable(this.entropy) ? { strongRead: true, conditionalDelete } : { strongRead: true, canWrite: false, conditionalDelete };
133
+ }
134
+ /** Whether a delete removes a row born with an incarnation id: the store has a delete, and vouches for it. */
135
+ removesRows() {
136
+ return this.store.conditionalDelete === true && typeof this.store.delete === "function";
121
137
  }
122
138
  async get(ref) {
123
139
  const current = await this.readRow(registryObjectKey(this.prefix, ref));
124
140
  return current && !current.env.deleted ? current.env.record : null;
125
141
  }
126
142
  async create(ref, record) {
127
- validateNewRegistryRecord(record);
143
+ const checked = validateNewRegistryRecord(record);
128
144
  const key = registryObjectKey(this.prefix, ref);
129
145
  const current = await this.readRow(key);
130
146
  if (current !== null && !current.env.deleted) {
131
147
  throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);
132
148
  }
133
- const token = String(current ? registryCounterOf(current.env.record) + 1 : 0);
149
+ const token = newIncarnationToken(this.entropy, current?.env.record);
134
150
  const env = {
135
151
  deleted: false,
136
- record: recordFromNew(ref, record, this.now(), token)
152
+ record: recordFromNew(ref, checked, this.now(), token)
137
153
  };
138
154
  await this.putRow(key, env, current ? { version: current.version } : "absent");
139
155
  return { token };
140
156
  }
141
157
  async compareAndSwap(ref, expected, patch) {
142
- validateRegistryPatch(patch);
158
+ const checked = validateRegistryPatch(patch);
143
159
  const key = registryObjectKey(this.prefix, ref);
144
160
  const current = await this.readRow(key);
145
161
  if (current === null || current.env.deleted || current.env.record.token !== expected) {
146
162
  throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
147
163
  }
148
- const token = String(registryCounterOf(current.env.record) + 1);
164
+ const token = nextRegistryToken(current.env.record, this.entropy);
149
165
  const env = {
150
166
  deleted: false,
151
- record: applyRegistryPatch(current.env.record, patch, this.now(), token)
167
+ record: applyRegistryPatch(current.env.record, checked, this.now(), token)
152
168
  };
153
169
  await this.putRow(key, env, { version: current.version });
154
170
  return { token };
@@ -187,12 +203,17 @@ var ObjectStoreRegistry = class {
187
203
  } else if (current === null || current.env.deleted) {
188
204
  return;
189
205
  }
190
- const token = String(registryCounterOf(current.env.record) + 1);
191
- const env = {
192
- deleted: true,
193
- record: { ...current.env.record, token, updatedAt: this.now() }
194
- };
195
206
  try {
207
+ const remove = this.removesRows() ? this.store.delete : void 0;
208
+ if (remove !== void 0 && incarnationOf(current.env.record.token) !== void 0) {
209
+ await remove.call(this.store, key, { version: current.version });
210
+ return;
211
+ }
212
+ const token = nextRegistryToken(current.env.record, this.entropy);
213
+ const env = {
214
+ deleted: true,
215
+ record: { ...current.env.record, token, updatedAt: this.now() }
216
+ };
196
217
  await this.putRow(key, env, { version: current.version });
197
218
  return;
198
219
  } catch (err) {
@@ -200,9 +221,7 @@ var ObjectStoreRegistry = class {
200
221
  throw err;
201
222
  }
202
223
  }
203
- throw new WriteConflictError(
204
- `registry delete: contention tombstoning "${ref.segment}" \u2014 retry`
205
- );
224
+ throw new WriteConflictError(`registry delete: contention deleting "${ref.segment}" \u2014 retry`);
206
225
  }
207
226
  /**
208
227
  * Read + parse a registry object, returning its envelope and version fence, or null if absent.
@@ -210,7 +229,7 @@ var ObjectStoreRegistry = class {
210
229
  * A store that pins a version to read consistently ({@link ObjectVersionRaced}) loses that pin whenever a
211
230
  * concurrent writer lands mid-read. Re-reading is the whole answer — the next read simply observes the
212
231
  * newer version — and it is bounded so a row under permanent write saturation fails typed instead of
213
- * spinning. Only a store racing itself gets here; S3 never does.
232
+ * spinning. Only a store that reads in two calls gets here; none of the shipped stores does.
214
233
  */
215
234
  async readRow(objectKey) {
216
235
  const row = await this.readRaced(objectKey);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/drivers/_shared/object-registry-keys.ts", "../src/drivers/_shared/object-registry.ts"],
4
- "sourcesContent": ["/**\n * The registry object layout, shared by every object-store registry (S3, GCS, Azure Blob).\n *\n * One tiny JSON object per segment at `<prefix>registry/<ns>/<segment>.reg`, **registry-first** \u2014 separate\n * from the namespace-first storage layout (`<prefix><ns>/segments/\u2026`) \u2014 so discovery is a single list over\n * `registry/` for all namespaces, or `registry/<ns>/` for one, never entangled with the `.crbm` payloads.\n *\n * It lives here rather than in a driver because all three object stores encode names the same way\n * (`encodeNameForKey`, `namespaceKeyPart`) and build byte-identical keys. Three copies of this would be\n * three chances for one cloud's layout to drift from the others \u2014 and a drifted key is not a visible bug, it\n * is a segment that quietly cannot be found by a store pointed at the same bucket.\n */\nimport { ValidationError } from '@/core/errors';\nimport { validateSegmentRef } from '@/core/validate';\nimport type { SegmentRef } from '@/core/ports';\nimport { DEFAULT_NAMESPACE, decodeNameFromKey, encodeNameForKey, namespaceKeyPart } from './keys';\n\nconst REGISTRY_SUFFIX = '.reg';\n\n/** Normalize an optional caller prefix to either `''` or `trimmed/` (no leading/trailing slashes). */\nexport function prefixPart(prefix: string | undefined): string {\n if (prefix === undefined) return '';\n const trimmed = prefix.replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `${trimmed}/`;\n}\n\n/**\n * Validate a caller-supplied key prefix (trusted config, but make it a real containment boundary): reject\n * control characters and `.`/`..` path segments so a prefix cannot traverse out of its intended space.\n * Returns it unchanged \u2014 the key builders normalize slashes.\n *\n * The traversal check covers three spellings, because the tools that walk these buckets do. A bare `..` is\n * the obvious one. A **backslash** segment matters on ADLS Gen2 and to Windows-side tooling, which treat it\n * as a separator this code otherwise would not. And a percent-encoded `..` matters because `gsutil`,\n * `s3fs`, `gcsfuse` and `azcopy` decode a key on their way to a local path \u2014 the same reasoning\n * `name-codec.ts` already applies to segment names, which a prefix has no reason to be exempt from.\n */\nexport function normalizeObjectPrefix(prefix: string | undefined): string | undefined {\n if (prefix === undefined) return undefined;\n for (const ch of prefix) {\n const code = ch.charCodeAt(0);\n // C0 plus DEL: a bare `< 0x20` lets U+007F through, and it is as unprintable as the rest.\n if (code < 0x20 || code === 0x7f) {\n throw new ValidationError('prefix must not contain control characters');\n }\n }\n if (prefix.includes('\\\\')) {\n throw new ValidationError(\n `prefix must not contain backslashes (a path separator on some backends): ${JSON.stringify(prefix)}`,\n );\n }\n // Check the decoded spelling too, so an encoded `..` cannot slip past the literal comparison.\n let decoded: string;\n try {\n decoded = decodeURIComponent(prefix);\n } catch {\n decoded = prefix; // not valid percent-encoding; the literal check still applies\n }\n for (const source of decoded === prefix ? [prefix] : [prefix, decoded]) {\n for (const segment of source.split('/')) {\n if (segment === '.' || segment === '..') {\n throw new ValidationError(\n `prefix must not contain \".\" or \"..\" path segments: ${JSON.stringify(prefix)}`,\n );\n }\n }\n }\n return prefix;\n}\n\n/** The key prefix under which every registry object lives: `<prefix>registry/`. */\nexport function registryPrefix(prefix: string | undefined): string {\n return `${prefixPart(prefix)}registry/`;\n}\n\n/** The full key of one segment's registry object: `<prefix>registry/<ns>/<segment>.reg`. */\nexport function registryObjectKey(prefix: string | undefined, ref: SegmentRef): string {\n validateSegmentRef(ref);\n return `${registryPrefix(prefix)}${namespaceKeyPart(ref.namespace)}/${encodeNameForKey(ref.segment)}${REGISTRY_SUFFIX}`;\n}\n\n/** The list prefix for discovery: registry-wide, or scoped to one namespace. */\nexport function registryListPrefix(prefix: string | undefined, namespace?: string): string {\n const base = registryPrefix(prefix);\n return namespace === undefined ? base : `${base}${namespaceKeyPart(namespace)}/`;\n}\n\n/**\n * Parse a `<prefix>registry/<ns>/<segment>.reg` key back to its {@link SegmentRef}, or `null` if it does not\n * match \u2014 a stray or foreign object under the prefix, or one whose parsed ref fails the round-trip check.\n * `_default` maps back to the absent namespace. A name is percent-encoded on the way in, so no encoded name\n * can contain `/` and the split stays unambiguous whatever the caller named their segment.\n */\nexport function parseRegistryKey(prefix: string | undefined, objectKey: string): SegmentRef | null {\n const base = registryPrefix(prefix);\n if (!objectKey.startsWith(base) || !objectKey.endsWith(REGISTRY_SUFFIX)) return null;\n const rest = objectKey.slice(base.length, objectKey.length - REGISTRY_SUFFIX.length);\n const slash = rest.indexOf('/');\n if (slash < 0) return null;\n const nsPart = rest.slice(0, slash);\n const encodedSegment = rest.slice(slash + 1);\n if (encodedSegment.length === 0 || encodedSegment.includes('/')) return null;\n const segment = decodeNameFromKey(encodedSegment);\n const namespace = nsPart === DEFAULT_NAMESPACE ? undefined : decodeNameFromKey(nsPart);\n // The encoding must ROUND-TRIP, not merely decode: a foreign object placed under our prefix could spell a\n // name two ways, and reporting both would hand a sweep one segment under two identities.\n if (encodeNameForKey(segment) !== encodedSegment) return null;\n if (namespace !== undefined && encodeNameForKey(namespace) !== nsPart) return null;\n const ref: SegmentRef = { segment, namespace };\n try {\n validateSegmentRef(ref); // reject a hostile/foreign key that isn't a valid ref\n } catch {\n return null;\n }\n return ref;\n}\n", "/**\n * `ObjectStoreRegistry` \u2014 an {@link IRegistryDriver} over any object store with conditional writes.\n *\n * Lets a **read-mostly deployment run on one bucket alone** \u2014 storage `.crbm` generations plus the registry in\n * the same place, with no separate database. One tiny JSON object per segment at\n * `<prefix>registry/<ns>/<segment>.reg` holding the `{ deleted, record }` envelope (the same shape LocalFs\n * persists). The OCC token is a monotonic counter, advanced on every mutation and even across a `delete`\n * (which **tombstones** rather than removes the object) so a deleted-then-recreated row never re-issues an\n * old token (ABA-safe) \u2014 identical semantics to the LocalFs registry, so it passes the same conformance suite.\n *\n * **The atomic swap is offloaded to the store's conditional writes.** `create` writes only if absent (or\n * over a tombstone under its version), and `compareAndSwap`/`delete` write only if the object still carries\n * the version we read \u2014 so a concurrent writer between our read and our write loses, and the loss surfaces\n * as {@link WriteConflictError}. No in-process lock is needed (unlike LocalFs): the precondition fences\n * writers *across processes*.\n *\n * **Why this is shared rather than written per cloud.** Every object store worth using has the same two\n * primitives under different names \u2014 S3 `If-None-Match: *` / `If-Match: <etag>`, GCS `ifGenerationMatch: 0`\n * / `ifGenerationMatch: <generation>`, Azure `If-None-Match: *` / `If-Match: <etag>` \u2014 so the only thing\n * that differs is three I/O calls, which is what {@link ObjectRegistryStore} abstracts. The parts that are\n * genuinely hard (the ABA-safe counter, the tombstone, the bounded delete retry, the round-trip key check)\n * exist once. Three copies would be three chances for one cloud to drift, and a drifted registry is not a\n * visible bug \u2014 it is a lost `currentGen` swap.\n *\n * **Deployment requirements** (a backend that violates these silently corrupts the registry):\n * - The store **must honor the conditional write**. One that accepts the precondition header and ignores it\n * degrades compare-and-swap to last-write-wins \u2192 lost `currentGen` swaps.\n * - Reads must be **strongly consistent** \u2014 true of S3 (since 2020), GCS and Azure Blob \u2014 since the registry\n * advertises `strongRead` and generation resolution depends on it.\n * - **Do not apply a lifecycle-expiration rule to the `registry/` prefix that expires a current version.**\n * `delete` tombstones (keeps the object with an advanced counter) for ABA-safety; expiring a tombstone would let a recreate reset the\n * token to 0 and re-issue a stale one.\n *\n * **`list()` is fail-closed, and one bad object stops it for everyone.** An object under the `registry/`\n * prefix whose key parses but whose body does not aborts the whole enumeration \u2014 every namespace, not just\n * the affected segment \u2014 and it stays that way until an operator removes the object. The error names the\n * offending key.\n *\n * That is the deliberate choice, and it is the *safer* one rather than the more convenient one. Skipping the\n * unreadable row would keep discovery running, but `list()` is what tells orphan generation collection which\n * segments exist: a row missing from the enumeration makes its storage `.crbm` generations look unreferenced,\n * and the next GC pass would delete them. Silently skipping turns a parse error into data loss. Refusing to\n * enumerate keeps every sweep off a set it cannot vouch for (invariant 5 \u2014 bytes from the tier are\n * untrusted). **Recovery:** read the named object, and either restore a valid envelope or delete it; nothing\n * else is damaged, and discovery resumes on its own.\n */\nimport {\n IntegrityError,\n TransientError,\n ValidationError,\n WriteConflictError,\n isWriteConflictError,\n} from '@/core/errors';\nimport type {\n IRegistryDriver,\n NewRegistryRecord,\n RegCaps,\n RegistryPatch,\n RegistryRecord,\n SegmentRef,\n Token,\n} from '@/core/ports';\nimport { mapWithConcurrency } from '@/core/concurrency';\nimport {\n applyRegistryPatch,\n parseRegistryEnvelope,\n recordFromNew,\n registryCounterOf,\n serializeRegistryEnvelope,\n validateNewRegistryRecord,\n validateRegistryPatch,\n type RegistryEnvelope,\n} from './registry';\nimport { parseRegistryKey, registryListPrefix, registryObjectKey } from './object-registry-keys';\n\n/** Defensive cap on a single registry object read from storage, before allocation (rows are tiny; ~1 KB). */\nexport const MAX_ROW_BYTES = 1 * 1024 * 1024;\n/** Bounded retry for `delete`'s read\u2192tombstone under cross-process contention (converges; then fails typed). */\nconst MAX_DELETE_ATTEMPTS = 8;\n/** Bounded re-read when a store's pinned version is overwritten mid-read (see {@link ObjectVersionRaced}). */\nconst MAX_READ_ATTEMPTS = 8;\n/** In-flight reads per `list()` page \u2014 turns the serial N+1 into one list plus bounded parallel reads. */\nconst LIST_READ_CONCURRENCY = 16;\n\n/**\n * Raised by a store when the version it pinned for a read vanished before it could fetch the bytes \u2014\n * a concurrent writer overwrote or deleted the object in between.\n *\n * This is deliberately **neither** of the two things it superficially resembles. It is not absence: the row\n * is very likely still live, just one version further on. And it is not a write conflict: the caller may be\n * a read-only `get()`, which has nothing to conflict with. Either is an easy misreading for a store that reads a\n * row in two calls, as the Azure Blob store does, which is why the signal is part of the port rather than\n * left to each store's judgement. {@link ObjectStoreRegistry} answers it the only way that is correct for every\n * caller: it reads again.\n *\n * Stores whose read is atomic (S3 serves bytes and `ETag` from one `GetObject`, GCS bytes and `generation` from one GET) can never raise it.\n */\nexport class ObjectVersionRaced extends Error {\n constructor(objectKey: string) {\n super(`registry object was overwritten mid-read: ${objectKey}`);\n this.name = 'ObjectVersionRaced';\n }\n}\n\n/** One object as read, with the opaque version that fences a later conditional write. */\nexport interface ObjectRow {\n readonly bytes: Uint8Array;\n /** The store's version fence \u2014 an S3/Azure ETag, a GCS generation. Opaque to this class. */\n readonly version: string;\n}\n\n/**\n * The three I/O calls an object store must provide to host a registry. Everything else \u2014 the OCC counter,\n * tombstones, the delete retry, key parsing \u2014 is implemented once above.\n */\nexport interface ObjectRegistryStore {\n /** A label for error messages, e.g. `GCS` or `Azure Blob`. */\n readonly label: string;\n /**\n * Read one object, or `null` when it is absent.\n *\n * **`null` means the object does not exist** \u2014 nothing weaker. A store that reads in two round trips\n * (metadata for the version fence, then the bytes pinned to it) can find that version already gone; it\n * MUST report that with {@link ObjectVersionRaced} rather than `null` or {@link WriteConflictError}, and\n * the caller re-reads. Retryable faults throw {@link TransientError}.\n *\n * `bytes` and `version` MUST describe the **same** observation of the object: `version` is the fence a\n * later conditional write is conditioned on, so a pair drawn from two different versions would either\n * fail a write that should have succeeded or, worse, pass one that should not.\n */\n read(key: string): Promise<ObjectRow | null>;\n /**\n * Write one object under a precondition: `'absent'` means create-only, a version means\n * compare-and-swap against exactly that version. A lost race MUST throw {@link WriteConflictError}.\n *\n * A store must not replay this write after a lost response without telling the replay apart: the replay meets\n * the row it just wrote and its precondition fails, which would read as a lost race. Send it once, or store a\n * write id with the row and treat a conflict whose stored row carries it as success. Retryable faults throw\n * {@link TransientError}.\n */\n write(key: string, body: Uint8Array, expect: 'absent' | { version: string }): Promise<void>;\n /** Every object key under `prefix`, paginated internally. */\n listKeys(prefix: string): AsyncIterable<string>;\n}\n\nexport class ObjectStoreRegistry implements IRegistryDriver {\n constructor(\n private readonly store: ObjectRegistryStore,\n private readonly prefix: string | undefined,\n private readonly now: () => number,\n ) {}\n\n capabilities(): RegCaps {\n return { strongRead: true };\n }\n\n async get(ref: SegmentRef): Promise<RegistryRecord | null> {\n const current = await this.readRow(registryObjectKey(this.prefix, ref));\n return current && !current.env.deleted ? current.env.record : null;\n }\n\n async create(ref: SegmentRef, record: NewRegistryRecord): Promise<{ token: Token }> {\n validateNewRegistryRecord(record);\n const key = registryObjectKey(this.prefix, ref);\n const current = await this.readRow(key);\n if (current !== null && !current.env.deleted) {\n throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);\n }\n // Advance across a tombstone so a recreate never re-issues an old token (ABA-safe).\n const token = String(current ? registryCounterOf(current.env.record) + 1 : 0);\n const env: RegistryEnvelope = {\n deleted: false,\n record: recordFromNew(ref, record, this.now(), token),\n };\n // Create-only when truly absent; overwrite the tombstone under its version when recreating. Either way\n // a concurrent create loses the precondition and surfaces as a conflict.\n await this.putRow(key, env, current ? { version: current.version } : 'absent');\n return { token };\n }\n\n async compareAndSwap(\n ref: SegmentRef,\n expected: Token,\n patch: RegistryPatch,\n ): Promise<{ token: Token }> {\n validateRegistryPatch(patch);\n const key = registryObjectKey(this.prefix, ref);\n const current = await this.readRow(key);\n if (current === null || current.env.deleted || current.env.record.token !== expected) {\n throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);\n }\n const token = String(registryCounterOf(current.env.record) + 1);\n const env: RegistryEnvelope = {\n deleted: false,\n record: applyRegistryPatch(current.env.record, patch, this.now(), token),\n };\n // The version we read fences a concurrent writer between that read and this write.\n await this.putRow(key, env, { version: current.version });\n return { token };\n }\n\n async *list(namespace?: string): AsyncIterable<RegistryRecord> {\n const prefix = registryListPrefix(this.prefix, namespace);\n // One list plus bounded parallel reads. A serial await-in-loop made this O(N) sequential round trips.\n let page: string[] = [];\n const flush = async function* (\n this: ObjectStoreRegistry,\n keys: string[],\n ): AsyncIterable<RegistryRecord> {\n const rows = await mapWithConcurrency(keys, LIST_READ_CONCURRENCY, (key) =>\n this.readRow(key),\n );\n for (const current of rows) {\n if (current && !current.env.deleted) yield current.env.record;\n }\n }.bind(this);\n\n for await (const key of this.store.listKeys(prefix)) {\n if (parseRegistryKey(this.prefix, key) === null) continue; // stray or foreign object\n page.push(key);\n if (page.length >= LIST_READ_CONCURRENCY) {\n yield* flush(page);\n page = [];\n }\n }\n if (page.length > 0) yield* flush(page);\n }\n\n async delete(ref: SegmentRef, expected?: Token): Promise<void> {\n const key = registryObjectKey(this.prefix, ref);\n // Tombstone (advance the counter) rather than remove the object \u2014 keeps the token monotonic for\n // ABA-safety. Retry the read\u2192tombstone on a cross-process race; it converges, then fails typed rather\n // than silently leaving the row live.\n for (let attempt = 0; attempt < MAX_DELETE_ATTEMPTS; attempt++) {\n const current = await this.readRow(key);\n if (expected !== undefined) {\n // Fenced: the row must still carry the token the caller read. Re-checked on every attempt, so a row\n // that was swapped, or deleted and re-created, between two attempts is refused rather than tombstoned.\n if (current === null || current.env.deleted || current.env.record.token !== expected) {\n throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);\n }\n } else if (current === null || current.env.deleted) {\n return; // idempotent \u2014 already gone\n }\n const token = String(registryCounterOf(current.env.record) + 1);\n const env: RegistryEnvelope = {\n deleted: true,\n record: { ...current.env.record, token, updatedAt: this.now() },\n };\n try {\n await this.putRow(key, env, { version: current.version });\n return;\n } catch (err) {\n if (isWriteConflictError(err)) continue; // raced \u2014 re-read and re-tombstone\n throw err;\n }\n }\n throw new WriteConflictError(\n `registry delete: contention tombstoning \"${ref.segment}\" \u2014 retry`,\n );\n }\n\n /**\n * Read + parse a registry object, returning its envelope and version fence, or null if absent.\n *\n * A store that pins a version to read consistently ({@link ObjectVersionRaced}) loses that pin whenever a\n * concurrent writer lands mid-read. Re-reading is the whole answer \u2014 the next read simply observes the\n * newer version \u2014 and it is bounded so a row under permanent write saturation fails typed instead of\n * spinning. Only a store racing itself gets here; S3 never does.\n */\n private async readRow(\n objectKey: string,\n ): Promise<{ env: RegistryEnvelope; version: string } | null> {\n const row = await this.readRaced(objectKey);\n if (row === null) return null;\n if (row.bytes.length > MAX_ROW_BYTES) {\n throw new IntegrityError(\n `registry object ${row.bytes.length}B exceeds cap ${MAX_ROW_BYTES}B`,\n );\n }\n if (row.version.length === 0) {\n // The version is the OCC fence for CAS; a backend that omits it cannot be used safely as a registry.\n throw new IntegrityError(\n `registry object has no version fence (needed for compare-and-swap): ${objectKey}`,\n );\n }\n const env = parseRegistryEnvelope(new TextDecoder().decode(row.bytes), objectKey);\n return { env, version: row.version };\n }\n\n /** {@link ObjectRegistryStore.read}, retrying the bounded number of times a mid-read overwrite allows. */\n private async readRaced(objectKey: string): Promise<ObjectRow | null> {\n for (let attempt = 0; attempt < MAX_READ_ATTEMPTS; attempt++) {\n try {\n return await this.store.read(objectKey);\n } catch (err) {\n if (!(err instanceof ObjectVersionRaced)) throw err;\n }\n }\n throw new TransientError(\n `registry read: ${this.store.label} object \"${objectKey}\" was overwritten on every one of ` +\n `${MAX_READ_ATTEMPTS} attempts \u2014 retry`,\n );\n }\n\n /** Conditional write of an envelope. The store maps a lost precondition to {@link WriteConflictError}. */\n private async putRow(\n objectKey: string,\n env: RegistryEnvelope,\n expect: 'absent' | { version: string },\n ): Promise<void> {\n const body = new TextEncoder().encode(serializeRegistryEnvelope(env));\n if (body.length > MAX_ROW_BYTES) {\n // Belt-and-braces: the governance/DEK fields are already capped by validate*, so this cannot fire for\n // valid input \u2014 but never write a row that would later be unreadable.\n throw new ValidationError(`registry object ${body.length}B exceeds cap ${MAX_ROW_BYTES}B`);\n }\n await this.store.write(objectKey, body, expect);\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBA,IAAM,kBAAkB;AAGjB,SAAS,WAAW,QAAoC;AAC7D,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,UAAU,OAAO,QAAQ,cAAc,EAAE;AAC/C,SAAO,YAAY,KAAK,KAAK,GAAG,OAAO;AACzC;AAaO,SAAS,sBAAsB,QAAgD;AACpF,MAAI,WAAW,OAAW,QAAO;AACjC,aAAW,MAAM,QAAQ;AACvB,UAAM,OAAO,GAAG,WAAW,CAAC;AAE5B,QAAI,OAAO,MAAQ,SAAS,KAAM;AAChC,YAAM,IAAI,gBAAgB,4CAA4C;AAAA,IACxE;AAAA,EACF;AACA,MAAI,OAAO,SAAS,IAAI,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,4EAA4E,KAAK,UAAU,MAAM,CAAC;AAAA,IACpG;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,mBAAmB,MAAM;AAAA,EACrC,QAAQ;AACN,cAAU;AAAA,EACZ;AACA,aAAW,UAAU,YAAY,SAAS,CAAC,MAAM,IAAI,CAAC,QAAQ,OAAO,GAAG;AACtE,eAAW,WAAW,OAAO,MAAM,GAAG,GAAG;AACvC,UAAI,YAAY,OAAO,YAAY,MAAM;AACvC,cAAM,IAAI;AAAA,UACR,sDAAsD,KAAK,UAAU,MAAM,CAAC;AAAA,QAC9E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,eAAe,QAAoC;AACjE,SAAO,GAAG,WAAW,MAAM,CAAC;AAC9B;AAGO,SAAS,kBAAkB,QAA4B,KAAyB;AACrF,qBAAmB,GAAG;AACtB,SAAO,GAAG,eAAe,MAAM,CAAC,GAAG,iBAAiB,IAAI,SAAS,CAAC,IAAI,iBAAiB,IAAI,OAAO,CAAC,GAAG,eAAe;AACvH;AAGO,SAAS,mBAAmB,QAA4B,WAA4B;AACzF,QAAM,OAAO,eAAe,MAAM;AAClC,SAAO,cAAc,SAAY,OAAO,GAAG,IAAI,GAAG,iBAAiB,SAAS,CAAC;AAC/E;AAQO,SAAS,iBAAiB,QAA4B,WAAsC;AACjG,QAAM,OAAO,eAAe,MAAM;AAClC,MAAI,CAAC,UAAU,WAAW,IAAI,KAAK,CAAC,UAAU,SAAS,eAAe,EAAG,QAAO;AAChF,QAAM,OAAO,UAAU,MAAM,KAAK,QAAQ,UAAU,SAAS,gBAAgB,MAAM;AACnF,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,QAAM,iBAAiB,KAAK,MAAM,QAAQ,CAAC;AAC3C,MAAI,eAAe,WAAW,KAAK,eAAe,SAAS,GAAG,EAAG,QAAO;AACxE,QAAM,UAAU,kBAAkB,cAAc;AAChD,QAAM,YAAY,WAAW,oBAAoB,SAAY,kBAAkB,MAAM;AAGrF,MAAI,iBAAiB,OAAO,MAAM,eAAgB,QAAO;AACzD,MAAI,cAAc,UAAa,iBAAiB,SAAS,MAAM,OAAQ,QAAO;AAC9E,QAAM,MAAkB,EAAE,SAAS,UAAU;AAC7C,MAAI;AACF,uBAAmB,GAAG;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;ACvCO,IAAM,gBAAgB,IAAI,OAAO;AAExC,IAAM,sBAAsB;AAE5B,IAAM,oBAAoB;AAE1B,IAAM,wBAAwB;AAevB,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAY,WAAmB;AAC7B,UAAM,6CAA6C,SAAS,EAAE;AAC9D,SAAK,OAAO;AAAA,EACd;AACF;AA2CO,IAAM,sBAAN,MAAqD;AAAA,EAC1D,YACmB,OACA,QACA,KACjB;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAHgB;AAAA,EACA;AAAA,EACA;AAAA,EAGnB,eAAwB;AACtB,WAAO,EAAE,YAAY,KAAK;AAAA,EAC5B;AAAA,EAEA,MAAM,IAAI,KAAiD;AACzD,UAAM,UAAU,MAAM,KAAK,QAAQ,kBAAkB,KAAK,QAAQ,GAAG,CAAC;AACtE,WAAO,WAAW,CAAC,QAAQ,IAAI,UAAU,QAAQ,IAAI,SAAS;AAAA,EAChE;AAAA,EAEA,MAAM,OAAO,KAAiB,QAAsD;AAClF,8BAA0B,MAAM;AAChC,UAAM,MAAM,kBAAkB,KAAK,QAAQ,GAAG;AAC9C,UAAM,UAAU,MAAM,KAAK,QAAQ,GAAG;AACtC,QAAI,YAAY,QAAQ,CAAC,QAAQ,IAAI,SAAS;AAC5C,YAAM,IAAI,mBAAmB,2CAA2C,IAAI,OAAO,EAAE;AAAA,IACvF;AAEA,UAAM,QAAQ,OAAO,UAAU,kBAAkB,QAAQ,IAAI,MAAM,IAAI,IAAI,CAAC;AAC5E,UAAM,MAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,QAAQ,cAAc,KAAK,QAAQ,KAAK,IAAI,GAAG,KAAK;AAAA,IACtD;AAGA,UAAM,KAAK,OAAO,KAAK,KAAK,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC7E,WAAO,EAAE,MAAM;AAAA,EACjB;AAAA,EAEA,MAAM,eACJ,KACA,UACA,OAC2B;AAC3B,0BAAsB,KAAK;AAC3B,UAAM,MAAM,kBAAkB,KAAK,QAAQ,GAAG;AAC9C,UAAM,UAAU,MAAM,KAAK,QAAQ,GAAG;AACtC,QAAI,YAAY,QAAQ,QAAQ,IAAI,WAAW,QAAQ,IAAI,OAAO,UAAU,UAAU;AACpF,YAAM,IAAI,mBAAmB,uCAAuC,IAAI,OAAO,EAAE;AAAA,IACnF;AACA,UAAM,QAAQ,OAAO,kBAAkB,QAAQ,IAAI,MAAM,IAAI,CAAC;AAC9D,UAAM,MAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,QAAQ,mBAAmB,QAAQ,IAAI,QAAQ,OAAO,KAAK,IAAI,GAAG,KAAK;AAAA,IACzE;AAEA,UAAM,KAAK,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ,QAAQ,CAAC;AACxD,WAAO,EAAE,MAAM;AAAA,EACjB;AAAA,EAEA,OAAO,KAAK,WAAmD;AAC7D,UAAM,SAAS,mBAAmB,KAAK,QAAQ,SAAS;AAExD,QAAI,OAAiB,CAAC;AACtB,UAAM,QAAQ,iBAEZ,MAC+B;AAC/B,YAAM,OAAO,MAAM;AAAA,QAAmB;AAAA,QAAM;AAAA,QAAuB,CAAC,QAClE,KAAK,QAAQ,GAAG;AAAA,MAClB;AACA,iBAAW,WAAW,MAAM;AAC1B,YAAI,WAAW,CAAC,QAAQ,IAAI,QAAS,OAAM,QAAQ,IAAI;AAAA,MACzD;AAAA,IACF,EAAE,KAAK,IAAI;AAEX,qBAAiB,OAAO,KAAK,MAAM,SAAS,MAAM,GAAG;AACnD,UAAI,iBAAiB,KAAK,QAAQ,GAAG,MAAM,KAAM;AACjD,WAAK,KAAK,GAAG;AACb,UAAI,KAAK,UAAU,uBAAuB;AACxC,eAAO,MAAM,IAAI;AACjB,eAAO,CAAC;AAAA,MACV;AAAA,IACF;AACA,QAAI,KAAK,SAAS,EAAG,QAAO,MAAM,IAAI;AAAA,EACxC;AAAA,EAEA,MAAM,OAAO,KAAiB,UAAiC;AAC7D,UAAM,MAAM,kBAAkB,KAAK,QAAQ,GAAG;AAI9C,aAAS,UAAU,GAAG,UAAU,qBAAqB,WAAW;AAC9D,YAAM,UAAU,MAAM,KAAK,QAAQ,GAAG;AACtC,UAAI,aAAa,QAAW;AAG1B,YAAI,YAAY,QAAQ,QAAQ,IAAI,WAAW,QAAQ,IAAI,OAAO,UAAU,UAAU;AACpF,gBAAM,IAAI,mBAAmB,uCAAuC,IAAI,OAAO,EAAE;AAAA,QACnF;AAAA,MACF,WAAW,YAAY,QAAQ,QAAQ,IAAI,SAAS;AAClD;AAAA,MACF;AACA,YAAM,QAAQ,OAAO,kBAAkB,QAAQ,IAAI,MAAM,IAAI,CAAC;AAC9D,YAAM,MAAwB;AAAA,QAC5B,SAAS;AAAA,QACT,QAAQ,EAAE,GAAG,QAAQ,IAAI,QAAQ,OAAO,WAAW,KAAK,IAAI,EAAE;AAAA,MAChE;AACA,UAAI;AACF,cAAM,KAAK,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ,QAAQ,CAAC;AACxD;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,qBAAqB,GAAG,EAAG;AAC/B,cAAM;AAAA,MACR;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,4CAA4C,IAAI,OAAO;AAAA,IACzD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,QACZ,WAC4D;AAC5D,UAAM,MAAM,MAAM,KAAK,UAAU,SAAS;AAC1C,QAAI,QAAQ,KAAM,QAAO;AACzB,QAAI,IAAI,MAAM,SAAS,eAAe;AACpC,YAAM,IAAI;AAAA,QACR,mBAAmB,IAAI,MAAM,MAAM,iBAAiB,aAAa;AAAA,MACnE;AAAA,IACF;AACA,QAAI,IAAI,QAAQ,WAAW,GAAG;AAE5B,YAAM,IAAI;AAAA,QACR,uEAAuE,SAAS;AAAA,MAClF;AAAA,IACF;AACA,UAAM,MAAM,sBAAsB,IAAI,YAAY,EAAE,OAAO,IAAI,KAAK,GAAG,SAAS;AAChF,WAAO,EAAE,KAAK,SAAS,IAAI,QAAQ;AAAA,EACrC;AAAA;AAAA,EAGA,MAAc,UAAU,WAA8C;AACpE,aAAS,UAAU,GAAG,UAAU,mBAAmB,WAAW;AAC5D,UAAI;AACF,eAAO,MAAM,KAAK,MAAM,KAAK,SAAS;AAAA,MACxC,SAAS,KAAK;AACZ,YAAI,EAAE,eAAe,oBAAqB,OAAM;AAAA,MAClD;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,kBAAkB,KAAK,MAAM,KAAK,YAAY,SAAS,qCAClD,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,OACZ,WACA,KACA,QACe;AACf,UAAM,OAAO,IAAI,YAAY,EAAE,OAAO,0BAA0B,GAAG,CAAC;AACpE,QAAI,KAAK,SAAS,eAAe;AAG/B,YAAM,IAAI,gBAAgB,mBAAmB,KAAK,MAAM,iBAAiB,aAAa,GAAG;AAAA,IAC3F;AACA,UAAM,KAAK,MAAM,MAAM,WAAW,MAAM,MAAM;AAAA,EAChD;AACF;",
4
+ "sourcesContent": ["/**\n * The registry object layout, shared by every object-store registry (S3, GCS, Azure Blob).\n *\n * One tiny JSON object per segment at `<prefix>registry/<ns>/<segment>.reg`, **registry-first** \u2014 separate\n * from the namespace-first storage layout (`<prefix><ns>/segments/\u2026`) \u2014 so discovery is a single list over\n * `registry/` for all namespaces, or `registry/<ns>/` for one, never entangled with the `.crbm` payloads.\n *\n * It lives here rather than in a driver because all three object stores encode names the same way\n * (`encodeNameForKey`, `namespaceKeyPart`) and build byte-identical keys. Three copies of this would be\n * three chances for one cloud's layout to drift from the others \u2014 and a drifted key is not a visible bug, it\n * is a segment that quietly cannot be found by a store pointed at the same bucket.\n */\nimport { ValidationError } from '@/core/errors';\nimport { validateSegmentRef } from '@/core/validate';\nimport type { SegmentRef } from '@/core/ports';\nimport { DEFAULT_NAMESPACE, decodeNameFromKey, encodeNameForKey, namespaceKeyPart } from './keys';\n\nconst REGISTRY_SUFFIX = '.reg';\n\n/** Normalize an optional caller prefix to either `''` or `trimmed/` (no leading/trailing slashes). */\nexport function prefixPart(prefix: string | undefined): string {\n if (prefix === undefined) return '';\n const trimmed = prefix.replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `${trimmed}/`;\n}\n\n/**\n * Validate a caller-supplied key prefix (trusted config, but make it a real containment boundary): reject\n * control characters and `.`/`..` path segments so a prefix cannot traverse out of its intended space.\n * Returns it unchanged \u2014 the key builders normalize slashes.\n *\n * The traversal check covers three spellings, because the tools that walk these buckets do. A bare `..` is\n * the obvious one. A **backslash** segment matters on ADLS Gen2 and to Windows-side tooling, which treat it\n * as a separator this code otherwise would not. And a percent-encoded `..` matters because `gsutil`,\n * `s3fs`, `gcsfuse` and `azcopy` decode a key on their way to a local path \u2014 the same reasoning\n * `name-codec.ts` already applies to segment names, which a prefix has no reason to be exempt from.\n */\nexport function normalizeObjectPrefix(prefix: string | undefined): string | undefined {\n if (prefix === undefined) return undefined;\n for (const ch of prefix) {\n const code = ch.charCodeAt(0);\n // C0 plus DEL: a bare `< 0x20` lets U+007F through, and it is as unprintable as the rest.\n if (code < 0x20 || code === 0x7f) {\n throw new ValidationError('prefix must not contain control characters');\n }\n }\n if (prefix.includes('\\\\')) {\n throw new ValidationError(\n `prefix must not contain backslashes (a path separator on some backends): ${JSON.stringify(prefix)}`,\n );\n }\n // Check the decoded spelling too, so an encoded `..` cannot slip past the literal comparison.\n let decoded: string;\n try {\n decoded = decodeURIComponent(prefix);\n } catch {\n decoded = prefix; // not valid percent-encoding; the literal check still applies\n }\n for (const source of decoded === prefix ? [prefix] : [prefix, decoded]) {\n for (const segment of source.split('/')) {\n if (segment === '.' || segment === '..') {\n throw new ValidationError(\n `prefix must not contain \".\" or \"..\" path segments: ${JSON.stringify(prefix)}`,\n );\n }\n }\n }\n return prefix;\n}\n\n/** The key prefix under which every registry object lives: `<prefix>registry/`. */\nexport function registryPrefix(prefix: string | undefined): string {\n return `${prefixPart(prefix)}registry/`;\n}\n\n/** The full key of one segment's registry object: `<prefix>registry/<ns>/<segment>.reg`. */\nexport function registryObjectKey(prefix: string | undefined, ref: SegmentRef): string {\n validateSegmentRef(ref);\n return `${registryPrefix(prefix)}${namespaceKeyPart(ref.namespace)}/${encodeNameForKey(ref.segment)}${REGISTRY_SUFFIX}`;\n}\n\n/** The list prefix for discovery: registry-wide, or scoped to one namespace. */\nexport function registryListPrefix(prefix: string | undefined, namespace?: string): string {\n const base = registryPrefix(prefix);\n return namespace === undefined ? base : `${base}${namespaceKeyPart(namespace)}/`;\n}\n\n/**\n * Parse a `<prefix>registry/<ns>/<segment>.reg` key back to its {@link SegmentRef}, or `null` if it does not\n * match \u2014 a stray or foreign object under the prefix, or one whose parsed ref fails the round-trip check.\n * `_default` maps back to the absent namespace. A name is percent-encoded on the way in, so no encoded name\n * can contain `/` and the split stays unambiguous whatever the caller named their segment.\n */\nexport function parseRegistryKey(prefix: string | undefined, objectKey: string): SegmentRef | null {\n const base = registryPrefix(prefix);\n if (!objectKey.startsWith(base) || !objectKey.endsWith(REGISTRY_SUFFIX)) return null;\n const rest = objectKey.slice(base.length, objectKey.length - REGISTRY_SUFFIX.length);\n const slash = rest.indexOf('/');\n if (slash < 0) return null;\n const nsPart = rest.slice(0, slash);\n const encodedSegment = rest.slice(slash + 1);\n if (encodedSegment.length === 0 || encodedSegment.includes('/')) return null;\n const segment = decodeNameFromKey(encodedSegment);\n const namespace = nsPart === DEFAULT_NAMESPACE ? undefined : decodeNameFromKey(nsPart);\n // The encoding must ROUND-TRIP, not merely decode: a foreign object placed under our prefix could spell a\n // name two ways, and reporting both would hand a sweep one segment under two identities.\n if (encodeNameForKey(segment) !== encodedSegment) return null;\n if (namespace !== undefined && encodeNameForKey(namespace) !== nsPart) return null;\n const ref: SegmentRef = { segment, namespace };\n try {\n validateSegmentRef(ref); // reject a hostile/foreign key that isn't a valid ref\n } catch {\n return null;\n }\n return ref;\n}\n", "/**\n * `ObjectStoreRegistry` \u2014 an {@link IRegistryDriver} over any object store with conditional writes.\n *\n * Lets a **read-mostly deployment run on one bucket alone** \u2014 storage `.crbm` generations plus the registry in\n * the same place, with no separate database. One tiny JSON object per segment at\n * `<prefix>registry/<ns>/<segment>.reg` holding the `{ deleted, record }` envelope (the same shape LocalFs\n * persists). The OCC token is a random incarnation id drawn when the row is created, a counter advanced on every\n * mutation, and a random part drawn for every write, so with overwhelming probability a deleted-then-recreated row, or\n * one restored from a backup, never re-issues an old token (ABA-safe) \u2014 identical semantics to the LocalFs registry, so it passes the same\n * conformance suite.\n *\n * **`delete` removes a row for good where the store vouches for a conditional delete** (`If-Match: <etag>`,\n * `ifGenerationMatch: <generation>`), and only a row whose token carries an incarnation id: a re-create draws a new\n * incarnation, so nothing of the old row is needed to keep its tokens apart, and a full `list()` no longer reads a\n * row for every name it ever held. Every other row is **tombstoned**: the object stays, its counter advanced, and a\n * re-create carries the counter on. A row a release before 0.12 wrote has a bare decimal token and is always\n * tombstoned, since a process still on that release, re-creating the name over nothing, would start its counter at 0\n * again and issue the old row's tokens.\n *\n * **The atomic swap is offloaded to the store's conditional writes.** `create` writes only if absent (or\n * over a tombstone under its version), and `compareAndSwap`/`delete` write (or delete) only if the object still\n * carries the version we read \u2014 so a concurrent writer between our read and our write loses, and the loss surfaces\n * as {@link WriteConflictError}. No in-process lock is needed (unlike LocalFs): the precondition fences\n * writers *across processes*.\n *\n * **Why this is shared rather than written per cloud.** Every object store worth using has the same two\n * primitives under different names \u2014 S3 `If-None-Match: *` / `If-Match: <etag>`, GCS `ifGenerationMatch: 0`\n * / `ifGenerationMatch: <generation>`, Azure `If-None-Match: *` / `If-Match: <etag>` \u2014 so the only thing\n * that differs is a few I/O calls, which is what {@link ObjectRegistryStore} abstracts. The parts that are\n * genuinely hard (the ABA-safe token, the delete and its tombstone, the bounded delete retry, the round-trip key check)\n * exist once. Three copies would be three chances for one cloud to drift, and a drifted registry is not a\n * visible bug \u2014 it is a lost `currentGen` swap.\n *\n * **Deployment requirements** (a backend that violates these silently corrupts the registry):\n * - The store **must honor the conditional write**. One that accepts the precondition header and ignores it\n * degrades compare-and-swap to last-write-wins \u2192 lost `currentGen` swaps.\n * - Reads must be **strongly consistent** \u2014 true of S3 (since 2020), GCS and Azure Blob \u2014 since the registry\n * advertises `strongRead` and generation resolution depends on it.\n * - **Do not apply a lifecycle-expiration rule to the `registry/` prefix that expires a current version.**\n * Expiring a live row loses its pointer. A tombstone `delete` left keeps the counter for a re-create; one expired\n * by a rule is no worse than a row removed for good, since a recreate draws a fresh incarnation and its tokens are\n * new but for a collision of probability 2^-128 per pair of incarnations, but a rule cannot tell a tombstone from a\n * live row.\n * - **A store's conditional delete must be honoured by its backend** before the store sets `conditionalDelete`.\n * One that ignores the precondition lets two sweepers and a re-create delete a live row.\n *\n * **`list()` is fail-closed, and one bad object stops it for everyone.** An object under the `registry/`\n * prefix whose key parses but whose body does not aborts the whole enumeration \u2014 every namespace, not just\n * the affected segment \u2014 and it stays that way until an operator removes the object. The error names the\n * offending key.\n *\n * That is the deliberate choice, and it is the *safer* one rather than the more convenient one. Skipping the\n * unreadable row would keep discovery running, but `list()` is what tells orphan generation collection which\n * segments exist: a row missing from the enumeration makes its storage `.crbm` generations look unreferenced,\n * and the next GC pass would delete them. Silently skipping turns a parse error into data loss. Refusing to\n * enumerate keeps every sweep off a set it cannot vouch for (invariant 5 \u2014 bytes from the tier are\n * untrusted). **Recovery:** read the named object, and either restore a valid envelope or delete it; nothing\n * else is damaged, and discovery resumes on its own.\n */\nimport {\n IntegrityError,\n TransientError,\n ValidationError,\n WriteConflictError,\n isWriteConflictError,\n} from '@/core/errors';\nimport type {\n IRegistryDriver,\n NewRegistryRecord,\n RegCaps,\n RegistryPatch,\n RegistryRecord,\n SegmentRef,\n Token,\n} from '@/core/ports';\nimport type { Entropy } from '@/core/determinism';\nimport { mapWithConcurrency } from '@/core/concurrency';\nimport {\n applyRegistryPatch,\n incarnationOf,\n newIncarnationToken,\n nextRegistryToken,\n parseRegistryEnvelope,\n recordFromNew,\n serializeRegistryEnvelope,\n validateNewRegistryRecord,\n validateRegistryPatch,\n type RegistryEnvelope,\n} from './registry';\nimport { entropyIsAvailable, webCryptoEntropy } from './entropy';\nimport { parseRegistryKey, registryListPrefix, registryObjectKey } from './object-registry-keys';\n\n/** Defensive cap on a single registry object read from storage, before allocation (rows are tiny; ~1 KB). */\nexport const MAX_ROW_BYTES = 1 * 1024 * 1024;\n/** Bounded retry for `delete`'s read then delete or tombstone, under cross-process contention (then fails typed). */\nconst MAX_DELETE_ATTEMPTS = 8;\n/** Bounded re-read when a store's pinned version is overwritten mid-read (see {@link ObjectVersionRaced}). */\nconst MAX_READ_ATTEMPTS = 8;\n/** In-flight reads per `list()` page \u2014 turns the serial N+1 into one list plus bounded parallel reads. */\nconst LIST_READ_CONCURRENCY = 16;\n\n/**\n * Raised by a store when the version it pinned for a read vanished before it could fetch the bytes \u2014\n * a concurrent writer overwrote or deleted the object in between.\n *\n * This is deliberately **neither** of the two things it superficially resembles. It is not absence: the row\n * is very likely still live, just one version further on. And it is not a write conflict: the caller may be\n * a read-only `get()`, which has nothing to conflict with. Either is an easy misreading for a store that reads a\n * row in two calls, metadata and then bytes pinned to it, which is why the signal is part of the port rather than\n * left to each store's judgement. {@link ObjectStoreRegistry} answers it the only way that is correct for every\n * caller: it reads again.\n *\n * Stores whose read is atomic can never raise it, and every shipped store's is: S3 serves bytes and `ETag` from one\n * `GetObject`, GCS bytes and `generation` from one GET, and Azure Blob bytes and `ETag` from one GET.\n */\nexport class ObjectVersionRaced extends Error {\n constructor(objectKey: string) {\n super(`registry object was overwritten mid-read: ${objectKey}`);\n this.name = 'ObjectVersionRaced';\n }\n}\n\n/** One object as read, with the opaque version that fences a later conditional write. */\nexport interface ObjectRow {\n readonly bytes: Uint8Array;\n /** The store's version fence \u2014 an S3/Azure ETag, a GCS generation. Opaque to this class. */\n readonly version: string;\n}\n\n/**\n * The I/O calls an object store must provide to host a registry: three, and an optional conditional delete. Everything\n * else \u2014 the OCC token, tombstones, the delete retry, key parsing \u2014 is implemented once above.\n */\nexport interface ObjectRegistryStore {\n /** A label for error messages, e.g. `GCS` or `Azure Blob`. */\n readonly label: string;\n /**\n * Read one object, or `null` when it is absent.\n *\n * **`null` means the object does not exist** \u2014 nothing weaker. A store that reads in two round trips\n * (metadata for the version fence, then the bytes pinned to it) can find that version already gone; it\n * MUST report that with {@link ObjectVersionRaced} rather than `null` or {@link WriteConflictError}, and\n * the caller re-reads. Retryable faults throw {@link TransientError}.\n *\n * `bytes` and `version` MUST describe the **same** observation of the object: `version` is the fence a\n * later conditional write is conditioned on, so a pair drawn from two different versions would either\n * fail a write that should have succeeded or, worse, pass one that should not.\n */\n read(key: string): Promise<ObjectRow | null>;\n /**\n * Write one object under a precondition: `'absent'` means create-only, a version means\n * compare-and-swap against exactly that version. A lost race MUST throw {@link WriteConflictError}.\n *\n * A store must not replay this write after a lost response without telling the replay apart: the replay meets\n * the row it just wrote and its precondition fails, which would read as a lost race. Send it once, or store a\n * write id with the row and treat a conflict whose stored row carries it as success. Retryable faults throw\n * {@link TransientError}.\n */\n write(key: string, body: Uint8Array, expect: 'absent' | { version: string }): Promise<void>;\n /** Every object key under `prefix`, paginated internally. */\n listKeys(prefix: string): AsyncIterable<string>;\n /**\n * Optional: delete one object, and only while it is still at exactly `expect.version`, by a precondition the\n * backend applies (`If-Match: <etag>`, `ifGenerationMatch: <generation>`). An object that was overwritten, or is\n * gone, MUST throw {@link WriteConflictError} and delete nothing. Retryable faults throw {@link TransientError}.\n *\n * Sending it again after a lost response is safe, unlike a write: the precondition names one version, so a second\n * copy can remove nothing the first was not allowed to. A copy that meets its own landed delete fails the\n * precondition and reports a conflict; the registry re-reads, and finds the row gone.\n *\n * The registry calls it only when {@link conditionalDelete} is `true`.\n */\n delete?(key: string, expect: { version: string }): Promise<void>;\n /**\n * Whether {@link delete} may be relied on: `true` only where the backend is known to apply the precondition. A\n * backend that accepts the header and ignores it would let a delete remove a row written after the delete read it,\n * a live one included. Absent or `false`, the registry never calls `delete` and tombstones every row instead.\n */\n readonly conditionalDelete?: boolean;\n}\n\nexport class ObjectStoreRegistry implements IRegistryDriver {\n /**\n * `entropy` draws the random parts of every token: a new row's incarnation id, and each write's own part. It\n * defaults to the platform's Web Crypto. Inject one only to make a test replayable: a seeded source in production\n * gives every process the same ids.\n */\n constructor(\n private readonly store: ObjectRegistryStore,\n private readonly prefix: string | undefined,\n private readonly now: () => number,\n private readonly entropy: Entropy = webCryptoEntropy,\n ) {}\n\n capabilities(): RegCaps {\n const conditionalDelete = this.removesRows();\n return entropyIsAvailable(this.entropy)\n ? { strongRead: true, conditionalDelete }\n : { strongRead: true, canWrite: false, conditionalDelete };\n }\n\n /** Whether a delete removes a row born with an incarnation id: the store has a delete, and vouches for it. */\n private removesRows(): boolean {\n return this.store.conditionalDelete === true && typeof this.store.delete === 'function';\n }\n\n async get(ref: SegmentRef): Promise<RegistryRecord | null> {\n const current = await this.readRow(registryObjectKey(this.prefix, ref));\n return current && !current.env.deleted ? current.env.record : null;\n }\n\n async create(ref: SegmentRef, record: NewRegistryRecord): Promise<{ token: Token }> {\n const checked = validateNewRegistryRecord(record);\n const key = registryObjectKey(this.prefix, ref);\n const current = await this.readRow(key);\n if (current !== null && !current.env.deleted) {\n throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);\n }\n // A new incarnation, whose counter continues across a tombstone, so with overwhelming probability a recreate never re-issues an old token.\n const token = newIncarnationToken(this.entropy, current?.env.record);\n const env: RegistryEnvelope = {\n deleted: false,\n record: recordFromNew(ref, checked, this.now(), token),\n };\n // Create-only when truly absent; overwrite the tombstone under its version when recreating. Either way\n // a concurrent create loses the precondition and surfaces as a conflict.\n await this.putRow(key, env, current ? { version: current.version } : 'absent');\n return { token };\n }\n\n async compareAndSwap(\n ref: SegmentRef,\n expected: Token,\n patch: RegistryPatch,\n ): Promise<{ token: Token }> {\n const checked = validateRegistryPatch(patch);\n const key = registryObjectKey(this.prefix, ref);\n const current = await this.readRow(key);\n if (current === null || current.env.deleted || current.env.record.token !== expected) {\n throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);\n }\n const token = nextRegistryToken(current.env.record, this.entropy);\n const env: RegistryEnvelope = {\n deleted: false,\n record: applyRegistryPatch(current.env.record, checked, this.now(), token),\n };\n // The version we read fences a concurrent writer between that read and this write.\n await this.putRow(key, env, { version: current.version });\n return { token };\n }\n\n async *list(namespace?: string): AsyncIterable<RegistryRecord> {\n const prefix = registryListPrefix(this.prefix, namespace);\n // One list plus bounded parallel reads. A serial await-in-loop made this O(N) sequential round trips.\n let page: string[] = [];\n const flush = async function* (\n this: ObjectStoreRegistry,\n keys: string[],\n ): AsyncIterable<RegistryRecord> {\n const rows = await mapWithConcurrency(keys, LIST_READ_CONCURRENCY, (key) =>\n this.readRow(key),\n );\n for (const current of rows) {\n if (current && !current.env.deleted) yield current.env.record;\n }\n }.bind(this);\n\n for await (const key of this.store.listKeys(prefix)) {\n if (parseRegistryKey(this.prefix, key) === null) continue; // stray or foreign object\n page.push(key);\n if (page.length >= LIST_READ_CONCURRENCY) {\n yield* flush(page);\n page = [];\n }\n }\n if (page.length > 0) yield* flush(page);\n }\n\n async delete(ref: SegmentRef, expected?: Token): Promise<void> {\n const key = registryObjectKey(this.prefix, ref);\n // A row born with an incarnation id is removed from the store, by a delete the store applies only while the\n // object is still the version read here: a later create draws a new incarnation, so nothing of this row is needed\n // to keep its tokens apart. Any other row is tombstoned (the counter advanced, the object kept), so a re-create\n // carries the counter on. Retry the read and the write on a cross-process race; it converges, then fails typed\n // rather than silently leaving the row live.\n for (let attempt = 0; attempt < MAX_DELETE_ATTEMPTS; attempt++) {\n const current = await this.readRow(key);\n if (expected !== undefined) {\n // Fenced: the row must still carry the token the caller read. Re-checked on every attempt, so a row\n // that was swapped, or deleted and re-created, between two attempts is refused rather than tombstoned.\n if (current === null || current.env.deleted || current.env.record.token !== expected) {\n throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);\n }\n } else if (current === null || current.env.deleted) {\n return; // idempotent \u2014 already gone\n }\n try {\n const remove = this.removesRows() ? this.store.delete : undefined;\n if (remove !== undefined && incarnationOf(current.env.record.token) !== undefined) {\n // The version read above is the row the caller's token names, so this removes that row and no later one.\n await remove.call(this.store, key, { version: current.version });\n return;\n }\n const token = nextRegistryToken(current.env.record, this.entropy);\n const env: RegistryEnvelope = {\n deleted: true,\n record: { ...current.env.record, token, updatedAt: this.now() },\n };\n await this.putRow(key, env, { version: current.version });\n return;\n } catch (err) {\n if (isWriteConflictError(err)) continue; // raced \u2014 re-read and try again\n throw err;\n }\n }\n throw new WriteConflictError(`registry delete: contention deleting \"${ref.segment}\" \u2014 retry`);\n }\n\n /**\n * Read + parse a registry object, returning its envelope and version fence, or null if absent.\n *\n * A store that pins a version to read consistently ({@link ObjectVersionRaced}) loses that pin whenever a\n * concurrent writer lands mid-read. Re-reading is the whole answer \u2014 the next read simply observes the\n * newer version \u2014 and it is bounded so a row under permanent write saturation fails typed instead of\n * spinning. Only a store that reads in two calls gets here; none of the shipped stores does.\n */\n private async readRow(\n objectKey: string,\n ): Promise<{ env: RegistryEnvelope; version: string } | null> {\n const row = await this.readRaced(objectKey);\n if (row === null) return null;\n if (row.bytes.length > MAX_ROW_BYTES) {\n throw new IntegrityError(\n `registry object ${row.bytes.length}B exceeds cap ${MAX_ROW_BYTES}B`,\n );\n }\n if (row.version.length === 0) {\n // The version is the OCC fence for CAS; a backend that omits it cannot be used safely as a registry.\n throw new IntegrityError(\n `registry object has no version fence (needed for compare-and-swap): ${objectKey}`,\n );\n }\n const env = parseRegistryEnvelope(new TextDecoder().decode(row.bytes), objectKey);\n return { env, version: row.version };\n }\n\n /** {@link ObjectRegistryStore.read}, retrying the bounded number of times a mid-read overwrite allows. */\n private async readRaced(objectKey: string): Promise<ObjectRow | null> {\n for (let attempt = 0; attempt < MAX_READ_ATTEMPTS; attempt++) {\n try {\n return await this.store.read(objectKey);\n } catch (err) {\n if (!(err instanceof ObjectVersionRaced)) throw err;\n }\n }\n throw new TransientError(\n `registry read: ${this.store.label} object \"${objectKey}\" was overwritten on every one of ` +\n `${MAX_READ_ATTEMPTS} attempts \u2014 retry`,\n );\n }\n\n /** Conditional write of an envelope. The store maps a lost precondition to {@link WriteConflictError}. */\n private async putRow(\n objectKey: string,\n env: RegistryEnvelope,\n expect: 'absent' | { version: string },\n ): Promise<void> {\n const body = new TextEncoder().encode(serializeRegistryEnvelope(env));\n if (body.length > MAX_ROW_BYTES) {\n // Belt-and-braces: the governance/DEK fields are already capped by validate*, so this cannot fire for\n // valid input \u2014 but never write a row that would later be unreadable.\n throw new ValidationError(`registry object ${body.length}B exceeds cap ${MAX_ROW_BYTES}B`);\n }\n await this.store.write(objectKey, body, expect);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,5 @@
1
+ import type { Entropy } from '../../core/determinism.js';
2
+ /** Draws from the platform's Web Crypto; `UnsupportedError` on a runtime without it. */
3
+ export declare const webCryptoEntropy: Entropy;
4
+ /** Whether `entropy` can draw here: an injected source always, the default only where Web Crypto exists. */
5
+ export declare function entropyIsAvailable(entropy: Entropy): boolean;