@cloudbitmaps/core 0.22.0 → 0.23.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.
- package/README.md +8 -4
- package/dist/chunk-7GT632UF.js +641 -0
- package/dist/chunk-7GT632UF.js.map +7 -0
- package/dist/chunk-CGILDXGP.js +73 -0
- package/dist/chunk-CGILDXGP.js.map +7 -0
- package/dist/chunk-GKHERVVQ.js +121 -0
- package/dist/chunk-GKHERVVQ.js.map +7 -0
- package/dist/{chunk-OSU7W2Y3.js → chunk-MVUD4R72.js} +200 -756
- package/dist/chunk-MVUD4R72.js.map +7 -0
- package/dist/core/audit.d.ts +53 -15
- package/dist/core/bit-route.d.ts +2 -0
- package/dist/core/blob.d.ts +4 -0
- package/dist/core/budget.d.ts +8 -3
- package/dist/core/check.d.ts +49 -0
- package/dist/core/combine-many.d.ts +11 -0
- package/dist/core/consistency.d.ts +11 -2
- package/dist/core/cooperative.d.ts +7 -0
- package/dist/core/crbm-storage-source.d.ts +7 -4
- package/dist/core/crypto.d.ts +5 -0
- package/dist/core/discover.d.ts +5 -3
- package/dist/core/engine.d.ts +4 -4
- package/dist/core/erase-id.d.ts +35 -12
- package/dist/core/erasure.d.ts +61 -27
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/leases.d.ts +2 -0
- package/dist/core/load-input.d.ts +48 -0
- package/dist/core/load.d.ts +23 -2
- package/dist/core/metrics.d.ts +21 -0
- package/dist/core/pinned-storage-source.d.ts +27 -8
- package/dist/core/pointer-id.d.ts +9 -0
- package/dist/core/ports.d.ts +100 -7
- package/dist/core/preflight.d.ts +80 -0
- package/dist/core/retention-sweep.d.ts +60 -15
- package/dist/core/retention.d.ts +17 -6
- package/dist/core/retry.d.ts +1 -2
- package/dist/core/rollback.d.ts +22 -12
- package/dist/core/validate.d.ts +33 -2
- package/dist/driver-kit.d.ts +3 -3
- package/dist/driver-kit.js +61 -20
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +24 -1
- package/dist/drivers/_shared/options.d.ts +15 -7
- package/dist/drivers/backends.d.ts +17 -2
- package/dist/drivers/crypto.d.ts +17 -2
- package/dist/export/index.d.ts +16 -2
- package/dist/index.d.ts +10 -9
- package/dist/index.js +1743 -1701
- package/dist/index.js.map +4 -4
- package/dist/node.d.ts +14 -0
- package/dist/node.js +1064 -0
- package/dist/node.js.map +7 -0
- package/package.json +11 -2
- package/dist/chunk-OSU7W2Y3.js.map +0 -7
package/dist/core/validate.d.ts
CHANGED
|
@@ -16,6 +16,19 @@ export declare function isWellFormedString(value: string): boolean;
|
|
|
16
16
|
* same way, and a second copy of that rule is how the two drifted apart before.
|
|
17
17
|
*/
|
|
18
18
|
export declare function encodedNameLength(value: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* The keys a segment ref takes: its address, and the two a `segments()` row also carries so a listing row can be
|
|
21
|
+
* passed straight back. `discover.ts` holds this list to `SegmentInfo` at compile time, so a field added there is a
|
|
22
|
+
* type error until it is listed here.
|
|
23
|
+
*/
|
|
24
|
+
export declare const REF_KEYS: readonly ["segment", "namespace", "currentGen", "status"];
|
|
25
|
+
/**
|
|
26
|
+
* Throws `ValidationError` unless `ref` is an object whose `segment` and, when given, `namespace` are valid names, and
|
|
27
|
+
* which holds no other key: a misspelt `namespace` would otherwise address the segment of that name in the default
|
|
28
|
+
* namespace. The keys it takes are a port's: `segment`, `namespace`, a `GenKey`'s `generation`, a `ChunkRef`'s
|
|
29
|
+
* `chunkKey`, and a registry row's own fields and a `segments()` row's, so a row a driver listed can be passed back.
|
|
30
|
+
* It does not refuse a reserved namespace, which a driver must be able to store.
|
|
31
|
+
*/
|
|
19
32
|
export declare function validateSegmentRef(ref: SegmentRef): void;
|
|
20
33
|
/**
|
|
21
34
|
* The namespace prefix the library keeps for its own bookkeeping: the due index stores one pointer row per
|
|
@@ -40,8 +53,26 @@ export declare function isReservedNamespace(namespace: string | undefined): bool
|
|
|
40
53
|
* rows live in the reserved namespace and go through the drivers.
|
|
41
54
|
*/
|
|
42
55
|
export declare function validateUserNamespace(namespace: string): void;
|
|
43
|
-
/**
|
|
44
|
-
|
|
56
|
+
/**
|
|
57
|
+
* The keys a driver port takes beyond a ref's own. The store hands its ports a generation key (a ref and a
|
|
58
|
+
* `generation`) or a chunk ref (a ref and a `chunkKey`) wherever they name a segment, and an operator repairing a
|
|
59
|
+
* registry hands them a row from `registry.list()` or `get()`, whose fields are fixed and listed here. A test holds
|
|
60
|
+
* the row fields to `RegistryRecord`'s.
|
|
61
|
+
*/
|
|
62
|
+
export declare const PORT_KEYS: readonly string[];
|
|
63
|
+
/**
|
|
64
|
+
* Refuse a segment ref that holds a key it does not take, naming each such key, and a value that is a segment
|
|
65
|
+
* handle or an array rather than a ref. A misspelt `namespace` would otherwise address the segment of that name in
|
|
66
|
+
* the default namespace, and a drop, a rollback or a crypto-shred would act on the wrong segment. Only the ref's own
|
|
67
|
+
* enumerable string keys are read, one whose value is `undefined` included. `verb` names the call in the message.
|
|
68
|
+
*/
|
|
69
|
+
export declare function refuseUnknownRefKeys(ref: object, verb: string, extraKeys?: readonly string[]): void;
|
|
70
|
+
/**
|
|
71
|
+
* Validate a segment ref an application hands in: the name rules, and a namespace outside the reserved prefix. With
|
|
72
|
+
* `verb`, the call a caller made, it also refuses a key a ref does not take, beyond `extraKeys`; leave `verb` out only
|
|
73
|
+
* for an internal key that extends a ref with keys of its own.
|
|
74
|
+
*/
|
|
75
|
+
export declare function validateUserRef(ref: SegmentRef, verb?: string, extraKeys?: readonly string[]): void;
|
|
45
76
|
/**
|
|
46
77
|
* Validate the keys of a `getChunks` stream: in ascending order, a key may repeat. Checked before anything is
|
|
47
78
|
* resolved or read, so a caller's mistake costs no request.
|
package/dist/driver-kit.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, StorageDeleteOptions, TailRead, } from './core/ports.js';
|
|
1
|
+
export type { GenKey, IRegistryDriver, IStorageDriver, LifecycleVerdict, SegmentRef, StorageBackend, StorageCaps, StorageDeleteOptions, TailRead, } from './core/ports.js';
|
|
2
2
|
export { brandAsBackend, STORAGE_BACKEND } from './core/ports.js';
|
|
3
3
|
export type { ClearRegistrySummary, GenerationMetadata, GovernanceMeta, KnownGeneration, LeaseEntry, NewRegistryRecord, RegCaps, RegistryPatch, RegistryRecord, RegistryStatus, RegistrySummary, RegistryWriteOptions, SealedRegistrySummary, Token, } from './core/ports.js';
|
|
4
4
|
export type { ChunkRead, ChunkRef, GenerationSummary, ReadChunksOptions, SegmentSize, StorageChunkSource, } from './core/ports.js';
|
|
5
5
|
export type { Aead, AeadSealed, IKeystore, WrappedDek } from './core/crypto.js';
|
|
6
6
|
export type { Entropy } from './core/determinism.js';
|
|
7
7
|
export type { IMetricsSink, MetricEvent, MetricOpName } from './core/metrics.js';
|
|
8
|
-
export { CloudRoaringError, errorKind, IntegrityError, NotFoundError, TransientError, ValidationError, WriteConflictError, } from './core/errors.js';
|
|
8
|
+
export { CloudRoaringError, errorKind, IntegrityError, NotFoundError, TransientError, unsettledConflict, ValidationError, WriteConflictError, } from './core/errors.js';
|
|
9
9
|
export type { ErrorKind } from './core/errors.js';
|
|
10
10
|
export { RESOLVED_FIELDS, pointerIdOf, renewPointer, renewsPointer } from './core/pointer-id.js';
|
|
11
11
|
export { validateSegmentRef } from './core/validate.js';
|
|
@@ -15,6 +15,6 @@ export type { ObjectRegistryStore, ObjectRow } from './drivers/_shared/object-re
|
|
|
15
15
|
export { encodeNameForKey, encodeNameForPath, namespaceKeyPart, namespacePathPart, } from './drivers/_shared/keys.js';
|
|
16
16
|
export { normalizeObjectPrefix, prefixPart } from './drivers/_shared/object-registry-keys.js';
|
|
17
17
|
export { parseGenerationFromKey, segmentObjectPrefix, storageObjectKey, } from './drivers/_shared/object-keys.js';
|
|
18
|
-
export { refuseUnknownOptions, requireBool, requirePositiveInt } from './drivers/_shared/options.js';
|
|
18
|
+
export { checkDeleteOptions, checkRegistryWriteOptions, refuseUnknownOptions, requireBool, requireNonEmptyString, requirePositiveInt, } from './drivers/_shared/options.js';
|
|
19
19
|
export { isTransportFault, toTransient } from './drivers/_shared/faults.js';
|
|
20
20
|
export { concatBytes } from './drivers/_shared/bytes.js';
|
package/dist/driver-kit.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
mapWithConcurrency
|
|
3
|
+
} from "./chunk-CGILDXGP.js";
|
|
4
|
+
import {
|
|
3
5
|
DEFAULT_NAMESPACE,
|
|
6
|
+
applyRegistryPatch,
|
|
7
|
+
checkDeleteOptions,
|
|
8
|
+
checkRegistryWriteOptions,
|
|
9
|
+
entropyIsAvailable,
|
|
10
|
+
namespaceKeyPart,
|
|
11
|
+
namespacePathPart,
|
|
12
|
+
newIncarnationToken,
|
|
13
|
+
nextRegistryToken,
|
|
14
|
+
parseRegistryEnvelope,
|
|
15
|
+
recordFromNew,
|
|
16
|
+
requirePositiveInt,
|
|
17
|
+
serializeRegistryEnvelope,
|
|
18
|
+
validateNewRegistryRecord,
|
|
19
|
+
validateRegistryPatch,
|
|
20
|
+
webCryptoEntropy
|
|
21
|
+
} from "./chunk-7GT632UF.js";
|
|
22
|
+
import {
|
|
23
|
+
CloudRoaringError,
|
|
4
24
|
IntegrityError,
|
|
5
25
|
NotFoundError,
|
|
6
26
|
RESOLVED_FIELDS,
|
|
@@ -8,34 +28,24 @@ import {
|
|
|
8
28
|
TransientError,
|
|
9
29
|
ValidationError,
|
|
10
30
|
WriteConflictError,
|
|
11
|
-
applyRegistryPatch,
|
|
12
31
|
brandAsBackend,
|
|
13
32
|
decodeNameFromKey,
|
|
14
33
|
encodeNameForKey,
|
|
15
34
|
encodeNameForPath,
|
|
16
|
-
entropyIsAvailable,
|
|
17
35
|
errorKind,
|
|
36
|
+
isTransientError,
|
|
37
|
+
isUnsettledConflict,
|
|
18
38
|
isWriteConflictError,
|
|
19
|
-
mapWithConcurrency,
|
|
20
|
-
namespaceKeyPart,
|
|
21
|
-
namespacePathPart,
|
|
22
|
-
newIncarnationToken,
|
|
23
|
-
nextRegistryToken,
|
|
24
|
-
parseRegistryEnvelope,
|
|
25
39
|
pointerIdOf,
|
|
26
|
-
recordFromNew,
|
|
27
40
|
refuseUnknownOptions,
|
|
28
41
|
renewPointer,
|
|
29
42
|
renewsPointer,
|
|
30
43
|
requireBool,
|
|
31
44
|
requireInteger,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
validateSegmentRef,
|
|
37
|
-
webCryptoEntropy
|
|
38
|
-
} from "./chunk-OSU7W2Y3.js";
|
|
45
|
+
requireNonEmptyString,
|
|
46
|
+
unsettledConflict,
|
|
47
|
+
validateSegmentRef
|
|
48
|
+
} from "./chunk-MVUD4R72.js";
|
|
39
49
|
|
|
40
50
|
// src/drivers/_shared/object-registry-keys.ts
|
|
41
51
|
var REGISTRY_SUFFIX = ".reg";
|
|
@@ -50,6 +60,12 @@ function prefixPart(prefix) {
|
|
|
50
60
|
}
|
|
51
61
|
function normalizeObjectPrefix(prefix) {
|
|
52
62
|
if (prefix === void 0) return void 0;
|
|
63
|
+
const given = prefix;
|
|
64
|
+
if (typeof given !== "string") {
|
|
65
|
+
throw new ValidationError(
|
|
66
|
+
`prefix must be a string; got ${given === null ? "null" : Array.isArray(given) ? "array" : typeof given}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
53
69
|
for (const ch of prefix) {
|
|
54
70
|
const code = ch.charCodeAt(0);
|
|
55
71
|
if (code < 32 || code === 127) {
|
|
@@ -119,6 +135,7 @@ var MAX_DELETE_ATTEMPTS = 8;
|
|
|
119
135
|
var MAX_READ_ATTEMPTS = 8;
|
|
120
136
|
var LIST_READ_CONCURRENCY = 48;
|
|
121
137
|
var ObjectVersionRaced = class extends Error {
|
|
138
|
+
/** Builds the error naming the object whose pinned version vanished. */
|
|
122
139
|
constructor(objectKey) {
|
|
123
140
|
super(`registry object was overwritten mid-read: ${objectKey}`);
|
|
124
141
|
this.name = "ObjectVersionRaced";
|
|
@@ -141,6 +158,10 @@ var ObjectStoreRegistry = class {
|
|
|
141
158
|
prefix;
|
|
142
159
|
now;
|
|
143
160
|
entropy;
|
|
161
|
+
/**
|
|
162
|
+
* Strong reads, always. `canWrite` is `false` when the runtime has no source of random bytes for tokens, and
|
|
163
|
+
* `conditionalDelete` is `true` when the store deletes a row only while it is still the version read.
|
|
164
|
+
*/
|
|
144
165
|
capabilities() {
|
|
145
166
|
const conditionalDelete = this.removesRows();
|
|
146
167
|
return entropyIsAvailable(this.entropy) ? { strongRead: true, conditionalDelete } : { strongRead: true, canWrite: false, conditionalDelete };
|
|
@@ -162,7 +183,13 @@ var ObjectStoreRegistry = class {
|
|
|
162
183
|
this.observed.set(row.env.record, { key, bytes: row.bytes, version: row.version });
|
|
163
184
|
return row.env.record;
|
|
164
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Write a new row, create-only over none or over a tombstone. `held: null` is the publish's contract: its caller read
|
|
188
|
+
* the row absent. A direct caller that passes it over a tombstone may see a conflict when the read-back after the
|
|
189
|
+
* lost create faults, though no live row exists.
|
|
190
|
+
*/
|
|
165
191
|
async create(ref, record, options) {
|
|
192
|
+
checkRegistryWriteOptions(options);
|
|
166
193
|
const checked = validateNewRegistryRecord(record);
|
|
167
194
|
const key = registryObjectKey(this.prefix, ref);
|
|
168
195
|
let lost;
|
|
@@ -174,9 +201,16 @@ var ObjectStoreRegistry = class {
|
|
|
174
201
|
lost = err;
|
|
175
202
|
}
|
|
176
203
|
}
|
|
177
|
-
|
|
204
|
+
let current;
|
|
205
|
+
try {
|
|
206
|
+
current = await this.readRow(key);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (lost !== void 0 && isTransientError(err)) throw lost;
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
178
211
|
if (current !== null && !current.env.deleted) {
|
|
179
|
-
|
|
212
|
+
const message = `registry row already exists for segment ${ref.segment}`;
|
|
213
|
+
throw isUnsettledConflict(lost) ? unsettledConflict(message) : new WriteConflictError(message);
|
|
180
214
|
}
|
|
181
215
|
if (lost !== void 0 && current === null) throw lost;
|
|
182
216
|
return this.createOver(key, ref, checked, current ?? void 0, options);
|
|
@@ -193,6 +227,7 @@ var ObjectStoreRegistry = class {
|
|
|
193
227
|
return { token };
|
|
194
228
|
}
|
|
195
229
|
async compareAndSwap(ref, expected, patch, options) {
|
|
230
|
+
checkRegistryWriteOptions(options);
|
|
196
231
|
const checked = validateRegistryPatch(patch);
|
|
197
232
|
const key = registryObjectKey(this.prefix, ref);
|
|
198
233
|
const current = this.observedRow(key, expected, options?.held) ?? await this.readRow(key);
|
|
@@ -332,8 +367,10 @@ function segmentObjectPrefix(prefix, ref) {
|
|
|
332
367
|
return `${prefixPart(prefix)}${namespaceKeyPart(ref.namespace)}/segments/${encodeNameForKey(ref.segment)}.`;
|
|
333
368
|
}
|
|
334
369
|
function storageObjectKey(prefix, key) {
|
|
370
|
+
validateSegmentRef(key);
|
|
335
371
|
requireInteger(key.generation, { min: 0, what: "generation" });
|
|
336
|
-
|
|
372
|
+
const ref = { segment: key.segment, namespace: key.namespace };
|
|
373
|
+
return `${segmentObjectPrefix(prefix, ref)}${key.generation}${SUFFIX}`;
|
|
337
374
|
}
|
|
338
375
|
function parseGenerationFromKey(segmentPrefix, objectKey) {
|
|
339
376
|
if (!objectKey.startsWith(segmentPrefix) || !objectKey.endsWith(SUFFIX)) return null;
|
|
@@ -388,6 +425,8 @@ export {
|
|
|
388
425
|
ValidationError,
|
|
389
426
|
WriteConflictError,
|
|
390
427
|
brandAsBackend,
|
|
428
|
+
checkDeleteOptions,
|
|
429
|
+
checkRegistryWriteOptions,
|
|
391
430
|
concatBytes,
|
|
392
431
|
encodeNameForKey,
|
|
393
432
|
encodeNameForPath,
|
|
@@ -403,10 +442,12 @@ export {
|
|
|
403
442
|
renewPointer,
|
|
404
443
|
renewsPointer,
|
|
405
444
|
requireBool,
|
|
445
|
+
requireNonEmptyString,
|
|
406
446
|
requirePositiveInt,
|
|
407
447
|
segmentObjectPrefix,
|
|
408
448
|
storageObjectKey,
|
|
409
449
|
toTransient,
|
|
450
|
+
unsettledConflict,
|
|
410
451
|
validateSegmentRef
|
|
411
452
|
};
|
|
412
453
|
//# sourceMappingURL=driver-kit.js.map
|
package/dist/driver-kit.js.map
CHANGED
|
@@ -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", "../src/drivers/_shared/object-keys.ts", "../src/drivers/_shared/faults.ts", "../src/drivers/_shared/bytes.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 // Index scans, not a regex: `/\\/+$/` retries from every slash of a run that is not at the end, which is\n // quadratic in the run's length.\n let start = 0;\n let end = prefix.length;\n while (start < end && prefix.charCodeAt(start) === 0x2f) start++;\n while (end > start && prefix.charCodeAt(end - 1) === 0x2f) end--;\n const trimmed = prefix.slice(start, end);\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/`. */\nfunction 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>`): a re-create draws a new incarnation, so nothing of the old row is needed to keep\n * its tokens apart, and a full `list()` no longer reads a row for every name it ever held. On a store that cannot\n * vouch for one, the row is **tombstoned**: the object stays, its counter advanced, and a re-create carries the counter\n * on.\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 * **A caller that already read the row can spare the read before its write.** `get` remembers the version of the\n * object it read, against the record it returned. A `compareAndSwap` handed that very record as `held` conditions its\n * write on that version and sends it at once, and a `create` handed `null` (the caller found no row) sends its\n * create-only write at once. The store's condition stays the fence: a row that changed since fails the write as a lost\n * race, so a stale `held` can lose a write and never land one. A record this registry did not return (a copy, another\n * registry's) is read for as without the hint.\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 RegistryWriteOptions,\n SegmentRef,\n Token,\n} from '@/core/ports';\nimport type { Entropy } from '@/core/determinism';\nimport { mapWithConcurrency } from '@/core/concurrency';\nimport {\n applyRegistryPatch,\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';\nimport { validateSegmentRef } from '@/core/validate';\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;\nconst UTF8 = new TextEncoder();\nconst UTF8_DECODER = new TextDecoder();\n\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 = 48;\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/**\n * Whether `err` is an {@link ObjectVersionRaced}, by its name: a store built against another copy of this package raises\n * that copy's class, which `instanceof` does not recognise.\n */\nconst isVersionRaced = (err: unknown): boolean =>\n err instanceof Error && err.name === 'ObjectVersionRaced';\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\n/** A row as read: its envelope, the bytes it was parsed from, and the version that fences a write to it. */\ninterface ReadRow extends ObjectRow {\n readonly env: RegistryEnvelope;\n}\n\n/** What a record `get` returned was read from. */\ninterface ObservedRow extends ObjectRow {\n readonly key: string;\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 /** Every write reports its token to `onToken` before the request that carries it is sent. */\n get reportsWriteTokens(): true {\n return true;\n }\n\n /** Whether a delete removes a row: 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 /** The object each record `get` returned was read from, to fence a write made against it ({@link RegistryWriteOptions.held}). */\n private readonly observed = new WeakMap<RegistryRecord, ObservedRow>();\n\n async get(ref: SegmentRef): Promise<RegistryRecord | null> {\n const key = registryObjectKey(this.prefix, ref);\n const row = await this.readRow(key);\n if (row === null || row.env.deleted) return null;\n this.observed.set(row.env.record, { key, bytes: row.bytes, version: row.version });\n return row.env.record;\n }\n\n async create(\n ref: SegmentRef,\n record: NewRegistryRecord,\n options?: RegistryWriteOptions,\n ): Promise<{ token: Token }> {\n const checked = validateNewRegistryRecord(record);\n const key = registryObjectKey(this.prefix, ref);\n let lost: unknown;\n if (options?.held === null) {\n // The caller found no row: send the create-only write without reading. One that loses to a row, or to a tombstone\n // (which a create-only write cannot go over), is told apart by the read below.\n try {\n return await this.createOver(key, ref, checked, undefined, options);\n } catch (err) {\n if (!isWriteConflictError(err)) throw err;\n lost = err;\n }\n }\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 // No row at all now: the create-only write lost to something that has gone since, and it is not sent again.\n if (lost !== undefined && current === null) throw lost;\n return this.createOver(key, ref, checked, current ?? undefined, options);\n }\n\n /** Write a new incarnation of the row: over `tombstone` under its version, or create-only when there is none. */\n private async createOver(\n key: string,\n ref: SegmentRef,\n checked: NewRegistryRecord,\n tombstone: ReadRow | undefined,\n options: RegistryWriteOptions | undefined,\n ): Promise<{ token: Token }> {\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, tombstone?.env.record);\n options?.onToken?.(token);\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, tombstone ? { version: tombstone.version } : 'absent');\n return { token };\n }\n\n async compareAndSwap(\n ref: SegmentRef,\n expected: Token,\n patch: RegistryPatch,\n options?: RegistryWriteOptions,\n ): Promise<{ token: Token }> {\n const checked = validateRegistryPatch(patch);\n const key = registryObjectKey(this.prefix, ref);\n // The row the caller read, as this registry read it, when it is the row `expected` names; otherwise read it now.\n const current = this.observedRow(key, expected, options?.held) ?? (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 options?.onToken?.(token);\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 /**\n * The row a caller's `held` record was read as, decoded again from the bytes `get` saw so that nothing the caller did\n * to its copy reaches the write, or `undefined` when `held` is not a record this registry returned for this row under\n * the token the write expects. The version it carries is the store's fence, so a row that has moved on since fails\n * the write.\n */\n private observedRow(\n key: string,\n expected: Token,\n held: RegistryRecord | null | undefined,\n ): ReadRow | undefined {\n const seen = held ? this.observed.get(held) : undefined;\n if (seen === undefined || seen.key !== key || held?.token !== expected) return undefined;\n return this.decodeRow(key, seen);\n }\n\n async *list(namespace?: string): AsyncIterable<RegistryRecord> {\n // A namespace that is given is checked as every other name is: `''` names none, and reads as an error, not as empty.\n if (namespace !== undefined) validateSegmentRef({ segment: 'x', namespace });\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 // Where the store vouches for a conditional delete, the row is removed from it, by a delete the store applies only\n // while the object is still the version read here: a later create draws a new incarnation, so nothing of this row\n // is needed to keep its tokens apart. Otherwise it is tombstoned (the counter advanced, the object kept), so a\n // re-create carries the counter on. Retry the read and the write on a cross-process race; it converges, then fails\n // typed 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) {\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(objectKey: string): Promise<ReadRow | null> {\n const row = await this.readRaced(objectKey);\n return row === null ? null : this.decodeRow(objectKey, row);\n }\n\n /** Check and parse the bytes of a registry object. */\n private decodeRow(objectKey: string, row: ObjectRow): ReadRow {\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 // The name the key spells is the name the row must carry: a key outside the layout has none to hold it to.\n const stored = parseRegistryKey(this.prefix, objectKey) ?? undefined;\n const env = parseRegistryEnvelope(UTF8_DECODER.decode(row.bytes), objectKey, stored);\n return { env, bytes: row.bytes, 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 (!isVersionRaced(err)) 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 = UTF8.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", "/**\n * The storage object layout, shared by every object-store driver (S3, GCS, Azure Blob): one `.crbm` object per\n * generation at `<prefix><ns>/segments/<segment>.<gen>.crbm`, namespace-first, beside the registry's own layout\n * (`object-registry-keys.ts`) under the same caller prefix.\n *\n * One definition, so a segment written by one driver reads identically under another: two drivers disagreeing about\n * how a name becomes a key would be a silent cross-driver incompatibility on the same bucket, a segment that quietly\n * cannot be found. Pure string logic, no SDK. The names are re-validated here, because a driver can be constructed and\n * driven directly rather than through the engine that would otherwise have validated them. The default (absent)\n * namespace maps to `_default`, which cannot collide with a real namespace because a caller's `_default` encodes to\n * `%5Fdefault` while the sentinel is emitted literally.\n */\nimport { requireInteger } from '@/core/check';\nimport { validateSegmentRef } from '@/core/validate';\nimport type { GenKey, SegmentRef } from '@/core/ports';\nimport { encodeNameForKey, namespaceKeyPart } from './keys';\nimport { prefixPart } from './object-registry-keys';\n\nconst SUFFIX = '.crbm';\n\n/**\n * The key prefix shared by all of a segment's generations, `<prefix><ns>/segments/<segment>.`: the prefix a listing of\n * the segment's generations asks for, and the string {@link parseGenerationFromKey} strips.\n */\nexport function segmentObjectPrefix(prefix: string | undefined, ref: SegmentRef): string {\n validateSegmentRef(ref);\n return `${prefixPart(prefix)}${namespaceKeyPart(ref.namespace)}/segments/${encodeNameForKey(ref.segment)}.`;\n}\n\n/**\n * The full key of one generation's object, `<segmentPrefix><gen>.crbm`. A generation that is not a safe integer from 0\n * is refused, so every key built here is one {@link parseGenerationFromKey} reads back.\n */\nexport function storageObjectKey(prefix: string | undefined, key: GenKey): string {\n requireInteger(key.generation, { min: 0, what: 'generation' });\n return `${segmentObjectPrefix(prefix, key)}${key.generation}${SUFFIX}`;\n}\n\n/**\n * The generation number in a full object key, given its segment's prefix, or `null` when the key is not one of that\n * segment's generations. Canonical decimal only: no leading zeros (so `\u2026s.07.crbm` cannot alias `\u2026s.7.crbm`), within\n * the safe-integer range. A *different* segment whose name merely shares the prefix does not parse either: a key for\n * segment `s.x` under segment `s`'s prefix has a middle that is not all digits.\n */\nexport function parseGenerationFromKey(segmentPrefix: string, objectKey: string): number | null {\n if (!objectKey.startsWith(segmentPrefix) || !objectKey.endsWith(SUFFIX)) return null;\n const middle = objectKey.slice(segmentPrefix.length, objectKey.length - SUFFIX.length);\n if (!/^(0|[1-9]\\d*)$/.test(middle)) return null;\n const generation = Number(middle);\n return Number.isSafeInteger(generation) ? generation : null;\n}\n", "/**\n * Fault classification shared by the cloud drivers: which connection failures are transient whatever the service, and\n * how a driver hands a transient fault on as {@link TransientError}. Each driver keeps its own reading of its service's\n * statuses and error codes; what is the same for every service built on Node's networking lives here once, so a write\n * and a read, or two drivers, cannot disagree about it.\n */\nimport { TransientError } from '@/core/errors';\n\n/**\n * The codes Node gives a connection that failed or was cut off: refused, reset, aborted, timed out, a DNS failure, an\n * unreachable host or network, and a broken pipe. Only these: a missing or unreadable credentials file (`ENOENT`,\n * `EACCES`) and a TLS failure (`EPROTO`, a certificate error) carry codes of the same shape, and would fail the same\n * way again.\n */\nconst TRANSPORT_FAULT_CODES: ReadonlySet<string> = new Set([\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'ETIMEDOUT',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'EHOSTUNREACH',\n 'ENETUNREACH',\n 'EPIPE',\n]);\n\n/**\n * Whether `err` is a connection that failed or was cut off before the service answered: its `code` is one of the\n * strings Node gives such a failure (`ECONNRESET`, `ECONNREFUSED`, `ECONNABORTED`, `ETIMEDOUT`, `ENOTFOUND`,\n * `EAI_AGAIN`, `EHOSTUNREACH`, `ENETUNREACH`, `EPIPE`). A retry on a fresh connection usually succeeds, so a driver\n * raises one as {@link TransientError}. It reads nothing else: a driver whose service also answers with a status\n * checks that first.\n */\nexport function isTransportFault(err: unknown): boolean {\n const code = (err as { code?: unknown } | null | undefined)?.code;\n return typeof code === 'string' && TRANSPORT_FAULT_CODES.has(code);\n}\n\n/**\n * `err` as a {@link TransientError} when `isTransient(err)` holds, with `message(err)` as its message and `err` as its\n * `cause`; any other error unchanged. The last step of a driver's error mapping, so the store's read retry can ride a\n * transient fault out and a write's caller can tell one from a deterministic failure. Run whatever scrubs credentials\n * from an error before it, since the error becomes the cause.\n */\nexport function toTransient(\n err: unknown,\n isTransient: (err: unknown) => boolean,\n message: (err: unknown) => string,\n): unknown {\n return isTransient(err) ? new TransientError(message(err), { cause: err }) : err;\n}\n", "/** Byte-buffer helpers shared by the drivers. */\n\n/**\n * The chunks of `parts`, in order, copied into one new `Uint8Array` that owns its bytes: no view of any part survives in\n * it, so a part's buffer can be reused or let go once this returns.\n */\nexport function concatBytes(parts: readonly Uint8Array[]): Uint8Array {\n let total = 0;\n for (const p of parts) total += p.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const p of parts) {\n out.set(p, offset);\n offset += p.length;\n }\n return out;\n}\n"],
|
|
5
|
-
"mappings": "
|
|
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 // Index scans, not a regex: `/\\/+$/` retries from every slash of a run that is not at the end, which is\n // quadratic in the run's length.\n let start = 0;\n let end = prefix.length;\n while (start < end && prefix.charCodeAt(start) === 0x2f) start++;\n while (end > start && prefix.charCodeAt(end - 1) === 0x2f) end--;\n const trimmed = prefix.slice(start, end);\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 const given: unknown = prefix;\n if (typeof given !== 'string') {\n throw new ValidationError(\n `prefix must be a string; got ${given === null ? 'null' : Array.isArray(given) ? 'array' : typeof given}`,\n );\n }\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/`. */\nfunction 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>`): a re-create draws a new incarnation, so nothing of the old row is needed to keep\n * its tokens apart, and a full `list()` no longer reads a row for every name it ever held. On a store that cannot\n * vouch for one, the row is **tombstoned**: the object stays, its counter advanced, and a re-create carries the counter\n * on.\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 * **A caller that already read the row can spare the read before its write.** `get` remembers the version of the\n * object it read, against the record it returned. A `compareAndSwap` handed that very record as `held` conditions its\n * write on that version and sends it at once, and a `create` handed `null` (the caller found no row) sends its\n * create-only write at once. The store's condition stays the fence: a row that changed since fails the write as a lost\n * race, so a stale `held` can lose a write and never land one. A record this registry did not return (a copy, another\n * registry's) is read for as without the hint.\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 { checkRegistryWriteOptions } from './options';\nimport {\n IntegrityError,\n TransientError,\n ValidationError,\n WriteConflictError,\n isTransientError,\n isUnsettledConflict,\n isWriteConflictError,\n unsettledConflict,\n} from '@/core/errors';\nimport type {\n IRegistryDriver,\n NewRegistryRecord,\n RegCaps,\n RegistryPatch,\n RegistryRecord,\n RegistryWriteOptions,\n SegmentRef,\n Token,\n} from '@/core/ports';\nimport type { Entropy } from '@/core/determinism';\nimport { mapWithConcurrency } from '@/core/concurrency';\nimport {\n applyRegistryPatch,\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';\nimport { validateSegmentRef } from '@/core/validate';\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;\nconst UTF8 = new TextEncoder();\nconst UTF8_DECODER = new TextDecoder();\n\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 = 48;\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 /** Builds the error naming the object whose pinned version vanished. */\n constructor(objectKey: string) {\n super(`registry object was overwritten mid-read: ${objectKey}`);\n this.name = 'ObjectVersionRaced';\n }\n}\n\n/**\n * Whether `err` is an {@link ObjectVersionRaced}, by its name: a store built against another copy of this package raises\n * that copy's class, which `instanceof` does not recognise.\n */\nconst isVersionRaced = (err: unknown): boolean =>\n err instanceof Error && err.name === 'ObjectVersionRaced';\n\n/** One object as read, with the opaque version that fences a later conditional write. */\nexport interface ObjectRow {\n /** The object's bytes. */\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 * A store whose client may have sent the write more than once throws every conflict it cannot prove the write did not\n * land before as `unsettledConflict` (from the driver kit), not a plain {@link WriteConflictError}: the read-back can\n * find another writer's write over a row this write landed on, or the row gone after it landed. The registry passes\n * the mark through, and a load refused after it reports `unanswered: true`. A store that sends the write once throws\n * the plain conflict.\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. An unfenced delete then returns,\n * and one fenced on a token reports the conflict, which its caller settles by reading the row (the retention sweep\n * does).\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\n/** A row as read: its envelope, the bytes it was parsed from, and the version that fences a write to it. */\ninterface ReadRow extends ObjectRow {\n readonly env: RegistryEnvelope;\n}\n\n/** What a record `get` returned was read from. */\ninterface ObservedRow extends ObjectRow {\n readonly key: string;\n}\n\n/**\n * A registry driver over any object store with conditional writes: one small JSON object per segment, fenced by the\n * store's own preconditions, so a deployment can run on one bucket.\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 /**\n * Strong reads, always. `canWrite` is `false` when the runtime has no source of random bytes for tokens, and\n * `conditionalDelete` is `true` when the store deletes a row only while it is still the version read.\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 /** Every write reports its token to `onToken` before the request that carries it is sent. */\n get reportsWriteTokens(): true {\n return true;\n }\n\n /** Whether a delete removes a row: 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 /** The object each record `get` returned was read from, to fence a write made against it ({@link RegistryWriteOptions.held}). */\n private readonly observed = new WeakMap<RegistryRecord, ObservedRow>();\n\n async get(ref: SegmentRef): Promise<RegistryRecord | null> {\n const key = registryObjectKey(this.prefix, ref);\n const row = await this.readRow(key);\n if (row === null || row.env.deleted) return null;\n this.observed.set(row.env.record, { key, bytes: row.bytes, version: row.version });\n return row.env.record;\n }\n\n /**\n * Write a new row, create-only over none or over a tombstone. `held: null` is the publish's contract: its caller read\n * the row absent. A direct caller that passes it over a tombstone may see a conflict when the read-back after the\n * lost create faults, though no live row exists.\n */\n async create(\n ref: SegmentRef,\n record: NewRegistryRecord,\n options?: RegistryWriteOptions,\n ): Promise<{ token: Token }> {\n checkRegistryWriteOptions(options);\n const checked = validateNewRegistryRecord(record);\n const key = registryObjectKey(this.prefix, ref);\n let lost: unknown;\n if (options?.held === null) {\n // The caller found no row: send the create-only write without reading. One that loses to a row, or to a tombstone\n // (which a create-only write cannot go over), is told apart by the read below.\n try {\n return await this.createOver(key, ref, checked, undefined, options);\n } catch (err) {\n if (!isWriteConflictError(err)) throw err;\n lost = err;\n }\n }\n let current: ReadRow | null;\n try {\n current = await this.readRow(key);\n } catch (err) {\n // The read only tells a live row from a tombstone. A fault on it leaves the write's own answer standing: the\n // conflict, as it was, with its mark if it has one. The publish reads the row again at its next attempt.\n if (lost !== undefined && isTransientError(err)) throw lost;\n throw err;\n }\n if (current !== null && !current.env.deleted) {\n // The create-only write's own conflict, if it was one that may have landed, keeps that mark over this message.\n const message = `registry row already exists for segment ${ref.segment}`;\n throw isUnsettledConflict(lost)\n ? unsettledConflict(message)\n : new WriteConflictError(message);\n }\n // No row at all now: the create-only write lost to something that has gone since, and it is not sent again.\n if (lost !== undefined && current === null) throw lost;\n return this.createOver(key, ref, checked, current ?? undefined, options);\n }\n\n /** Write a new incarnation of the row: over `tombstone` under its version, or create-only when there is none. */\n private async createOver(\n key: string,\n ref: SegmentRef,\n checked: NewRegistryRecord,\n tombstone: ReadRow | undefined,\n options: RegistryWriteOptions | undefined,\n ): Promise<{ token: Token }> {\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, tombstone?.env.record);\n options?.onToken?.(token);\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, tombstone ? { version: tombstone.version } : 'absent');\n return { token };\n }\n\n async compareAndSwap(\n ref: SegmentRef,\n expected: Token,\n patch: RegistryPatch,\n options?: RegistryWriteOptions,\n ): Promise<{ token: Token }> {\n checkRegistryWriteOptions(options);\n const checked = validateRegistryPatch(patch);\n const key = registryObjectKey(this.prefix, ref);\n // The row the caller read, as this registry read it, when it is the row `expected` names; otherwise read it now.\n const current = this.observedRow(key, expected, options?.held) ?? (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 options?.onToken?.(token);\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 /**\n * The row a caller's `held` record was read as, decoded again from the bytes `get` saw so that nothing the caller did\n * to its copy reaches the write, or `undefined` when `held` is not a record this registry returned for this row under\n * the token the write expects. The version it carries is the store's fence, so a row that has moved on since fails\n * the write.\n */\n private observedRow(\n key: string,\n expected: Token,\n held: RegistryRecord | null | undefined,\n ): ReadRow | undefined {\n const seen = held ? this.observed.get(held) : undefined;\n if (seen === undefined || seen.key !== key || held?.token !== expected) return undefined;\n return this.decodeRow(key, seen);\n }\n\n async *list(namespace?: string): AsyncIterable<RegistryRecord> {\n // A namespace that is given is checked as every other name is: `''` names none, and reads as an error, not as empty.\n if (namespace !== undefined) validateSegmentRef({ segment: 'x', namespace });\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 // Where the store vouches for a conditional delete, the row is removed from it, by a delete the store applies only\n // while the object is still the version read here: a later create draws a new incarnation, so nothing of this row\n // is needed to keep its tokens apart. Otherwise it is tombstoned (the counter advanced, the object kept), so a\n // re-create carries the counter on. Retry the read and the write on a cross-process race; it converges, then fails\n // typed 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) {\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(objectKey: string): Promise<ReadRow | null> {\n const row = await this.readRaced(objectKey);\n return row === null ? null : this.decodeRow(objectKey, row);\n }\n\n /** Check and parse the bytes of a registry object. */\n private decodeRow(objectKey: string, row: ObjectRow): ReadRow {\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 // The name the key spells is the name the row must carry: a key outside the layout has none to hold it to.\n const stored = parseRegistryKey(this.prefix, objectKey) ?? undefined;\n const env = parseRegistryEnvelope(UTF8_DECODER.decode(row.bytes), objectKey, stored);\n return { env, bytes: row.bytes, 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 (!isVersionRaced(err)) 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 = UTF8.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", "/**\n * The storage object layout, shared by every object-store driver (S3, GCS, Azure Blob): one `.crbm` object per\n * generation at `<prefix><ns>/segments/<segment>.<gen>.crbm`, namespace-first, beside the registry's own layout\n * (`object-registry-keys.ts`) under the same caller prefix.\n *\n * One definition, so a segment written by one driver reads identically under another: two drivers disagreeing about\n * how a name becomes a key would be a silent cross-driver incompatibility on the same bucket, a segment that quietly\n * cannot be found. Pure string logic, no SDK. The names are re-validated here, because a driver can be constructed and\n * driven directly rather than through the engine that would otherwise have validated them. The default (absent)\n * namespace maps to `_default`, which cannot collide with a real namespace because a caller's `_default` encodes to\n * `%5Fdefault` while the sentinel is emitted literally.\n */\nimport { requireInteger } from '@/core/check';\nimport { validateSegmentRef } from '@/core/validate';\nimport type { GenKey, SegmentRef } from '@/core/ports';\nimport { encodeNameForKey, namespaceKeyPart } from './keys';\nimport { prefixPart } from './object-registry-keys';\n\nconst SUFFIX = '.crbm';\n\n/**\n * The key prefix shared by all of a segment's generations, `<prefix><ns>/segments/<segment>.`: the prefix a listing of\n * the segment's generations asks for, and the string {@link parseGenerationFromKey} strips.\n */\nexport function segmentObjectPrefix(prefix: string | undefined, ref: SegmentRef): string {\n validateSegmentRef(ref);\n return `${prefixPart(prefix)}${namespaceKeyPart(ref.namespace)}/segments/${encodeNameForKey(ref.segment)}.`;\n}\n\n/**\n * The full key of one generation's object, `<segmentPrefix><gen>.crbm`. A generation that is not a safe integer from 0\n * is refused, so every key built here is one {@link parseGenerationFromKey} reads back.\n */\nexport function storageObjectKey(prefix: string | undefined, key: GenKey): string {\n validateSegmentRef(key);\n requireInteger(key.generation, { min: 0, what: 'generation' });\n const ref: SegmentRef = { segment: key.segment, namespace: key.namespace };\n return `${segmentObjectPrefix(prefix, ref)}${key.generation}${SUFFIX}`;\n}\n\n/**\n * The generation number in a full object key, given its segment's prefix, or `null` when the key is not one of that\n * segment's generations. Canonical decimal only: no leading zeros (so `\u2026s.07.crbm` cannot alias `\u2026s.7.crbm`), within\n * the safe-integer range. A *different* segment whose name merely shares the prefix does not parse either: a key for\n * segment `s.x` under segment `s`'s prefix has a middle that is not all digits.\n */\nexport function parseGenerationFromKey(segmentPrefix: string, objectKey: string): number | null {\n if (!objectKey.startsWith(segmentPrefix) || !objectKey.endsWith(SUFFIX)) return null;\n const middle = objectKey.slice(segmentPrefix.length, objectKey.length - SUFFIX.length);\n if (!/^(0|[1-9]\\d*)$/.test(middle)) return null;\n const generation = Number(middle);\n return Number.isSafeInteger(generation) ? generation : null;\n}\n", "/**\n * Fault classification shared by the cloud drivers: which connection failures are transient whatever the service, and\n * how a driver hands a transient fault on as {@link TransientError}. Each driver keeps its own reading of its service's\n * statuses and error codes; what is the same for every service built on Node's networking lives here once, so a write\n * and a read, or two drivers, cannot disagree about it.\n */\nimport { TransientError } from '@/core/errors';\n\n/**\n * The codes Node gives a connection that failed or was cut off: refused, reset, aborted, timed out, a DNS failure, an\n * unreachable host or network, and a broken pipe. Only these: a missing or unreadable credentials file (`ENOENT`,\n * `EACCES`) and a TLS failure (`EPROTO`, a certificate error) carry codes of the same shape, and would fail the same\n * way again.\n */\nconst TRANSPORT_FAULT_CODES: ReadonlySet<string> = new Set([\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'ETIMEDOUT',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'EHOSTUNREACH',\n 'ENETUNREACH',\n 'EPIPE',\n]);\n\n/**\n * Whether `err` is a connection that failed or was cut off before the service answered: its `code` is one of the\n * strings Node gives such a failure (`ECONNRESET`, `ECONNREFUSED`, `ECONNABORTED`, `ETIMEDOUT`, `ENOTFOUND`,\n * `EAI_AGAIN`, `EHOSTUNREACH`, `ENETUNREACH`, `EPIPE`). A retry on a fresh connection usually succeeds, so a driver\n * raises one as {@link TransientError}. It reads nothing else: a driver whose service also answers with a status\n * checks that first.\n */\nexport function isTransportFault(err: unknown): boolean {\n const code = (err as { code?: unknown } | null | undefined)?.code;\n return typeof code === 'string' && TRANSPORT_FAULT_CODES.has(code);\n}\n\n/**\n * `err` as a {@link TransientError} when `isTransient(err)` holds, with `message(err)` as its message and `err` as its\n * `cause`; any other error unchanged. The last step of a driver's error mapping, so the store's read retry can ride a\n * transient fault out and a write's caller can tell one from a deterministic failure. Run whatever scrubs credentials\n * from an error before it, since the error becomes the cause.\n */\nexport function toTransient(\n err: unknown,\n isTransient: (err: unknown) => boolean,\n message: (err: unknown) => string,\n): unknown {\n return isTransient(err) ? new TransientError(message(err), { cause: err }) : err;\n}\n", "/** Byte-buffer helpers shared by the drivers. */\n\n/**\n * The chunks of `parts`, in order, copied into one new `Uint8Array` that owns its bytes: no view of any part survives in\n * it, so a part's buffer can be reused or let go once this returns.\n */\nexport function concatBytes(parts: readonly Uint8Array[]): Uint8Array {\n let total = 0;\n for (const p of parts) total += p.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const p of parts) {\n out.set(p, offset);\n offset += p.length;\n }\n return out;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -17,10 +17,12 @@ export declare const MAX_ROW_BYTES: number;
|
|
|
17
17
|
* `GetObject`, GCS bytes and `generation` from one GET, and Azure Blob bytes and `ETag` from one GET.
|
|
18
18
|
*/
|
|
19
19
|
export declare class ObjectVersionRaced extends Error {
|
|
20
|
+
/** Builds the error naming the object whose pinned version vanished. */
|
|
20
21
|
constructor(objectKey: string);
|
|
21
22
|
}
|
|
22
23
|
/** One object as read, with the opaque version that fences a later conditional write. */
|
|
23
24
|
export interface ObjectRow {
|
|
25
|
+
/** The object's bytes. */
|
|
24
26
|
readonly bytes: Uint8Array;
|
|
25
27
|
/** The store's version fence — an S3/Azure ETag, a GCS generation. Opaque to this class. */
|
|
26
28
|
readonly version: string;
|
|
@@ -53,6 +55,12 @@ export interface ObjectRegistryStore {
|
|
|
53
55
|
* the row it just wrote and its precondition fails, which would read as a lost race. Send it once, or store a
|
|
54
56
|
* write id with the row and treat a conflict whose stored row carries it as success. Retryable faults throw
|
|
55
57
|
* {@link TransientError}.
|
|
58
|
+
*
|
|
59
|
+
* A store whose client may have sent the write more than once throws every conflict it cannot prove the write did not
|
|
60
|
+
* land before as `unsettledConflict` (from the driver kit), not a plain {@link WriteConflictError}: the read-back can
|
|
61
|
+
* find another writer's write over a row this write landed on, or the row gone after it landed. The registry passes
|
|
62
|
+
* the mark through, and a load refused after it reports `unanswered: true`. A store that sends the write once throws
|
|
63
|
+
* the plain conflict.
|
|
56
64
|
*/
|
|
57
65
|
write(key: string, body: Uint8Array, expect: 'absent' | {
|
|
58
66
|
version: string;
|
|
@@ -66,7 +74,9 @@ export interface ObjectRegistryStore {
|
|
|
66
74
|
*
|
|
67
75
|
* Sending it again after a lost response is safe, unlike a write: the precondition names one version, so a second
|
|
68
76
|
* copy can remove nothing the first was not allowed to. A copy that meets its own landed delete fails the
|
|
69
|
-
* precondition and reports a conflict; the registry re-reads
|
|
77
|
+
* precondition and reports a conflict; the registry re-reads and finds the row gone. An unfenced delete then returns,
|
|
78
|
+
* and one fenced on a token reports the conflict, which its caller settles by reading the row (the retention sweep
|
|
79
|
+
* does).
|
|
70
80
|
*
|
|
71
81
|
* The registry calls it only when {@link conditionalDelete} is `true`.
|
|
72
82
|
*/
|
|
@@ -80,6 +90,10 @@ export interface ObjectRegistryStore {
|
|
|
80
90
|
*/
|
|
81
91
|
readonly conditionalDelete?: boolean;
|
|
82
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* A registry driver over any object store with conditional writes: one small JSON object per segment, fenced by the
|
|
95
|
+
* store's own preconditions, so a deployment can run on one bucket.
|
|
96
|
+
*/
|
|
83
97
|
export declare class ObjectStoreRegistry implements IRegistryDriver {
|
|
84
98
|
private readonly store;
|
|
85
99
|
private readonly prefix;
|
|
@@ -91,6 +105,10 @@ export declare class ObjectStoreRegistry implements IRegistryDriver {
|
|
|
91
105
|
* gives every process the same ids.
|
|
92
106
|
*/
|
|
93
107
|
constructor(store: ObjectRegistryStore, prefix: string | undefined, now: () => number, entropy?: Entropy);
|
|
108
|
+
/**
|
|
109
|
+
* Strong reads, always. `canWrite` is `false` when the runtime has no source of random bytes for tokens, and
|
|
110
|
+
* `conditionalDelete` is `true` when the store deletes a row only while it is still the version read.
|
|
111
|
+
*/
|
|
94
112
|
capabilities(): RegCaps;
|
|
95
113
|
/** Every write reports its token to `onToken` before the request that carries it is sent. */
|
|
96
114
|
get reportsWriteTokens(): true;
|
|
@@ -99,6 +117,11 @@ export declare class ObjectStoreRegistry implements IRegistryDriver {
|
|
|
99
117
|
/** The object each record `get` returned was read from, to fence a write made against it ({@link RegistryWriteOptions.held}). */
|
|
100
118
|
private readonly observed;
|
|
101
119
|
get(ref: SegmentRef): Promise<RegistryRecord | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Write a new row, create-only over none or over a tombstone. `held: null` is the publish's contract: its caller read
|
|
122
|
+
* the row absent. A direct caller that passes it over a tombstone may see a conflict when the read-back after the
|
|
123
|
+
* lost create faults, though no live row exists.
|
|
124
|
+
*/
|
|
102
125
|
create(ref: SegmentRef, record: NewRegistryRecord, options?: RegistryWriteOptions): Promise<{
|
|
103
126
|
token: Token;
|
|
104
127
|
}>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Option-bag checks shared by the backends and the drivers, so each states "refuse what can never be honoured" once:
|
|
3
|
+
* an options value that is not an object, a key the backend does not take (named, with the keys it does), a `now` that
|
|
4
|
+
* is not a function, a size that is not a positive safe integer and a switch that is not a boolean. Each throws
|
|
5
|
+
* `ValidationError`, so a wiring mistake fails where it is made rather than at the first read.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
+
import { refuseUnknownOptions, requireBool, requireNonEmptyString } from '../../core/check.js';
|
|
8
|
+
export { refuseUnknownOptions, requireBool, requireNonEmptyString };
|
|
7
9
|
/** Refuse a `now` that is present and not a function. */
|
|
8
10
|
export declare function checkNow(name: string, now: unknown): void;
|
|
9
11
|
/**
|
|
@@ -13,7 +15,13 @@ export declare function checkNow(name: string, now: unknown): void;
|
|
|
13
15
|
*/
|
|
14
16
|
export declare function requirePositiveInt(name: string, value: unknown): number | undefined;
|
|
15
17
|
/**
|
|
16
|
-
* The
|
|
17
|
-
*
|
|
18
|
+
* The per-call options of a storage port's `delete`, checked: absent, or an object holding only `ifVersion`, a string.
|
|
19
|
+
* A misspelt key would otherwise read as no condition and delete whatever is under the key. Returns the `ifVersion`.
|
|
18
20
|
*/
|
|
19
|
-
export declare function
|
|
21
|
+
export declare function checkDeleteOptions(options: unknown): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The per-call options of a registry port's `create` and `compareAndSwap`, checked: absent, or an object holding only
|
|
24
|
+
* `held` (a record or `null`) and `onToken` (a function). A misspelt key would otherwise drop the hint or the token report
|
|
25
|
+
* a publish relies on.
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkRegistryWriteOptions(options: unknown): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { STORAGE_BACKEND } from '../core/ports.js';
|
|
2
|
-
import type { IRegistryDriver, IStorageDriver, StorageBackend } from '../core/ports.js';
|
|
2
|
+
import type { IRegistryDriver, IStorageDriver, LifecycleVerdict, StorageBackend } from '../core/ports.js';
|
|
3
3
|
/** Options of {@link MemoryStorage}: only `now`, the clock for the registry's timestamps. */
|
|
4
4
|
export interface MemoryStorageOptions {
|
|
5
5
|
/** Injected clock for the registry's `createdAt`/`updatedAt`; defaults to `Date.now`. */
|
|
@@ -16,7 +16,13 @@ export declare class MemoryStorage implements StorageBackend {
|
|
|
16
16
|
readonly [STORAGE_BACKEND]: true;
|
|
17
17
|
readonly storage: IStorageDriver;
|
|
18
18
|
readonly registry: IRegistryDriver;
|
|
19
|
+
/**
|
|
20
|
+
* Builds an empty in-process backend. `options.now` is the clock for the registry's timestamps, `Date.now` by
|
|
21
|
+
* default; any other option throws `ValidationError`.
|
|
22
|
+
*/
|
|
19
23
|
constructor(options?: MemoryStorageOptions);
|
|
24
|
+
/** Nothing to check: process memory has no lifecycle rules. */
|
|
25
|
+
checkLifecycle(): Promise<LifecycleVerdict>;
|
|
20
26
|
}
|
|
21
27
|
/** Options of {@link LocalFsStorage}: only `now`, the clock for the registry's timestamps. */
|
|
22
28
|
export interface LocalFsStorageOptions {
|
|
@@ -36,10 +42,19 @@ export interface LocalFsStorageOptions {
|
|
|
36
42
|
* one token. Two processes on one root are **not** fenced from each other.
|
|
37
43
|
*/
|
|
38
44
|
export declare class LocalFsStorage implements StorageBackend {
|
|
45
|
+
/** The directory the backend is rooted at. */
|
|
39
46
|
readonly root: string;
|
|
40
47
|
/** Cross-bundle brand, stamped non-enumerably in the constructor so a spread cannot carry it. */
|
|
41
48
|
readonly [STORAGE_BACKEND]: true;
|
|
42
49
|
readonly storage: IStorageDriver;
|
|
43
50
|
readonly registry: IRegistryDriver;
|
|
44
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Builds a backend rooted at `root`, a non-empty directory path. `options.now` is the clock for the registry's
|
|
53
|
+
* timestamps, `Date.now` by default; any other option throws `ValidationError`.
|
|
54
|
+
*/
|
|
55
|
+
constructor(
|
|
56
|
+
/** The directory the backend is rooted at. */
|
|
57
|
+
root: string, options?: LocalFsStorageOptions);
|
|
58
|
+
/** Nothing to check: a local directory has no lifecycle rules. */
|
|
59
|
+
checkLifecycle(): Promise<LifecycleVerdict>;
|
|
45
60
|
}
|