@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.
- package/dist/{chunk-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
- package/dist/chunk-5W3XS2IU.js.map +7 -0
- package/dist/core/audit.d.ts +6 -0
- package/dist/core/chunk-window.d.ts +24 -0
- package/dist/core/codec.d.ts +33 -1
- package/dist/core/consistency.d.ts +21 -2
- package/dist/core/cost.d.ts +91 -10
- package/dist/core/crbm/format.d.ts +42 -3
- package/dist/core/crbm/reader.d.ts +45 -3
- package/dist/core/crbm/writer.d.ts +32 -3
- package/dist/core/crbm-storage-source.d.ts +204 -23
- package/dist/core/crypto.d.ts +11 -7
- package/dist/core/determinism.d.ts +7 -0
- package/dist/core/engine.d.ts +41 -8
- package/dist/core/erase-id.d.ts +23 -3
- package/dist/core/erasure.d.ts +8 -4
- package/dist/core/generation-gc.d.ts +102 -4
- package/dist/core/load-input.d.ts +55 -0
- package/dist/core/load.d.ts +65 -10
- package/dist/core/metadata.d.ts +42 -0
- package/dist/core/metrics.d.ts +7 -1
- package/dist/core/pinned-storage-source.d.ts +2 -1
- package/dist/core/ports.d.ts +119 -13
- package/dist/core/registry-scan.d.ts +6 -0
- package/dist/core/retention-sweep.d.ts +47 -2
- package/dist/core/retry.d.ts +14 -0
- package/dist/core/rollback.d.ts +34 -4
- package/dist/core/summary.d.ts +61 -0
- package/dist/core/token.d.ts +36 -0
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.js +39 -20
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/entropy.d.ts +5 -0
- package/dist/drivers/_shared/object-registry.d.ts +36 -6
- package/dist/drivers/_shared/registry.d.ts +47 -19
- package/dist/drivers/localfs/registry.d.ts +10 -0
- package/dist/drivers/memory.d.ts +9 -3
- package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1878 -501
- package/dist/index.js.map +4 -4
- package/dist/testing/fuzz-core.d.ts +7 -6
- package/package.json +1 -1
- package/dist/chunk-5OMC5U7U.js.map +0 -7
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type IAuditSink } from './audit.js';
|
|
2
2
|
import type { Yielder } from './cooperative.js';
|
|
3
|
-
import type { Clock } from './determinism.js';
|
|
3
|
+
import type { Clock, Rng } from './determinism.js';
|
|
4
4
|
import type { CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
|
|
5
|
-
import type { ChunkRef, StorageChunkSource, GenKey, IStorageDriver, IRegistryDriver, SegmentRef, SegmentSize, Token } from './ports.js';
|
|
5
|
+
import type { ChunkRef, StorageChunkSource, GenerationMetadata, GenerationSummary, GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, RegistrySummary, SegmentRef, SegmentSize, Token } from './ports.js';
|
|
6
6
|
import { CrbmReader } from './crbm/reader.js';
|
|
7
7
|
import type { CrbmReaderOptions } from './crbm/reader.js';
|
|
8
8
|
import type { CodecBitmap, CodecInterface } from './codec.js';
|
|
9
|
+
import { DecodedLoadInput } from './load-input.js';
|
|
9
10
|
export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
|
|
10
11
|
/**
|
|
11
12
|
* Optional {@link IRegistryDriver}. When provided, the current generation is resolved via the registry's
|
|
@@ -57,8 +58,8 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
|
|
|
57
58
|
* Aggregate byte ceiling on the parsed `.crbm` indices resident in the reader cache (default 64 MiB) — the
|
|
58
59
|
* **second half of that memory bound**. `maxOpenSegments` alone bounds by *count*, but a wide/dense segment's
|
|
59
60
|
* parsed index can reach about 1.3 MB, so 1024 wide indices could pin over a GB and blow a small heap (e.g. a 128 MB
|
|
60
|
-
* Lambda) while the count is nominally "in bounds". This caps the summed {@link CrbmReader.
|
|
61
|
-
* across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
|
|
61
|
+
* Lambda) while the count is nominally "in bounds". This caps the summed {@link CrbmReader.retainedBytes} (the
|
|
62
|
+
* parsed index, and any metadata a generation carries) across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
|
|
62
63
|
* of the count/byte bounds binds first. Lower it for memory-tight deployments with wide segments; a single
|
|
63
64
|
* segment whose index alone exceeds the budget is still cached (it can't be shrunk) but nothing else alongside.
|
|
64
65
|
*/
|
|
@@ -110,6 +111,13 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
110
111
|
* check again.
|
|
111
112
|
*/
|
|
112
113
|
private invalidations;
|
|
114
|
+
/**
|
|
115
|
+
* Generations whose row summary this process found to disagree with their object, by segment and generation. A
|
|
116
|
+
* summary here is not used again: a count of that generation reads the object. Bounded as the readers are, so a
|
|
117
|
+
* long-lived process cannot grow it without limit; one that falls out is trusted again until its next open finds
|
|
118
|
+
* the disagreement again.
|
|
119
|
+
*/
|
|
120
|
+
private readonly distrusted;
|
|
113
121
|
private readonly registry;
|
|
114
122
|
private readonly keystore;
|
|
115
123
|
private readonly requireEncryption;
|
|
@@ -117,7 +125,12 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
117
125
|
private readonly clock;
|
|
118
126
|
private readonly currentGenTtlMs;
|
|
119
127
|
constructor(driver: IStorageDriver, options?: CrbmStorageChunkSourceOptions);
|
|
120
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* The segment's live snapshot, refreshing on the TTL. Cheap within the TTL window. The snapshot holds the resolved
|
|
130
|
+
* target, and its reader once a read has asked for one.
|
|
131
|
+
*/
|
|
132
|
+
private liveSnapshot;
|
|
133
|
+
/** The current resolved reader for a segment, refreshing on the TTL. */
|
|
121
134
|
private resolvedReader;
|
|
122
135
|
/**
|
|
123
136
|
* A refresh failed transiently and the prior reader keeps serving: make this snapshot lapse after
|
|
@@ -125,12 +138,16 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
125
138
|
* that asks the registry again. It costs no read of its own; it only changes when the next refresh happens.
|
|
126
139
|
*/
|
|
127
140
|
private retrySoon;
|
|
128
|
-
/** TTL elapsed: cheaply re-resolve `currentGen`; reopen only if it actually changed, else reuse the prior reader. */
|
|
129
|
-
private computeRefreshedReader;
|
|
130
141
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
142
|
+
* TTL elapsed: cheaply re-resolve the pointer. The row read is always fresh, so what the new snapshot says of the
|
|
143
|
+
* generation (its summary included) is what the row says now, never the prior snapshot's. The reader is reopened
|
|
144
|
+
* only if the generation or the row changed, and only when a read asks for it.
|
|
145
|
+
*/
|
|
146
|
+
private refreshedTarget;
|
|
147
|
+
/**
|
|
148
|
+
* Memoize a snapshot as the segment's at `now()`, forgetting it later if it resolves to null / throws
|
|
149
|
+
* (identity-guarded, so a stale cleanup never clobbers a fresher snapshot). Installing the (pending) snapshot
|
|
150
|
+
* **synchronously** is what lets concurrent callers coalesce onto one in-flight resolve.
|
|
134
151
|
*/
|
|
135
152
|
private install;
|
|
136
153
|
private now;
|
|
@@ -273,26 +290,51 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
273
290
|
*/
|
|
274
291
|
exists(ref: SegmentRef): Promise<boolean>;
|
|
275
292
|
/**
|
|
276
|
-
* Evict a snapshot we just failed to read from — matched by
|
|
293
|
+
* Evict a snapshot we just failed to read from — matched by the snapshot itself, so a concurrent call's
|
|
277
294
|
* fresher snapshot is never clobbered. The identity guard is the whole point: `install` replaces the entry
|
|
278
295
|
* wholesale, so comparing anything else would drop a good snapshot on the floor.
|
|
279
296
|
*/
|
|
280
297
|
private dropStale;
|
|
281
|
-
private resolveLatest;
|
|
282
298
|
/** Open a {@link CrbmReader} for an already-resolved generation target (decrypting if the segment is encrypted). */
|
|
283
299
|
private openForTarget;
|
|
300
|
+
private openGeneration;
|
|
301
|
+
/**
|
|
302
|
+
* Open the generation a resolution found, and hold the row's summary against the object while it is open anyway:
|
|
303
|
+
* the same count and metadata, or the summary is not believed again in this process ({@link distrusted}). This
|
|
304
|
+
* costs no request; a mismatch is not an error, since the object is the truth and this read has it.
|
|
305
|
+
*/
|
|
306
|
+
private openLive;
|
|
307
|
+
private distrustKey;
|
|
308
|
+
/** What the row says of the generation, unless it is a summary this process has found to disagree with its object. */
|
|
309
|
+
private trustedSummary;
|
|
284
310
|
/**
|
|
285
311
|
* The current generation + its DEK wrappings: the registry's authoritative record (one strong read), or a
|
|
286
312
|
* `list` scan for the max generation when there's no registry (a registry-less source can only read cleartext
|
|
287
|
-
* — there's nowhere a wrapped DEK could live).
|
|
313
|
+
* — there's nowhere a wrapped DEK could live). The row's summary rides along, to be used or not by whoever
|
|
314
|
+
* asks.
|
|
288
315
|
*/
|
|
289
|
-
private
|
|
316
|
+
private resolveLive;
|
|
290
317
|
/** Build the per-generation decryption context for an encrypted segment (undefined for cleartext). */
|
|
291
318
|
private cryptoForRead;
|
|
292
319
|
getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
|
|
293
320
|
listChunkKeys(ref: SegmentRef): Promise<number[]>;
|
|
294
321
|
sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
|
|
295
322
|
cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
|
|
323
|
+
/**
|
|
324
|
+
* The current generation's number, id count and metadata. The answer comes from the row the snapshot resolved when
|
|
325
|
+
* it can: the row's summary of the generation it names, on an active row, in the shape the row's keys call for
|
|
326
|
+
* (a sealed one is opened with the segment's key), and one this process has not found to disagree with its
|
|
327
|
+
* object. That is one registry read when the snapshot is cold, none when it is warm, and no read of the object.
|
|
328
|
+
* With no summary it can use (an older row, one for another generation, one that does not open), it opens the
|
|
329
|
+
* generation and answers from the object. A row that names an object that is gone still answers, while a read
|
|
330
|
+
* of the object throws: the number is true of the generation the row names.
|
|
331
|
+
*
|
|
332
|
+
* What the row says is not confirmed here: a summary edited to agree with itself by whoever can write the row is
|
|
333
|
+
* believed until the next time the object is opened, which holds it against the summary.
|
|
334
|
+
*/
|
|
335
|
+
summary(ref: SegmentRef): Promise<GenerationSummary | null>;
|
|
336
|
+
/** {@link summary} of a specific generation, from the object a pin holds. `held` as for {@link getChunkAt}. */
|
|
337
|
+
summaryAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<GenerationSummary | null>;
|
|
296
338
|
/**
|
|
297
339
|
* Run `read` against the pinned snapshot, healing the torn-read window generation GC can open: if the
|
|
298
340
|
* generation we resolved was superseded *and* swept (the grace window elapsed), the Storage driver throws
|
|
@@ -320,6 +362,17 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
320
362
|
* `intersect` would do it N times. Gated in `tests/core/storage-source-heal-open.test.ts` by counting calls.
|
|
321
363
|
*/
|
|
322
364
|
private withFreshSnapshot;
|
|
365
|
+
/**
|
|
366
|
+
* Whether `err`, raised by a read through the snapshot's reader, came from an object that is no longer the one the
|
|
367
|
+
* reader opened. A generation number can be taken again once its object is deleted (an erasure removes the
|
|
368
|
+
* generations above a rolled-back pointer that held the id, a collection pass can stop part-way, and a load then
|
|
369
|
+
* numbers `currentGen + 1` again), so a reader that still holds the old object's index reads the new object's
|
|
370
|
+
* bytes at the old offsets: a chunk checksum fails, or a range runs past the end. The object's footer says which it
|
|
371
|
+
* was, as it does for a pin ({@link CrbmReader.sameObject}): replaced, or gone, re-resolves; the same object means
|
|
372
|
+
* the error is real, and it stands. Only these two errors pay for the footer read, never a read that succeeded. A
|
|
373
|
+
* transient fault in the check is rethrown; a footer that cannot be read says nothing, and the error stands.
|
|
374
|
+
*/
|
|
375
|
+
private replacedUnder;
|
|
323
376
|
}
|
|
324
377
|
/**
|
|
325
378
|
* Write one immutable generation from in-memory bitmaps. Chunks are
|
|
@@ -333,9 +386,30 @@ export declare function writeCrbmGeneration(driver: IStorageDriver, key: GenKey,
|
|
|
333
386
|
}>, options?: {
|
|
334
387
|
crypto?: CrbmCrypto;
|
|
335
388
|
clock?: Yielder;
|
|
389
|
+
metadata?: GenerationMetadata;
|
|
336
390
|
}): Promise<{
|
|
337
391
|
size: number;
|
|
338
392
|
sha256: string;
|
|
393
|
+
fingerprint: string;
|
|
394
|
+
}>;
|
|
395
|
+
/**
|
|
396
|
+
* Whether the object under `key` is provably the one `fingerprint` names ({@link CrbmReader.fingerprint}), from one
|
|
397
|
+
* read of its footer. False when it is another object, when it is gone, and when the read fails in any way: a caller
|
|
398
|
+
* that deletes on `true` deletes nothing it cannot prove is its own.
|
|
399
|
+
*/
|
|
400
|
+
export declare function holdsObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
|
|
401
|
+
/** Whether the object under `key` says it is encrypted, from one read of its footer, with no key. */
|
|
402
|
+
export declare function objectIsEncrypted(storage: IStorageDriver, key: GenKey): Promise<boolean>;
|
|
403
|
+
/**
|
|
404
|
+
* What one tail read of the object under `key` tells a rollback about it: whether the footer says it is encrypted, and,
|
|
405
|
+
* when the object and the key agree (an encrypted object opened with `crypto`, a cleartext one with none), the reader
|
|
406
|
+
* the open gives, which holds its count and metadata. No reader when they disagree, which the caller refuses as a
|
|
407
|
+
* target the row cannot use, and none for an encrypted object when the caller has no key. A footer that fails its own
|
|
408
|
+
* checks, an object another generation's, and an index or metadata that do not open are an {@link IntegrityError}.
|
|
409
|
+
*/
|
|
410
|
+
export declare function openRollbackTarget(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined): Promise<{
|
|
411
|
+
encrypted: boolean;
|
|
412
|
+
reader: CrbmReader | undefined;
|
|
339
413
|
}>;
|
|
340
414
|
/** What {@link writeCrbmGenerationStream} wrote: the driver's `{ size, sha256 }` + a tally of the generation. */
|
|
341
415
|
export interface StreamWriteResult {
|
|
@@ -345,12 +419,14 @@ export interface StreamWriteResult {
|
|
|
345
419
|
readonly chunkKeys: number[];
|
|
346
420
|
/** Total ids written. */
|
|
347
421
|
readonly cardinality: number;
|
|
422
|
+
/** The fingerprint of what this call wrote ({@link CrbmReader.fingerprint}), which tells it from another object under its key. */
|
|
423
|
+
readonly fingerprint: string;
|
|
348
424
|
}
|
|
349
425
|
/**
|
|
350
426
|
* Streaming variant of {@link writeCrbmGeneration} for a **constant-memory** rewrite (the erasure rewrite, a
|
|
351
427
|
* sorted load): consumes an **already-ascending** async stream of `{ chunkKey, bitmap }`, feeding each to the
|
|
352
428
|
* codec and freeing it, instead of materializing the whole generation. Paired with a streaming storage sink (S3
|
|
353
|
-
* multipart / LocalFs temp file),
|
|
429
|
+
* multipart / LocalFs temp file), the writer itself holds ~one chunk + one part. Returns a tally so the caller can verify
|
|
354
430
|
* the re-opened object without re-iterating the (now-consumed) stream. Input **must** be ascending by `chunkKey`
|
|
355
431
|
* — the codec rejects an out-of-order chunk; empty bitmaps are skipped.
|
|
356
432
|
*/
|
|
@@ -360,6 +436,7 @@ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: G
|
|
|
360
436
|
}>, options?: {
|
|
361
437
|
crypto?: CrbmCrypto;
|
|
362
438
|
clock?: Yielder;
|
|
439
|
+
metadata?: GenerationMetadata;
|
|
363
440
|
}): Promise<StreamWriteResult>;
|
|
364
441
|
/**
|
|
365
442
|
* Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
|
|
@@ -385,17 +462,43 @@ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: G
|
|
|
385
462
|
* so the check and the write see the same row.
|
|
386
463
|
*
|
|
387
464
|
* **`expectToken` fences the LINEAGE, and a derived writer needs both.** A generation number identifies a
|
|
388
|
-
* generation only within one incarnation of a name:
|
|
465
|
+
* generation only within one incarnation of a name: the numbering restarts at `0` once the row is purged and
|
|
389
466
|
* the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
|
|
390
467
|
* `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
|
|
391
468
|
* over another's, deleted the live objects with its `keep: 0` collection, and returned `erased: true`. The row's
|
|
392
|
-
* OCC token is the identity that survives this:
|
|
393
|
-
*
|
|
469
|
+
* OCC token is the identity that survives this: a shipped registry's later `create` gets a token never issued before
|
|
470
|
+
* under that name, but for a collision of probability 2^-128 per pair of incarnations, so no token is reused across them.
|
|
394
471
|
*
|
|
395
472
|
* Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
|
|
396
473
|
* The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
|
|
397
474
|
* all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
|
|
398
475
|
* the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
|
|
476
|
+
*
|
|
477
|
+
* **The driver sends a registry write once, and an unanswered one is reconciled by its effect.** A `create` or
|
|
478
|
+
* `compareAndSwap` that fails with {@link TransientError} (a throttle, a lost response, a timeout) may or may not
|
|
479
|
+
* have landed, and may still be in flight. The publish reads the row once more and decides from what it holds:
|
|
480
|
+
*
|
|
481
|
+
* - the pointer names `key.generation`, on the incarnation the write was made against (the row's `createdAt`), and
|
|
482
|
+
* `holdsOwnObject` proves the object under it the caller's: the write landed, and the publish returns `true`. Only
|
|
483
|
+
* the writer of a write-once object publishes its number, and a number is never taken while an object holds it;
|
|
484
|
+
* - the row is still the one the write was made against (the same token, or still no row): the write may land yet. The
|
|
485
|
+
* publish then waits, on the injected clock, and sends a **new** write from the row it just read: a request of its
|
|
486
|
+
* own, not a replay of the driver's, carrying the same version, so under the registry's fence at most one of the two
|
|
487
|
+
* lands and the other meets a conflict, which the next read settles. At most {@link UNANSWERED_RESENDS} follow, after
|
|
488
|
+
* a wait under 500 ms, then 1 s, then 2 s (full jitter when an `rng` is given); each is settled the same way. Still
|
|
489
|
+
* unanswered, or with no clock to wait on, this throws the registry's own `TransientError` and writes nothing more.
|
|
490
|
+
* A caller must not delete its object on it;
|
|
491
|
+
* - anything else: the row has moved past the state the write was conditioned on, so that write can never land, and
|
|
492
|
+
* the publish goes on as after a lost race, from the row it just read. A pointer at `key.generation` that is not over
|
|
493
|
+
* the caller's own object, or is on another incarnation, is a lost race too and answers `false`, never an already
|
|
494
|
+
* current re-publish.
|
|
495
|
+
*
|
|
496
|
+
* A failed read throws `TransientError` too. The same check of the pointer runs after a {@link WriteConflictError},
|
|
497
|
+
* so a write that landed and then met itself (a registry whose client re-sent it) is not taken for a lost race.
|
|
498
|
+
*
|
|
499
|
+
* **A `summary` goes in the same write that moves the pointer**, so a reader that sees this generation as current
|
|
500
|
+
* sees the count and metadata that describe it, and every attempt sends the same one. It must be the shape the row's
|
|
501
|
+
* keys call for: sealed for an object written with a key, clear for one written without.
|
|
399
502
|
*/
|
|
400
503
|
export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: {
|
|
401
504
|
wrappedDeks?: readonly WrappedDek[];
|
|
@@ -417,7 +520,55 @@ export declare function publishGeneration(registry: IRegistryDriver, key: GenKey
|
|
|
417
520
|
* destination that does not exist yet is the ordinary first run of a pipeline.
|
|
418
521
|
*/
|
|
419
522
|
expectAbsent?: boolean;
|
|
523
|
+
/**
|
|
524
|
+
* The generation's summary (see {@link RegistrySummary}), written by the write that moves the pointer to it. It
|
|
525
|
+
* names `key.generation`. Absent, the row is left with none for this generation.
|
|
526
|
+
*/
|
|
527
|
+
summary?: RegistrySummary;
|
|
528
|
+
/**
|
|
529
|
+
* The segment's row as the caller already read it (`null`: it found none). The first attempt acts on it
|
|
530
|
+
* instead of reading the row again, which is sound because that attempt writes only under the row's own
|
|
531
|
+
* fence: its compare-and-swap carries this row's token, and with no row it creates create-only, so a row
|
|
532
|
+
* that changed or appeared since makes the write lose and the next attempt reads the row afresh. Only the
|
|
533
|
+
* idempotent "already current" answer writes nothing, so a reused row never gives it.
|
|
534
|
+
*/
|
|
535
|
+
row?: RegistryRecord | null;
|
|
536
|
+
/**
|
|
537
|
+
* The object was written without encryption. Its publish is refused, with {@link KeyUnavailableError}, onto a row
|
|
538
|
+
* that carries key material: a reader of that segment opens every generation with its key, and a later
|
|
539
|
+
* `destroySegment` would attest that shredding it made a readable object unreadable. A caller decides its key
|
|
540
|
+
* from the row it read before the write, so this closes the window in which another writer creates the
|
|
541
|
+
* segment encrypted meanwhile.
|
|
542
|
+
*/
|
|
543
|
+
cleartext?: boolean;
|
|
544
|
+
/**
|
|
545
|
+
* Whether the object under `key` is the one the caller wrote, and throws when it cannot tell. Asked only when a
|
|
546
|
+
* registry write failed and the row then names `key.generation`, to tell this publish's write from another
|
|
547
|
+
* writer's at the same number. Every caller that writes the object passes it. Without it, a write that ended
|
|
548
|
+
* without an answer over a row that names `key.generation` cannot be settled: the publish throws
|
|
549
|
+
* {@link TransientError} and the caller keeps its object.
|
|
550
|
+
*/
|
|
551
|
+
holdsOwnObject?: () => Promise<boolean>;
|
|
552
|
+
/**
|
|
553
|
+
* What a publish waits on before it sends a fresh compare-and-swap, after a write that ended without an answer and
|
|
554
|
+
* left the row as it was. Without a `sleep`, such a write throws at once instead of being sent again.
|
|
555
|
+
*/
|
|
556
|
+
clock?: Yielder;
|
|
557
|
+
/** Spreads that wait (full jitter) so concurrent publishers do not retry together. Without it the wait is the bound. */
|
|
558
|
+
rng?: Rng;
|
|
559
|
+
/**
|
|
560
|
+
* Called once for each registry write that ended without an answer, before the row is read back. A caller whose
|
|
561
|
+
* publish then answers `false` can say that it cannot tell whether its own write landed first: the row may have
|
|
562
|
+
* held its generation for a while before another writer moved on.
|
|
563
|
+
*/
|
|
564
|
+
onUnanswered?: () => void;
|
|
420
565
|
}): Promise<boolean>;
|
|
566
|
+
/**
|
|
567
|
+
* Whether the object under `key` is the one `fingerprint` names, from one read of its footer: false when it is another
|
|
568
|
+
* object or gone. Unlike {@link holdsObject}, a read that cannot tell throws, for a caller that must not take "could
|
|
569
|
+
* not look" for "not this object".
|
|
570
|
+
*/
|
|
571
|
+
export declare function provesOwnObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
|
|
421
572
|
/** What a bulk-load wrote — the driver's `{ size, sha256 }` plus a summary of the built generation. */
|
|
422
573
|
export interface BulkLoadResult {
|
|
423
574
|
/** Bytes written to the Storage object. */
|
|
@@ -436,8 +587,18 @@ export interface BulkLoadResult {
|
|
|
436
587
|
* unreadable: the bytes are encrypted with a key nothing recorded.
|
|
437
588
|
*/
|
|
438
589
|
readonly wrappedDeks?: readonly WrappedDek[];
|
|
590
|
+
/** Whether the object was written encrypted: under the segment's key, or under one this call minted. */
|
|
591
|
+
readonly encrypted: boolean;
|
|
592
|
+
/** The written object's fingerprint, which tells it from an object stored later under the same key. */
|
|
593
|
+
readonly fingerprint: string;
|
|
439
594
|
/** Total distinct ids in the generation (post-dedup). */
|
|
440
595
|
readonly cardinality: number;
|
|
596
|
+
/**
|
|
597
|
+
* The summary of what was written, for the write that makes it current: its id count and metadata, sealed under the
|
|
598
|
+
* segment's key when the object was written with one. A caller that defers the publish (`publish: false`) passes it
|
|
599
|
+
* to {@link publishGeneration}.
|
|
600
|
+
*/
|
|
601
|
+
readonly summary: RegistrySummary;
|
|
441
602
|
/**
|
|
442
603
|
* Whether this generation is now the segment's **current** one — `undefined` when no `registry` was wired, so
|
|
443
604
|
* there is no pointer and no publish step to report on.
|
|
@@ -467,11 +628,12 @@ export interface BulkLoadResult {
|
|
|
467
628
|
* commits only after the callback resolves). An empty source writes a valid empty generation.
|
|
468
629
|
*
|
|
469
630
|
* Writes a fresh full snapshot of a segment. The caller picks the generation number in `key` — `nextGeneration`
|
|
470
|
-
* computes
|
|
631
|
+
* computes one from the registry and the bucket, as `nextLoadGeneration` does for a load — and re-using an existing
|
|
632
|
+
* generation throws
|
|
471
633
|
* {@link WriteConflictError} (write-once). Without a registry a `StorageChunkSource` serves the **highest**
|
|
472
634
|
* generation present, so a too-high number silently shadows real data; with one, `publishGeneration` decides.
|
|
473
635
|
*/
|
|
474
|
-
export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number
|
|
636
|
+
export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput, options?: {
|
|
475
637
|
registry?: IRegistryDriver;
|
|
476
638
|
keystore?: IKeystore;
|
|
477
639
|
requireEncryption?: boolean;
|
|
@@ -502,6 +664,22 @@ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenK
|
|
|
502
664
|
* current.
|
|
503
665
|
*/
|
|
504
666
|
publish?: boolean;
|
|
667
|
+
/**
|
|
668
|
+
* The generation's metadata (see {@link GenerationMetadata}), written into the object and carried in the summary.
|
|
669
|
+
* The caller has checked it against the metadata rules; the writer checks it again, and refuses a record that breaks
|
|
670
|
+
* one before a byte is written.
|
|
671
|
+
*/
|
|
672
|
+
metadata?: GenerationMetadata;
|
|
673
|
+
/**
|
|
674
|
+
* The segment's row as the caller already read it (`null`: it found none), for a caller that defers the
|
|
675
|
+
* publish (`publish: false`) and fences it on that same row. A present cleartext row is then used as read,
|
|
676
|
+
* rather than read again: a row that changes in between (a publish, a drop, a purge and re-create) makes the
|
|
677
|
+
* fenced publish lose, so the object is never published on the strength of the stale read. A row read as
|
|
678
|
+
* absent, or one carrying key material, is read again after the ids, as with no row passed: a first load must
|
|
679
|
+
* see a row another writer created meanwhile, and an encrypted segment's key is unwrapped only from a row read
|
|
680
|
+
* after the ids, so a segment shredded while they streamed is refused before its key is used.
|
|
681
|
+
*/
|
|
682
|
+
row?: RegistryRecord | null;
|
|
505
683
|
}): Promise<BulkLoadResult>;
|
|
506
684
|
/**
|
|
507
685
|
* Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
|
|
@@ -511,12 +689,15 @@ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenK
|
|
|
511
689
|
export declare function openGenerationReader(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, options?: Omit<CrbmReaderOptions, 'crypto'>): Promise<CrbmReader>;
|
|
512
690
|
/**
|
|
513
691
|
* Re-open a freshly written generation and assert it round-trips exactly what was streamed into it: the same
|
|
514
|
-
* per-chunk key set *and* the same total cardinality (on top of the codec's own per-chunk CRC + footer checks)
|
|
515
|
-
* `expected` is the streaming writer's tally (the stream is consumed, so it can't be
|
|
516
|
-
* comparison catches a dropped/extra chunk that a cardinality-only check could miss when
|
|
692
|
+
* per-chunk key set *and* the same total cardinality (on top of the codec's own per-chunk CRC + footer checks), and the
|
|
693
|
+
* metadata it was given. `expected` is the streaming writer's tally (the stream is consumed, so it can't be
|
|
694
|
+
* re-iterated) — the key-set comparison catches a dropped/extra chunk that a cardinality-only check could miss when
|
|
695
|
+
* two errors cancel out. The count and the metadata are what the generation's row summary is about to claim, so the
|
|
696
|
+
* summary is held to the object it describes before it is published.
|
|
517
697
|
* Throws {@link IntegrityError}: the object is on disk but must not be published.
|
|
518
698
|
*/
|
|
519
699
|
export declare function verifyGeneration(storage: IStorageDriver, key: GenKey, expected: {
|
|
520
700
|
readonly chunkKeys: readonly number[];
|
|
521
701
|
readonly cardinality: number;
|
|
702
|
+
readonly metadata?: GenerationMetadata | undefined;
|
|
522
703
|
}, crypto: CrbmCrypto | undefined): Promise<void>;
|
package/dist/core/crypto.d.ts
CHANGED
|
@@ -62,19 +62,23 @@ export interface IKeystore {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* What the encrypting codec needs: the {@link Aead} for this segment's DEK plus a way to build the AAD for a
|
|
65
|
-
* given scope (`'index'
|
|
66
|
-
* codec itself stays segment-agnostic.
|
|
65
|
+
* given scope (a chunkKey, `'index'`, or `'metadata'` for the generation's metadata section). Built per
|
|
66
|
+
* (segment, generation) by the storage-source bridge so the codec itself stays segment-agnostic.
|
|
67
67
|
*/
|
|
68
68
|
export interface CrbmCrypto {
|
|
69
69
|
readonly aead: Aead;
|
|
70
|
-
aadFor(scope: number | 'index'): Uint8Array;
|
|
70
|
+
aadFor(scope: number | 'index' | 'metadata'): Uint8Array;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* Build the AEAD associated-data binding a chunk
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* Build the AEAD associated-data binding a chunk, the index, the metadata section or a registry row's sealed summary
|
|
74
|
+
* to its exact location:
|
|
75
|
+
* `v1 ‖ len(namespace) ‖ namespace ‖ len(segment) ‖ segment ‖ generation ‖ scope ‖ chunkKey`, where scope is
|
|
76
|
+
* `0` for a chunk, `1` for the index, `2` for the metadata and `3` for the row's summary, and chunkKey is `0` for
|
|
77
|
+
* the last three.
|
|
78
|
+
* Length-prefixed so distinct `(namespace, segment)` pairs can never collide. Pure (no crypto) — just the
|
|
79
|
+
* authenticated label.
|
|
76
80
|
*/
|
|
77
81
|
export declare function aadFor(ref: {
|
|
78
82
|
readonly namespace?: string;
|
|
79
83
|
readonly segment: string;
|
|
80
|
-
}, generation: number, scope: number | 'index'): Uint8Array;
|
|
84
|
+
}, generation: number, scope: number | 'index' | 'metadata' | 'summary'): Uint8Array;
|
|
@@ -40,3 +40,10 @@ export interface Rng {
|
|
|
40
40
|
/** A float in the half-open interval [0, 1). */
|
|
41
41
|
next(): number;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Injected entropy: `length` cryptographically strong random bytes, for an identity that must never repeat (a
|
|
45
|
+
* registry row's incarnation id, and the random part every registry write adds to its token). Deliberately not an
|
|
46
|
+
* {@link Rng}: an `Rng` is seedable for simulation, and a seeded source hands every process the same "random"
|
|
47
|
+
* identity. Production wiring uses the platform's Web Crypto; a test injects a deterministic source to replay a run.
|
|
48
|
+
*/
|
|
49
|
+
export type Entropy = (length: number) => Uint8Array;
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -3,11 +3,12 @@ import type { Budget, BudgetOption } from './budget.js';
|
|
|
3
3
|
import type { Clock } from './determinism.js';
|
|
4
4
|
import type { BoundedLru } from './lru.js';
|
|
5
5
|
import type { IMetricsSink } from './metrics.js';
|
|
6
|
-
import type { StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
6
|
+
import type { GenerationMetadata, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
7
7
|
export interface EngineDeps {
|
|
8
8
|
readonly storage: StorageChunkSource;
|
|
9
9
|
/** Optional cache of decoded (immutable) Storage chunks. */
|
|
10
10
|
readonly cache?: BoundedLru<string, CodecBitmap>;
|
|
11
|
+
/** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. The `.crbm` reader refuses an entry above its own `maxPayloadBytes` (1 MiB, plus 28 bytes when encrypted) at open, so raise that on the chunk source too. */
|
|
11
12
|
readonly maxBitmapBytes?: number;
|
|
12
13
|
/**
|
|
13
14
|
* The bitmap codec — **required**. `core/` is codec-agnostic: it can have no default, because the concrete codec
|
|
@@ -47,7 +48,13 @@ export interface IdRange {
|
|
|
47
48
|
}
|
|
48
49
|
/** Options common to the chunk-aligned combines. */
|
|
49
50
|
export interface CombineOptions extends IdRange {
|
|
50
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Max chunk keys resolved concurrently — bounds the Storage footprint (about `concurrency × operands` chunks in
|
|
53
|
+
* flight). A positive integer; default 32. The window opens 8 keys wide (or `concurrency`, if lower) and widens
|
|
54
|
+
* as keys are taken, so a read that stops early fetches no more than a few keys ahead; one that runs to the end
|
|
55
|
+
* spends nearly all of its round trips at the full width. A read that stops early has requested up to
|
|
56
|
+
* `concurrency` keys per operand past the last one it used.
|
|
57
|
+
*/
|
|
51
58
|
readonly concurrency?: number;
|
|
52
59
|
/** Override the store's per-op budget for this call (`false` lifts it). */
|
|
53
60
|
readonly budget?: BudgetOption;
|
|
@@ -70,19 +77,37 @@ export declare class SegmentEngine {
|
|
|
70
77
|
private readonly metricsOn;
|
|
71
78
|
/** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. */
|
|
72
79
|
private readonly budget;
|
|
80
|
+
/**
|
|
81
|
+
* The chunk reads open right now, by cache key: a caller that misses the cache and finds its key here awaits that
|
|
82
|
+
* read instead of making its own. An entry lives from the request until it settles, so the map holds at most one
|
|
83
|
+
* promise per distinct key in flight.
|
|
84
|
+
*/
|
|
85
|
+
private readonly openReads;
|
|
73
86
|
constructor(deps: EngineDeps);
|
|
74
87
|
/** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
|
|
75
88
|
has(seg: SegmentRef, id: number): Promise<boolean>;
|
|
76
89
|
/**
|
|
77
|
-
* Cardinality. When the Storage source can
|
|
78
|
-
*
|
|
79
|
-
*
|
|
90
|
+
* Cardinality. When the Storage source can describe the current generation (the `.crbm` source can), the count is
|
|
91
|
+
* that description's: the registry row's summary when the row has one it can use, which is one request when cold
|
|
92
|
+
* and no read of the object, and otherwise the sum of the object's index. Either way, **zero payload reads**. A
|
|
93
|
+
* source that can only serve per-chunk cardinality sums those, and one with neither (the in-memory source) falls back
|
|
94
|
+
* to fetching every chunk.
|
|
80
95
|
*
|
|
81
|
-
* The index sum
|
|
96
|
+
* The row's summary and the index sum are trusted, not confirmed: the source holds the summary against the object
|
|
97
|
+
* whenever it opens it, and the reader checked the index for internal consistency when it opened
|
|
82
98
|
* the object, and a corrupt index that is still internally consistent yields a wrong count. `iterate()` and the
|
|
83
99
|
* combines decode the payloads.
|
|
84
100
|
*/
|
|
85
101
|
count(seg: SegmentRef): Promise<number>;
|
|
102
|
+
/**
|
|
103
|
+
* What the segment's current generation is: its number, its id count and its metadata, from one resolution, so the
|
|
104
|
+
* three cannot straddle a publish. A source with no summary answers from `count()` and the generation it resolves.
|
|
105
|
+
*/
|
|
106
|
+
stat(seg: SegmentRef): Promise<{
|
|
107
|
+
generation: number | null;
|
|
108
|
+
cardinality: number;
|
|
109
|
+
metadata?: GenerationMetadata;
|
|
110
|
+
}>;
|
|
86
111
|
/** Whether the Storage source can measure segment size (for grounded cost); false ⇒ storage isn't grounded. */
|
|
87
112
|
get supportsStorageSize(): boolean;
|
|
88
113
|
/** How often the Storage source re-reads a pointer while reading (cost reporting); undefined if it does not say. */
|
|
@@ -92,7 +117,7 @@ export declare class SegmentEngine {
|
|
|
92
117
|
/** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
|
|
93
118
|
private chunkWindow;
|
|
94
119
|
/**
|
|
95
|
-
* Every id, ascending, reading ahead through a window of up to
|
|
120
|
+
* Every id, ascending, reading ahead through a window of up to 32 chunk fetches that opens 1, 2, 4 … 32 wide, so a
|
|
96
121
|
* read that stops early has fetched only a handful of chunks past the one it stopped in; with `range`, only the ids in `(after, through]`, fetching only the
|
|
97
122
|
* chunks the range overlaps (see {@link IdRange}).
|
|
98
123
|
*
|
|
@@ -243,7 +268,15 @@ export declare class SegmentEngine {
|
|
|
243
268
|
* `gen === undefined` ⇒ the source can't report a generation (it only ever serves one) ⇒ the key stays
|
|
244
269
|
* generation-free. Superseded-generation entries age out under the LRU ceiling — no active purge.
|
|
245
270
|
*
|
|
246
|
-
*
|
|
271
|
+
* Concurrent misses of one key share **one** request, with or without a cache: the first caller starts the read,
|
|
272
|
+
* later ones await it, and the entry goes when the read settles, so a failure is every waiting caller's and the
|
|
273
|
+
* next call reads again. The key carries the version exactly as the cache's does, so two versions of one chunk
|
|
274
|
+
* are two reads, never one.
|
|
275
|
+
*
|
|
276
|
+
* The returned instance is **shared** (it may be the cached one, and every caller that joined a read gets the same
|
|
277
|
+
* one): callers read it or clone it, never mutate it.
|
|
247
278
|
*/
|
|
248
279
|
private storageChunk;
|
|
280
|
+
/** One request for one chunk, decoded, range-checked and cached. */
|
|
281
|
+
private fetchChunk;
|
|
249
282
|
}
|
package/dist/core/erase-id.d.ts
CHANGED
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
* is what every other write in this library is: a new generation. The current generation is streamed chunk by
|
|
7
7
|
* chunk through the ascending writer — every chunk decoded, range-checked and re-encoded, the one holding the id
|
|
8
8
|
* with that bit cleared — then published fenced on the generation it streamed, and that generation is collected immediately
|
|
9
|
-
* (`keep: 0`), so the bit is **physically gone from the bucket when this returns**.
|
|
10
|
-
*
|
|
9
|
+
* (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Bounded memory: a window of 32
|
|
10
|
+
* chunk reads ahead of the writer, never the whole segment.
|
|
11
|
+
*
|
|
12
|
+
* The rewrite is the same generation without one id, so it keeps everything else: the new object carries the source's
|
|
13
|
+
* metadata as it is, and the row's summary of it, built from what was written, counts one id fewer and holds the same
|
|
14
|
+
* metadata. Metadata is not scanned for the id. On an encrypted segment, a source object with no metadata block whose
|
|
15
|
+
* row's sealed summary of it has metadata is rewritten with the row's: the block's presence is not authenticated, so
|
|
16
|
+
* the authenticated copy decides, and the erasure still goes through.
|
|
11
17
|
*
|
|
12
18
|
* **`erased: true` is a claim about every generation of the segment, not only the one it replaced.** A rollback
|
|
13
19
|
* leaves generations above the pointer that were once current and can be made current again, so a holder can sit
|
|
@@ -69,8 +75,10 @@
|
|
|
69
75
|
import { type IAuditSink } from './audit.js';
|
|
70
76
|
import type { CodecInterface } from './codec.js';
|
|
71
77
|
import type { Yielder } from './cooperative.js';
|
|
78
|
+
import type { Rng } from './determinism.js';
|
|
72
79
|
import type { IKeystore } from './crypto.js';
|
|
73
80
|
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
81
|
+
import { type ReadRetry } from './retry.js';
|
|
74
82
|
/** What {@link eraseIdFromSegment} needs: the objects, the pointer, the codec, and the key material if encrypted. */
|
|
75
83
|
export interface EraseIdDeps {
|
|
76
84
|
readonly storage: IStorageDriver;
|
|
@@ -90,8 +98,20 @@ export interface EraseIdDeps {
|
|
|
90
98
|
readonly requireEncryption?: boolean;
|
|
91
99
|
/** Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. */
|
|
92
100
|
readonly clock?: Yielder;
|
|
93
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* The random source that spreads the waits between the rewrite's fresh compare-and-swaps. Absent, the read retry's
|
|
103
|
+
* source is used if it has one, and otherwise each wait is its bound.
|
|
104
|
+
*/
|
|
105
|
+
readonly rng?: Rng;
|
|
106
|
+
/** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. The `.crbm` reader refuses an entry above its own `maxPayloadBytes` (1 MiB, plus 28 bytes when encrypted) at open, so raise that on the chunk source too. */
|
|
94
107
|
readonly maxBitmapBytes?: number;
|
|
108
|
+
/**
|
|
109
|
+
* The store's read retry, for the reads the rewrite makes along the way: the generation it rewrites and each of its
|
|
110
|
+
* chunks, the read-back that verifies the generation it wrote, and any other generation it checks for the id. A
|
|
111
|
+
* transient fault on one is run again under it rather than failing the erasure. Absent, each read is made once.
|
|
112
|
+
* The deletes are not retried, and the rewrite's registry write is settled as a load's is.
|
|
113
|
+
*/
|
|
114
|
+
readonly readRetry?: ReadRetry;
|
|
95
115
|
}
|
|
96
116
|
export interface EraseIdResult {
|
|
97
117
|
readonly segment: string;
|
package/dist/core/erasure.d.ts
CHANGED
|
@@ -159,10 +159,14 @@ export interface DropResult {
|
|
|
159
159
|
* cleaned up by re-running; a torn pointer costs correctness and is not self-healing. Given the choice, leak
|
|
160
160
|
* bytes — but say so: whatever survives the sweep is reported in {@link DropResult.generationsRemaining}.
|
|
161
161
|
*
|
|
162
|
-
* **Why step 2 sweeps more than once.** A load that
|
|
163
|
-
* still finishes the write — its publish is then refused,
|
|
164
|
-
* set. A single list-then-delete misses it entirely. The re-sweep
|
|
165
|
-
* fence on **publishing**, so only already
|
|
162
|
+
* **Why step 2 sweeps more than once.** A load that had read the row before the tombstone landed, whether it was
|
|
163
|
+
* already writing its object or still consuming its ids, still finishes the write — its publish is then refused,
|
|
164
|
+
* but the object exists, and it holds the complete set. A single list-then-delete misses it entirely. The re-sweep
|
|
165
|
+
* converges because the tombstone *is* a hard fence on **publishing**, so only loads already under way can write
|
|
166
|
+
* and they are finite. One that writes after the last pass deletes its own object when its publish is refused, as a
|
|
167
|
+
* refused load does under a `destroyed` row; only one whose process stops in between, or whose publish fails without
|
|
168
|
+
* a definite answer (a lost response, a timeout), leaves it behind, for a re-run of the drop, and
|
|
169
|
+
* `generationsRemaining` cannot report an object written after this call returned.
|
|
166
170
|
*
|
|
167
171
|
* **When "reads as empty" starts being true.** Not instantly, for a store that has already read this segment: a
|
|
168
172
|
* resolved generation is cached and decoded chunks sit in the cache, so an in-flight reader can answer from
|