@cloudbitmaps/core 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/chunk-7GT632UF.js +641 -0
- package/dist/chunk-7GT632UF.js.map +7 -0
- package/dist/chunk-CGILDXGP.js +73 -0
- package/dist/chunk-CGILDXGP.js.map +7 -0
- package/dist/chunk-GKHERVVQ.js +121 -0
- package/dist/chunk-GKHERVVQ.js.map +7 -0
- package/dist/{chunk-OSU7W2Y3.js → chunk-MVUD4R72.js} +200 -756
- package/dist/chunk-MVUD4R72.js.map +7 -0
- package/dist/core/audit.d.ts +53 -15
- package/dist/core/bit-route.d.ts +2 -0
- package/dist/core/blob.d.ts +4 -0
- package/dist/core/budget.d.ts +8 -3
- package/dist/core/check.d.ts +49 -0
- package/dist/core/combine-many.d.ts +11 -0
- package/dist/core/consistency.d.ts +11 -2
- package/dist/core/cooperative.d.ts +7 -0
- package/dist/core/crbm-storage-source.d.ts +7 -4
- package/dist/core/crypto.d.ts +5 -0
- package/dist/core/discover.d.ts +5 -3
- package/dist/core/engine.d.ts +4 -4
- package/dist/core/erase-id.d.ts +35 -12
- package/dist/core/erasure.d.ts +61 -27
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/leases.d.ts +2 -0
- package/dist/core/load-input.d.ts +48 -0
- package/dist/core/load.d.ts +23 -2
- package/dist/core/metrics.d.ts +21 -0
- package/dist/core/pinned-storage-source.d.ts +27 -8
- package/dist/core/pointer-id.d.ts +9 -0
- package/dist/core/ports.d.ts +100 -7
- package/dist/core/preflight.d.ts +80 -0
- package/dist/core/retention-sweep.d.ts +60 -15
- package/dist/core/retention.d.ts +17 -6
- package/dist/core/retry.d.ts +1 -2
- package/dist/core/rollback.d.ts +22 -12
- package/dist/core/validate.d.ts +33 -2
- package/dist/driver-kit.d.ts +3 -3
- package/dist/driver-kit.js +61 -20
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +24 -1
- package/dist/drivers/_shared/options.d.ts +15 -7
- package/dist/drivers/backends.d.ts +17 -2
- package/dist/drivers/crypto.d.ts +17 -2
- package/dist/export/index.d.ts +16 -2
- package/dist/index.d.ts +10 -9
- package/dist/index.js +1743 -1701
- package/dist/index.js.map +4 -4
- package/dist/node.d.ts +14 -0
- package/dist/node.js +1064 -0
- package/dist/node.js.map +7 -0
- package/package.json +11 -2
- package/dist/chunk-OSU7W2Y3.js.map +0 -7
package/dist/core/ports.d.ts
CHANGED
|
@@ -25,16 +25,29 @@ export type Token = string;
|
|
|
25
25
|
* segment in such a namespace would be invisible to an erasure, a consistency check, an export and a retention sweep.
|
|
26
26
|
* A driver takes the prefix, since the due index's own rows go through it. Only that exact prefix is reserved:
|
|
27
27
|
* `cbm.dueX` and `cbmdue.eu` are ordinary namespaces, and a segment name is never restricted.
|
|
28
|
+
*
|
|
29
|
+
* **A key other than `segment` and `namespace` throws `ValidationError`**, in every call of the store and of the free
|
|
30
|
+
* functions behind it that takes a ref, and at a driver port (which also takes a `generation`, a `chunkKey` and a
|
|
31
|
+
* registry row's own fields): a misspelt `namespace` would otherwise address the segment of that name in the default
|
|
32
|
+
* namespace. A `segments()` row is the one exception at the store: its `currentGen` and `status` are taken and read for
|
|
33
|
+
* nothing. A segment handle, a registry row from `registry.get()` or `list()` (a driver port takes one, a store call does
|
|
34
|
+
* not), and a result row such as a consistency issue, a ledger entry, an export manifest entry or an audit event, is not
|
|
35
|
+
* a ref at the store: pass `{ segment, namespace }`.
|
|
28
36
|
*/
|
|
29
37
|
export interface SegmentRef {
|
|
38
|
+
/** The segment's namespace, or `undefined` for the default one. */
|
|
30
39
|
readonly namespace?: string;
|
|
40
|
+
/** The segment's name. */
|
|
31
41
|
readonly segment: string;
|
|
32
42
|
}
|
|
43
|
+
/** One chunk of a segment: the segment, and the chunk's key. */
|
|
33
44
|
export interface ChunkRef extends SegmentRef {
|
|
45
|
+
/** The chunk's key: an integer from 0 to 65,535, the high 16 bits of the ids the chunk holds. */
|
|
34
46
|
readonly chunkKey: number;
|
|
35
47
|
}
|
|
36
48
|
/** Identifies one immutable `.crbm` object — a single generation of a segment. */
|
|
37
49
|
export interface GenKey extends SegmentRef {
|
|
50
|
+
/** The generation number of the object. */
|
|
38
51
|
readonly generation: number;
|
|
39
52
|
}
|
|
40
53
|
/**
|
|
@@ -43,6 +56,7 @@ export interface GenKey extends SegmentRef {
|
|
|
43
56
|
* holds for an operand's stream.
|
|
44
57
|
*/
|
|
45
58
|
export interface SegmentSize {
|
|
59
|
+
/** The size of the current generation's object, in bytes. */
|
|
46
60
|
readonly sizeBytes: number;
|
|
47
61
|
}
|
|
48
62
|
/**
|
|
@@ -52,8 +66,11 @@ export interface SegmentSize {
|
|
|
52
66
|
* can key what it keeps by the generation the bytes came from rather than the one it resolved earlier.
|
|
53
67
|
*/
|
|
54
68
|
export interface ChunkRead {
|
|
69
|
+
/** The chunk key that was asked for. */
|
|
55
70
|
readonly key: number;
|
|
71
|
+
/** The chunk's bytes, or `null` where the generation holds none. */
|
|
56
72
|
readonly bytes: Uint8Array | null;
|
|
73
|
+
/** The version of the generation the bytes were read from, or `null` when the segment has no generation. */
|
|
57
74
|
readonly version: string | null;
|
|
58
75
|
}
|
|
59
76
|
/** Options of {@link StorageChunkSource.getChunks}. */
|
|
@@ -91,6 +108,7 @@ export interface ReadChunksOptions {
|
|
|
91
108
|
export interface StorageChunkSource {
|
|
92
109
|
/** Read-only bytes for the chunk, or `null` if absent. Callers must not mutate the buffer. */
|
|
93
110
|
getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
|
|
111
|
+
/** The chunk keys the segment's current generation holds, or an empty list when it has none. */
|
|
94
112
|
listChunkKeys(ref: SegmentRef): Promise<number[]>;
|
|
95
113
|
/**
|
|
96
114
|
* Optional: several chunks of one segment as a stream, read **from one generation** with fewer storage requests than
|
|
@@ -330,6 +348,7 @@ export interface StorageDeleteOptions {
|
|
|
330
348
|
* retry can ride it out and a write's caller can tell it from a deterministic failure.
|
|
331
349
|
*/
|
|
332
350
|
export interface IStorageDriver {
|
|
351
|
+
/** What this driver can do. Range reads and `maxObjectBytes` are required; the other capabilities are optional. */
|
|
333
352
|
capabilities(): StorageCaps;
|
|
334
353
|
/**
|
|
335
354
|
* Stream a new immutable generation. The driver opens a destination, hands `write` a {@link BlobSink},
|
|
@@ -337,6 +356,12 @@ export interface IStorageDriver {
|
|
|
337
356
|
* already exists (write-once). Call `write` once: the object committed is the bytes that one call wrote, because
|
|
338
357
|
* the caller records the write identity of that call's object (its footer) for every later proof; a driver that
|
|
339
358
|
* runs `write` again after a failed attempt must commit only the last run's bytes.
|
|
359
|
+
*
|
|
360
|
+
* What a second run of `write` does depends on what is written. A load of ids, of a bitmap or of a combine's chunks
|
|
361
|
+
* holds its chunks in memory, so each run writes the whole object again, under a write identity of its own, and the
|
|
362
|
+
* load records the run that was committed. A write that streams its chunks, a load with `sorted: true` and an
|
|
363
|
+
* erasure's rewrite, reads them once, so its second run throws {@link IntegrityError} before writing a byte: commit
|
|
364
|
+
* nothing then, and the call throws that error, with the first run's error as its `cause`.
|
|
340
365
|
*/
|
|
341
366
|
putImmutable(key: GenKey, write: (sink: BlobSink) => Promise<void>): Promise<{
|
|
342
367
|
size: number;
|
|
@@ -368,8 +393,9 @@ export interface IStorageDriver {
|
|
|
368
393
|
list(ref: SegmentRef): AsyncIterable<GenKey>;
|
|
369
394
|
}
|
|
370
395
|
/**
|
|
371
|
-
* Lifecycle status of a segment. `active` is the steady state; `destroyed` is the
|
|
372
|
-
* (the row is kept for audit but the segment is logically gone
|
|
396
|
+
* Lifecycle status of a segment. `active` is the steady state; `destroyed` is the tombstone left by a crypto-shred, a
|
|
397
|
+
* cleartext destroy or a drop (the row is kept for audit but the segment is logically gone: no read resolves a
|
|
398
|
+
* generation through it).
|
|
373
399
|
*/
|
|
374
400
|
export type RegistryStatus = 'active' | 'destroyed';
|
|
375
401
|
/**
|
|
@@ -404,7 +430,9 @@ export type RegistrySummary = ClearRegistrySummary | SealedRegistrySummary;
|
|
|
404
430
|
export type GenerationMetadata = Readonly<Record<string, string | number>>;
|
|
405
431
|
/** What a generation is, as one read answers it: its number, its id count and the metadata it was written with. */
|
|
406
432
|
export interface GenerationSummary {
|
|
433
|
+
/** The generation's number. */
|
|
407
434
|
readonly generation: number;
|
|
435
|
+
/** The number of ids the generation holds. */
|
|
408
436
|
readonly cardinality: number;
|
|
409
437
|
/** Absent when the generation has none. */
|
|
410
438
|
readonly metadata?: GenerationMetadata;
|
|
@@ -486,8 +514,9 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
486
514
|
readonly currentGen: number | null;
|
|
487
515
|
/**
|
|
488
516
|
* Per-segment data-key (DEK) wrappings for encryption-at-rest: the DEK envelope-wrapped under one or more KEKs
|
|
489
|
-
* (active + optional recovery). Reading unwraps with any held KEK; **crypto-shred deletes this whole list
|
|
490
|
-
*
|
|
517
|
+
* (active + optional recovery). Reading unwraps with any held KEK; **crypto-shred deletes this whole list** from
|
|
518
|
+
* the current row, so the segment's at-rest bytes are unrecoverable once no retained copy of the row (a noncurrent
|
|
519
|
+
* version, a backup) still holds it, or every KEK that wrapped it is destroyed. Absent ⇒ the segment is cleartext.
|
|
491
520
|
*/
|
|
492
521
|
readonly wrappedDeks?: readonly WrappedDek[];
|
|
493
522
|
/**
|
|
@@ -496,6 +525,10 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
496
525
|
* keystore. Clearable on crypto-shred.
|
|
497
526
|
*/
|
|
498
527
|
readonly keyId?: string;
|
|
528
|
+
/**
|
|
529
|
+
* `'active'`, or `'destroyed'` for the tombstone of a segment that was crypto-shredded, destroyed in the clear or
|
|
530
|
+
* dropped.
|
|
531
|
+
*/
|
|
499
532
|
readonly status: RegistryStatus;
|
|
500
533
|
/**
|
|
501
534
|
* Governance policy. `retention.expiresAt` drives the retention sweep (see {@link GovernanceMeta}); `residency`
|
|
@@ -503,6 +536,7 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
503
536
|
* `list()` projection — a fleet sweep reads the policy from the enumeration rather than per-segment.
|
|
504
537
|
*/
|
|
505
538
|
readonly retention?: GovernanceMeta;
|
|
539
|
+
/** Residency metadata, a plain object. It is stored and returned as given; nothing reads it yet. */
|
|
506
540
|
readonly residency?: GovernanceMeta;
|
|
507
541
|
/**
|
|
508
542
|
* A cached description of the current generation (see {@link RegistrySummary}). Optional: a row without one is
|
|
@@ -539,6 +573,7 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
539
573
|
readonly leases?: readonly LeaseEntry[];
|
|
540
574
|
/** Epoch-ms of creation / last mutation (from the driver's injected clock). */
|
|
541
575
|
readonly createdAt: number;
|
|
576
|
+
/** Epoch-ms of the row's last write, from the driver's clock. */
|
|
542
577
|
readonly updatedAt: number;
|
|
543
578
|
/**
|
|
544
579
|
* Opaque OCC token — compare-by-equality, ABA-safe. A shipped registry gives no two writes under one name the same
|
|
@@ -566,11 +601,15 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
566
601
|
export interface NewRegistryRecord {
|
|
567
602
|
/** `null` ⇒ the segment has no Storage generation yet — see {@link RegistryRecord.currentGen}. */
|
|
568
603
|
readonly currentGen: number | null;
|
|
604
|
+
/** The segment's wrapped data keys, which make it an encrypted segment. Absent for a cleartext one. */
|
|
569
605
|
readonly wrappedDeks?: readonly WrappedDek[];
|
|
606
|
+
/** An external key reference. Reserved: the in-process keystore does not use it. */
|
|
570
607
|
readonly keyId?: string;
|
|
571
608
|
/** Defaults to `'active'`. */
|
|
572
609
|
readonly status?: RegistryStatus;
|
|
610
|
+
/** The retention policy, a plain object; its `expiresAt` is what the retention sweep reads. */
|
|
573
611
|
readonly retention?: GovernanceMeta;
|
|
612
|
+
/** Residency metadata, a plain object. It is stored and returned as given; nothing reads it yet. */
|
|
574
613
|
readonly residency?: GovernanceMeta;
|
|
575
614
|
/** Must name `currentGen` when given. */
|
|
576
615
|
readonly summary?: RegistrySummary;
|
|
@@ -615,6 +654,11 @@ export interface RegistryWriteOptions {
|
|
|
615
654
|
* when the write names a resolved field. A write that does not land leaves no row carrying it. A call refused before
|
|
616
655
|
* it has a token to send, on a conflict its own read finds, reports none.
|
|
617
656
|
*
|
|
657
|
+
* A publish relies on the report coming first, both ways. A write that faults after its report may have landed, and
|
|
658
|
+
* is settled by reading the row. A `TransientError` before any report is taken for a fault before sending, such as
|
|
659
|
+
* the driver's own read of the row: the publish throws it at once, with no read of the row and no fresh write. So
|
|
660
|
+
* nothing that can land is sent before the report, a client's own resend included.
|
|
661
|
+
*
|
|
618
662
|
* The token is unique to that write attempt, also against another writer racing the same version: two writers that
|
|
619
663
|
* send a write against the same row version never carry the same token, so a version counter alone is not enough.
|
|
620
664
|
* Otherwise a loser's write would pass for the winner's.
|
|
@@ -701,6 +745,38 @@ export interface StorageBackend {
|
|
|
701
745
|
* exception propagates from the store's constructor, so a backend's bug fails fast rather than going unseen.
|
|
702
746
|
*/
|
|
703
747
|
attachMetrics?(sink: IMetricsSink): void;
|
|
748
|
+
/**
|
|
749
|
+
* Optional. Reads the setup the backend cannot change but can be broken by, and says whether it will delete or
|
|
750
|
+
* archive what the store keeps: today, the bucket's lifecycle rules over the generation objects and the
|
|
751
|
+
* `<prefix>registry/` rows. It is meant for a check run by hand or at deploy time, not for the read or write path, and
|
|
752
|
+
* costs one request, plus the SDK's own retries. It never rejects: a check that could not be made resolves to `unchecked` with the
|
|
753
|
+
* reason in `detail`. A backend without it, a third-party one or a {@link brandAsBackend} wrapper that does not
|
|
754
|
+
* forward it, offers no lifecycle check, and `store.preflight()` reads that as `unchecked`.
|
|
755
|
+
*
|
|
756
|
+
* The cloud rules stay in the driver. A `detail` is for a person, carries no credential, and never changes what a
|
|
757
|
+
* caller does: act on `status`.
|
|
758
|
+
*/
|
|
759
|
+
checkLifecycle?(): Promise<LifecycleVerdict>;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* What {@link StorageBackend.checkLifecycle} found.
|
|
763
|
+
*
|
|
764
|
+
* - `ok`: no rule that could delete or archive the store's objects. `detail` may still carry advice, such as a missing
|
|
765
|
+
* rule that cleans up abandoned multipart uploads, which is never a `problem` by itself.
|
|
766
|
+
* - `problem`: a rule that deletes or archives the store's current objects, or one that cannot be told apart from it.
|
|
767
|
+
* Where a rule is unclear, the check reports a `problem`: a condition it does not interpret, or a value it does not
|
|
768
|
+
* recognise (a storage class, an action).
|
|
769
|
+
* - `unchecked`: the check could not be made (no permission to read the configuration, an error, or a backend whose
|
|
770
|
+
* rules are out of the data plane's reach). `detail` says what to grant or how to check by hand.
|
|
771
|
+
*/
|
|
772
|
+
export interface LifecycleVerdict {
|
|
773
|
+
/**
|
|
774
|
+
* `'ok'` when no rule would delete or archive the store's objects, `'problem'` when one would or may, `'unchecked'`
|
|
775
|
+
* when the check could not be made.
|
|
776
|
+
*/
|
|
777
|
+
readonly status: 'ok' | 'problem' | 'unchecked';
|
|
778
|
+
/** A sentence or two for a person. Never a credential. */
|
|
779
|
+
readonly detail: string;
|
|
704
780
|
}
|
|
705
781
|
/**
|
|
706
782
|
* The brand key, for the type-only `declare` field a backend class carries.
|
|
@@ -750,7 +826,7 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
|
|
|
750
826
|
* probability (ABA-safe across delete→recreate).
|
|
751
827
|
*
|
|
752
828
|
* **What a driver must do** — callers rely on each of these, and the registry conformance suite holds every
|
|
753
|
-
* shipped driver to them:
|
|
829
|
+
* shipped driver to them, except the rule on `unsettledConflict`, which the suite does not test:
|
|
754
830
|
*
|
|
755
831
|
* - **`create` and `compareAndSwap` are atomic conditional writes.** A `create` over a live row, and a
|
|
756
832
|
* `compareAndSwap` whose token is not the stored one, throw {@link WriteConflictError} and change nothing.
|
|
@@ -780,9 +856,19 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
|
|
|
780
856
|
* Either send each write once, with the backend client's retry off for that request, or recognise your own
|
|
781
857
|
* write when the precondition fails by reading an id you stored with it back. The shipped registries do
|
|
782
858
|
* one or the other.
|
|
859
|
+
* - **Raise a conflict that may follow your own write's landing with `unsettledConflict`** (from the driver kit). A
|
|
860
|
+
* driver whose client may have sent the write more than once cannot say, after a conflict, that this write did not
|
|
861
|
+
* land first: a row is overwritten, so another writer's write found on the read-back may have gone over this one, and
|
|
862
|
+
* a row that is gone may have been removed after it landed. Raise every conflict it cannot rule that out for (another
|
|
863
|
+
* writer's write on the read-back, a compare-and-swap that finds the row gone, a read-back that finds nothing) as
|
|
864
|
+
* `unsettledConflict`, so a load refused after it reports `unanswered: true`. A conflict that proves the write did
|
|
865
|
+
* not land (a write sent once, a check made before sending anything) is a plain {@link WriteConflictError}. A
|
|
866
|
+
* registry that wraps another rethrows the conflict it was given, unchanged: the check that reads the mark is
|
|
867
|
+
* internal, so a conflict built anew loses it.
|
|
783
868
|
* - **Raise a transient fault as {@link TransientError}.**
|
|
784
869
|
*/
|
|
785
870
|
export interface IRegistryDriver {
|
|
871
|
+
/** What this registry can do. Strongly consistent reads are required; the other capabilities are optional. */
|
|
786
872
|
capabilities(): RegCaps;
|
|
787
873
|
/**
|
|
788
874
|
* `true` when every `create` and `compareAndSwap` of this registry reports the token its write carries to
|
|
@@ -793,8 +879,15 @@ export interface IRegistryDriver {
|
|
|
793
879
|
*
|
|
794
880
|
* The flag may be a getter or a field. A registry that wraps or extends another and overrides `create` or
|
|
795
881
|
* `compareAndSwap` either passes `options` through to the write it forwards, or sets `reportsWriteTokens` to
|
|
796
|
-
* `false`. One that does neither reads
|
|
797
|
-
*
|
|
882
|
+
* `false`. One that does neither reads a write of its own that landed and then met a conflict as not its own
|
|
883
|
+
* (`superseded`), and takes every `TransientError` of its writes, a throttle and a lost answer alike, for a fault
|
|
884
|
+
* before sending: the publish throws it at once, with no read of the row and no fresh write. Either way it deletes
|
|
885
|
+
* nothing.
|
|
886
|
+
*
|
|
887
|
+
* A publish relies on the report coming first, both ways. A write that faults after its report may have landed, and
|
|
888
|
+
* is settled by reading the row. A `TransientError` before any report is taken for a fault before sending, such as
|
|
889
|
+
* the driver's own read of the row: the publish throws it at once, with no read of the row and no fresh write. So
|
|
890
|
+
* nothing that can land is sent before the report, a client's own resend included.
|
|
798
891
|
*/
|
|
799
892
|
readonly reportsWriteTokens?: boolean;
|
|
800
893
|
/**
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runPreflight` — the first-run check of a store's storage and registry, behind `store.preflight()`.
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* registry ── get(probe row) must be null, canWrite !== false
|
|
6
|
+
* write ───── put marker A under the probe key
|
|
7
|
+
* write-once put marker B under the same key: must throw WriteConflictError
|
|
8
|
+
* read ────── getTail: must return A (read-after-write); gives the version
|
|
9
|
+
* list ────── list the probe segment: must name the probe
|
|
10
|
+
* delete ──── (finally) delete this run's own key, with the tail's version when it has one
|
|
11
|
+
* lifecycle ─ backend.checkLifecycle(), bounded by a timer, unchecked when the backend has none
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The probe key is `cbm.due.preflight/segments/probe.<n>.crbm`, where `<n>` is a 48-bit random number. That
|
|
15
|
+
* namespace sits inside the reserved `cbm.due.` prefix an application can never name, so no segment of the application's can collide with it, and the registry
|
|
16
|
+
* writes nothing: the probe is a storage object only, which no registry row names and no scan of the registry
|
|
17
|
+
* reaches. Storage is only ever listed one segment at a time, from rows or from refs the application gave.
|
|
18
|
+
*
|
|
19
|
+
* A finding never throws. A step that fails (a transient fault included) is a `problem` whose detail says so, and the
|
|
20
|
+
* checks that cannot run without it are `unchecked`; the lifecycle step is the exception, whose failures, its timeout
|
|
21
|
+
* included, are `unchecked`. Every step runs through one guard, so nothing a step meets (an error whose getters throw,
|
|
22
|
+
* a verdict whose getters throw, a clock whose sleep rejects, an entropy source that throws) escapes. The only throw
|
|
23
|
+
* is a store with nothing to check, which the facade refuses before it gets here.
|
|
24
|
+
*
|
|
25
|
+
* Not checked: read-after-delete, the registry's `If-Match` on compare-and-swap, multipart abort permission,
|
|
26
|
+
* versioning and backups.
|
|
27
|
+
*
|
|
28
|
+
* Time arrives through the injected {@link Clock}: the lifecycle step is bounded by {@link LIFECYCLE_TIMEOUT_MS},
|
|
29
|
+
* counted in short sleeps of {@link LIFECYCLE_TICK_MS} so that a check that answers at once leaves no long timer
|
|
30
|
+
* holding a short-lived process open.
|
|
31
|
+
*/
|
|
32
|
+
import type { Clock, Entropy } from './determinism.js';
|
|
33
|
+
import type { IRegistryDriver, IStorageDriver, LifecycleVerdict } from './ports.js';
|
|
34
|
+
/**
|
|
35
|
+
* What one check found.
|
|
36
|
+
*
|
|
37
|
+
* - `ok`: the check ran and found nothing wrong.
|
|
38
|
+
* - `problem`: the check ran and found something wrong, or the request it makes failed (a transient fault included:
|
|
39
|
+
* `detail` says to run again then). The lifecycle check is the exception: its failures, its timeout included, are
|
|
40
|
+
* `unchecked`.
|
|
41
|
+
* - `unchecked`: the check could not be made, because a step it depends on failed (`write-once`, `read`, `list` and
|
|
42
|
+
* `delete` after a failed `write`), or the backend offers no such check, or the lifecycle check did not answer in
|
|
43
|
+
* time. `ok` on the result does not vouch for it.
|
|
44
|
+
*/
|
|
45
|
+
export interface PreflightCheck {
|
|
46
|
+
/** Which check this is. A run lists all seven, in this order. */
|
|
47
|
+
readonly name: 'registry' | 'write' | 'write-once' | 'read' | 'list' | 'delete' | 'lifecycle';
|
|
48
|
+
/** What the check found. */
|
|
49
|
+
readonly status: 'ok' | 'problem' | 'unchecked';
|
|
50
|
+
/** A sentence or two for a person: what was found, and what to do. The name and message of a failed step's error. */
|
|
51
|
+
readonly detail: string;
|
|
52
|
+
}
|
|
53
|
+
/** What `store.preflight()` found: every check, and whether none of them is a `problem`. */
|
|
54
|
+
export interface PreflightResult {
|
|
55
|
+
/** True when no check is a `problem`. A check that is `unchecked` does not make it false. */
|
|
56
|
+
readonly ok: boolean;
|
|
57
|
+
/** All seven checks, in the order the type of `PreflightCheck.name` lists them. */
|
|
58
|
+
readonly checks: PreflightCheck[];
|
|
59
|
+
}
|
|
60
|
+
/** What a run needs: the store's raw drivers, an entropy source for the probe's number, and a clock for the bound. */
|
|
61
|
+
export interface PreflightDeps {
|
|
62
|
+
readonly storage: IStorageDriver;
|
|
63
|
+
readonly registry: IRegistryDriver;
|
|
64
|
+
readonly clock: Clock;
|
|
65
|
+
readonly entropy: Entropy;
|
|
66
|
+
/** The backend's lifecycle check, bound to the backend; absent for a backend that has none. */
|
|
67
|
+
readonly checkLifecycle?: (() => Promise<LifecycleVerdict>) | undefined;
|
|
68
|
+
}
|
|
69
|
+
/** The probe's namespace, inside the reserved prefix; no application ref can name it. */
|
|
70
|
+
export declare const PREFLIGHT_NAMESPACE = "cbm.due.preflight";
|
|
71
|
+
/** The probe's segment name. */
|
|
72
|
+
export declare const PREFLIGHT_SEGMENT = "probe";
|
|
73
|
+
/** How long the lifecycle step may take before it is read as `unchecked`: 30 seconds of the injected clock. */
|
|
74
|
+
export declare const LIFECYCLE_TIMEOUT_MS = 30000;
|
|
75
|
+
/** The length of each sleep the bound is counted in. */
|
|
76
|
+
export declare const LIFECYCLE_TICK_MS = 250;
|
|
77
|
+
/**
|
|
78
|
+
* Run the seven checks. Never rejects: see the module comment.
|
|
79
|
+
*/
|
|
80
|
+
export declare function runPreflight(deps: PreflightDeps): Promise<PreflightResult>;
|
|
@@ -2,8 +2,24 @@ import { type IAuditSink } from './audit.js';
|
|
|
2
2
|
import type { DropDeps, DropResult } from './erasure.js';
|
|
3
3
|
/** Default cap on retirements per sweep — a bounded batch, so a policy mistake costs one batch, not the fleet. */
|
|
4
4
|
export declare const DEFAULT_RETIRE_LIMIT = 100;
|
|
5
|
+
/**
|
|
6
|
+
* The shortest `tombstoneGraceMs` a sweep takes: one hour. A first load whose create got no answer is sent again, and a
|
|
7
|
+
* tombstone purged before that copy arrives lets it create the row again over the purged name, wrapped key included.
|
|
8
|
+
* A grace of seconds is for a test, which advances an injected clock.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MIN_TOMBSTONE_GRACE_MS = 3600000;
|
|
11
|
+
/**
|
|
12
|
+
* Options of a retention sweep: the instant to judge policies at (`now`), which segments it considers, how many it
|
|
13
|
+
* retires in one call, how it finds them, whether it only reports (`dryRun`), where retirements are audited (`audit`),
|
|
14
|
+
* and whether and when it purges the tombstones of earlier retirements.
|
|
15
|
+
*/
|
|
5
16
|
export interface RetireExpiredOptions {
|
|
6
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Scope the sweep to one namespace. Omit to sweep every namespace the registry knows, segments loaded with no
|
|
19
|
+
* namespace included. `'_default'`, the label the export tool and the disaster-recovery samples print, and the
|
|
20
|
+
* bucket's object keys use, for those segments, is an ordinary namespace name here: it scopes the sweep to a
|
|
21
|
+
* namespace of that name, not to them.
|
|
22
|
+
*/
|
|
7
23
|
readonly namespace?: string;
|
|
8
24
|
/**
|
|
9
25
|
* The instant to compare policies against (epoch-ms). Required here because `core/` takes its time from the
|
|
@@ -11,7 +27,7 @@ export interface RetireExpiredOptions {
|
|
|
11
27
|
*/
|
|
12
28
|
readonly now: number;
|
|
13
29
|
/**
|
|
14
|
-
* Maximum segments to retire in this cycle (default 100). It also bounds the purges attempted (a purge the registry
|
|
30
|
+
* Maximum segments to retire in this cycle (default 100; an integer of at least 1, `null` refused). It also bounds the purges attempted (a purge the registry
|
|
15
31
|
* refuses is not charged; {@link RetireExpiredResult.purgeFaults} says how purging stops after refusals), the
|
|
16
32
|
* due-index pointers to nothing one call removes, and, counted apart from the retirements, the tombstones this sweep
|
|
17
33
|
* did not write whose pointers one call removes.
|
|
@@ -21,7 +37,10 @@ export interface RetireExpiredOptions {
|
|
|
21
37
|
readonly dryRun?: boolean;
|
|
22
38
|
/** Forwarded to each `dropSegment`, so every retirement lands in the audit trail as `segment.dispose`. */
|
|
23
39
|
readonly audit?: IAuditSink;
|
|
24
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Ceiling on rows enumerated (default 250,000); exceeding it throws. On an `'index'` scan the check runs before
|
|
42
|
+
* each pointer's row is read, so live rows equal to the ceiling followed by a pointer whose row is gone throw too.
|
|
43
|
+
*/
|
|
25
44
|
readonly maxScanSegments?: number;
|
|
26
45
|
/**
|
|
27
46
|
* **Where the candidates come from.**
|
|
@@ -41,6 +60,8 @@ export interface RetireExpiredOptions {
|
|
|
41
60
|
* tombstone under the day the tombstone's grace ends, and the scan reads it back with the expiry pointers. A pointer
|
|
42
61
|
* whose segment is gone is removed once read, by an index scan in the buckets it reads, and by an unscoped `'fleet'`
|
|
43
62
|
* scan (no `namespace`) in every bucket.
|
|
63
|
+
*
|
|
64
|
+
* Any other value, a misspelt `'index'` or a `null` included, throws `ValidationError` rather than running the fleet scan.
|
|
44
65
|
*/
|
|
45
66
|
readonly scan?: 'fleet' | 'index';
|
|
46
67
|
/**
|
|
@@ -49,14 +70,17 @@ export interface RetireExpiredOptions {
|
|
|
49
70
|
* across restarts.
|
|
50
71
|
*/
|
|
51
72
|
readonly shards?: readonly number[];
|
|
52
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Total shards the fleet is split into. Required with {@link shards}; ignored without it. `shards` must list at least
|
|
75
|
+
* one number from 0 to `totalShards - 1`, each once, else `ValidationError`.
|
|
76
|
+
*/
|
|
53
77
|
readonly totalShards?: number;
|
|
54
78
|
/**
|
|
55
79
|
* How many **past** buckets an `'index'` scan reads besides the current one (default 7, one per day).
|
|
56
80
|
* A sweep that did not run — scaled to zero, a failed deploy, a paused
|
|
57
81
|
* schedule — leaves its buckets behind, and this is how far back a later cycle reaches for them. Bounded so a
|
|
58
82
|
* long outage costs a bounded number of list calls per cycle rather than one per day since the epoch;
|
|
59
|
-
* anything older is the `'fleet'` repair pass's job.
|
|
83
|
+
* anything older is the `'fleet'` repair pass's job. An integer from 0 to 3660 on either scan, else `ValidationError`.
|
|
60
84
|
*/
|
|
61
85
|
readonly lookbackBuckets?: number;
|
|
62
86
|
/**
|
|
@@ -76,7 +100,13 @@ export interface RetireExpiredOptions {
|
|
|
76
100
|
* zero is the opposite of another's is a reading hazard for whoever tunes both.
|
|
77
101
|
*/
|
|
78
102
|
readonly purgeTombstones?: boolean;
|
|
79
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* How long a retirement's tombstone must age before this sweep deletes it: at least one hour (`ValidationError`
|
|
105
|
+
* below), 24 h by default. The floor keeps the tombstone in place longer than a first load's create can take to be
|
|
106
|
+
* sent again after it got no answer, which would otherwise create the row again over the purged name, wrapped key
|
|
107
|
+
* included. Keep it well above the longest time a first load's registry write can still arrive (seconds on S3 and
|
|
108
|
+
* GCS, over a minute on Azure Blob at the SDK's default retry schedule). `null` is refused.
|
|
109
|
+
*/
|
|
80
110
|
readonly tombstoneGraceMs?: number;
|
|
81
111
|
}
|
|
82
112
|
/** What happened to one segment in a sweep. */
|
|
@@ -88,10 +118,13 @@ export type RetireEntry = {
|
|
|
88
118
|
readonly expiresAt: number;
|
|
89
119
|
readonly result: DropResult;
|
|
90
120
|
/**
|
|
91
|
-
* Present when the
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
121
|
+
* Present when the **tombstone landed and the retirement then faulted**: its storage sweep or listing threw, or
|
|
122
|
+
* the shred's own write got no answer that a read of the row could settle (then no `segment.erase` was emitted:
|
|
123
|
+
* record the crypto-shred yourself). `result.generationsRemaining` lists what a listing of the bucket found after
|
|
124
|
+
* the fault (the generations the row named, when that listing failed too). A sweep's re-run does not collect
|
|
125
|
+
* those objects inside `tombstoneGraceMs`: run `store.dropSegment(ref, { confirmSegment })`. Reported as `retired`
|
|
126
|
+
* rather than `skipped` because the segment really is retired, and saying otherwise is the one thing a caller
|
|
127
|
+
* cannot recover from.
|
|
95
128
|
*/
|
|
96
129
|
readonly fault?: `failed: ${string}`;
|
|
97
130
|
} | {
|
|
@@ -114,7 +147,8 @@ export type RetireEntry = {
|
|
|
114
147
|
* `'invalid-policy'` — the row has an `expiresAt` that is not usable (hand-edited, or restored from another
|
|
115
148
|
* schema). Reported rather than ignored: reading as "never expires" on a segment someone believes is
|
|
116
149
|
* expiring is the silence that costs a retention commitment.
|
|
117
|
-
* `'limit'` —
|
|
150
|
+
* `'limit'` — never written: a segment the cycle's `limit` deferred gets no entry, and
|
|
151
|
+
* {@link RetireExpiredResult.limited} is what says the cap cut the pass short. Re-run to continue.
|
|
118
152
|
* `'tombstone-not-empty'` — a tombstone whose Storage generations are not gone even after a GC
|
|
119
153
|
* attempt, so its row is kept, and, unless the collection lost a race to a write of the row, a refused purge
|
|
120
154
|
* (see {@link RetireExpiredResult.purgeFaults}): the row is what keeps the segment reachable by the generation collection and
|
|
@@ -127,9 +161,14 @@ export type RetireEntry = {
|
|
|
127
161
|
* re-reads before every deletion precisely so cancelling an expiry works on a sweep already in flight.
|
|
128
162
|
* `` `failed: …` `` — any fault, isolated to this segment. A fault that happened *after* the tombstone
|
|
129
163
|
* landed is reported as `retired` with a `fault` instead, because that segment IS retired.
|
|
164
|
+
* When the row cannot be read afterwards either, the reason says the tombstone may have landed.
|
|
130
165
|
*/
|
|
131
166
|
readonly reason: 'invalid-policy' | 'limit' | 'tombstone-not-empty' | 'policy-changed' | `failed: ${string}`;
|
|
132
167
|
};
|
|
168
|
+
/**
|
|
169
|
+
* What a retention sweep did: the rows it read, what it retired, and a ledger entry for each segment it acted on or
|
|
170
|
+
* skipped.
|
|
171
|
+
*/
|
|
133
172
|
export interface RetireExpiredResult {
|
|
134
173
|
/** Registry rows enumerated. */
|
|
135
174
|
readonly scanned: number;
|
|
@@ -146,11 +185,15 @@ export interface RetireExpiredResult {
|
|
|
146
185
|
readonly retired: number;
|
|
147
186
|
/** Segments a real sweep **would** have retired. Only ever non-zero under `dryRun`. */
|
|
148
187
|
readonly wouldRetire: number;
|
|
149
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* Tombstone rows this sweep saw deleted. Zero under `dryRun`; the `would-purge-tombstone` entries carry the preview.
|
|
190
|
+
* A purge another sweep made at the same moment is counted by both.
|
|
191
|
+
*/
|
|
150
192
|
readonly tombstonesPurged: number;
|
|
151
193
|
/**
|
|
152
|
-
* True when `limit` cut the
|
|
153
|
-
* of tombstones this sweep did not write leaves
|
|
194
|
+
* True when `limit` cut the pass short — **more work remains**: retirements, or purges of this sweep's tombstones,
|
|
195
|
+
* which the same `limit` counts. Re-run. The cap on removing pointers of tombstones this sweep did not write leaves
|
|
196
|
+
* it false.
|
|
154
197
|
*/
|
|
155
198
|
readonly limited: boolean;
|
|
156
199
|
/**
|
|
@@ -171,8 +214,10 @@ export interface RetireExpiredResult {
|
|
|
171
214
|
readonly purgeFaults: number;
|
|
172
215
|
/** The first of those faults, as a ledger reason (`failed: …` with the provider's message). Absent when there were none. */
|
|
173
216
|
readonly firstPurgeFault?: `failed: ${string}`;
|
|
217
|
+
/** Whether the sweep was a dry run, which changes nothing. */
|
|
174
218
|
readonly dryRun: boolean;
|
|
175
|
-
/** Per-segment ledger. Inspect it: a `skipped` or failed entry is a segment that still holds data
|
|
219
|
+
/** Per-segment ledger. Inspect it: a `skipped` or failed entry is a segment that still holds data, except one whose reason says the row could
|
|
220
|
+
* not be read afterwards: its tombstone may have landed. */
|
|
176
221
|
readonly entries: readonly RetireEntry[];
|
|
177
222
|
}
|
|
178
223
|
/**
|
package/dist/core/retention.d.ts
CHANGED
|
@@ -22,15 +22,23 @@ export interface RetentionPolicy {
|
|
|
22
22
|
export interface RetentionDeps extends ChurnDeps {
|
|
23
23
|
readonly registry: IRegistryDriver;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* What setting a segment's retention policy did: the policy now stored, and whether it created the row and wrote the
|
|
27
|
+
* due-index pointer.
|
|
28
|
+
*/
|
|
25
29
|
export interface SetRetentionResult {
|
|
30
|
+
/** The segment's name. */
|
|
26
31
|
readonly segment: string;
|
|
32
|
+
/** The segment's namespace, or `undefined` for the default one. */
|
|
27
33
|
readonly namespace?: string;
|
|
28
34
|
/** The policy now stored on the row. */
|
|
29
35
|
readonly expiresAt: number;
|
|
30
36
|
/**
|
|
31
37
|
* True iff this call **minted the registry row** — a policy recorded before the segment's first load. The
|
|
32
38
|
* segment is now enumerable by `registry.list()` and therefore by every fleet-wide operation; the row claims no
|
|
33
|
-
* Storage generation (`currentGen: null`), so reads answer empty until the first load publishes onto it.
|
|
39
|
+
* Storage generation (`currentGen: null`), so reads answer empty until the first load publishes onto it. A create
|
|
40
|
+
* that got no answer is read back and counts when the row carries this call's write token: a later policy or lease
|
|
41
|
+
* write does not hide it, and a later load's publish does (the call then decides again, and answers `false`).
|
|
34
42
|
*/
|
|
35
43
|
readonly createdRow: boolean;
|
|
36
44
|
/**
|
|
@@ -70,14 +78,17 @@ export declare function getSegmentRetention(ref: SegmentRef, deps: RetentionDeps
|
|
|
70
78
|
* Idempotent and safe to re-run: writing the same instant twice is a no-op in effect. Other keys already in the
|
|
71
79
|
* row's `retention` metadata are preserved — this owns one key, not the whole object.
|
|
72
80
|
*
|
|
73
|
-
* Refuses a `destroyed` segment: a
|
|
74
|
-
* a
|
|
81
|
+
* Refuses a `destroyed` segment: a sweep never retires a tombstone (it purges only its own, by their stamp), so a
|
|
82
|
+
* policy on one would never act. Objects a destroy left in the bucket are deleted by `dropSegment` on the tombstone,
|
|
83
|
+
* not by a policy.
|
|
75
84
|
*/
|
|
76
85
|
export declare function setSegmentRetention(ref: SegmentRef, deps: RetentionDeps, policy: RetentionPolicy): Promise<SetRetentionResult>;
|
|
77
86
|
/**
|
|
78
|
-
* Remove a segment's expiry so no sweep will retire it. Returns whether
|
|
79
|
-
*
|
|
80
|
-
*
|
|
87
|
+
* Remove a segment's expiry so no sweep will retire it. Returns whether this call removed a policy (`false` for a
|
|
88
|
+
* segment with no row, or one that had no policy, and also when a write that got no answer cannot be shown to be this
|
|
89
|
+
* call's and the row holds no expiry: the expiry is gone either way, and its due-index pointer is removed. A later
|
|
90
|
+
* write that set an expiry is cleared too, and the answer is `true`). A separate verb from setting one, because "cancel
|
|
91
|
+
* the expiry" and "expire at time T" are different intents and a magic sentinel value for the first is how a typo
|
|
81
92
|
* becomes a deletion.
|
|
82
93
|
*
|
|
83
94
|
* Other `retention` keys are left in place; if removing the expiry empties the metadata, the field is cleared.
|
package/dist/core/retry.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export declare function backoffDelayMs(policy: RetryPolicy, attempt: number): nu
|
|
|
48
48
|
/** Apply the policy's jitter to a computed delay using the injected RNG. */
|
|
49
49
|
export declare function applyJitter(policy: RetryPolicy, delayMs: number, rng: Rng): number;
|
|
50
50
|
/** A clock that can wait: the one member {@link jitteredWait} uses. */
|
|
51
|
-
interface Sleeper {
|
|
51
|
+
export interface Sleeper {
|
|
52
52
|
readonly sleep?: ((ms: number) => Promise<void>) | undefined;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
@@ -83,4 +83,3 @@ export declare function checkedRetryPolicy(policy: RetryPolicy): RetryPolicy;
|
|
|
83
83
|
* the *operation's* error — never a wrapper — so callers keep their typed-error branching.
|
|
84
84
|
*/
|
|
85
85
|
export declare function withRetry<T>(op: () => Promise<T>, policy: RetryPolicy, deps: RetryDeps): Promise<T>;
|
|
86
|
-
export {};
|
package/dist/core/rollback.d.ts
CHANGED
|
@@ -17,8 +17,12 @@ export interface GenerationListDeps extends ChurnDeps {
|
|
|
17
17
|
}
|
|
18
18
|
/** One generation present in the bucket. */
|
|
19
19
|
export interface GenerationEntry {
|
|
20
|
+
/** The generation's number. */
|
|
20
21
|
readonly generation: number;
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Whether the registry pointer currently names this generation. `false` for every generation of a destroyed
|
|
24
|
+
* segment: a destroyed row keeps the pointer it had, but no read resolves a generation through it.
|
|
25
|
+
*/
|
|
22
26
|
readonly current: boolean;
|
|
23
27
|
/**
|
|
24
28
|
* Whether the row's record of the generations it published, `knownGens`, names this generation. A rollback onto a known
|
|
@@ -76,8 +80,8 @@ export interface RollbackResult {
|
|
|
76
80
|
* - a generation whose object is **not in the bucket** — collected, or never written — is refused with
|
|
77
81
|
* `NotFoundError` rather than pointed at, because a pointer naming a missing object is the one state the whole
|
|
78
82
|
* design exists to avoid;
|
|
79
|
-
* - a **crypto-shredded
|
|
80
|
-
*
|
|
83
|
+
* - a **destroyed** segment (crypto-shredded, destroyed in the clear or dropped) is refused: no generation resolves
|
|
84
|
+
* through a destroyed row, so moving its pointer would change nothing a reader sees;
|
|
81
85
|
* - a target that is **not what the row says the segment is** is refused with `IntegrityError`: a cleartext object
|
|
82
86
|
* under a row with keys, or an encrypted one under a row with none, either of which every read would then refuse.
|
|
83
87
|
* One tail read of the target decides it, as does a footer that fails its own checks;
|
|
@@ -110,15 +114,21 @@ export interface RollbackResult {
|
|
|
110
114
|
* failure of the keystore is thrown. After the swap the target's footer is read once more, and the pointer put back,
|
|
111
115
|
* restoring the summary and the record the row had, unless it is still the object that was read: collection can delete a
|
|
112
116
|
* target below the old pointer between the read and the swap, and an erasure can delete one above it and a load take its
|
|
113
|
-
* number. A swap that gets no answer is settled by reading the row
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
117
|
+
* number. A swap that meets a conflict or gets no answer is settled by reading the row. On a registry that reports its
|
|
118
|
+
* writes' tokens, one the row shows landed, its pointer at the target over the token of a swap this call sent, is
|
|
119
|
+
* checked as any swap is, the move audited and reported, whatever else was written to the row since (a lease, a
|
|
120
|
+
* retention or residency write, or a write of the ledger alone). Otherwise the call throws, and nothing is audited or
|
|
121
|
+
* deleted: the `WriteConflictError` after a conflict, and the `TransientError` after no answer, since it may have
|
|
122
|
+
* landed or may still land and a re-run finds the pointer where it is. Another writer's write of the same pointer is not
|
|
123
|
+
* taken for it, another rollback's identical move included, and neither is a write that renewed the row's `pointerId` over
|
|
124
|
+
* this swap, which cannot be told from it. A registry that reports no tokens settles a swap that got no answer by the
|
|
125
|
+
* row it would leave, and never settles a conflict as landed. Over such a registry a write by another writer is not
|
|
126
|
+
* taken for the swap unless it leaves the very row this swap would. On a cleartext segment two writes can: another
|
|
127
|
+
* rollback onto the same target, whatever row it read, and a load that wrote under that number the very object the
|
|
128
|
+
* rollback read and carried the ledger the rollback would (a load that carries a usable ledger, as one with a `keep`
|
|
129
|
+
* above 64 does; over a row with no usable ledger the rollback writes none and a load seeds one, so those rows differ).
|
|
130
|
+
* On an encrypted segment a rollback with no key at hand writes no summary, so another such rollback onto the same
|
|
131
|
+
* target can too. That call then reports and audits the move as its own; the pointer is where it asked for it.
|
|
122
132
|
*
|
|
123
133
|
* What it does **not** do is delete anything. The generations above the new pointer stay in the bucket, which is
|
|
124
134
|
* what makes the rollback reversible — roll forward again by naming one of them. They are also then *above*
|