@cloudbitmaps/core 0.8.2 → 0.10.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/NOTICE +4 -4
- package/README.md +28 -6
- package/dist/chunk-BUPDGZRR.js +461 -0
- package/dist/chunk-BUPDGZRR.js.map +7 -0
- package/dist/core/audit.d.ts +127 -0
- package/dist/core/bit-route.d.ts +21 -0
- package/dist/core/blob.d.ts +34 -0
- package/dist/core/budget.d.ts +66 -0
- package/dist/core/codec.d.ts +114 -0
- package/dist/core/concurrency.d.ts +15 -0
- package/dist/core/consistency.d.ts +73 -0
- package/dist/core/cooperative.d.ts +78 -0
- package/dist/core/cost.d.ts +102 -0
- package/dist/core/crbm/crc32c.d.ts +10 -0
- package/dist/core/crbm/format.d.ts +116 -0
- package/dist/core/crbm/reader.d.ts +69 -0
- package/dist/core/crbm/varint.d.ts +13 -0
- package/dist/core/crbm/writer.d.ts +39 -0
- package/dist/core/crbm-storage-source.d.ts +432 -0
- package/dist/core/crypto.d.ts +80 -0
- package/dist/core/determinism.d.ts +42 -0
- package/dist/core/discover.d.ts +75 -0
- package/dist/core/due-index.d.ts +55 -0
- package/dist/core/engine.d.ts +204 -0
- package/dist/core/erase-id.d.ts +141 -0
- package/dist/core/erasure.d.ts +179 -0
- package/dist/core/errors.d.ts +120 -0
- package/dist/core/generation-gc.d.ts +52 -0
- package/dist/core/keys.d.ts +23 -0
- package/dist/core/load.d.ts +102 -0
- package/dist/core/lru.d.ts +69 -0
- package/dist/core/metrics.d.ts +132 -0
- package/dist/core/name-codec.d.ts +79 -0
- package/dist/core/pinned-storage-source.d.ts +69 -0
- package/dist/core/ports.d.ts +340 -0
- package/dist/core/registry-scan.d.ts +38 -0
- package/dist/core/retention-sweep.d.ts +190 -0
- package/dist/core/retention.d.ts +67 -0
- package/dist/core/retry.d.ts +65 -0
- package/dist/core/rollback.d.ts +71 -0
- package/dist/core/validate.d.ts +20 -0
- package/dist/driver-kit.d.ts +10 -0
- package/dist/driver-kit.js +314 -0
- package/dist/driver-kit.js.map +7 -0
- package/dist/drivers/_shared/aws-errors.d.ts +18 -0
- package/dist/drivers/_shared/keys.d.ts +27 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
- package/dist/drivers/_shared/object-registry.d.ts +84 -0
- package/dist/drivers/_shared/registry.d.ts +62 -0
- package/dist/drivers/backends.d.ts +49 -0
- package/dist/drivers/crypto.d.ts +38 -0
- package/dist/drivers/localfs/fs-util.d.ts +20 -0
- package/dist/drivers/localfs/paths.d.ts +33 -0
- package/dist/drivers/localfs/registry.d.ts +28 -0
- package/dist/drivers/localfs/storage.d.ts +19 -0
- package/dist/drivers/memory.d.ts +57 -0
- package/dist/drivers/retry/retrying-drivers.d.ts +90 -0
- package/dist/export/index.d.ts +92 -0
- package/dist/index.d.ts +85 -2534
- package/dist/index.js +3076 -2978
- package/dist/index.js.map +7 -1
- package/dist/testing/fuzz-core.d.ts +14 -0
- package/package.json +19 -115
- package/dist/azure/index.cjs +0 -368
- package/dist/azure/index.cjs.map +0 -1
- package/dist/azure/index.d.cts +0 -48
- package/dist/azure/index.d.ts +0 -48
- package/dist/azure/index.js +0 -304
- package/dist/azure/index.js.map +0 -1
- package/dist/cassandra/index.cjs +0 -265
- package/dist/cassandra/index.cjs.map +0 -1
- package/dist/cassandra/index.d.cts +0 -52
- package/dist/cassandra/index.d.ts +0 -52
- package/dist/cassandra/index.js +0 -204
- package/dist/cassandra/index.js.map +0 -1
- package/dist/chunk-7LMLYSVJ.js +0 -43
- package/dist/chunk-7LMLYSVJ.js.map +0 -1
- package/dist/chunk-L54FL5DW.js +0 -203
- package/dist/chunk-L54FL5DW.js.map +0 -1
- package/dist/chunk-NUIDEEFZ.js +0 -91
- package/dist/chunk-NUIDEEFZ.js.map +0 -1
- package/dist/chunk-SNJVZ227.js +0 -35
- package/dist/chunk-SNJVZ227.js.map +0 -1
- package/dist/chunk-YEHMG6W2.js +0 -6
- package/dist/chunk-YEHMG6W2.js.map +0 -1
- package/dist/dynamodb/index.cjs +0 -740
- package/dist/dynamodb/index.cjs.map +0 -1
- package/dist/dynamodb/index.d.cts +0 -106
- package/dist/dynamodb/index.d.ts +0 -106
- package/dist/dynamodb/index.js +0 -469
- package/dist/dynamodb/index.js.map +0 -1
- package/dist/gcs/index.cjs +0 -343
- package/dist/gcs/index.cjs.map +0 -1
- package/dist/gcs/index.d.cts +0 -46
- package/dist/gcs/index.d.ts +0 -46
- package/dist/gcs/index.js +0 -279
- package/dist/gcs/index.js.map +0 -1
- package/dist/index.cjs +0 -4608
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2535
- package/dist/mongodb/index.cjs +0 -260
- package/dist/mongodb/index.cjs.map +0 -1
- package/dist/mongodb/index.d.cts +0 -45
- package/dist/mongodb/index.d.ts +0 -45
- package/dist/mongodb/index.js +0 -199
- package/dist/mongodb/index.js.map +0 -1
- package/dist/mysql/index.cjs +0 -281
- package/dist/mysql/index.cjs.map +0 -1
- package/dist/mysql/index.d.cts +0 -56
- package/dist/mysql/index.d.ts +0 -56
- package/dist/mysql/index.js +0 -211
- package/dist/mysql/index.js.map +0 -1
- package/dist/ports-5BQTGKze.d.cts +0 -356
- package/dist/ports-5BQTGKze.d.ts +0 -356
- package/dist/postgres/index.cjs +0 -273
- package/dist/postgres/index.cjs.map +0 -1
- package/dist/postgres/index.d.cts +0 -54
- package/dist/postgres/index.d.ts +0 -54
- package/dist/postgres/index.js +0 -203
- package/dist/postgres/index.js.map +0 -1
- package/dist/redis/index.cjs +0 -257
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -42
- package/dist/redis/index.d.ts +0 -42
- package/dist/redis/index.js +0 -197
- package/dist/redis/index.js.map +0 -1
- package/dist/s3/index.cjs +0 -891
- package/dist/s3/index.cjs.map +0 -1
- package/dist/s3/index.d.cts +0 -112
- package/dist/s3/index.d.ts +0 -112
- package/dist/s3/index.js +0 -555
- package/dist/s3/index.js.map +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `withRetry` — the one retry/backoff primitive.
|
|
3
|
+
*
|
|
4
|
+
* A pure, storage-agnostic helper: it knows nothing about HTTP, AWS, or any SDK. It retries an operation
|
|
5
|
+
* while the operation keeps failing with a **retryable** error (by default {@link TransientError}), waiting
|
|
6
|
+
* a bounded, jittered, exponentially-growing delay between attempts. Time and randomness are **injected**
|
|
7
|
+
* (the determinism seam) — production wires a `setTimeout`-backed clock + a real RNG; the
|
|
8
|
+
* tests wire virtual ones — so a backoff schedule is replayable from a seed and unit tests run with no
|
|
9
|
+
* real sleeping.
|
|
10
|
+
*
|
|
11
|
+
* It deliberately does **not** retry deterministic failures ({@link WriteConflictError},
|
|
12
|
+
* {@link ValidationError}, {@link IntegrityError}, {@link NotFoundError}, …): retrying those either can't
|
|
13
|
+
* help or would be incorrect. A pointer conflict is retried by a *separate* loop inside
|
|
14
|
+
* `publishGeneration`, because each attempt must re-read the row and re-decide — a blind replay of the same
|
|
15
|
+
* compare-and-swap would either fail again on a stale token or, worse, advance a pointer whose state has
|
|
16
|
+
* changed underneath it. A write-once object collision is never replayed at all: the generation number is
|
|
17
|
+
* taken, so the caller has to pick a new one.
|
|
18
|
+
*/
|
|
19
|
+
import type { Clock, Rng } from './determinism.js';
|
|
20
|
+
export interface RetryPolicy {
|
|
21
|
+
/** Total attempts including the first (so `maxAttempts: 4` = 1 try + 3 retries). Must be ≥ 1. */
|
|
22
|
+
readonly maxAttempts: number;
|
|
23
|
+
/** Delay before the first retry, in ms. Grows by `backoffFactor` each subsequent retry. */
|
|
24
|
+
readonly baseDelayMs: number;
|
|
25
|
+
/** Upper bound on a single (pre-jitter) delay, in ms — caps the exponential growth. */
|
|
26
|
+
readonly maxDelayMs: number;
|
|
27
|
+
/** Exponential growth factor between retries (e.g. 2 ⇒ base, 2·base, 4·base, …). */
|
|
28
|
+
readonly backoffFactor: number;
|
|
29
|
+
/**
|
|
30
|
+
* `'full'` ⇒ the actual wait is uniform in `[0, computed]` (AWS-recommended full jitter: decorrelates
|
|
31
|
+
* concurrent retriers so they don't reconverge into a thundering herd). `'none'` ⇒ wait exactly `computed`.
|
|
32
|
+
*/
|
|
33
|
+
readonly jitter: 'full' | 'none';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Conservative defaults: 4 attempts, 50ms → 100 → 200 (×2), capped at 2s, full jitter. Tuned for a cloud
|
|
37
|
+
* backend's brief throttle/5xx blip — enough to ride out a transient fault without turning a hard outage
|
|
38
|
+
* into a long hang. Override per store/driver if your latency budget differs.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DEFAULT_RETRY_POLICY: RetryPolicy;
|
|
41
|
+
export interface RetryDeps {
|
|
42
|
+
readonly clock: Clock;
|
|
43
|
+
readonly rng: Rng;
|
|
44
|
+
/** Override which errors are retryable. Default: any {@link TransientError} (incl. `TimeoutError`). */
|
|
45
|
+
readonly isRetryable?: (err: unknown) => boolean;
|
|
46
|
+
/** Optional hook (observability) fired before each backoff wait. `attempt` is 1-based (the one that failed). */
|
|
47
|
+
readonly onRetry?: (info: {
|
|
48
|
+
attempt: number;
|
|
49
|
+
delayMs: number;
|
|
50
|
+
err: unknown;
|
|
51
|
+
}) => void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Backoff delay (ms) for the retry that follows a given 1-based attempt, before jitter is applied. Exposed
|
|
55
|
+
* for tests. `attempt` 1 ⇒ `baseDelayMs`, 2 ⇒ `base·factor`, … capped at `maxDelayMs`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function backoffDelayMs(policy: RetryPolicy, attempt: number): number;
|
|
58
|
+
/** Apply the policy's jitter to a computed delay using the injected RNG. */
|
|
59
|
+
export declare function applyJitter(policy: RetryPolicy, delayMs: number, rng: Rng): number;
|
|
60
|
+
/**
|
|
61
|
+
* Run `op`, retrying transient failures per `policy`. Resolves with `op`'s result, or rejects with the last
|
|
62
|
+
* error once attempts are exhausted (or immediately for a non-retryable error). The thrown error is always
|
|
63
|
+
* the *operation's* error — never a wrapper — so callers keep their typed-error branching.
|
|
64
|
+
*/
|
|
65
|
+
export declare function withRetry<T>(op: () => Promise<T>, policy: RetryPolicy, deps: RetryDeps): Promise<T>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `listGenerations` and `rollbackSegment` — see what a segment has been, and put it back.
|
|
3
|
+
*
|
|
4
|
+
* Immutable generations mean the previous version of a segment is usually still sitting in the bucket: a load
|
|
5
|
+
* that replaced it did not overwrite anything, it wrote a new object and moved a pointer. So recovering from a
|
|
6
|
+
* bad load is, in principle, moving the pointer back — and until now there was no way to do it, because every
|
|
7
|
+
* write path in the library is deliberately **forward-only** and refuses a regression.
|
|
8
|
+
*
|
|
9
|
+
* That refusal is right for a *writer*: a load whose ids came from upstream loses nothing by being out-raced, and
|
|
10
|
+
* letting it regress the pointer would let a slow loader silently undo a fast one. It is wrong for an *operator*,
|
|
11
|
+
* who has looked at the segment, decided the current generation is wrong, and knows which one they want. So
|
|
12
|
+
* rollback is the one call that moves the pointer backwards, and it is deliberately not reachable from any
|
|
13
|
+
* automatic path — no sweep, no retry, no reconciliation calls it.
|
|
14
|
+
*
|
|
15
|
+
* It is audited for the same reason. Every other pointer move can be reconstructed from "a load happened"; this
|
|
16
|
+
* one is a human overriding the ordering rule the rest of the system relies on, which is exactly the event an
|
|
17
|
+
* Art. 30 record or an incident review wants to find.
|
|
18
|
+
*/
|
|
19
|
+
import { type IAuditSink } from './audit.js';
|
|
20
|
+
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
21
|
+
/** What the generation helpers need: the objects, and the pointer that says which one is current. */
|
|
22
|
+
export interface GenerationListDeps {
|
|
23
|
+
readonly storage: IStorageDriver;
|
|
24
|
+
readonly registry: IRegistryDriver;
|
|
25
|
+
}
|
|
26
|
+
/** One generation present in the bucket. */
|
|
27
|
+
export interface GenerationEntry {
|
|
28
|
+
readonly generation: number;
|
|
29
|
+
/** Whether the registry pointer currently names this generation. */
|
|
30
|
+
readonly current: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every generation still present for a segment, ascending, with the current one marked.
|
|
34
|
+
*
|
|
35
|
+
* This is what the bucket holds, not what the segment has ever been: generation collection deletes superseded
|
|
36
|
+
* objects, so the list is the grace window plus whatever has not been collected yet. It is the set a
|
|
37
|
+
* {@link rollbackSegment} can choose from, which is the reason to look at it.
|
|
38
|
+
*
|
|
39
|
+
* One `list` call. It does not open the objects, so it costs nothing per generation and tells you nothing about
|
|
40
|
+
* their contents — read the sizes through the store's own report if you need them.
|
|
41
|
+
*/
|
|
42
|
+
export declare function listGenerations(ref: SegmentRef, deps: GenerationListDeps): Promise<GenerationEntry[]>;
|
|
43
|
+
export interface RollbackResult {
|
|
44
|
+
/** The generation the pointer named before the rollback. `null` when the segment had no current generation. */
|
|
45
|
+
readonly fromGeneration: number | null;
|
|
46
|
+
/** The generation the pointer names now. */
|
|
47
|
+
readonly generation: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Move a segment's pointer **back** to a generation that is still in the bucket.
|
|
51
|
+
*
|
|
52
|
+
* The only non-forward-only publish in the library, and the only one an operator has to ask for by name. It
|
|
53
|
+
* refuses rather than guesses:
|
|
54
|
+
*
|
|
55
|
+
* - a generation whose object is **not in the bucket** — collected, or never written — is refused with
|
|
56
|
+
* `NotFoundError` rather than pointed at, because a pointer naming a missing object is the one state the whole
|
|
57
|
+
* design exists to avoid;
|
|
58
|
+
* - a **crypto-shredded** segment is refused: every generation of it is unreadable, so a rollback would produce a
|
|
59
|
+
* segment that resolves and then fails;
|
|
60
|
+
* - rolling to the generation **already current** is a no-op that reports itself, not an error.
|
|
61
|
+
*
|
|
62
|
+
* What it does **not** do is delete anything. The generations above the new pointer stay in the bucket, which is
|
|
63
|
+
* what makes the rollback reversible — roll forward again by naming one of them. They are also then *above*
|
|
64
|
+
* `currentGen`, where generation collection never looks, so they will sit there until a later load raises the
|
|
65
|
+
* pointer past them or they are deleted deliberately. That is a deliberate trade: an operator who has just
|
|
66
|
+
* undone a bad load should not have the evidence collected out from under them.
|
|
67
|
+
*/
|
|
68
|
+
export declare function rollbackSegment(ref: SegmentRef, toGeneration: number, deps: GenerationListDeps, options?: {
|
|
69
|
+
audit?: IAuditSink;
|
|
70
|
+
allowForward?: boolean;
|
|
71
|
+
}): Promise<RollbackResult>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChunkRef, SegmentRef } from './ports.js';
|
|
2
|
+
/**
|
|
3
|
+
* The most an encoded name may occupy in a physical key.
|
|
4
|
+
*
|
|
5
|
+
* S3's limit is 1024 bytes for the WHOLE key, which also carries the caller's prefix, the namespace, a fixed
|
|
6
|
+
* infix (`/segments/`) and the `.<generation>.crbm` suffix. 256 leaves generous room for all of that with both
|
|
7
|
+
* a namespace and a segment at the ceiling, and matches the limit the previous grammar advertised, so no name
|
|
8
|
+
* that was legal before becomes illegal now.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_NAME_LENGTH = 256;
|
|
11
|
+
/**
|
|
12
|
+
* How long a name is once storage has it — the longer of the two encodings.
|
|
13
|
+
*
|
|
14
|
+
* Exported because anything that composes a name out of other names (the due index does) has to measure it the
|
|
15
|
+
* same way, and a second copy of that rule is how the two drifted apart before.
|
|
16
|
+
*/
|
|
17
|
+
export declare function encodedNameLength(value: string): number;
|
|
18
|
+
export declare function validateSegmentRef(ref: SegmentRef): void;
|
|
19
|
+
/** Validate a chunk ref: the segment/namespace rules plus `chunkKey` ∈ `[0, 65535]` (a u16). */
|
|
20
|
+
export declare function validateChunkRef(ref: ChunkRef): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, } from './core/ports.js';
|
|
2
|
+
export { brandAsBackend, STORAGE_BACKEND } from './core/ports.js';
|
|
3
|
+
export { IntegrityError, isNotFoundError, isValidationError, isWriteConflictError, NotFoundError, TransientError, ValidationError, WriteConflictError, } from './core/errors.js';
|
|
4
|
+
export { validateSegmentRef } from './core/validate.js';
|
|
5
|
+
export type { BlobSink } from './core/blob.js';
|
|
6
|
+
export { MAX_ROW_BYTES, ObjectStoreRegistry, ObjectVersionRaced, } from './drivers/_shared/object-registry.js';
|
|
7
|
+
export type { ObjectRegistryStore, ObjectRow } from './drivers/_shared/object-registry.js';
|
|
8
|
+
export { encodeNameForKey, namespaceKeyPart } from './drivers/_shared/keys.js';
|
|
9
|
+
export { normalizeObjectPrefix, parseRegistryKey, prefixPart, registryListPrefix, registryObjectKey, registryPrefix, } from './drivers/_shared/object-registry-keys.js';
|
|
10
|
+
export { errorName, httpStatus, isNetworkOrTimeout, isSdkRetryable, isServerSide, } from './drivers/_shared/aws-errors.js';
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_NAMESPACE,
|
|
3
|
+
IntegrityError,
|
|
4
|
+
NotFoundError,
|
|
5
|
+
STORAGE_BACKEND,
|
|
6
|
+
TransientError,
|
|
7
|
+
ValidationError,
|
|
8
|
+
WriteConflictError,
|
|
9
|
+
applyRegistryPatch,
|
|
10
|
+
brandAsBackend,
|
|
11
|
+
decodeNameFromKey,
|
|
12
|
+
encodeNameForKey,
|
|
13
|
+
isNotFoundError,
|
|
14
|
+
isValidationError,
|
|
15
|
+
isWriteConflictError,
|
|
16
|
+
mapWithConcurrency,
|
|
17
|
+
namespaceKeyPart,
|
|
18
|
+
parseRegistryEnvelope,
|
|
19
|
+
recordFromNew,
|
|
20
|
+
registryCounterOf,
|
|
21
|
+
serializeRegistryEnvelope,
|
|
22
|
+
validateNewRegistryRecord,
|
|
23
|
+
validateRegistryPatch,
|
|
24
|
+
validateSegmentRef
|
|
25
|
+
} from "./chunk-BUPDGZRR.js";
|
|
26
|
+
|
|
27
|
+
// src/drivers/_shared/object-registry-keys.ts
|
|
28
|
+
var REGISTRY_SUFFIX = ".reg";
|
|
29
|
+
function prefixPart(prefix) {
|
|
30
|
+
if (prefix === void 0) return "";
|
|
31
|
+
const trimmed = prefix.replace(/^\/+|\/+$/g, "");
|
|
32
|
+
return trimmed === "" ? "" : `${trimmed}/`;
|
|
33
|
+
}
|
|
34
|
+
function normalizeObjectPrefix(prefix) {
|
|
35
|
+
if (prefix === void 0) return void 0;
|
|
36
|
+
for (const ch of prefix) {
|
|
37
|
+
const code = ch.charCodeAt(0);
|
|
38
|
+
if (code < 32 || code === 127) {
|
|
39
|
+
throw new ValidationError("prefix must not contain control characters");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (prefix.includes("\\")) {
|
|
43
|
+
throw new ValidationError(
|
|
44
|
+
`prefix must not contain backslashes (a path separator on some backends): ${JSON.stringify(prefix)}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
let decoded;
|
|
48
|
+
try {
|
|
49
|
+
decoded = decodeURIComponent(prefix);
|
|
50
|
+
} catch {
|
|
51
|
+
decoded = prefix;
|
|
52
|
+
}
|
|
53
|
+
for (const source of decoded === prefix ? [prefix] : [prefix, decoded]) {
|
|
54
|
+
for (const segment of source.split("/")) {
|
|
55
|
+
if (segment === "." || segment === "..") {
|
|
56
|
+
throw new ValidationError(
|
|
57
|
+
`prefix must not contain "." or ".." path segments: ${JSON.stringify(prefix)}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return prefix;
|
|
63
|
+
}
|
|
64
|
+
function registryPrefix(prefix) {
|
|
65
|
+
return `${prefixPart(prefix)}registry/`;
|
|
66
|
+
}
|
|
67
|
+
function registryObjectKey(prefix, ref) {
|
|
68
|
+
validateSegmentRef(ref);
|
|
69
|
+
return `${registryPrefix(prefix)}${namespaceKeyPart(ref.namespace)}/${encodeNameForKey(ref.segment)}${REGISTRY_SUFFIX}`;
|
|
70
|
+
}
|
|
71
|
+
function registryListPrefix(prefix, namespace) {
|
|
72
|
+
const base = registryPrefix(prefix);
|
|
73
|
+
return namespace === void 0 ? base : `${base}${namespaceKeyPart(namespace)}/`;
|
|
74
|
+
}
|
|
75
|
+
function parseRegistryKey(prefix, objectKey) {
|
|
76
|
+
const base = registryPrefix(prefix);
|
|
77
|
+
if (!objectKey.startsWith(base) || !objectKey.endsWith(REGISTRY_SUFFIX)) return null;
|
|
78
|
+
const rest = objectKey.slice(base.length, objectKey.length - REGISTRY_SUFFIX.length);
|
|
79
|
+
const slash = rest.indexOf("/");
|
|
80
|
+
if (slash < 0) return null;
|
|
81
|
+
const nsPart = rest.slice(0, slash);
|
|
82
|
+
const encodedSegment = rest.slice(slash + 1);
|
|
83
|
+
if (encodedSegment.length === 0 || encodedSegment.includes("/")) return null;
|
|
84
|
+
const segment = decodeNameFromKey(encodedSegment);
|
|
85
|
+
const namespace = nsPart === DEFAULT_NAMESPACE ? void 0 : decodeNameFromKey(nsPart);
|
|
86
|
+
if (encodeNameForKey(segment) !== encodedSegment) return null;
|
|
87
|
+
if (namespace !== void 0 && encodeNameForKey(namespace) !== nsPart) return null;
|
|
88
|
+
const ref = { segment, namespace };
|
|
89
|
+
try {
|
|
90
|
+
validateSegmentRef(ref);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return ref;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/drivers/_shared/object-registry.ts
|
|
98
|
+
var MAX_ROW_BYTES = 1 * 1024 * 1024;
|
|
99
|
+
var MAX_DELETE_ATTEMPTS = 8;
|
|
100
|
+
var MAX_READ_ATTEMPTS = 8;
|
|
101
|
+
var LIST_READ_CONCURRENCY = 16;
|
|
102
|
+
var ObjectVersionRaced = class extends Error {
|
|
103
|
+
constructor(objectKey) {
|
|
104
|
+
super(`registry object was overwritten mid-read: ${objectKey}`);
|
|
105
|
+
this.name = "ObjectVersionRaced";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var ObjectStoreRegistry = class {
|
|
109
|
+
constructor(store, prefix, now) {
|
|
110
|
+
this.store = store;
|
|
111
|
+
this.prefix = prefix;
|
|
112
|
+
this.now = now;
|
|
113
|
+
}
|
|
114
|
+
store;
|
|
115
|
+
prefix;
|
|
116
|
+
now;
|
|
117
|
+
capabilities() {
|
|
118
|
+
return { strongRead: true };
|
|
119
|
+
}
|
|
120
|
+
async get(ref) {
|
|
121
|
+
const current = await this.readRow(registryObjectKey(this.prefix, ref));
|
|
122
|
+
return current && !current.env.deleted ? current.env.record : null;
|
|
123
|
+
}
|
|
124
|
+
async create(ref, record) {
|
|
125
|
+
validateNewRegistryRecord(record);
|
|
126
|
+
const key = registryObjectKey(this.prefix, ref);
|
|
127
|
+
const current = await this.readRow(key);
|
|
128
|
+
if (current !== null && !current.env.deleted) {
|
|
129
|
+
throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);
|
|
130
|
+
}
|
|
131
|
+
const token = String(current ? registryCounterOf(current.env.record) + 1 : 0);
|
|
132
|
+
const env = {
|
|
133
|
+
deleted: false,
|
|
134
|
+
record: recordFromNew(ref, record, this.now(), token)
|
|
135
|
+
};
|
|
136
|
+
await this.putRow(key, env, current ? { version: current.version } : "absent");
|
|
137
|
+
return { token };
|
|
138
|
+
}
|
|
139
|
+
async compareAndSwap(ref, expected, patch) {
|
|
140
|
+
validateRegistryPatch(patch);
|
|
141
|
+
const key = registryObjectKey(this.prefix, ref);
|
|
142
|
+
const current = await this.readRow(key);
|
|
143
|
+
if (current === null || current.env.deleted || current.env.record.token !== expected) {
|
|
144
|
+
throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
|
|
145
|
+
}
|
|
146
|
+
const token = String(registryCounterOf(current.env.record) + 1);
|
|
147
|
+
const env = {
|
|
148
|
+
deleted: false,
|
|
149
|
+
record: applyRegistryPatch(current.env.record, patch, this.now(), token)
|
|
150
|
+
};
|
|
151
|
+
await this.putRow(key, env, { version: current.version });
|
|
152
|
+
return { token };
|
|
153
|
+
}
|
|
154
|
+
async *list(namespace) {
|
|
155
|
+
const prefix = registryListPrefix(this.prefix, namespace);
|
|
156
|
+
let page = [];
|
|
157
|
+
const flush = async function* (keys) {
|
|
158
|
+
const rows = await mapWithConcurrency(
|
|
159
|
+
keys,
|
|
160
|
+
LIST_READ_CONCURRENCY,
|
|
161
|
+
(key) => this.readRow(key)
|
|
162
|
+
);
|
|
163
|
+
for (const current of rows) {
|
|
164
|
+
if (current && !current.env.deleted) yield current.env.record;
|
|
165
|
+
}
|
|
166
|
+
}.bind(this);
|
|
167
|
+
for await (const key of this.store.listKeys(prefix)) {
|
|
168
|
+
if (parseRegistryKey(this.prefix, key) === null) continue;
|
|
169
|
+
page.push(key);
|
|
170
|
+
if (page.length >= LIST_READ_CONCURRENCY) {
|
|
171
|
+
yield* flush(page);
|
|
172
|
+
page = [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (page.length > 0) yield* flush(page);
|
|
176
|
+
}
|
|
177
|
+
async delete(ref) {
|
|
178
|
+
const key = registryObjectKey(this.prefix, ref);
|
|
179
|
+
for (let attempt = 0; attempt < MAX_DELETE_ATTEMPTS; attempt++) {
|
|
180
|
+
const current = await this.readRow(key);
|
|
181
|
+
if (current === null || current.env.deleted) return;
|
|
182
|
+
const token = String(registryCounterOf(current.env.record) + 1);
|
|
183
|
+
const env = {
|
|
184
|
+
deleted: true,
|
|
185
|
+
record: { ...current.env.record, token, updatedAt: this.now() }
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
await this.putRow(key, env, { version: current.version });
|
|
189
|
+
return;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (isWriteConflictError(err)) continue;
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
throw new WriteConflictError(
|
|
196
|
+
`registry delete: contention tombstoning "${ref.segment}" \u2014 retry`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Read + parse a registry object, returning its envelope and version fence, or null if absent.
|
|
201
|
+
*
|
|
202
|
+
* A store that pins a version to read consistently ({@link ObjectVersionRaced}) loses that pin whenever a
|
|
203
|
+
* concurrent writer lands mid-read. Re-reading is the whole answer — the next read simply observes the
|
|
204
|
+
* newer version — and it is bounded so a row under permanent write saturation fails typed instead of
|
|
205
|
+
* spinning. Only a store racing itself gets here; S3 never does.
|
|
206
|
+
*/
|
|
207
|
+
async readRow(objectKey) {
|
|
208
|
+
const row = await this.readRaced(objectKey);
|
|
209
|
+
if (row === null) return null;
|
|
210
|
+
if (row.bytes.length > MAX_ROW_BYTES) {
|
|
211
|
+
throw new IntegrityError(
|
|
212
|
+
`registry object ${row.bytes.length}B exceeds cap ${MAX_ROW_BYTES}B`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (row.version.length === 0) {
|
|
216
|
+
throw new IntegrityError(
|
|
217
|
+
`registry object has no version fence (needed for compare-and-swap): ${objectKey}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const env = parseRegistryEnvelope(new TextDecoder().decode(row.bytes), objectKey);
|
|
221
|
+
return { env, version: row.version };
|
|
222
|
+
}
|
|
223
|
+
/** {@link ObjectRegistryStore.read}, retrying the bounded number of times a mid-read overwrite allows. */
|
|
224
|
+
async readRaced(objectKey) {
|
|
225
|
+
for (let attempt = 0; attempt < MAX_READ_ATTEMPTS; attempt++) {
|
|
226
|
+
try {
|
|
227
|
+
return await this.store.read(objectKey);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (!(err instanceof ObjectVersionRaced)) throw err;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw new TransientError(
|
|
233
|
+
`registry read: ${this.store.label} object "${objectKey}" was overwritten on every one of ${MAX_READ_ATTEMPTS} attempts \u2014 retry`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
/** Conditional write of an envelope. The store maps a lost precondition to {@link WriteConflictError}. */
|
|
237
|
+
async putRow(objectKey, env, expect) {
|
|
238
|
+
const body = new TextEncoder().encode(serializeRegistryEnvelope(env));
|
|
239
|
+
if (body.length > MAX_ROW_BYTES) {
|
|
240
|
+
throw new ValidationError(`registry object ${body.length}B exceeds cap ${MAX_ROW_BYTES}B`);
|
|
241
|
+
}
|
|
242
|
+
await this.store.write(objectKey, body, expect);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/drivers/_shared/aws-errors.ts
|
|
247
|
+
function httpStatus(err) {
|
|
248
|
+
return err?.$metadata?.httpStatusCode;
|
|
249
|
+
}
|
|
250
|
+
function errorName(err) {
|
|
251
|
+
return err?.name;
|
|
252
|
+
}
|
|
253
|
+
function errorCode(err) {
|
|
254
|
+
return err?.code;
|
|
255
|
+
}
|
|
256
|
+
function isSdkRetryable(err) {
|
|
257
|
+
return err?.$retryable != null;
|
|
258
|
+
}
|
|
259
|
+
function isServerSide(err) {
|
|
260
|
+
const status = httpStatus(err);
|
|
261
|
+
return status !== void 0 && status >= 500 && status <= 599;
|
|
262
|
+
}
|
|
263
|
+
var NETWORK_NAMES = /* @__PURE__ */ new Set([
|
|
264
|
+
"TimeoutError",
|
|
265
|
+
"RequestTimeout",
|
|
266
|
+
"RequestTimeoutException",
|
|
267
|
+
"NetworkingError",
|
|
268
|
+
"AbortError"
|
|
269
|
+
]);
|
|
270
|
+
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
271
|
+
"ETIMEDOUT",
|
|
272
|
+
"ECONNRESET",
|
|
273
|
+
"ECONNREFUSED",
|
|
274
|
+
"EPIPE",
|
|
275
|
+
"ENOTFOUND",
|
|
276
|
+
"EAI_AGAIN",
|
|
277
|
+
"ECONNABORTED"
|
|
278
|
+
]);
|
|
279
|
+
var NETWORK_MESSAGE = /socket hang up|network (error|failure)|(connection|request|socket|operation|read|write)\s+tim(e|ed)\s?out|connection (reset|refused|aborted|closed)/i;
|
|
280
|
+
function isNetworkOrTimeout(err) {
|
|
281
|
+
if (NETWORK_NAMES.has(errorName(err) ?? "")) return true;
|
|
282
|
+
if (NETWORK_CODES.has(errorCode(err) ?? "")) return true;
|
|
283
|
+
return NETWORK_MESSAGE.test(err?.message ?? "");
|
|
284
|
+
}
|
|
285
|
+
export {
|
|
286
|
+
IntegrityError,
|
|
287
|
+
MAX_ROW_BYTES,
|
|
288
|
+
NotFoundError,
|
|
289
|
+
ObjectStoreRegistry,
|
|
290
|
+
ObjectVersionRaced,
|
|
291
|
+
STORAGE_BACKEND,
|
|
292
|
+
TransientError,
|
|
293
|
+
ValidationError,
|
|
294
|
+
WriteConflictError,
|
|
295
|
+
brandAsBackend,
|
|
296
|
+
encodeNameForKey,
|
|
297
|
+
errorName,
|
|
298
|
+
httpStatus,
|
|
299
|
+
isNetworkOrTimeout,
|
|
300
|
+
isNotFoundError,
|
|
301
|
+
isSdkRetryable,
|
|
302
|
+
isServerSide,
|
|
303
|
+
isValidationError,
|
|
304
|
+
isWriteConflictError,
|
|
305
|
+
namespaceKeyPart,
|
|
306
|
+
normalizeObjectPrefix,
|
|
307
|
+
parseRegistryKey,
|
|
308
|
+
prefixPart,
|
|
309
|
+
registryListPrefix,
|
|
310
|
+
registryObjectKey,
|
|
311
|
+
registryPrefix,
|
|
312
|
+
validateSegmentRef
|
|
313
|
+
};
|
|
314
|
+
//# sourceMappingURL=driver-kit.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/drivers/_shared/object-registry-keys.ts", "../src/drivers/_shared/object-registry.ts", "../src/drivers/_shared/aws-errors.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The registry object layout, shared by every object-store registry (S3, GCS, Azure Blob).\n *\n * One tiny JSON object per segment at `<prefix>registry/<ns>/<segment>.reg`, **registry-first** \u2014 separate\n * from the namespace-first storage layout (`<prefix><ns>/segments/\u2026`) \u2014 so discovery is a single list over\n * `registry/` for all namespaces, or `registry/<ns>/` for one, never entangled with the `.crbm` payloads.\n *\n * It lives here rather than in a driver because all three object stores encode names the same way\n * (`encodeNameForKey`, `namespaceKeyPart`) and build byte-identical keys. Three copies of this would be\n * three chances for one cloud's layout to drift from the others \u2014 and a drifted key is not a visible bug, it\n * is a segment that quietly cannot be found by a store pointed at the same bucket.\n */\nimport { ValidationError } from '@/core/errors';\nimport { validateSegmentRef } from '@/core/validate';\nimport type { SegmentRef } from '@/core/ports';\nimport { DEFAULT_NAMESPACE, decodeNameFromKey, encodeNameForKey, namespaceKeyPart } from './keys';\n\nconst REGISTRY_SUFFIX = '.reg';\n\n/** Normalize an optional caller prefix to either `''` or `trimmed/` (no leading/trailing slashes). */\nexport function prefixPart(prefix: string | undefined): string {\n if (prefix === undefined) return '';\n const trimmed = prefix.replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `${trimmed}/`;\n}\n\n/**\n * Validate a caller-supplied key prefix (trusted config, but make it a real containment boundary): reject\n * control characters and `.`/`..` path segments so a prefix cannot traverse out of its intended space.\n * Returns it unchanged \u2014 the key builders normalize slashes.\n *\n * The traversal check covers three spellings, because the tools that walk these buckets do. A bare `..` is\n * the obvious one. A **backslash** segment matters on ADLS Gen2 and to Windows-side tooling, which treat it\n * as a separator this code otherwise would not. And a percent-encoded `..` matters because `gsutil`,\n * `s3fs`, `gcsfuse` and `azcopy` decode a key on their way to a local path \u2014 the same reasoning\n * `name-codec.ts` already applies to segment names, which a prefix has no reason to be exempt from.\n */\nexport function normalizeObjectPrefix(prefix: string | undefined): string | undefined {\n if (prefix === undefined) return undefined;\n for (const ch of prefix) {\n const code = ch.charCodeAt(0);\n // C0 plus DEL: a bare `< 0x20` lets U+007F through, and it is as unprintable as the rest.\n if (code < 0x20 || code === 0x7f) {\n throw new ValidationError('prefix must not contain control characters');\n }\n }\n if (prefix.includes('\\\\')) {\n throw new ValidationError(\n `prefix must not contain backslashes (a path separator on some backends): ${JSON.stringify(prefix)}`,\n );\n }\n // Check the decoded spelling too, so an encoded `..` cannot slip past the literal comparison.\n let decoded: string;\n try {\n decoded = decodeURIComponent(prefix);\n } catch {\n decoded = prefix; // not valid percent-encoding; the literal check still applies\n }\n for (const source of decoded === prefix ? [prefix] : [prefix, decoded]) {\n for (const segment of source.split('/')) {\n if (segment === '.' || segment === '..') {\n throw new ValidationError(\n `prefix must not contain \".\" or \"..\" path segments: ${JSON.stringify(prefix)}`,\n );\n }\n }\n }\n return prefix;\n}\n\n/** The key prefix under which every registry object lives: `<prefix>registry/`. */\nexport function registryPrefix(prefix: string | undefined): string {\n return `${prefixPart(prefix)}registry/`;\n}\n\n/** The full key of one segment's registry object: `<prefix>registry/<ns>/<segment>.reg`. */\nexport function registryObjectKey(prefix: string | undefined, ref: SegmentRef): string {\n validateSegmentRef(ref);\n return `${registryPrefix(prefix)}${namespaceKeyPart(ref.namespace)}/${encodeNameForKey(ref.segment)}${REGISTRY_SUFFIX}`;\n}\n\n/** The list prefix for discovery: registry-wide, or scoped to one namespace. */\nexport function registryListPrefix(prefix: string | undefined, namespace?: string): string {\n const base = registryPrefix(prefix);\n return namespace === undefined ? base : `${base}${namespaceKeyPart(namespace)}/`;\n}\n\n/**\n * Parse a `<prefix>registry/<ns>/<segment>.reg` key back to its {@link SegmentRef}, or `null` if it does not\n * match \u2014 a stray or foreign object under the prefix, or one whose parsed ref fails the round-trip check.\n * `_default` maps back to the absent namespace. A name is percent-encoded on the way in, so no encoded name\n * can contain `/` and the split stays unambiguous whatever the caller named their segment.\n */\nexport function parseRegistryKey(prefix: string | undefined, objectKey: string): SegmentRef | null {\n const base = registryPrefix(prefix);\n if (!objectKey.startsWith(base) || !objectKey.endsWith(REGISTRY_SUFFIX)) return null;\n const rest = objectKey.slice(base.length, objectKey.length - REGISTRY_SUFFIX.length);\n const slash = rest.indexOf('/');\n if (slash < 0) return null;\n const nsPart = rest.slice(0, slash);\n const encodedSegment = rest.slice(slash + 1);\n if (encodedSegment.length === 0 || encodedSegment.includes('/')) return null;\n const segment = decodeNameFromKey(encodedSegment);\n const namespace = nsPart === DEFAULT_NAMESPACE ? undefined : decodeNameFromKey(nsPart);\n // The encoding must ROUND-TRIP, not merely decode: a foreign object placed under our prefix could spell a\n // name two ways, and reporting both would hand a sweep one segment under two identities.\n if (encodeNameForKey(segment) !== encodedSegment) return null;\n if (namespace !== undefined && encodeNameForKey(namespace) !== nsPart) return null;\n const ref: SegmentRef = { segment, namespace };\n try {\n validateSegmentRef(ref); // reject a hostile/foreign key that isn't a valid ref\n } catch {\n return null;\n }\n return ref;\n}\n", "/**\n * `ObjectStoreRegistry` \u2014 an {@link IRegistryDriver} over any object store with conditional writes.\n *\n * Lets a **read-mostly deployment run on one bucket alone** \u2014 storage `.crbm` generations plus the registry in\n * the same place, with no separate database. One tiny JSON object per segment at\n * `<prefix>registry/<ns>/<segment>.reg` holding the `{ deleted, record }` envelope (the same shape LocalFs\n * persists). The OCC token is a monotonic counter, advanced on every mutation and even across a `delete`\n * (which **tombstones** rather than removes the object) so a deleted-then-recreated row never re-issues an\n * old token (ABA-safe) \u2014 identical semantics to the LocalFs registry, so it passes the same conformance suite.\n *\n * **The atomic swap is offloaded to the store's conditional writes.** `create` writes only if absent (or\n * over a tombstone under its version), and `compareAndSwap`/`delete` write only if the object still carries\n * the version we read \u2014 so a concurrent writer between our read and our write loses, and the loss surfaces\n * as {@link WriteConflictError}. No in-process lock is needed (unlike LocalFs): the precondition fences\n * writers *across processes*.\n *\n * **Why this is shared rather than written per cloud.** Every object store worth using has the same two\n * primitives under different names \u2014 S3 `If-None-Match: *` / `If-Match: <etag>`, GCS `ifGenerationMatch: 0`\n * / `ifGenerationMatch: <generation>`, Azure `If-None-Match: *` / `If-Match: <etag>` \u2014 so the only thing\n * that differs is three I/O calls, which is what {@link ObjectRegistryStore} abstracts. The parts that are\n * genuinely hard (the ABA-safe counter, the tombstone, the bounded delete retry, the round-trip key check)\n * exist once. Three copies would be three chances for one cloud to drift, and a drifted registry is not a\n * visible bug \u2014 it is a lost `currentGen` swap.\n *\n * **Deployment requirements** (a backend that violates these silently corrupts the registry):\n * - The store **must honor the conditional write**. One that accepts the precondition header and ignores it\n * degrades compare-and-swap to last-write-wins \u2192 lost `currentGen` swaps.\n * - Reads must be **strongly consistent** \u2014 true of S3 (since 2020), GCS and Azure Blob \u2014 since the registry\n * advertises `strongRead` and generation resolution depends on it.\n * - **Do not apply a lifecycle-expiration rule to the `registry/` prefix.** `delete` tombstones (keeps the\n * object with an advanced counter) for ABA-safety; expiring a tombstone would let a recreate reset the\n * token to 0 and re-issue a stale one.\n *\n * **`list()` is fail-closed, and one bad object stops it for everyone.** An object under the `registry/`\n * prefix whose key parses but whose body does not aborts the whole enumeration \u2014 every namespace, not just\n * the affected segment \u2014 and it stays that way until an operator removes the object. The error names the\n * offending key.\n *\n * That is the deliberate choice, and it is the *safer* one rather than the more convenient one. Skipping the\n * unreadable row would keep discovery running, but `list()` is what tells orphan generation collection which\n * segments exist: a row missing from the enumeration makes its storage `.crbm` generations look unreferenced,\n * and the next GC pass would delete them. Silently skipping turns a parse error into data loss. Refusing to\n * enumerate keeps every sweep off a set it cannot vouch for (invariant 5 \u2014 bytes from the tier are\n * untrusted). **Recovery:** read the named object, and either restore a valid envelope or delete it; nothing\n * else is damaged, and discovery resumes on its own.\n */\nimport {\n IntegrityError,\n TransientError,\n ValidationError,\n WriteConflictError,\n isWriteConflictError,\n} from '@/core/errors';\nimport type {\n IRegistryDriver,\n NewRegistryRecord,\n RegCaps,\n RegistryPatch,\n RegistryRecord,\n SegmentRef,\n Token,\n} from '@/core/ports';\nimport { mapWithConcurrency } from '@/core/concurrency';\nimport {\n applyRegistryPatch,\n parseRegistryEnvelope,\n recordFromNew,\n registryCounterOf,\n serializeRegistryEnvelope,\n validateNewRegistryRecord,\n validateRegistryPatch,\n type RegistryEnvelope,\n} from './registry';\nimport { parseRegistryKey, registryListPrefix, registryObjectKey } from './object-registry-keys';\n\n/** Defensive cap on a single registry object read from storage, before allocation (rows are tiny; ~1 KB). */\nexport const MAX_ROW_BYTES = 1 * 1024 * 1024;\n/** Bounded retry for `delete`'s read\u2192tombstone under cross-process contention (converges; then fails typed). */\nconst MAX_DELETE_ATTEMPTS = 8;\n/** Bounded re-read when a store's pinned version is overwritten mid-read (see {@link ObjectVersionRaced}). */\nconst MAX_READ_ATTEMPTS = 8;\n/** In-flight reads per `list()` page \u2014 turns the serial N+1 into one list plus bounded parallel reads. */\nconst LIST_READ_CONCURRENCY = 16;\n\n/**\n * Raised by a store when the version it pinned for a read vanished before it could fetch the bytes \u2014\n * a concurrent writer overwrote or deleted the object in between.\n *\n * This is deliberately **neither** of the two things it superficially resembles. It is not absence: the row\n * is very likely still live, just one version further on. And it is not a write conflict: the caller may be\n * a read-only `get()`, which has nothing to conflict with. Both of those mistakes were made \u2014 one per cloud \u2014\n * before this type existed, which is exactly why the signal is now part of the port rather than left to each\n * store's judgement. {@link ObjectStoreRegistry} answers it the only way that is correct for every caller:\n * it reads again.\n *\n * Stores whose read is atomic (S3 serves bytes and `ETag` from one `GetObject`) can never raise it.\n */\nexport class ObjectVersionRaced extends Error {\n constructor(objectKey: string) {\n super(`registry object was overwritten mid-read: ${objectKey}`);\n this.name = 'ObjectVersionRaced';\n }\n}\n\n/** One object as read, with the opaque version that fences a later conditional write. */\nexport interface ObjectRow {\n readonly bytes: Uint8Array;\n /** The store's version fence \u2014 an S3/Azure ETag, a GCS generation. Opaque to this class. */\n readonly version: string;\n}\n\n/**\n * The three I/O calls an object store must provide to host a registry. Everything else \u2014 the OCC counter,\n * tombstones, the delete retry, key parsing \u2014 is implemented once above.\n */\nexport interface ObjectRegistryStore {\n /** A label for error messages, e.g. `GCS` or `Azure Blob`. */\n readonly label: string;\n /**\n * Read one object, or `null` when it is absent.\n *\n * **`null` means the object does not exist** \u2014 nothing weaker. A store that reads in two round trips\n * (metadata for the version fence, then the bytes pinned to it) can find that version already gone; it\n * MUST report that with {@link ObjectVersionRaced} rather than `null` or {@link WriteConflictError}, and\n * the caller re-reads. Retryable faults throw {@link TransientError}.\n *\n * `bytes` and `version` MUST describe the **same** observation of the object: `version` is the fence a\n * later conditional write is conditioned on, so a pair drawn from two different versions would either\n * fail a write that should have succeeded or, worse, pass one that should not.\n */\n read(key: string): Promise<ObjectRow | null>;\n /**\n * Write one object under a precondition: `'absent'` means create-only, a version means\n * compare-and-swap against exactly that version. A lost race MUST throw {@link WriteConflictError}.\n */\n write(key: string, body: Uint8Array, expect: 'absent' | { version: string }): Promise<void>;\n /** Every object key under `prefix`, paginated internally. */\n listKeys(prefix: string): AsyncIterable<string>;\n}\n\nexport class ObjectStoreRegistry implements IRegistryDriver {\n constructor(\n private readonly store: ObjectRegistryStore,\n private readonly prefix: string | undefined,\n private readonly now: () => number,\n ) {}\n\n capabilities(): RegCaps {\n return { strongRead: true };\n }\n\n async get(ref: SegmentRef): Promise<RegistryRecord | null> {\n const current = await this.readRow(registryObjectKey(this.prefix, ref));\n return current && !current.env.deleted ? current.env.record : null;\n }\n\n async create(ref: SegmentRef, record: NewRegistryRecord): Promise<{ token: Token }> {\n validateNewRegistryRecord(record);\n const key = registryObjectKey(this.prefix, ref);\n const current = await this.readRow(key);\n if (current !== null && !current.env.deleted) {\n throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);\n }\n // Advance across a tombstone so a recreate never re-issues an old token (ABA-safe).\n const token = String(current ? registryCounterOf(current.env.record) + 1 : 0);\n const env: RegistryEnvelope = {\n deleted: false,\n record: recordFromNew(ref, record, this.now(), token),\n };\n // Create-only when truly absent; overwrite the tombstone under its version when recreating. Either way\n // a concurrent create loses the precondition and surfaces as a conflict.\n await this.putRow(key, env, current ? { version: current.version } : 'absent');\n return { token };\n }\n\n async compareAndSwap(\n ref: SegmentRef,\n expected: Token,\n patch: RegistryPatch,\n ): Promise<{ token: Token }> {\n validateRegistryPatch(patch);\n const key = registryObjectKey(this.prefix, ref);\n const current = await this.readRow(key);\n if (current === null || current.env.deleted || current.env.record.token !== expected) {\n throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);\n }\n const token = String(registryCounterOf(current.env.record) + 1);\n const env: RegistryEnvelope = {\n deleted: false,\n record: applyRegistryPatch(current.env.record, patch, this.now(), token),\n };\n // The version we read fences a concurrent writer between that read and this write.\n await this.putRow(key, env, { version: current.version });\n return { token };\n }\n\n async *list(namespace?: string): AsyncIterable<RegistryRecord> {\n const prefix = registryListPrefix(this.prefix, namespace);\n // One list plus bounded parallel reads. A serial await-in-loop made this O(N) sequential round trips.\n let page: string[] = [];\n const flush = async function* (\n this: ObjectStoreRegistry,\n keys: string[],\n ): AsyncIterable<RegistryRecord> {\n const rows = await mapWithConcurrency(keys, LIST_READ_CONCURRENCY, (key) =>\n this.readRow(key),\n );\n for (const current of rows) {\n if (current && !current.env.deleted) yield current.env.record;\n }\n }.bind(this);\n\n for await (const key of this.store.listKeys(prefix)) {\n if (parseRegistryKey(this.prefix, key) === null) continue; // stray or foreign object\n page.push(key);\n if (page.length >= LIST_READ_CONCURRENCY) {\n yield* flush(page);\n page = [];\n }\n }\n if (page.length > 0) yield* flush(page);\n }\n\n async delete(ref: SegmentRef): Promise<void> {\n const key = registryObjectKey(this.prefix, ref);\n // Tombstone (advance the counter) rather than remove the object \u2014 keeps the token monotonic for\n // ABA-safety. Retry the read\u2192tombstone on a cross-process race; it converges, then fails typed rather\n // than silently leaving the row live.\n for (let attempt = 0; attempt < MAX_DELETE_ATTEMPTS; attempt++) {\n const current = await this.readRow(key);\n if (current === null || current.env.deleted) return; // idempotent \u2014 already gone\n const token = String(registryCounterOf(current.env.record) + 1);\n const env: RegistryEnvelope = {\n deleted: true,\n record: { ...current.env.record, token, updatedAt: this.now() },\n };\n try {\n await this.putRow(key, env, { version: current.version });\n return;\n } catch (err) {\n if (isWriteConflictError(err)) continue; // raced \u2014 re-read and re-tombstone\n throw err;\n }\n }\n throw new WriteConflictError(\n `registry delete: contention tombstoning \"${ref.segment}\" \u2014 retry`,\n );\n }\n\n /**\n * Read + parse a registry object, returning its envelope and version fence, or null if absent.\n *\n * A store that pins a version to read consistently ({@link ObjectVersionRaced}) loses that pin whenever a\n * concurrent writer lands mid-read. Re-reading is the whole answer \u2014 the next read simply observes the\n * newer version \u2014 and it is bounded so a row under permanent write saturation fails typed instead of\n * spinning. Only a store racing itself gets here; S3 never does.\n */\n private async readRow(\n objectKey: string,\n ): Promise<{ env: RegistryEnvelope; version: string } | null> {\n const row = await this.readRaced(objectKey);\n if (row === null) return null;\n if (row.bytes.length > MAX_ROW_BYTES) {\n throw new IntegrityError(\n `registry object ${row.bytes.length}B exceeds cap ${MAX_ROW_BYTES}B`,\n );\n }\n if (row.version.length === 0) {\n // The version is the OCC fence for CAS; a backend that omits it cannot be used safely as a registry.\n throw new IntegrityError(\n `registry object has no version fence (needed for compare-and-swap): ${objectKey}`,\n );\n }\n const env = parseRegistryEnvelope(new TextDecoder().decode(row.bytes), objectKey);\n return { env, version: row.version };\n }\n\n /** {@link ObjectRegistryStore.read}, retrying the bounded number of times a mid-read overwrite allows. */\n private async readRaced(objectKey: string): Promise<ObjectRow | null> {\n for (let attempt = 0; attempt < MAX_READ_ATTEMPTS; attempt++) {\n try {\n return await this.store.read(objectKey);\n } catch (err) {\n if (!(err instanceof ObjectVersionRaced)) throw err;\n }\n }\n throw new TransientError(\n `registry read: ${this.store.label} object \"${objectKey}\" was overwritten on every one of ` +\n `${MAX_READ_ATTEMPTS} attempts \u2014 retry`,\n );\n }\n\n /** Conditional write of an envelope. The store maps a lost precondition to {@link WriteConflictError}. */\n private async putRow(\n objectKey: string,\n env: RegistryEnvelope,\n expect: 'absent' | { version: string },\n ): Promise<void> {\n const body = new TextEncoder().encode(serializeRegistryEnvelope(env));\n if (body.length > MAX_ROW_BYTES) {\n // Belt-and-braces: the governance/DEK fields are already capped by validate*, so this cannot fire for\n // valid input \u2014 but never write a row that would later be unreadable.\n throw new ValidationError(`registry object ${body.length}B exceeds cap ${MAX_ROW_BYTES}B`);\n }\n await this.store.write(objectKey, body, expect);\n }\n}\n", "/**\n * Shared, SDK-free helpers for classifying AWS-style errors (used by the S3 drivers).\n *\n * These only read structural shapes an AWS SDK v3 error carries \u2014 `name`, `$metadata.httpStatusCode`, a\n * lower-level `code`/`errno`, and the SDK's own `$retryable` marker \u2014 so the (subtle, easy-to-get-wrong)\n * transient-vs-fatal decision is unit-testable without a live backend or even the SDK installed. They live\n * under `drivers/_shared` (part of the SDK-free core bundle): a driver may import them; they import no SDK.\n */\n\nexport function httpStatus(err: unknown): number | undefined {\n return (err as { $metadata?: { httpStatusCode?: number } } | null)?.$metadata?.httpStatusCode;\n}\n\nexport function errorName(err: unknown): string | undefined {\n return (err as { name?: string } | null)?.name;\n}\n\n/** A lower-level transport code (e.g. `ECONNRESET`) \u2014 the Node networking layer sets `code`. */\nexport function errorCode(err: unknown): string | undefined {\n return (err as { code?: string } | null)?.code;\n}\n\n/** The AWS SDK v3 tags retryable errors with a `$retryable` object (throttling faults carry `.throttling`). */\nexport function isSdkRetryable(err: unknown): boolean {\n return (err as { $retryable?: unknown } | null)?.$retryable != null;\n}\n\n/** Any 5xx is a server-side fault that's safe to retry (the request didn't deterministically fail). */\nexport function isServerSide(err: unknown): boolean {\n const status = httpStatus(err);\n return status !== undefined && status >= 500 && status <= 599;\n}\n\nconst NETWORK_NAMES = new Set([\n 'TimeoutError',\n 'RequestTimeout',\n 'RequestTimeoutException',\n 'NetworkingError',\n 'AbortError',\n]);\nconst NETWORK_CODES = new Set([\n 'ETIMEDOUT',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'EPIPE',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'ECONNABORTED',\n]);\n\n// Message-text fallback for when the structural signals (name/code/$metadata/$retryable) are absent. Kept\n// SPECIFIC on purpose: a loose `/timed? ?out/` matches deterministic messages like \"value timed out of\n// range\" and would wrongly retry them, so we only match timeout/network phrases anchored to a transport word\n// (connection/request/socket/read/write) plus the unambiguous standalone phrases.\nconst NETWORK_MESSAGE =\n /socket hang up|network (error|failure)|(connection|request|socket|operation|read|write)\\s+tim(e|ed)\\s?out|connection (reset|refused|aborted|closed)/i;\n\n/** A dropped/timed-out connection \u2014 transient by nature; a retry on a fresh connection usually succeeds. */\nexport function isNetworkOrTimeout(err: unknown): boolean {\n if (NETWORK_NAMES.has(errorName(err) ?? '')) return true;\n if (NETWORK_CODES.has(errorCode(err) ?? '')) return true;\n return NETWORK_MESSAGE.test((err as { message?: string } | null)?.message ?? '');\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBA,IAAM,kBAAkB;AAGjB,SAAS,WAAW,QAAoC;AAC7D,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,UAAU,OAAO,QAAQ,cAAc,EAAE;AAC/C,SAAO,YAAY,KAAK,KAAK,GAAG,OAAO;AACzC;AAaO,SAAS,sBAAsB,QAAgD;AACpF,MAAI,WAAW,OAAW,QAAO;AACjC,aAAW,MAAM,QAAQ;AACvB,UAAM,OAAO,GAAG,WAAW,CAAC;AAE5B,QAAI,OAAO,MAAQ,SAAS,KAAM;AAChC,YAAM,IAAI,gBAAgB,4CAA4C;AAAA,IACxE;AAAA,EACF;AACA,MAAI,OAAO,SAAS,IAAI,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,4EAA4E,KAAK,UAAU,MAAM,CAAC;AAAA,IACpG;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,mBAAmB,MAAM;AAAA,EACrC,QAAQ;AACN,cAAU;AAAA,EACZ;AACA,aAAW,UAAU,YAAY,SAAS,CAAC,MAAM,IAAI,CAAC,QAAQ,OAAO,GAAG;AACtE,eAAW,WAAW,OAAO,MAAM,GAAG,GAAG;AACvC,UAAI,YAAY,OAAO,YAAY,MAAM;AACvC,cAAM,IAAI;AAAA,UACR,sDAAsD,KAAK,UAAU,MAAM,CAAC;AAAA,QAC9E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,eAAe,QAAoC;AACjE,SAAO,GAAG,WAAW,MAAM,CAAC;AAC9B;AAGO,SAAS,kBAAkB,QAA4B,KAAyB;AACrF,qBAAmB,GAAG;AACtB,SAAO,GAAG,eAAe,MAAM,CAAC,GAAG,iBAAiB,IAAI,SAAS,CAAC,IAAI,iBAAiB,IAAI,OAAO,CAAC,GAAG,eAAe;AACvH;AAGO,SAAS,mBAAmB,QAA4B,WAA4B;AACzF,QAAM,OAAO,eAAe,MAAM;AAClC,SAAO,cAAc,SAAY,OAAO,GAAG,IAAI,GAAG,iBAAiB,SAAS,CAAC;AAC/E;AAQO,SAAS,iBAAiB,QAA4B,WAAsC;AACjG,QAAM,OAAO,eAAe,MAAM;AAClC,MAAI,CAAC,UAAU,WAAW,IAAI,KAAK,CAAC,UAAU,SAAS,eAAe,EAAG,QAAO;AAChF,QAAM,OAAO,UAAU,MAAM,KAAK,QAAQ,UAAU,SAAS,gBAAgB,MAAM;AACnF,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,QAAM,iBAAiB,KAAK,MAAM,QAAQ,CAAC;AAC3C,MAAI,eAAe,WAAW,KAAK,eAAe,SAAS,GAAG,EAAG,QAAO;AACxE,QAAM,UAAU,kBAAkB,cAAc;AAChD,QAAM,YAAY,WAAW,oBAAoB,SAAY,kBAAkB,MAAM;AAGrF,MAAI,iBAAiB,OAAO,MAAM,eAAgB,QAAO;AACzD,MAAI,cAAc,UAAa,iBAAiB,SAAS,MAAM,OAAQ,QAAO;AAC9E,QAAM,MAAkB,EAAE,SAAS,UAAU;AAC7C,MAAI;AACF,uBAAmB,GAAG;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;ACvCO,IAAM,gBAAgB,IAAI,OAAO;AAExC,IAAM,sBAAsB;AAE5B,IAAM,oBAAoB;AAE1B,IAAM,wBAAwB;AAevB,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAY,WAAmB;AAC7B,UAAM,6CAA6C,SAAS,EAAE;AAC9D,SAAK,OAAO;AAAA,EACd;AACF;AAsCO,IAAM,sBAAN,MAAqD;AAAA,EAC1D,YACmB,OACA,QACA,KACjB;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAHgB;AAAA,EACA;AAAA,EACA;AAAA,EAGnB,eAAwB;AACtB,WAAO,EAAE,YAAY,KAAK;AAAA,EAC5B;AAAA,EAEA,MAAM,IAAI,KAAiD;AACzD,UAAM,UAAU,MAAM,KAAK,QAAQ,kBAAkB,KAAK,QAAQ,GAAG,CAAC;AACtE,WAAO,WAAW,CAAC,QAAQ,IAAI,UAAU,QAAQ,IAAI,SAAS;AAAA,EAChE;AAAA,EAEA,MAAM,OAAO,KAAiB,QAAsD;AAClF,8BAA0B,MAAM;AAChC,UAAM,MAAM,kBAAkB,KAAK,QAAQ,GAAG;AAC9C,UAAM,UAAU,MAAM,KAAK,QAAQ,GAAG;AACtC,QAAI,YAAY,QAAQ,CAAC,QAAQ,IAAI,SAAS;AAC5C,YAAM,IAAI,mBAAmB,2CAA2C,IAAI,OAAO,EAAE;AAAA,IACvF;AAEA,UAAM,QAAQ,OAAO,UAAU,kBAAkB,QAAQ,IAAI,MAAM,IAAI,IAAI,CAAC;AAC5E,UAAM,MAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,QAAQ,cAAc,KAAK,QAAQ,KAAK,IAAI,GAAG,KAAK;AAAA,IACtD;AAGA,UAAM,KAAK,OAAO,KAAK,KAAK,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC7E,WAAO,EAAE,MAAM;AAAA,EACjB;AAAA,EAEA,MAAM,eACJ,KACA,UACA,OAC2B;AAC3B,0BAAsB,KAAK;AAC3B,UAAM,MAAM,kBAAkB,KAAK,QAAQ,GAAG;AAC9C,UAAM,UAAU,MAAM,KAAK,QAAQ,GAAG;AACtC,QAAI,YAAY,QAAQ,QAAQ,IAAI,WAAW,QAAQ,IAAI,OAAO,UAAU,UAAU;AACpF,YAAM,IAAI,mBAAmB,uCAAuC,IAAI,OAAO,EAAE;AAAA,IACnF;AACA,UAAM,QAAQ,OAAO,kBAAkB,QAAQ,IAAI,MAAM,IAAI,CAAC;AAC9D,UAAM,MAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,QAAQ,mBAAmB,QAAQ,IAAI,QAAQ,OAAO,KAAK,IAAI,GAAG,KAAK;AAAA,IACzE;AAEA,UAAM,KAAK,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ,QAAQ,CAAC;AACxD,WAAO,EAAE,MAAM;AAAA,EACjB;AAAA,EAEA,OAAO,KAAK,WAAmD;AAC7D,UAAM,SAAS,mBAAmB,KAAK,QAAQ,SAAS;AAExD,QAAI,OAAiB,CAAC;AACtB,UAAM,QAAQ,iBAEZ,MAC+B;AAC/B,YAAM,OAAO,MAAM;AAAA,QAAmB;AAAA,QAAM;AAAA,QAAuB,CAAC,QAClE,KAAK,QAAQ,GAAG;AAAA,MAClB;AACA,iBAAW,WAAW,MAAM;AAC1B,YAAI,WAAW,CAAC,QAAQ,IAAI,QAAS,OAAM,QAAQ,IAAI;AAAA,MACzD;AAAA,IACF,EAAE,KAAK,IAAI;AAEX,qBAAiB,OAAO,KAAK,MAAM,SAAS,MAAM,GAAG;AACnD,UAAI,iBAAiB,KAAK,QAAQ,GAAG,MAAM,KAAM;AACjD,WAAK,KAAK,GAAG;AACb,UAAI,KAAK,UAAU,uBAAuB;AACxC,eAAO,MAAM,IAAI;AACjB,eAAO,CAAC;AAAA,MACV;AAAA,IACF;AACA,QAAI,KAAK,SAAS,EAAG,QAAO,MAAM,IAAI;AAAA,EACxC;AAAA,EAEA,MAAM,OAAO,KAAgC;AAC3C,UAAM,MAAM,kBAAkB,KAAK,QAAQ,GAAG;AAI9C,aAAS,UAAU,GAAG,UAAU,qBAAqB,WAAW;AAC9D,YAAM,UAAU,MAAM,KAAK,QAAQ,GAAG;AACtC,UAAI,YAAY,QAAQ,QAAQ,IAAI,QAAS;AAC7C,YAAM,QAAQ,OAAO,kBAAkB,QAAQ,IAAI,MAAM,IAAI,CAAC;AAC9D,YAAM,MAAwB;AAAA,QAC5B,SAAS;AAAA,QACT,QAAQ,EAAE,GAAG,QAAQ,IAAI,QAAQ,OAAO,WAAW,KAAK,IAAI,EAAE;AAAA,MAChE;AACA,UAAI;AACF,cAAM,KAAK,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ,QAAQ,CAAC;AACxD;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,qBAAqB,GAAG,EAAG;AAC/B,cAAM;AAAA,MACR;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,4CAA4C,IAAI,OAAO;AAAA,IACzD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,QACZ,WAC4D;AAC5D,UAAM,MAAM,MAAM,KAAK,UAAU,SAAS;AAC1C,QAAI,QAAQ,KAAM,QAAO;AACzB,QAAI,IAAI,MAAM,SAAS,eAAe;AACpC,YAAM,IAAI;AAAA,QACR,mBAAmB,IAAI,MAAM,MAAM,iBAAiB,aAAa;AAAA,MACnE;AAAA,IACF;AACA,QAAI,IAAI,QAAQ,WAAW,GAAG;AAE5B,YAAM,IAAI;AAAA,QACR,uEAAuE,SAAS;AAAA,MAClF;AAAA,IACF;AACA,UAAM,MAAM,sBAAsB,IAAI,YAAY,EAAE,OAAO,IAAI,KAAK,GAAG,SAAS;AAChF,WAAO,EAAE,KAAK,SAAS,IAAI,QAAQ;AAAA,EACrC;AAAA;AAAA,EAGA,MAAc,UAAU,WAA8C;AACpE,aAAS,UAAU,GAAG,UAAU,mBAAmB,WAAW;AAC5D,UAAI;AACF,eAAO,MAAM,KAAK,MAAM,KAAK,SAAS;AAAA,MACxC,SAAS,KAAK;AACZ,YAAI,EAAE,eAAe,oBAAqB,OAAM;AAAA,MAClD;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,kBAAkB,KAAK,MAAM,KAAK,YAAY,SAAS,qCAClD,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,OACZ,WACA,KACA,QACe;AACf,UAAM,OAAO,IAAI,YAAY,EAAE,OAAO,0BAA0B,GAAG,CAAC;AACpE,QAAI,KAAK,SAAS,eAAe;AAG/B,YAAM,IAAI,gBAAgB,mBAAmB,KAAK,MAAM,iBAAiB,aAAa,GAAG;AAAA,IAC3F;AACA,UAAM,KAAK,MAAM,MAAM,WAAW,MAAM,MAAM;AAAA,EAChD;AACF;;;ACzSO,SAAS,WAAW,KAAkC;AAC3D,SAAQ,KAA4D,WAAW;AACjF;AAEO,SAAS,UAAU,KAAkC;AAC1D,SAAQ,KAAkC;AAC5C;AAGO,SAAS,UAAU,KAAkC;AAC1D,SAAQ,KAAkC;AAC5C;AAGO,SAAS,eAAe,KAAuB;AACpD,SAAQ,KAAyC,cAAc;AACjE;AAGO,SAAS,aAAa,KAAuB;AAClD,QAAM,SAAS,WAAW,GAAG;AAC7B,SAAO,WAAW,UAAa,UAAU,OAAO,UAAU;AAC5D;AAEA,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AACD,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMD,IAAM,kBACJ;AAGK,SAAS,mBAAmB,KAAuB;AACxD,MAAI,cAAc,IAAI,UAAU,GAAG,KAAK,EAAE,EAAG,QAAO;AACpD,MAAI,cAAc,IAAI,UAAU,GAAG,KAAK,EAAE,EAAG,QAAO;AACpD,SAAO,gBAAgB,KAAM,KAAqC,WAAW,EAAE;AACjF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, SDK-free helpers for classifying AWS-style errors (used by the S3 drivers).
|
|
3
|
+
*
|
|
4
|
+
* These only read structural shapes an AWS SDK v3 error carries — `name`, `$metadata.httpStatusCode`, a
|
|
5
|
+
* lower-level `code`/`errno`, and the SDK's own `$retryable` marker — so the (subtle, easy-to-get-wrong)
|
|
6
|
+
* transient-vs-fatal decision is unit-testable without a live backend or even the SDK installed. They live
|
|
7
|
+
* under `drivers/_shared` (part of the SDK-free core bundle): a driver may import them; they import no SDK.
|
|
8
|
+
*/
|
|
9
|
+
export declare function httpStatus(err: unknown): number | undefined;
|
|
10
|
+
export declare function errorName(err: unknown): string | undefined;
|
|
11
|
+
/** A lower-level transport code (e.g. `ECONNRESET`) — the Node networking layer sets `code`. */
|
|
12
|
+
export declare function errorCode(err: unknown): string | undefined;
|
|
13
|
+
/** The AWS SDK v3 tags retryable errors with a `$retryable` object (throttling faults carry `.throttling`). */
|
|
14
|
+
export declare function isSdkRetryable(err: unknown): boolean;
|
|
15
|
+
/** Any 5xx is a server-side fault that's safe to retry (the request didn't deterministically fail). */
|
|
16
|
+
export declare function isServerSide(err: unknown): boolean;
|
|
17
|
+
/** A dropped/timed-out connection — transient by nature; a retry on a fresh connection usually succeeds. */
|
|
18
|
+
export declare function isNetworkOrTimeout(err: unknown): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared key-grammar fragments used by every driver's logical-ref → physical-key mapping.
|
|
3
|
+
*
|
|
4
|
+
* Extracted once the registry drivers became the third consumer of the same `_default` namespace sentinel
|
|
5
|
+
* (it lived copy-pasted in `s3/keys` and `localfs/paths`). Pure string logic, no SDK, no I/O —
|
|
6
|
+
* lives in the SDK-free `_shared` bundle so any driver may import it.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The physical stand-in for an **absent** namespace. A caller MAY name a namespace `_default`; it simply does not collide, because
|
|
10
|
+
* `namespaceKeyPart`/`namespacePathPart` encode the caller's namespace (to `%5Fdefault`) and emit this
|
|
11
|
+
* sentinel literally. The separation is a property of the encoding, not of a grammar — an earlier version of
|
|
12
|
+
* this comment claimed the latter, and that claim is exactly what made the collision easy to reintroduce.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_NAMESPACE = "_default";
|
|
15
|
+
/**
|
|
16
|
+
* The physical namespace component of an **object key**: the caller's namespace encoded, or the sentinel.
|
|
17
|
+
*
|
|
18
|
+
* The sentinel is emitted **literally, never encoded**, and that asymmetry is the whole point. Encoding it too
|
|
19
|
+
* would send an absent namespace to `%5Fdefault` — exactly where a caller who names their namespace `_default`
|
|
20
|
+
* already goes, since the encoder escapes their leading underscore. Both would land in one place and read each
|
|
21
|
+
* other's data. Encoding only the caller's side means `_default` (ours) and `%5Fdefault` (theirs) are two
|
|
22
|
+
* different strings, and no caller can produce the first.
|
|
23
|
+
*/
|
|
24
|
+
export declare function namespaceKeyPart(namespace: string | undefined): string;
|
|
25
|
+
/** As {@link namespaceKeyPart}, for a filesystem path component. */
|
|
26
|
+
export declare function namespacePathPart(namespace: string | undefined): string;
|
|
27
|
+
export { encodeNameForKey, decodeNameFromKey, encodeNameForPath, decodeNameFromPath, } from '../../core/name-codec.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SegmentRef } from '../../core/ports.js';
|
|
2
|
+
/** Normalize an optional caller prefix to either `''` or `trimmed/` (no leading/trailing slashes). */
|
|
3
|
+
export declare function prefixPart(prefix: string | undefined): string;
|
|
4
|
+
/**
|
|
5
|
+
* Validate a caller-supplied key prefix (trusted config, but make it a real containment boundary): reject
|
|
6
|
+
* control characters and `.`/`..` path segments so a prefix cannot traverse out of its intended space.
|
|
7
|
+
* Returns it unchanged — the key builders normalize slashes.
|
|
8
|
+
*
|
|
9
|
+
* The traversal check covers three spellings, because the tools that walk these buckets do. A bare `..` is
|
|
10
|
+
* the obvious one. A **backslash** segment matters on ADLS Gen2 and to Windows-side tooling, which treat it
|
|
11
|
+
* as a separator this code otherwise would not. And a percent-encoded `..` matters because `gsutil`,
|
|
12
|
+
* `s3fs`, `gcsfuse` and `azcopy` decode a key on their way to a local path — the same reasoning
|
|
13
|
+
* `name-codec.ts` already applies to segment names, which a prefix has no reason to be exempt from.
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeObjectPrefix(prefix: string | undefined): string | undefined;
|
|
16
|
+
/** The key prefix under which every registry object lives: `<prefix>registry/`. */
|
|
17
|
+
export declare function registryPrefix(prefix: string | undefined): string;
|
|
18
|
+
/** The full key of one segment's registry object: `<prefix>registry/<ns>/<segment>.reg`. */
|
|
19
|
+
export declare function registryObjectKey(prefix: string | undefined, ref: SegmentRef): string;
|
|
20
|
+
/** The list prefix for discovery: registry-wide, or scoped to one namespace. */
|
|
21
|
+
export declare function registryListPrefix(prefix: string | undefined, namespace?: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Parse a `<prefix>registry/<ns>/<segment>.reg` key back to its {@link SegmentRef}, or `null` if it does not
|
|
24
|
+
* match — a stray or foreign object under the prefix, or one whose parsed ref fails the round-trip check.
|
|
25
|
+
* `_default` maps back to the absent namespace. A name is percent-encoded on the way in, so no encoded name
|
|
26
|
+
* can contain `/` and the split stays unambiguous whatever the caller named their segment.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseRegistryKey(prefix: string | undefined, objectKey: string): SegmentRef | null;
|