@cloudbitmaps/core 0.11.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/{chunk-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
  2. package/dist/chunk-5W3XS2IU.js.map +7 -0
  3. package/dist/core/audit.d.ts +6 -0
  4. package/dist/core/chunk-window.d.ts +24 -0
  5. package/dist/core/codec.d.ts +33 -1
  6. package/dist/core/consistency.d.ts +21 -2
  7. package/dist/core/cost.d.ts +91 -10
  8. package/dist/core/crbm/format.d.ts +42 -3
  9. package/dist/core/crbm/reader.d.ts +45 -3
  10. package/dist/core/crbm/writer.d.ts +32 -3
  11. package/dist/core/crbm-storage-source.d.ts +204 -23
  12. package/dist/core/crypto.d.ts +11 -7
  13. package/dist/core/determinism.d.ts +7 -0
  14. package/dist/core/engine.d.ts +41 -8
  15. package/dist/core/erase-id.d.ts +23 -3
  16. package/dist/core/erasure.d.ts +8 -4
  17. package/dist/core/generation-gc.d.ts +102 -4
  18. package/dist/core/load-input.d.ts +55 -0
  19. package/dist/core/load.d.ts +65 -10
  20. package/dist/core/metadata.d.ts +42 -0
  21. package/dist/core/metrics.d.ts +7 -1
  22. package/dist/core/pinned-storage-source.d.ts +2 -1
  23. package/dist/core/ports.d.ts +119 -13
  24. package/dist/core/registry-scan.d.ts +6 -0
  25. package/dist/core/retention-sweep.d.ts +47 -2
  26. package/dist/core/retry.d.ts +14 -0
  27. package/dist/core/rollback.d.ts +34 -4
  28. package/dist/core/summary.d.ts +61 -0
  29. package/dist/core/token.d.ts +36 -0
  30. package/dist/core/validate.d.ts +2 -0
  31. package/dist/driver-kit.js +39 -20
  32. package/dist/driver-kit.js.map +2 -2
  33. package/dist/drivers/_shared/entropy.d.ts +5 -0
  34. package/dist/drivers/_shared/object-registry.d.ts +36 -6
  35. package/dist/drivers/_shared/registry.d.ts +47 -19
  36. package/dist/drivers/localfs/registry.d.ts +10 -0
  37. package/dist/drivers/memory.d.ts +9 -3
  38. package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.js +1878 -501
  41. package/dist/index.js.map +4 -4
  42. package/dist/testing/fuzz-core.d.ts +7 -6
  43. package/package.json +1 -1
  44. package/dist/chunk-5OMC5U7U.js.map +0 -7
@@ -1,4 +1,5 @@
1
1
  import type { IRegistryDriver, NewRegistryRecord, RegCaps, RegistryPatch, RegistryRecord, SegmentRef, Token } from '../../core/ports.js';
2
+ import type { Entropy } from '../../core/determinism.js';
2
3
  /** Defensive cap on a single registry object read from storage, before allocation (rows are tiny; ~1 KB). */
3
4
  export declare const MAX_ROW_BYTES: number;
4
5
  /**
@@ -8,11 +9,12 @@ export declare const MAX_ROW_BYTES: number;
8
9
  * This is deliberately **neither** of the two things it superficially resembles. It is not absence: the row
9
10
  * is very likely still live, just one version further on. And it is not a write conflict: the caller may be
10
11
  * a read-only `get()`, which has nothing to conflict with. Either is an easy misreading for a store that reads a
11
- * row in two calls, as the Azure Blob store does, which is why the signal is part of the port rather than
12
+ * row in two calls, metadata and then bytes pinned to it, which is why the signal is part of the port rather than
12
13
  * left to each store's judgement. {@link ObjectStoreRegistry} answers it the only way that is correct for every
13
14
  * caller: it reads again.
14
15
  *
15
- * Stores whose read is atomic (S3 serves bytes and `ETag` from one `GetObject`, GCS bytes and `generation` from one GET) can never raise it.
16
+ * Stores whose read is atomic can never raise it, and every shipped store's is: S3 serves bytes and `ETag` from one
17
+ * `GetObject`, GCS bytes and `generation` from one GET, and Azure Blob bytes and `ETag` from one GET.
16
18
  */
17
19
  export declare class ObjectVersionRaced extends Error {
18
20
  constructor(objectKey: string);
@@ -24,8 +26,8 @@ export interface ObjectRow {
24
26
  readonly version: string;
25
27
  }
26
28
  /**
27
- * The three I/O calls an object store must provide to host a registry. Everything else — the OCC counter,
28
- * tombstones, the delete retry, key parsing — is implemented once above.
29
+ * The I/O calls an object store must provide to host a registry: three, and an optional conditional delete. Everything
30
+ * else — the OCC token, tombstones, the delete retry, key parsing — is implemented once above.
29
31
  */
30
32
  export interface ObjectRegistryStore {
31
33
  /** A label for error messages, e.g. `GCS` or `Azure Blob`. */
@@ -57,13 +59,41 @@ export interface ObjectRegistryStore {
57
59
  }): Promise<void>;
58
60
  /** Every object key under `prefix`, paginated internally. */
59
61
  listKeys(prefix: string): AsyncIterable<string>;
62
+ /**
63
+ * Optional: delete one object, and only while it is still at exactly `expect.version`, by a precondition the
64
+ * backend applies (`If-Match: <etag>`, `ifGenerationMatch: <generation>`). An object that was overwritten, or is
65
+ * gone, MUST throw {@link WriteConflictError} and delete nothing. Retryable faults throw {@link TransientError}.
66
+ *
67
+ * Sending it again after a lost response is safe, unlike a write: the precondition names one version, so a second
68
+ * 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, and finds the row gone.
70
+ *
71
+ * The registry calls it only when {@link conditionalDelete} is `true`.
72
+ */
73
+ delete?(key: string, expect: {
74
+ version: string;
75
+ }): Promise<void>;
76
+ /**
77
+ * Whether {@link delete} may be relied on: `true` only where the backend is known to apply the precondition. A
78
+ * backend that accepts the header and ignores it would let a delete remove a row written after the delete read it,
79
+ * a live one included. Absent or `false`, the registry never calls `delete` and tombstones every row instead.
80
+ */
81
+ readonly conditionalDelete?: boolean;
60
82
  }
61
83
  export declare class ObjectStoreRegistry implements IRegistryDriver {
62
84
  private readonly store;
63
85
  private readonly prefix;
64
86
  private readonly now;
65
- constructor(store: ObjectRegistryStore, prefix: string | undefined, now: () => number);
87
+ private readonly entropy;
88
+ /**
89
+ * `entropy` draws the random parts of every token: a new row's incarnation id, and each write's own part. It
90
+ * defaults to the platform's Web Crypto. Inject one only to make a test replayable: a seeded source in production
91
+ * gives every process the same ids.
92
+ */
93
+ constructor(store: ObjectRegistryStore, prefix: string | undefined, now: () => number, entropy?: Entropy);
66
94
  capabilities(): RegCaps;
95
+ /** Whether a delete removes a row born with an incarnation id: the store has a delete, and vouches for it. */
96
+ private removesRows;
67
97
  get(ref: SegmentRef): Promise<RegistryRecord | null>;
68
98
  create(ref: SegmentRef, record: NewRegistryRecord): Promise<{
69
99
  token: Token;
@@ -79,7 +109,7 @@ export declare class ObjectStoreRegistry implements IRegistryDriver {
79
109
  * A store that pins a version to read consistently ({@link ObjectVersionRaced}) loses that pin whenever a
80
110
  * concurrent writer lands mid-read. Re-reading is the whole answer — the next read simply observes the
81
111
  * newer version — and it is bounded so a row under permanent write saturation fails typed instead of
82
- * spinning. Only a store racing itself gets here; S3 never does.
112
+ * spinning. Only a store that reads in two calls gets here; none of the shipped stores does.
83
113
  */
84
114
  private readRow;
85
115
  /** {@link ObjectRegistryStore.read}, retrying the bounded number of times a mid-read overwrite allows. */
@@ -1,17 +1,27 @@
1
+ import type { Entropy } from '../../core/determinism.js';
2
+ import { incarnationOf } from '../../core/token.js';
1
3
  import type { NewRegistryRecord, RegistryPatch, RegistryRecord, SegmentRef, Token } from '../../core/ports.js';
2
4
  /**
3
5
  * The fields a stored record may carry: {@link RegistryRecord}'s, and nothing else. A field no reader resolves
4
6
  * through is refused on read-back like any other corruption (invariant 5), rather than ignored.
5
7
  */
6
- export declare const RECORD_FIELDS: readonly ["namespace", "segment", "currentGen", "wrappedDeks", "keyId", "status", "retention", "residency", "createdAt", "updatedAt", "token"];
7
- /** Validate the caller-settable fields at `create`. */
8
- export declare function validateNewRegistryRecord(rec: NewRegistryRecord): void;
9
- /** Validate the caller-settable fields in a `compareAndSwap` patch. */
10
- export declare function validateRegistryPatch(patch: RegistryPatch): void;
8
+ export declare const RECORD_FIELDS: readonly ["namespace", "segment", "currentGen", "wrappedDeks", "keyId", "status", "retention", "residency", "summary", "createdAt", "updatedAt", "token"];
9
+ /**
10
+ * Validate the caller-settable fields at `create`, and return the record to store: the caller's, with its summary
11
+ * replaced by the checked, frozen copy {@link validateSummary} builds.
12
+ */
13
+ export declare function validateNewRegistryRecord(rec: NewRegistryRecord): NewRegistryRecord;
14
+ /**
15
+ * Validate the caller-settable fields in a `compareAndSwap` patch, and return the patch to apply: the caller's, with
16
+ * its summary replaced by the checked, frozen copy. That a given `summary` names the row's resulting `currentGen`,
17
+ * and agrees with its resulting keys, needs the stored row, so {@link applyRegistryPatch} checks both.
18
+ */
19
+ export declare function validateRegistryPatch(patch: RegistryPatch): RegistryPatch;
11
20
  /**
12
21
  * The persisted envelope for a registry row: the record plus a tombstone flag. A **deleted** row keeps its
13
- * record (with an advanced counter) rather than being removed, so the monotonic token survives a
14
- * delete→recreate — ABA-safety. Shared by the persistent drivers (LocalFs file, S3 object).
22
+ * record (with an advanced counter) rather than being removed, so a re-create carries the counter on and the two
23
+ * incarnations' tokens are apart by construction, as well as by their random parts — ABA-safety. Shared by the
24
+ * persistent drivers (LocalFs file, S3 object).
15
25
  *
16
26
  * `schemaVersion` is a wire-only concern — it stamps the persisted bytes, not the in-memory domain object —
17
27
  * so it lives on the serialize/parse boundary ({@link serializeRegistryEnvelope} /
@@ -27,22 +37,35 @@ export interface RegistryEnvelope {
27
37
  * with this so a reader can fail-closed on a future, incompatible layout instead of misparsing it. Bump only
28
38
  * on a backward-incompatible change. Policy: a **higher** stamp than this build knows → `UnsupportedError`
29
39
  * (fail-closed); an **absent** or malformed stamp → `IntegrityError`, since every row this build writes has one.
40
+ *
41
+ * Schema 2 adds the record's `summary` and the incarnation-form token. Every row this build writes is stamped 2,
42
+ * whatever it holds, and a build that reads only schema 1 refuses it, so a fleet cannot go back once one has been
43
+ * written.
30
44
  */
31
- export declare const REGISTRY_SCHEMA_VERSION = 1;
45
+ export declare const REGISTRY_SCHEMA_VERSION = 2;
32
46
  /**
33
- * Validate a persisted registry row's `schemaVersion` (untrusted bytes, invariant 5): an absent or malformed value
34
- * → `IntegrityError`; a version newer than this build → `UnsupportedError` (fail-closed rather than misread a
35
- * format we don't understand).
47
+ * Validate a persisted registry row's `schemaVersion` (untrusted bytes, invariant 5) and return it: an absent or
48
+ * malformed value → `IntegrityError`; a version newer than this build → `UnsupportedError` (fail-closed rather than
49
+ * misread a format we don't understand).
36
50
  */
37
- export declare function assertRegistrySchemaVersion(raw: unknown, ctx: string): void;
51
+ export declare function assertRegistrySchemaVersion(raw: unknown, ctx: string): number;
38
52
  /** Serialize a registry envelope for persistence, stamping the current {@link REGISTRY_SCHEMA_VERSION}. */
39
53
  export declare function serializeRegistryEnvelope(env: RegistryEnvelope): string;
54
+ export { incarnationOf };
55
+ /** A fresh incarnation id from `entropy`: 128 bits as 32 lowercase hex digits. */
56
+ export declare function drawIncarnation(entropy: Entropy): string;
57
+ /** A fresh write part from `entropy`: 64 bits as 16 lowercase hex digits. */
58
+ export declare function drawWrite(entropy: Entropy): string;
59
+ /**
60
+ * The token for the write after `record`'s: the same incarnation, if it has one, the counter one on, and a fresh write
61
+ * part. For a compare-and-swap or a tombstone.
62
+ */
63
+ export declare function nextRegistryToken(record: RegistryRecord, entropy: Entropy): Token;
40
64
  /**
41
- * The OCC counter encoded in a record's token. A published row always has a **canonical decimal** token
42
- * (`String(counter)`), so anything else (`"1e3"`, `"0x10"`, `" 5 "`, `""`, a >2^53 value) is corruption /
43
- * tampering — reject it (invariant 5) rather than let `Number()` coerce it and silently break monotonicity.
65
+ * The token of a new row: a fresh incarnation and write part from `entropy`, and a counter that continues
66
+ * `tombstone`'s when the row is created over one, else starts at 0.
44
67
  */
45
- export declare function registryCounterOf(record: RegistryRecord): number;
68
+ export declare function newIncarnationToken(entropy: Entropy, tombstone: RegistryRecord | undefined): Token;
46
69
  /**
47
70
  * Parse + structurally validate a persisted `{ deleted, record }` envelope from stored bytes. A published row
48
71
  * is always whole (atomic write), so a parse failure or a missing/mistyped field means corruption/tampering —
@@ -54,13 +77,18 @@ export declare function recordFromNew(ref: SegmentRef, rec: NewRegistryRecord, n
54
77
  /**
55
78
  * Fail-fast structural check on a record read back from a persistent tier (untrusted bytes, invariant 5):
56
79
  * a published row is always whole, so a missing/mistyped required field means corruption/tampering — reject
57
- * it rather than silently report "absent". `token` is checked by the caller (its source differs per backend:
58
- * an in-body field vs a derived OCC counter).
80
+ * it rather than silently report "absent". A row of an older `schemaVersion` may carry only that schema's
81
+ * fields. `token` is checked by the caller.
59
82
  */
60
- export declare function assertStoredRecordShape(r: Record<string, unknown>, ctx: string): void;
83
+ export declare function assertStoredRecordShape(r: Record<string, unknown>, ctx: string, schemaVersion?: number): void;
61
84
  /**
62
85
  * Apply a patch to an existing record, returning a new one with a fresh `updatedAt` + `token` (identity and
63
86
  * `createdAt` are preserved). Optional fields use `'k' in patch` so a patch can *clear* them (set to
64
87
  * `undefined`, e.g. dropping `keyId` on crypto-shred); required fields use `??` (they always have a value).
88
+ *
89
+ * `summary` describes the current generation, so it follows the pointer and the keys: a patch that moves
90
+ * `currentGen`, or changes `wrappedDeks` so the summary's shape no longer agrees, without mentioning `summary` drops
91
+ * the old one; and a summary the patch gives must name the resulting `currentGen` and agree with the resulting keys
92
+ * (`ValidationError`, before anything is written).
65
93
  */
66
94
  export declare function applyRegistryPatch(prev: RegistryRecord, patch: RegistryPatch, now: number, token: Token): RegistryRecord;
@@ -1,14 +1,22 @@
1
+ import type { Entropy } from '../../core/determinism.js';
1
2
  import type { IRegistryDriver, NewRegistryRecord, RegCaps, RegistryPatch, RegistryRecord, SegmentRef, Token } from '../../core/ports.js';
2
3
  /** Entries in the process-wide row-lock map; the map is empty whenever no registry operation is in flight. */
3
4
  export declare function localFsRowLockCount(): number;
4
5
  export interface LocalFsRegistryDriverOptions {
5
6
  /** Injected clock for `createdAt`/`updatedAt`; defaults to `Date.now`. */
6
7
  readonly now?: () => number;
8
+ /** Draws every token's random parts; defaults to Web Crypto. Inject one only to make a test replayable. */
9
+ readonly entropy?: Entropy;
7
10
  }
8
11
  export declare class LocalFsRegistryDriver implements IRegistryDriver {
9
12
  private readonly root;
10
13
  private readonly now;
14
+ private readonly entropy;
11
15
  constructor(root: string, options?: LocalFsRegistryDriverOptions);
16
+ /**
17
+ * `conditionalDelete`: a delete reads the row, checks it, and unlinks it under the row's lock, which every writer on
18
+ * the root in this process takes, so no write can land between the check and the unlink.
19
+ */
12
20
  capabilities(): RegCaps;
13
21
  get(ref: SegmentRef): Promise<RegistryRecord | null>;
14
22
  create(ref: SegmentRef, record: NewRegistryRecord): Promise<{
@@ -19,6 +27,8 @@ export declare class LocalFsRegistryDriver implements IRegistryDriver {
19
27
  }>;
20
28
  list(namespace?: string): AsyncIterable<RegistryRecord>;
21
29
  delete(ref: SegmentRef, expected?: Token): Promise<void>;
30
+ /** Remove a row's file and make the removal durable (fsync the directory). */
31
+ private unlinkRow;
22
32
  /** Namespaces to scan: just the one requested, or every namespace dir under the root. */
23
33
  private namespaceDirs;
24
34
  private readRow;
@@ -1,20 +1,26 @@
1
1
  import type { BlobSink } from '../core/blob.js';
2
+ import type { Entropy } from '../core/determinism.js';
2
3
  import type { StorageCaps, GenKey, IStorageDriver, IRegistryDriver, NewRegistryRecord, RegCaps, RegistryPatch, RegistryRecord, SegmentRef, Token } from '../core/ports.js';
3
4
  export interface MemoryRegistryDriverOptions {
4
5
  /** Injected clock for `createdAt`/`updatedAt`; defaults to `Date.now` (drivers may use ambient time). */
5
6
  readonly now?: () => number;
7
+ /** Draws every token's random parts; defaults to Web Crypto. Inject one only to make a test replayable. */
8
+ readonly entropy?: Entropy;
6
9
  }
7
10
  /**
8
- * In-memory {@link IRegistryDriver} — one record per segment under OCC. The token is a single global,
9
- * monotonic, never-reused counter (so a record recreated after `delete` always gets a strictly-greater
10
- * token → ABA-safe even though `delete` removes the row physically).
11
+ * In-memory {@link IRegistryDriver} — one record per segment under OCC. The token has the form every shipped driver
12
+ * gives it, an incarnation id drawn at create, a counter and a per-write random part, but its counter is global to the
13
+ * driver: monotonic and never reused, so a record recreated after `delete` never meets an earlier token, by
14
+ * construction, even though `delete` removes the row physically.
11
15
  */
12
16
  export declare class MemoryRegistryDriver implements IRegistryDriver {
13
17
  private readonly rows;
14
18
  private readonly now;
19
+ private readonly entropy;
15
20
  private seq;
16
21
  constructor(options?: MemoryRegistryDriverOptions);
17
22
  private nextToken;
23
+ /** `conditionalDelete`: a delete checks the token and removes the row in one step, with no await between. */
18
24
  capabilities(): RegCaps;
19
25
  get(ref: SegmentRef): Promise<RegistryRecord | null>;
20
26
  create(ref: SegmentRef, record: NewRegistryRecord): Promise<{
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import type { Clock, Rng } from '../../core/determinism.js';
23
23
  import type { RetryPolicy } from '../../core/retry.js';
24
- import type { ChunkRef, StorageChunkSource, SegmentRef, SegmentSize } from '../../core/ports.js';
24
+ import type { ChunkRef, GenerationSummary, StorageChunkSource, SegmentRef, SegmentSize } from '../../core/ports.js';
25
25
  export interface RetryingOptions {
26
26
  readonly clock: Clock;
27
27
  readonly rng: Rng;
@@ -44,6 +44,7 @@ export declare class RetryingStorageChunkSource implements StorageChunkSource {
44
44
  /** Present only when the inner source supports it — so capability detection stays honest. */
45
45
  readonly sizeOf?: (ref: SegmentRef) => Promise<SegmentSize | null>;
46
46
  readonly cardinalities?: (ref: SegmentRef) => Promise<ReadonlyMap<number, number> | null>;
47
+ readonly summary?: (ref: SegmentRef) => Promise<GenerationSummary | null>;
47
48
  readonly currentGeneration?: (ref: SegmentRef) => Promise<number | null>;
48
49
  readonly invalidate?: (ref: SegmentRef) => void;
49
50
  readonly exists?: (ref: SegmentRef) => Promise<boolean>;
package/dist/index.d.ts CHANGED
@@ -37,13 +37,14 @@ export { PinnedStorageChunkSource } from './core/pinned-storage-source.js';
37
37
  export type { PinnedAt } from './core/pinned-storage-source.js';
38
38
  export { loadSegment } from './core/load.js';
39
39
  export type { LoadDeps, LoadOptions, LoadGuard, LoadResult, LoadRefusal } from './core/load.js';
40
+ export type { LoadInput, PortableBitmap } from './core/load-input.js';
40
41
  export { listGenerations, rollbackSegment } from './core/rollback.js';
41
42
  export type { GenerationListDeps, GenerationEntry, RollbackResult } from './core/rollback.js';
42
43
  export { eraseIdFromSegment } from './core/erase-id.js';
43
44
  export type { EraseIdDeps, EraseIdResult } from './core/erase-id.js';
44
- export type { CodecInterface, CodecBitmap } from './core/codec.js';
45
- export type { Clock, Rng } from './core/determinism.js';
46
- export type { StorageChunkSource, IStorageDriver, StorageCaps, ChunkRef, SegmentRef, GenKey, IRegistryDriver, StorageBackend, RegistryRecord, NewRegistryRecord, RegistryPatch, RegistryStatus, RegCaps, GovernanceMeta, SegmentSize, } from './core/ports.js';
45
+ export type { CodecInterface, CodecBitmap, EncodedChunk } from './core/codec.js';
46
+ export type { Clock, Entropy, Rng } from './core/determinism.js';
47
+ export type { StorageChunkSource, IStorageDriver, StorageCaps, ChunkRef, SegmentRef, GenKey, IRegistryDriver, StorageBackend, RegistryRecord, NewRegistryRecord, RegistryPatch, RegistryStatus, RegistrySummary, ClearRegistrySummary, SealedRegistrySummary, GenerationMetadata, GenerationSummary, RegCaps, GovernanceMeta, SegmentSize, } from './core/ports.js';
47
48
  export { CloudRoaringError, ValidationError, WriteConflictError, IntegrityError, NotFoundError, UnsupportedError, CapabilityError, TransientError, KeyUnavailableError, BudgetExceededError, isCloudRoaringError, isWriteConflictError, isTransientError, isNotFoundError, isIntegrityError, isValidationError, } from './core/errors.js';
48
49
  export type { Aead, AeadSealed, IKeystore, WrappedDek, CrbmCrypto } from './core/crypto.js';
49
50
  export { aadFor } from './core/crypto.js';