@cloudbitmaps/core 0.15.0 → 0.17.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-B7IGS3W6.js → chunk-CV7GJO3B.js} +363 -23
- package/dist/chunk-CV7GJO3B.js.map +7 -0
- package/dist/core/codec.d.ts +10 -0
- package/dist/core/cost.d.ts +4 -4
- package/dist/core/crbm-storage-source.d.ts +76 -68
- package/dist/core/engine.d.ts +59 -6
- package/dist/core/erasure.d.ts +2 -1
- package/dist/core/errors.d.ts +23 -0
- package/dist/core/generation-gc.d.ts +69 -45
- package/dist/core/kept-generations.d.ts +40 -0
- package/dist/core/leases.d.ts +119 -0
- package/dist/core/load-input.d.ts +21 -0
- package/dist/core/load.d.ts +33 -12
- package/dist/core/metrics.d.ts +24 -0
- package/dist/core/ports.d.ts +128 -6
- package/dist/core/registry-reap.d.ts +22 -0
- package/dist/core/retention.d.ts +2 -1
- package/dist/core/rollback.d.ts +2 -1
- package/dist/driver-kit.d.ts +1 -0
- package/dist/driver-kit.js +142 -10
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +34 -3
- package/dist/drivers/_shared/registry.d.ts +7 -9
- package/dist/drivers/localfs/registry.d.ts +4 -0
- package/dist/drivers/memory.d.ts +4 -0
- package/dist/export/index.d.ts +20 -8
- package/dist/index.d.ts +10 -4
- package/dist/index.js +745 -123
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-B7IGS3W6.js.map +0 -7
package/dist/index.js
CHANGED
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
DEFAULT_NAMESPACE,
|
|
6
6
|
IntegrityError,
|
|
7
7
|
KeyUnavailableError,
|
|
8
|
+
LEASE_SKEW_MS,
|
|
9
|
+
LeaseExpiredError,
|
|
10
|
+
LeaseLimitError,
|
|
11
|
+
MAX_KEPT_GENERATIONS,
|
|
12
|
+
MAX_LEASES_PER_SEGMENT,
|
|
13
|
+
MAX_LEASE_MS,
|
|
8
14
|
MAX_METADATA_BYTES,
|
|
9
15
|
MAX_NAME_LENGTH,
|
|
10
16
|
NotFoundError,
|
|
@@ -25,25 +31,35 @@ import {
|
|
|
25
31
|
encodeNameForPath,
|
|
26
32
|
encodedNameLength,
|
|
27
33
|
entropyIsAvailable,
|
|
34
|
+
heldGenerations,
|
|
28
35
|
incarnationOf,
|
|
29
36
|
isCloudRoaringError,
|
|
30
37
|
isIntegrityError,
|
|
38
|
+
isLeaseExpiredError,
|
|
39
|
+
isLeaseLimitError,
|
|
40
|
+
isLive,
|
|
31
41
|
isNotFoundError,
|
|
32
42
|
isReservedNamespace,
|
|
33
43
|
isStorageBackend,
|
|
34
44
|
isTransientError,
|
|
35
45
|
isValidationError,
|
|
36
46
|
isWriteConflictError,
|
|
47
|
+
leaseChurn,
|
|
37
48
|
mapWithConcurrency,
|
|
38
49
|
metadataBytes,
|
|
39
50
|
metadataFromBytes,
|
|
40
51
|
namespacePathPart,
|
|
41
52
|
newIncarnationToken,
|
|
53
|
+
nextKept,
|
|
42
54
|
nextRegistryToken,
|
|
55
|
+
onlyLeasesDiffer,
|
|
43
56
|
parseRegistryEnvelope,
|
|
44
57
|
recordFromNew,
|
|
58
|
+
releaseLease,
|
|
45
59
|
sameIncarnation,
|
|
46
60
|
serializeRegistryEnvelope,
|
|
61
|
+
takeLease,
|
|
62
|
+
usableKeptGens,
|
|
47
63
|
validateChunkKeyOrder,
|
|
48
64
|
validateChunkRef,
|
|
49
65
|
validateNewRegistryRecord,
|
|
@@ -52,7 +68,7 @@ import {
|
|
|
52
68
|
validateUserNamespace,
|
|
53
69
|
validateUserRef,
|
|
54
70
|
webCryptoEntropy
|
|
55
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-CV7GJO3B.js";
|
|
56
72
|
|
|
57
73
|
// src/core/bit-route.ts
|
|
58
74
|
var U32_MAX = 4294967295;
|
|
@@ -332,6 +348,8 @@ var CountingMetricsSink = class _CountingMetricsSink {
|
|
|
332
348
|
op.totalMs += event.ms;
|
|
333
349
|
break;
|
|
334
350
|
}
|
|
351
|
+
case "advisory":
|
|
352
|
+
break;
|
|
335
353
|
default: {
|
|
336
354
|
const exhaustive = event;
|
|
337
355
|
return exhaustive;
|
|
@@ -494,6 +512,8 @@ var SegmentEngine = class {
|
|
|
494
512
|
* they deliver, as a read whose entry an invalidation dropped does not (streams hold no `openReads` entry).
|
|
495
513
|
*/
|
|
496
514
|
openStreams = /* @__PURE__ */ new Set();
|
|
515
|
+
/** How many times {@link invalidate} has been called: a read compares it with the count it began under. */
|
|
516
|
+
invalidations = 0;
|
|
497
517
|
constructor(deps) {
|
|
498
518
|
this.storage = deps.storage;
|
|
499
519
|
this.cache = deps.cache;
|
|
@@ -578,8 +598,15 @@ var SegmentEngine = class {
|
|
|
578
598
|
* coalesced ranges when the source has `getChunks`, opening 1, 2, 4 … ranges wide up to {@link DEFAULT_INTERSECT_CONCURRENCY},
|
|
579
599
|
* else the chunk-by-chunk window of that many reads. `close` stops what is left of it.
|
|
580
600
|
*/
|
|
581
|
-
chunkSequence(seg, chunkKeys, gen) {
|
|
582
|
-
const streamed = this.openStreamed(
|
|
601
|
+
chunkSequence(seg, chunkKeys, gen, epoch) {
|
|
602
|
+
const streamed = this.openStreamed(
|
|
603
|
+
seg,
|
|
604
|
+
chunkKeys,
|
|
605
|
+
gen,
|
|
606
|
+
epoch,
|
|
607
|
+
DEFAULT_INTERSECT_CONCURRENCY,
|
|
608
|
+
1
|
|
609
|
+
);
|
|
583
610
|
if (streamed !== void 0) {
|
|
584
611
|
return {
|
|
585
612
|
take: (chunkKey) => this.streamedChunk(streamed, chunkKey),
|
|
@@ -604,10 +631,11 @@ var SegmentEngine = class {
|
|
|
604
631
|
return range === void 0 ? this.iterateAll(seg) : this.iterateRange(seg, range);
|
|
605
632
|
}
|
|
606
633
|
async *iterateAll(seg) {
|
|
634
|
+
const epoch = this.invalidations;
|
|
607
635
|
const chunkKeys = await this.chunkKeys(seg);
|
|
608
636
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
609
637
|
const gen = await this.cacheVersion(seg);
|
|
610
|
-
const chunks = this.chunkSequence(seg, chunkKeys, gen);
|
|
638
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
|
|
611
639
|
try {
|
|
612
640
|
for (const chunkKey of chunkKeys) {
|
|
613
641
|
const chunk = await chunks.take(chunkKey);
|
|
@@ -619,12 +647,13 @@ var SegmentEngine = class {
|
|
|
619
647
|
}
|
|
620
648
|
}
|
|
621
649
|
async *iterateRange(seg, range) {
|
|
650
|
+
const epoch = this.invalidations;
|
|
622
651
|
const w = windowOf(range) ?? WHOLE_ID_SPACE;
|
|
623
652
|
if (w === "empty") return;
|
|
624
653
|
const chunkKeys = keysWithin(await this.chunkKeys(seg), w);
|
|
625
654
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
626
655
|
const gen = await this.cacheVersion(seg);
|
|
627
|
-
const chunks = this.chunkSequence(seg, chunkKeys, gen);
|
|
656
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
|
|
628
657
|
try {
|
|
629
658
|
for (const chunkKey of chunkKeys) {
|
|
630
659
|
const chunk = await chunks.take(chunkKey);
|
|
@@ -646,12 +675,13 @@ var SegmentEngine = class {
|
|
|
646
675
|
* the caller's to keep.
|
|
647
676
|
*/
|
|
648
677
|
async *iterateBatches(seg, range) {
|
|
678
|
+
const epoch = this.invalidations;
|
|
649
679
|
const w = windowOf(range);
|
|
650
680
|
if (w === "empty") return;
|
|
651
681
|
const chunkKeys = w === null ? await this.chunkKeys(seg) : keysWithin(await this.chunkKeys(seg), w);
|
|
652
682
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
653
683
|
const gen = await this.cacheVersion(seg);
|
|
654
|
-
const chunks = this.chunkSequence(seg, chunkKeys, gen);
|
|
684
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
|
|
655
685
|
try {
|
|
656
686
|
for (const chunkKey of chunkKeys) {
|
|
657
687
|
const chunk = await chunks.take(chunkKey);
|
|
@@ -663,6 +693,104 @@ var SegmentEngine = class {
|
|
|
663
693
|
chunks.close();
|
|
664
694
|
}
|
|
665
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* The ids at 1-based ranks `n`, `2n`, `3n` … counted over the ids in `range`, ascending. The rank of each boundary
|
|
698
|
+
* is placed from the index's per-chunk counts, so only the chunks that hold a boundary are read, each once however
|
|
699
|
+
* many boundaries it holds, through the same coalesced stream, window and budget unit as {@link iterate}. A range with
|
|
700
|
+
* `after` inside a chunk reads that chunk too, to count the ids at or below `after`. A last partial window yields
|
|
701
|
+
* nothing.
|
|
702
|
+
*
|
|
703
|
+
* The counts place ids, and are trusted as `count()` trusts them: a chunk that is read must decode to the size its
|
|
704
|
+
* index says, or the read throws {@link IntegrityError} (invariant 5), but a chunk that is not read is not checked, so
|
|
705
|
+
* an index that lies consistently about one (on an unencrypted object, the sum still matches the footer's total,
|
|
706
|
+
* which opening the object checks before any boundary is placed) shifts the ranks after it. An encrypted object's
|
|
707
|
+
* index is authenticated and bound to the object, so a wrong count there needs the key. The index and the chunks must come from one
|
|
708
|
+
* generation (invariant 3): the caller reads through a source that holds one, a pinned one, and a source with no
|
|
709
|
+
* per-chunk counts is refused.
|
|
710
|
+
*
|
|
711
|
+
* Nothing is fetched when the chunks in range hold fewer than `n` ids. The budget is charged before the first fetch
|
|
712
|
+
* with an upper bound, the cut chunk plus one chunk per boundary the counts allow (capped by the chunks in range),
|
|
713
|
+
* which can exceed the chunks actually read. A range that cuts its first chunk reads it even when the ids above
|
|
714
|
+
* `after` turn out to be fewer than `n`, and one that cuts its last chunk may read it and yield nothing.
|
|
715
|
+
*/
|
|
716
|
+
async *everyNth(seg, n, range) {
|
|
717
|
+
if (!Number.isSafeInteger(n) || n < 1) {
|
|
718
|
+
throw new ValidationError(`n must be a positive integer; got ${String(n)}`);
|
|
719
|
+
}
|
|
720
|
+
const epoch = this.invalidations;
|
|
721
|
+
const w = windowOf(range);
|
|
722
|
+
if (w === "empty") return;
|
|
723
|
+
const { loKey, loRem, hiKey, hiRem } = w ?? WHOLE_ID_SPACE;
|
|
724
|
+
if (this.storage.cardinalities === void 0) {
|
|
725
|
+
throw new UnsupportedError("everyNth needs a storage source that reports per-chunk counts");
|
|
726
|
+
}
|
|
727
|
+
const counts = await this.storage.cardinalities(seg);
|
|
728
|
+
if (counts === null) return;
|
|
729
|
+
const keys = keysWithin(
|
|
730
|
+
[...counts.keys()].sort((a, b) => a - b),
|
|
731
|
+
{ loKey, loRem, hiKey, hiRem }
|
|
732
|
+
);
|
|
733
|
+
for (const k of keys) {
|
|
734
|
+
this.assertChunkKeyInRange(k);
|
|
735
|
+
const c = counts.get(k);
|
|
736
|
+
if (!Number.isInteger(c) || c < 1 || c > CHUNK_COUNT) {
|
|
737
|
+
throw new IntegrityError(`chunk cardinality from a tier is out of range: ${c}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const gen = await this.cacheVersion(seg);
|
|
741
|
+
const cutsFirst = loRem > 0 && keys[0] === loKey;
|
|
742
|
+
let upper = 0;
|
|
743
|
+
for (const k of keys) upper += counts.get(k);
|
|
744
|
+
if (upper < n) return;
|
|
745
|
+
const cut = cutsFirst ? 1 : 0;
|
|
746
|
+
checkBudget(this.budget, cut + Math.min(keys.length - cut, Math.floor(upper / n)), "everyNth");
|
|
747
|
+
let first;
|
|
748
|
+
let below = 0;
|
|
749
|
+
if (cutsFirst) {
|
|
750
|
+
first = this.chunkRemainders(
|
|
751
|
+
await this.storageChunk({ ...seg, chunkKey: loKey }, gen),
|
|
752
|
+
loKey,
|
|
753
|
+
counts
|
|
754
|
+
);
|
|
755
|
+
below = firstAbove(first, loRem - 1);
|
|
756
|
+
}
|
|
757
|
+
const hits = [];
|
|
758
|
+
let before = 0;
|
|
759
|
+
for (const key of keys) {
|
|
760
|
+
const skipped = key === loKey && cutsFirst ? below : 0;
|
|
761
|
+
const size = counts.get(key) - skipped;
|
|
762
|
+
const next = (Math.floor(before / n) + 1) * n;
|
|
763
|
+
if (next <= before + size) hits.push({ key, index: skipped + next - before - 1 });
|
|
764
|
+
before += size;
|
|
765
|
+
}
|
|
766
|
+
if (hits.length === 0) return;
|
|
767
|
+
const streamed = hits.filter((h) => !(cutsFirst && h.key === loKey)).map((h) => h.key);
|
|
768
|
+
const chunks = streamed.length > 0 ? this.chunkSequence(seg, streamed, gen, epoch) : void 0;
|
|
769
|
+
try {
|
|
770
|
+
for (const { key, index } of hits) {
|
|
771
|
+
const rem = cutsFirst && key === loKey && first !== void 0 ? first : this.chunkRemainders(await chunks.take(key), key, counts);
|
|
772
|
+
const base = key * CHUNK_COUNT;
|
|
773
|
+
for (let i = index; i < rem.length; i += n) {
|
|
774
|
+
const remainder = rem[i] & MAX_REMAINDER;
|
|
775
|
+
if (key === hiKey && remainder > hiRem) return;
|
|
776
|
+
yield base + remainder;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
} finally {
|
|
780
|
+
chunks?.close();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/** A chunk's remainders ascending, once its size is confirmed against the index's count for it (invariant 5). */
|
|
784
|
+
chunkRemainders(chunk, chunkKey, counts) {
|
|
785
|
+
const expected = counts.get(chunkKey);
|
|
786
|
+
if (chunk === null || chunk.size !== expected) {
|
|
787
|
+
const got = chunk === null ? "no bytes" : `${chunk.size} ids`;
|
|
788
|
+
throw new IntegrityError(
|
|
789
|
+
`chunk ${chunkKey} holds ${got}, but the object's index says ${expected}`
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
return chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
|
|
793
|
+
}
|
|
666
794
|
/**
|
|
667
795
|
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
668
796
|
*
|
|
@@ -731,6 +859,26 @@ var SegmentEngine = class {
|
|
|
731
859
|
andNotBatches(seg, excludes, options) {
|
|
732
860
|
return this.combineBatches([seg], excludes, "all", "andNot", options);
|
|
733
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* {@link intersect}, as the chunks the result is made of: `{ chunkKey, bitmap }` in ascending key order, each a
|
|
864
|
+
* non-empty bitmap of the chunk's 16-bit remainders, cut to the read's range. What a materialisation writes
|
|
865
|
+
* straight into its new generation, with no id built on the way.
|
|
866
|
+
*
|
|
867
|
+
* **Each bitmap belongs to the caller, who may change it:** it is the combine's own accumulator or a bitmap built
|
|
868
|
+
* for the range's edge, never a chunk the cache holds or another operand's. The reads, the budget, the metric and the
|
|
869
|
+
* errors are the id reads' own.
|
|
870
|
+
*/
|
|
871
|
+
intersectChunks(segs, options) {
|
|
872
|
+
return this.combineChunks(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
873
|
+
}
|
|
874
|
+
/** {@link union}, as chunks: see {@link intersectChunks}. */
|
|
875
|
+
unionChunks(segs, options) {
|
|
876
|
+
return this.combineChunks(segs, options?.exclude ?? [], "any", "union", options);
|
|
877
|
+
}
|
|
878
|
+
/** {@link andNot}, as chunks: see {@link intersectChunks}. */
|
|
879
|
+
andNotChunks(seg, excludes, options) {
|
|
880
|
+
return this.combineChunks([seg], excludes, "all", "andNot", options);
|
|
881
|
+
}
|
|
734
882
|
/**
|
|
735
883
|
* Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
|
|
736
884
|
* absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
|
|
@@ -753,10 +901,11 @@ var SegmentEngine = class {
|
|
|
753
901
|
const w = windowOf(options);
|
|
754
902
|
if (w === "empty") return null;
|
|
755
903
|
const extract = async (seg) => {
|
|
904
|
+
const epoch = this.invalidations;
|
|
756
905
|
const all = await this.chunkKeys(seg);
|
|
757
906
|
const gen = await this.cacheVersion(seg);
|
|
758
907
|
const keys = w === null ? all : keysWithin(all, w);
|
|
759
|
-
return { seg, keys: new Set(keys), chunkless: all.length === 0, gen };
|
|
908
|
+
return { seg, keys: new Set(keys), chunkless: all.length === 0, gen, epoch };
|
|
760
909
|
};
|
|
761
910
|
const [operands, excludes] = await Promise.all([
|
|
762
911
|
Promise.all(segs.map(extract)),
|
|
@@ -801,14 +950,15 @@ var SegmentEngine = class {
|
|
|
801
950
|
const open4 = (o, wanted) => {
|
|
802
951
|
o.streamed = this.openStreamed(
|
|
803
952
|
o.seg,
|
|
804
|
-
common.filter(wanted),
|
|
953
|
+
wanted === void 0 ? common : common.filter(wanted),
|
|
805
954
|
o.gen,
|
|
955
|
+
o.epoch,
|
|
806
956
|
limit,
|
|
807
957
|
COMBINE_RANGE_START
|
|
808
958
|
);
|
|
809
|
-
if (o.streamed
|
|
959
|
+
if (o.streamed) streams.push(o.streamed);
|
|
810
960
|
};
|
|
811
|
-
for (const o of operands) open4(o, mode === "all" ?
|
|
961
|
+
for (const o of operands) open4(o, mode === "all" ? void 0 : (k) => o.keys.has(k));
|
|
812
962
|
if (streamExcludes) for (const e of excludes) open4(e, (k) => e.keys.has(k));
|
|
813
963
|
return {
|
|
814
964
|
w,
|
|
@@ -853,6 +1003,28 @@ var SegmentEngine = class {
|
|
|
853
1003
|
for (const stream of plan.streams) this.closeStreamed(stream);
|
|
854
1004
|
}
|
|
855
1005
|
}
|
|
1006
|
+
/** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
|
|
1007
|
+
async *combineChunks(segs, excludeSegs, mode, op, options) {
|
|
1008
|
+
const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
|
|
1009
|
+
if (plan === null) return;
|
|
1010
|
+
const { w, window } = plan;
|
|
1011
|
+
try {
|
|
1012
|
+
for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
|
|
1013
|
+
if (!slot.result || slot.result.isEmpty) continue;
|
|
1014
|
+
const bitmap = w !== null && isEdge(slot.key, w) ? this.cutToWindow(slot.result, slot.key, w) : slot.result;
|
|
1015
|
+
if (!bitmap.isEmpty) yield { chunkKey: slot.key, bitmap };
|
|
1016
|
+
}
|
|
1017
|
+
} finally {
|
|
1018
|
+
for (const stream of plan.streams) this.closeStreamed(stream);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/** An edge chunk's bitmap cut to the window, as a new bitmap: the remainders `edgeIds` would yield. */
|
|
1022
|
+
cutToWindow(chunk, chunkKey, w) {
|
|
1023
|
+
const from = chunkKey === w.loKey ? w.loRem : 0;
|
|
1024
|
+
const to = chunkKey === w.hiKey ? w.hiRem : MAX_REMAINDER;
|
|
1025
|
+
const rem = chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
|
|
1026
|
+
return this.codec.fromValues(rem.subarray(firstAbove(rem, from - 1), firstAbove(rem, to)));
|
|
1027
|
+
}
|
|
856
1028
|
/**
|
|
857
1029
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
858
1030
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -902,25 +1074,43 @@ var SegmentEngine = class {
|
|
|
902
1074
|
return op.streamed === void 0 ? this.storageChunk({ ...op.seg, chunkKey }, op.gen) : this.streamedChunk(op.streamed, chunkKey);
|
|
903
1075
|
}
|
|
904
1076
|
/**
|
|
905
|
-
*
|
|
906
|
-
* `getChunks`, a segment with no generation, or nothing to read.
|
|
907
|
-
*
|
|
908
|
-
* requested until the first chunk is asked for.
|
|
1077
|
+
* `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
|
|
1078
|
+
* `getChunks`, a segment with no generation, or nothing to read. Nothing is requested, and no cache lookup made, until
|
|
1079
|
+
* the first chunk is asked for (see {@link StreamedChunks}).
|
|
909
1080
|
*/
|
|
910
|
-
openStreamed(seg, keys, gen, concurrency, rampStart) {
|
|
1081
|
+
openStreamed(seg, keys, gen, epoch, concurrency, rampStart) {
|
|
1082
|
+
if (this.storage.getChunks === void 0 || gen === null || keys.length === 0) return void 0;
|
|
1083
|
+
return {
|
|
1084
|
+
seg,
|
|
1085
|
+
gen,
|
|
1086
|
+
epoch,
|
|
1087
|
+
keys,
|
|
1088
|
+
concurrency,
|
|
1089
|
+
rampStart,
|
|
1090
|
+
opened: false,
|
|
1091
|
+
inStream: void 0,
|
|
1092
|
+
stream: void 0,
|
|
1093
|
+
invalidated: false
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Open the stream of `streamed`, at its first uncached chunk `from`: over that chunk and the later keys the cache does
|
|
1098
|
+
* not hold. Each later key is counted as a cache hit or miss here, so a key is counted once whichever way it is served.
|
|
1099
|
+
*/
|
|
1100
|
+
startStream(streamed, from) {
|
|
1101
|
+
const { seg, gen } = streamed;
|
|
911
1102
|
const getChunks = this.storage.getChunks;
|
|
912
|
-
|
|
913
|
-
let wanted = keys;
|
|
914
|
-
let inStream;
|
|
1103
|
+
let wanted = streamed.keys.slice(streamed.keys.indexOf(from));
|
|
915
1104
|
if (this.cache) {
|
|
916
|
-
const misses = [];
|
|
917
|
-
for (
|
|
1105
|
+
const misses = [from];
|
|
1106
|
+
for (let i = 1; i < wanted.length; i++) {
|
|
1107
|
+
const chunkKey = wanted[i];
|
|
918
1108
|
const hit = this.cache.get(this.chunkCacheKey({ ...seg, chunkKey }, gen));
|
|
919
1109
|
if (!hit) misses.push(chunkKey);
|
|
920
1110
|
if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
|
|
921
1111
|
}
|
|
1112
|
+
if (misses.length < wanted.length) streamed.inStream = new Set(misses);
|
|
922
1113
|
wanted = misses;
|
|
923
|
-
if (misses.length < keys.length) inStream = new Set(misses);
|
|
924
1114
|
}
|
|
925
1115
|
const onRequest = this.metricsOn ? (request) => {
|
|
926
1116
|
this.metrics.onEvent({
|
|
@@ -931,27 +1121,36 @@ var SegmentEngine = class {
|
|
|
931
1121
|
ms: request.ms
|
|
932
1122
|
});
|
|
933
1123
|
} : void 0;
|
|
934
|
-
|
|
1124
|
+
streamed.stream = new ChunkStream(
|
|
935
1125
|
getChunks.call(this.storage, seg, wanted, {
|
|
936
|
-
concurrency,
|
|
937
|
-
ramp: rampStart,
|
|
1126
|
+
concurrency: streamed.concurrency,
|
|
1127
|
+
ramp: streamed.rampStart,
|
|
938
1128
|
...onRequest === void 0 ? {} : { onRequest }
|
|
939
1129
|
})
|
|
940
1130
|
);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1131
|
+
streamed.opened = true;
|
|
1132
|
+
streamed.invalidated = this.storage.currentVersion === void 0 && this.invalidations !== streamed.epoch;
|
|
1133
|
+
this.openStreams.add(streamed);
|
|
944
1134
|
}
|
|
945
1135
|
closeStreamed(streamed) {
|
|
946
1136
|
streamed.stream?.close();
|
|
947
1137
|
this.openStreams.delete(streamed);
|
|
948
1138
|
}
|
|
949
1139
|
/**
|
|
950
|
-
* The decoded chunk at `chunkKey` of a streamed read: the
|
|
951
|
-
*
|
|
1140
|
+
* The decoded chunk at `chunkKey` of a streamed read: from the cache while the stream is unopened and the cache holds
|
|
1141
|
+
* it; else the next chunk of its stream, or, for a chunk the cache held when the stream opened, the cached instance
|
|
1142
|
+
* while it is still there, and a read of that chunk alone once it is not.
|
|
952
1143
|
*/
|
|
953
1144
|
streamedChunk(streamed, chunkKey) {
|
|
954
|
-
if (
|
|
1145
|
+
if (!streamed.opened) {
|
|
1146
|
+
const cache = this.cache;
|
|
1147
|
+
if (cache) {
|
|
1148
|
+
const hit = cache.get(this.chunkCacheKey({ ...streamed.seg, chunkKey }, streamed.gen));
|
|
1149
|
+
if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
|
|
1150
|
+
if (hit) return Promise.resolve(hit);
|
|
1151
|
+
}
|
|
1152
|
+
this.startStream(streamed, chunkKey);
|
|
1153
|
+
} else if (streamed.inStream !== void 0 && !streamed.inStream.has(chunkKey)) {
|
|
955
1154
|
const ref = { ...streamed.seg, chunkKey };
|
|
956
1155
|
const hit = this.cache?.get(this.chunkCacheKey(ref, streamed.gen));
|
|
957
1156
|
return hit ? Promise.resolve(hit) : this.storageChunk(ref, streamed.gen);
|
|
@@ -1074,6 +1273,7 @@ var SegmentEngine = class {
|
|
|
1074
1273
|
*/
|
|
1075
1274
|
invalidate(ref) {
|
|
1076
1275
|
const prefix = segmentPrefix(ref);
|
|
1276
|
+
this.invalidations += 1;
|
|
1077
1277
|
for (const streamed of this.openStreams) {
|
|
1078
1278
|
if (segmentPrefix(streamed.seg) === prefix) streamed.invalidated = true;
|
|
1079
1279
|
}
|
|
@@ -1360,63 +1560,121 @@ async function nextLoadGeneration(ref, deps, row) {
|
|
|
1360
1560
|
}
|
|
1361
1561
|
var LIST_COLLECTION_CADENCE = 16;
|
|
1362
1562
|
async function gcOrphanGenerations(ref, deps, options = {}) {
|
|
1563
|
+
return (await listingPass(ref, deps, { keep: options.keep })).deleted;
|
|
1564
|
+
}
|
|
1565
|
+
async function listingPass(ref, deps, options) {
|
|
1363
1566
|
const keep = options.keep ?? 1;
|
|
1567
|
+
const quiet = options.quiet === true;
|
|
1364
1568
|
if (!Number.isInteger(keep) || keep < 0) {
|
|
1365
1569
|
throw new ValidationError(`keep must be a non-negative integer; got ${String(keep)}`);
|
|
1366
1570
|
}
|
|
1367
1571
|
const record = await deps.registry.get(ref);
|
|
1368
|
-
if (record === null) return [];
|
|
1572
|
+
if (record === null) return { deleted: [], kept: [], complete: true };
|
|
1369
1573
|
const current = record.currentGen;
|
|
1370
1574
|
const seen = /* @__PURE__ */ new Set();
|
|
1371
1575
|
for await (const key of deps.storage.list(ref)) seen.add(key.generation);
|
|
1372
1576
|
const gens = [...seen];
|
|
1373
1577
|
const after = await deps.registry.get(ref);
|
|
1374
|
-
if (after === null)
|
|
1578
|
+
if (after === null) {
|
|
1579
|
+
if (quiet) return { deleted: [], kept: [], complete: false };
|
|
1375
1580
|
throw new WriteConflictError(`registry row for segment ${ref.segment} was purged mid-pass`);
|
|
1581
|
+
}
|
|
1376
1582
|
if (record.status === "destroyed" && after.token !== record.token) {
|
|
1583
|
+
if (quiet) return { deleted: [], kept: [], complete: false };
|
|
1377
1584
|
throw new WriteConflictError(
|
|
1378
1585
|
`segment ${ref.segment} changed incarnation while its generations were being listed`
|
|
1379
1586
|
);
|
|
1380
1587
|
}
|
|
1381
1588
|
const cutoff = current === null || after.currentGen === null ? null : Math.min(current, after.currentGen);
|
|
1382
|
-
const
|
|
1589
|
+
const named = record.status === "destroyed" ? void 0 : usableKeptGens(after);
|
|
1590
|
+
const protect = options.protect === void 0 || named === void 0 ? void 0 : /* @__PURE__ */ new Set([...options.protect, ...named]);
|
|
1591
|
+
const stillCollectable = async (generation) => {
|
|
1383
1592
|
const still = await deps.registry.get(ref);
|
|
1384
1593
|
if (still === null) {
|
|
1594
|
+
if (quiet) return "stop";
|
|
1385
1595
|
throw new WriteConflictError(`registry row for segment ${ref.segment} was purged mid-pass`);
|
|
1386
1596
|
}
|
|
1387
1597
|
const ok = record.status === "destroyed" ? still.token === after.token : cutoff === null || still.currentGen !== null && still.currentGen >= cutoff;
|
|
1388
|
-
|
|
1598
|
+
const now = protect === void 0 ? void 0 : usableKeptGens(still);
|
|
1599
|
+
const unnamed = protect === void 0 || record.status === "destroyed" || now !== void 0 && !now.includes(generation);
|
|
1600
|
+
if (!ok || !unnamed) {
|
|
1601
|
+
if (quiet) return "stop";
|
|
1389
1602
|
throw new WriteConflictError(
|
|
1390
1603
|
`segment ${ref.segment} changed incarnation while its generations were being collected`
|
|
1391
1604
|
);
|
|
1392
1605
|
}
|
|
1606
|
+
if (guard !== void 0 && heldGenerations(still, guard.now()).has(generation)) return "skip";
|
|
1607
|
+
return "delete";
|
|
1393
1608
|
};
|
|
1394
|
-
const
|
|
1609
|
+
const guard = record.status === "destroyed" ? void 0 : options.leases;
|
|
1610
|
+
const held = guard === void 0 ? void 0 : heldGenerations(after, guard.now());
|
|
1611
|
+
const below = record.status === "destroyed" || cutoff === null ? [] : gens.filter((g) => g < cutoff).sort((a, b) => b - a);
|
|
1612
|
+
const candidates = record.status === "destroyed" ? gens.sort((a, b) => a - b) : cutoff === null ? (
|
|
1395
1613
|
// No Storage pointer yet, so "below current" selects nothing and there is nothing safe to infer: an object
|
|
1396
1614
|
// here is either a load about to publish or an orphan we cannot tell apart from it. Deleting would race
|
|
1397
1615
|
// that publish into a dangling pointer. It is collected once a pointer exists.
|
|
1398
1616
|
[]
|
|
1617
|
+
) : protect !== void 0 ? (
|
|
1618
|
+
// The row's list is the window: everything below the pointer it does not name is garbage.
|
|
1619
|
+
below.filter((g) => !protect.has(g))
|
|
1399
1620
|
) : (
|
|
1400
1621
|
// Delete generations below the cutoff, except the newest `keep` of them (the grace window).
|
|
1401
|
-
|
|
1622
|
+
below.slice(keep)
|
|
1402
1623
|
);
|
|
1624
|
+
const leasedSpared = new Set(candidates.filter((g) => held?.has(g) === true));
|
|
1625
|
+
const toDelete = candidates.filter((g) => !leasedSpared.has(g));
|
|
1626
|
+
const done = [];
|
|
1403
1627
|
for (const generation of toDelete) {
|
|
1404
|
-
await stillCollectable();
|
|
1628
|
+
const verdict = await stillCollectable(generation);
|
|
1629
|
+
if (verdict === "stop") return { deleted: done, kept: [], complete: false };
|
|
1630
|
+
if (verdict === "skip") {
|
|
1631
|
+
leasedSpared.add(generation);
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1405
1634
|
await deps.storage.delete({ namespace: ref.namespace, segment: ref.segment, generation });
|
|
1635
|
+
done.push(generation);
|
|
1406
1636
|
}
|
|
1407
|
-
|
|
1637
|
+
const deleted = new Set(done);
|
|
1638
|
+
return {
|
|
1639
|
+
deleted: done,
|
|
1640
|
+
kept: below.filter((g) => !deleted.has(g) && !leasedSpared.has(g)).reverse(),
|
|
1641
|
+
complete: true
|
|
1642
|
+
};
|
|
1408
1643
|
}
|
|
1409
1644
|
async function collectAfterLoad(ref, deps, options) {
|
|
1410
|
-
const { generation, keep, byName, proveCurrent } = options;
|
|
1645
|
+
const { generation, keep, byName, currentGone, kept, proveCurrent, leases } = options;
|
|
1411
1646
|
if (keep >= generation) return [];
|
|
1647
|
+
const list = kept?.list;
|
|
1648
|
+
if (list === void 0 || currentGone === true)
|
|
1649
|
+
return reconcile(ref, deps, { keep, kept, leases });
|
|
1412
1650
|
const periodic = generation % LIST_COLLECTION_CADENCE === 0;
|
|
1413
|
-
if (byName &&
|
|
1414
|
-
if (keep
|
|
1415
|
-
return
|
|
1651
|
+
if (byName && !periodic) {
|
|
1652
|
+
if (keep >= 1 && proveCurrent !== void 0 && !await objectIsThere(deps.storage, proveCurrent)) {
|
|
1653
|
+
return reconcile(ref, deps, { keep, kept, leases });
|
|
1654
|
+
}
|
|
1655
|
+
return deleteEvicted(ref, deps, { generation, evict: kept.evict, leases });
|
|
1656
|
+
}
|
|
1657
|
+
return (await listingPass(ref, deps, { keep, protect: list, quiet: true, leases })).deleted;
|
|
1658
|
+
}
|
|
1659
|
+
async function reconcile(ref, deps, options) {
|
|
1660
|
+
const { keep, kept, leases } = options;
|
|
1661
|
+
const {
|
|
1662
|
+
deleted,
|
|
1663
|
+
kept: present,
|
|
1664
|
+
complete
|
|
1665
|
+
} = await listingPass(ref, deps, { keep, quiet: true, leases });
|
|
1666
|
+
const token = kept?.token;
|
|
1667
|
+
if (complete && token !== void 0 && keep <= MAX_KEPT_GENERATIONS && !sameList(kept?.list, present)) {
|
|
1668
|
+
try {
|
|
1669
|
+
await deps.registry.compareAndSwap(ref, token, { keptGens: present });
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
if (!(err instanceof WriteConflictError)) throw err;
|
|
1416
1672
|
}
|
|
1417
|
-
return collectByName(ref, deps, { generation, keep });
|
|
1418
1673
|
}
|
|
1419
|
-
return
|
|
1674
|
+
return deleted;
|
|
1675
|
+
}
|
|
1676
|
+
function sameList(a, b) {
|
|
1677
|
+
return a !== void 0 && a.length === b.length && a.every((g, i) => g === b[i]);
|
|
1420
1678
|
}
|
|
1421
1679
|
async function objectIsThere(storage, key) {
|
|
1422
1680
|
try {
|
|
@@ -1426,21 +1684,20 @@ async function objectIsThere(storage, key) {
|
|
|
1426
1684
|
return false;
|
|
1427
1685
|
}
|
|
1428
1686
|
}
|
|
1429
|
-
async function
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1687
|
+
async function deleteEvicted(ref, deps, options) {
|
|
1688
|
+
const deleted = [];
|
|
1689
|
+
for (const generation of options.evict) {
|
|
1690
|
+
const still = await deps.registry.get(ref);
|
|
1691
|
+
if (still === null || still.currentGen === null || still.currentGen < options.generation) break;
|
|
1692
|
+
const named = usableKeptGens(still);
|
|
1693
|
+
if (named === void 0 || generation >= still.currentGen || named.includes(generation)) break;
|
|
1694
|
+
if (options.leases !== void 0 && heldGenerations(still, options.leases.now()).has(generation)) {
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
await deps.storage.delete({ namespace: ref.namespace, segment: ref.segment, generation });
|
|
1698
|
+
deleted.push(generation);
|
|
1433
1699
|
}
|
|
1434
|
-
|
|
1435
|
-
if (evicted < 0) return [];
|
|
1436
|
-
const still = await deps.registry.get(ref);
|
|
1437
|
-
if (still === null || still.currentGen === null || still.currentGen < generation) return [];
|
|
1438
|
-
await deps.storage.delete({
|
|
1439
|
-
namespace: ref.namespace,
|
|
1440
|
-
segment: ref.segment,
|
|
1441
|
-
generation: evicted
|
|
1442
|
-
});
|
|
1443
|
-
return [evicted];
|
|
1700
|
+
return deleted;
|
|
1444
1701
|
}
|
|
1445
1702
|
|
|
1446
1703
|
// src/core/reader-defaults.ts
|
|
@@ -1491,7 +1748,7 @@ var HOURS_PER_MONTH = 730;
|
|
|
1491
1748
|
var SECONDS_PER_MONTH = HOURS_PER_MONTH * 3600;
|
|
1492
1749
|
var GIB = 1024 ** 3;
|
|
1493
1750
|
var STORE_LOAD_PUT_CLASS = 1 + 1 / LIST_COLLECTION_CADENCE;
|
|
1494
|
-
var STORE_LOAD_POINTER_READS =
|
|
1751
|
+
var STORE_LOAD_POINTER_READS = 2 + 2 / LIST_COLLECTION_CADENCE;
|
|
1495
1752
|
var STORE_LOAD_EXISTENCE_CHECKS = 2 - 1 / LIST_COLLECTION_CADENCE;
|
|
1496
1753
|
var RETENTION_SWEEP_REQUESTS = deepFreeze({
|
|
1497
1754
|
conditionalDelete: {
|
|
@@ -1946,13 +2203,30 @@ async function runExport(reader, registry, sink, options = {}) {
|
|
|
1946
2203
|
const segments = [];
|
|
1947
2204
|
const failed = [];
|
|
1948
2205
|
let totalIds = 0;
|
|
2206
|
+
if (typeof reader?.segment !== "function") {
|
|
2207
|
+
throw new ValidationError(
|
|
2208
|
+
"runExport: the reader must have a segment(name, { namespace? }) method returning a handle with pin(), as a store's does"
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
let probed = false;
|
|
2212
|
+
let unusable;
|
|
1949
2213
|
const exportRef = async (ref) => {
|
|
1950
2214
|
let writer;
|
|
1951
2215
|
let count = 0;
|
|
1952
2216
|
let bytes = 0;
|
|
1953
2217
|
try {
|
|
1954
2218
|
validateSegmentRef(ref);
|
|
1955
|
-
const
|
|
2219
|
+
const handle = reader.segment(ref.segment, { namespace: ref.namespace });
|
|
2220
|
+
if (!probed) {
|
|
2221
|
+
probed = true;
|
|
2222
|
+
if (typeof handle?.pin !== "function") {
|
|
2223
|
+
unusable = new ValidationError(
|
|
2224
|
+
"runExport: the reader's segment(name, { namespace? }) must return a handle with a pin() method; a segment is exported at one pinned generation"
|
|
2225
|
+
);
|
|
2226
|
+
throw unusable;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const ids = (await handle.pin()).iterate();
|
|
1956
2230
|
writer = await sink.open(ref, ext);
|
|
1957
2231
|
if (format === "roaring") {
|
|
1958
2232
|
const bm = requireCodec(options.codec, "runExport({ format: 'roaring' })").empty();
|
|
@@ -1983,6 +2257,7 @@ async function runExport(reader, registry, sink, options = {}) {
|
|
|
1983
2257
|
}
|
|
1984
2258
|
}
|
|
1985
2259
|
} catch (err) {
|
|
2260
|
+
if (err === unusable) throw err;
|
|
1986
2261
|
if (writer !== void 0) {
|
|
1987
2262
|
try {
|
|
1988
2263
|
await writer.abort?.();
|
|
@@ -2108,6 +2383,10 @@ var MemoryRegistryDriver = class {
|
|
|
2108
2383
|
this.rows.set(key, structuredClone(recordFromNew(ref, checked, this.now(), token)));
|
|
2109
2384
|
return { token };
|
|
2110
2385
|
}
|
|
2386
|
+
/**
|
|
2387
|
+
* A caller's `held` row (see `RegistryWriteOptions`) has no read to spare here: the token is compared with the stored row in the same step
|
|
2388
|
+
* as the write, in memory, so a stale one fails exactly as a lost race does.
|
|
2389
|
+
*/
|
|
2111
2390
|
async compareAndSwap(ref, expected, patch) {
|
|
2112
2391
|
validateSegmentRef(ref);
|
|
2113
2392
|
const checked = validateRegistryPatch(patch);
|
|
@@ -2463,6 +2742,10 @@ var LocalFsRegistryDriver = class {
|
|
|
2463
2742
|
return { token };
|
|
2464
2743
|
});
|
|
2465
2744
|
}
|
|
2745
|
+
/**
|
|
2746
|
+
* A caller's `held` row (see `RegistryWriteOptions`) has no read to spare here: the row's file is read under its lock and its token compared
|
|
2747
|
+
* there, a local read and no request, so a stale one fails exactly as a lost race does.
|
|
2748
|
+
*/
|
|
2466
2749
|
async compareAndSwap(ref, expected, patch) {
|
|
2467
2750
|
const checked = validateRegistryPatch(patch);
|
|
2468
2751
|
const path = registryRowPath(this.root, ref);
|
|
@@ -3717,6 +4000,12 @@ var DecodedLoadInput = class {
|
|
|
3717
4000
|
}
|
|
3718
4001
|
bitmap;
|
|
3719
4002
|
};
|
|
4003
|
+
var ChunkLoadInput = class {
|
|
4004
|
+
constructor(chunks) {
|
|
4005
|
+
this.chunks = chunks;
|
|
4006
|
+
}
|
|
4007
|
+
chunks;
|
|
4008
|
+
};
|
|
3720
4009
|
function tagOf(value) {
|
|
3721
4010
|
return Object.prototype.toString.call(value).slice(8, -1);
|
|
3722
4011
|
}
|
|
@@ -3750,7 +4039,7 @@ function isShared(buffer) {
|
|
|
3750
4039
|
}
|
|
3751
4040
|
function overCap(what, length) {
|
|
3752
4041
|
return new ValidationError(
|
|
3753
|
-
`${what} is ${length} bytes, more than any canonical 32-bit bitmap serializes to (${MAX_SERIALIZED_LOAD_BYTES}).
|
|
4042
|
+
`${what} is ${length} bytes, more than any canonical 32-bit bitmap serializes to (${MAX_SERIALIZED_LOAD_BYTES}). If it was serialized from a bitmap, call runOptimize() on the bitmap first.`
|
|
3754
4043
|
);
|
|
3755
4044
|
}
|
|
3756
4045
|
function prepareLoadInput(input, codec) {
|
|
@@ -3768,7 +4057,9 @@ function prepareLoadInput(input, codec) {
|
|
|
3768
4057
|
if (Symbol.iterator in input || Symbol.asyncIterator in input) return input;
|
|
3769
4058
|
const keys = Object.keys(input);
|
|
3770
4059
|
if (keys.length === 1 && keys[0] === "serialized") {
|
|
3771
|
-
return
|
|
4060
|
+
return new DecodedLoadInput(
|
|
4061
|
+
decodeSerialized(input.serialized, codec, "{ serialized }")
|
|
4062
|
+
);
|
|
3772
4063
|
}
|
|
3773
4064
|
if (keys.length === 1 && keys[0] === "bitmap") {
|
|
3774
4065
|
const bitmap = input.bitmap;
|
|
@@ -3780,13 +4071,13 @@ function prepareLoadInput(input, codec) {
|
|
|
3780
4071
|
const size = bitmap.getSerializationSizeInBytes("portable");
|
|
3781
4072
|
if (typeof size === "number" && size > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, size);
|
|
3782
4073
|
}
|
|
3783
|
-
return
|
|
4074
|
+
return new DecodedLoadInput(decodeSerialized(bitmap.serialize("portable"), codec, what));
|
|
3784
4075
|
}
|
|
3785
4076
|
throw new ValidationError(
|
|
3786
4077
|
`a load takes ids (an iterable of integers), { serialized } or { bitmap }, as the only key; got an object with keys [${keys.join(", ")}]. Load options go in the next argument.`
|
|
3787
4078
|
);
|
|
3788
4079
|
}
|
|
3789
|
-
function
|
|
4080
|
+
function decodeSerialized(bytes, codec, what = "the buffer") {
|
|
3790
4081
|
if (typeof bytes !== "object" || bytes === null || tagOf(bytes) !== "Uint8Array") {
|
|
3791
4082
|
throw new ValidationError(`${what} must be a Uint8Array of portable Roaring bytes`);
|
|
3792
4083
|
}
|
|
@@ -3794,9 +4085,7 @@ function decode(bytes, what, codec) {
|
|
|
3794
4085
|
if (view.byteLength > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, view.byteLength);
|
|
3795
4086
|
const own = isShared(view.buffer) ? new Uint8Array(view) : view;
|
|
3796
4087
|
try {
|
|
3797
|
-
return
|
|
3798
|
-
codec.safeDeserialize(own, MAX_SERIALIZED_LOAD_BYTES, { whole: true })
|
|
3799
|
-
);
|
|
4088
|
+
return codec.safeDeserialize(own, MAX_SERIALIZED_LOAD_BYTES, { whole: true });
|
|
3800
4089
|
} catch (err) {
|
|
3801
4090
|
if (!isIntegrityError(err)) throw err;
|
|
3802
4091
|
throw new ValidationError(`${what} is not a valid bitmap: ${err.message}`);
|
|
@@ -3939,6 +4228,8 @@ var notThePinned = (ref, generation) => new NotFoundError(
|
|
|
3939
4228
|
function versionOf(generation, lineage) {
|
|
3940
4229
|
return lineage === void 0 ? String(generation) : `${generation}:${String(lineage)}`;
|
|
3941
4230
|
}
|
|
4231
|
+
var PinMoved = class extends Error {
|
|
4232
|
+
};
|
|
3942
4233
|
function summaryOfGeneration(generation, description) {
|
|
3943
4234
|
const { cardinality, metadata } = description;
|
|
3944
4235
|
return metadata === void 0 || Object.keys(metadata).length === 0 ? { generation, cardinality } : { generation, cardinality, metadata: Object.freeze({ ...metadata }) };
|
|
@@ -4305,17 +4596,102 @@ var CrbmStorageChunkSource = class {
|
|
|
4305
4596
|
* and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
|
|
4306
4597
|
* once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
|
|
4307
4598
|
*/
|
|
4308
|
-
async pinGeneration(ref) {
|
|
4599
|
+
async pinGeneration(ref, lease) {
|
|
4600
|
+
for (let moved = 0; ; moved++) {
|
|
4601
|
+
try {
|
|
4602
|
+
try {
|
|
4603
|
+
return await this.pinOnce(ref, lease);
|
|
4604
|
+
} catch (err) {
|
|
4605
|
+
if (!isNotFoundError(err)) throw err;
|
|
4606
|
+
return await this.pinOnce(ref, lease);
|
|
4607
|
+
}
|
|
4608
|
+
} catch (err) {
|
|
4609
|
+
if (!(err instanceof PinMoved)) throw err;
|
|
4610
|
+
if (moved >= 2) {
|
|
4611
|
+
throw new WriteConflictError(
|
|
4612
|
+
`pin: the pointer of "${ref.segment}" moved on every attempt to lease its current generation`
|
|
4613
|
+
);
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
/**
|
|
4619
|
+
* Open one named generation and verify it is the object `at.fingerprint` names, for a pin the caller reopens. One
|
|
4620
|
+
* row read and one tail read with a registry (none of the row for a store without one): the open is the one a
|
|
4621
|
+
* pinned read makes ({@link readerAt}), memoised under the version the pin will read by, so the pin's first read
|
|
4622
|
+
* finds it open. A generation that is gone, above the row's pointer, on a destroyed or absent row, or another
|
|
4623
|
+
* object than the fingerprint names, throws `NotFoundError`; nothing here reads empty.
|
|
4624
|
+
*
|
|
4625
|
+
* A generation counts as published while the pointer is at or above it. A rollback deletes nothing, so after one
|
|
4626
|
+
* and a later load, a generation it rolled back from can be reopened while its object is still stored and the
|
|
4627
|
+
* fingerprint matches. Like `pinGeneration`'s pinned reads, this opens the object without the live open's check
|
|
4628
|
+
* of the row's summary against it, since that check is made where the row's own generation is read.
|
|
4629
|
+
*/
|
|
4630
|
+
async pinGenerationAt(ref, at, lease) {
|
|
4631
|
+
const { generation, fingerprint } = at;
|
|
4632
|
+
const gone = (why) => new NotFoundError(
|
|
4633
|
+
`segment "${ref.segment}" generation ${generation} cannot be pinned: ${why}`
|
|
4634
|
+
);
|
|
4635
|
+
let version = versionOf(generation, void 0);
|
|
4636
|
+
let installed;
|
|
4637
|
+
let pinKey;
|
|
4638
|
+
let memoised = false;
|
|
4639
|
+
if (this.registry !== void 0) {
|
|
4640
|
+
const record = await this.registry.get(ref);
|
|
4641
|
+
if (record === null || record.status === "destroyed")
|
|
4642
|
+
throw gone("the segment has no readable row");
|
|
4643
|
+
if (record.currentGen === null || generation > record.currentGen) {
|
|
4644
|
+
throw gone("it is not a published generation of this segment");
|
|
4645
|
+
}
|
|
4646
|
+
let lineage = record.token;
|
|
4647
|
+
if (lease !== void 0) {
|
|
4648
|
+
const taken = await lease.take(record);
|
|
4649
|
+
if (taken === "moved") throw gone("the segment moved while its lease was taken");
|
|
4650
|
+
lineage = taken.token;
|
|
4651
|
+
}
|
|
4652
|
+
version = versionOf(generation, lineage);
|
|
4653
|
+
const key = this.pinnedKey(ref, version);
|
|
4654
|
+
memoised = this.snapshots.peek(key) !== void 0;
|
|
4655
|
+
if (!memoised) {
|
|
4656
|
+
installed = this.install(
|
|
4657
|
+
key,
|
|
4658
|
+
Snapshot.eager(
|
|
4659
|
+
this.openForTarget(ref, {
|
|
4660
|
+
generation,
|
|
4661
|
+
lineage,
|
|
4662
|
+
wrappedDeks: record.wrappedDeks
|
|
4663
|
+
})
|
|
4664
|
+
)
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
4667
|
+
pinKey = key;
|
|
4668
|
+
}
|
|
4669
|
+
if (this.registry === void 0) {
|
|
4670
|
+
memoised = this.snapshots.peek(this.pinnedKey(ref, version, fingerprint)) !== void 0;
|
|
4671
|
+
}
|
|
4309
4672
|
try {
|
|
4310
|
-
|
|
4673
|
+
await this.readerAt(ref, generation, version, fingerprint);
|
|
4311
4674
|
} catch (err) {
|
|
4312
|
-
if (
|
|
4313
|
-
|
|
4675
|
+
if (installed !== void 0 && pinKey !== void 0 && this.snapshots.peek(pinKey) === installed) {
|
|
4676
|
+
this.snapshots.delete(pinKey);
|
|
4677
|
+
}
|
|
4678
|
+
throw err;
|
|
4314
4679
|
}
|
|
4680
|
+
if (!memoised) {
|
|
4681
|
+
const held = this.heldKey(ref, version, fingerprint);
|
|
4682
|
+
this.replacedPins.delete(held);
|
|
4683
|
+
this.checking.delete(held);
|
|
4684
|
+
}
|
|
4685
|
+
return { generation, version, fingerprint };
|
|
4315
4686
|
}
|
|
4316
|
-
async pinOnce(ref) {
|
|
4317
|
-
|
|
4687
|
+
async pinOnce(ref, lease) {
|
|
4688
|
+
let live = await this.resolveLive(ref);
|
|
4318
4689
|
if (live === null) return null;
|
|
4690
|
+
if (lease !== void 0 && live.row !== void 0) {
|
|
4691
|
+
const taken = await lease.take(live.row);
|
|
4692
|
+
if (taken === "moved") throw new PinMoved();
|
|
4693
|
+
live = { ...live, target: { ...live.target, lineage: taken.token } };
|
|
4694
|
+
}
|
|
4319
4695
|
const target = live.target;
|
|
4320
4696
|
const version = versionOf(target.generation, target.lineage);
|
|
4321
4697
|
let reader;
|
|
@@ -4611,6 +4987,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4611
4987
|
const unwrap = keys === void 0 || keys.length === 0 || this.keystore === void 0 ? void 0 : prior?.unwrap !== void 0 && sameKeys(prior.target.wrappedDeks, keys) ? prior.unwrap : once(() => this.keystore.openDek(keys));
|
|
4612
4988
|
const crypto = once(() => this.cryptoForRead(ref, generation, keys, unwrap));
|
|
4613
4989
|
return {
|
|
4990
|
+
row: record,
|
|
4614
4991
|
target: { generation, lineage: record.token, wrappedDeks: record.wrappedDeks },
|
|
4615
4992
|
crypto,
|
|
4616
4993
|
unwrap,
|
|
@@ -5048,19 +5425,36 @@ function refuseCleartextOntoKey(key, record, cleartext) {
|
|
|
5048
5425
|
}
|
|
5049
5426
|
var UNANSWERED_RESENDS = 3;
|
|
5050
5427
|
var UNANSWERED_RESEND_BASE_MS = 500;
|
|
5051
|
-
|
|
5428
|
+
function leasesPatch(record, options) {
|
|
5429
|
+
const held = record.leases;
|
|
5430
|
+
if (held === void 0) return {};
|
|
5431
|
+
if (options.clearLeases === true) return { leases: void 0 };
|
|
5432
|
+
if (options.leasesNow === void 0) return {};
|
|
5433
|
+
const now = options.leasesNow();
|
|
5434
|
+
const live = held.filter((e) => isLive(e, now));
|
|
5435
|
+
if (live.length === held.length) return {};
|
|
5436
|
+
return { leases: live.length === 0 ? void 0 : live };
|
|
5437
|
+
}
|
|
5438
|
+
async function publishGenerationKept(registry, key, options = {}) {
|
|
5052
5439
|
let fresh;
|
|
5053
5440
|
let failedOn;
|
|
5054
5441
|
let unanswered;
|
|
5055
5442
|
let resends = 0;
|
|
5443
|
+
const churn = leaseChurn({ clock: options.clock, rng: options.rng });
|
|
5444
|
+
let throughChurn = false;
|
|
5445
|
+
let waited;
|
|
5056
5446
|
let sawUnanswered = false;
|
|
5057
5447
|
let lastRecord;
|
|
5058
5448
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
5059
5449
|
const reused = attempt === 0 && options.row !== void 0;
|
|
5450
|
+
const holdsRow = fresh === void 0;
|
|
5060
5451
|
const record = fresh !== void 0 ? fresh : reused ? options.row ?? null : await registry.get(key);
|
|
5452
|
+
const write = holdsRow ? { held: record } : void 0;
|
|
5061
5453
|
fresh = void 0;
|
|
5062
5454
|
unanswered = void 0;
|
|
5063
5455
|
lastRecord = record;
|
|
5456
|
+
let next;
|
|
5457
|
+
let wrote;
|
|
5064
5458
|
if (failedOn !== void 0) {
|
|
5065
5459
|
let landed;
|
|
5066
5460
|
try {
|
|
@@ -5068,48 +5462,78 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5068
5462
|
} catch (proofErr) {
|
|
5069
5463
|
throw outcomeUnknown(key, proofErr);
|
|
5070
5464
|
}
|
|
5071
|
-
if (landed)
|
|
5465
|
+
if (landed)
|
|
5466
|
+
return {
|
|
5467
|
+
published: true,
|
|
5468
|
+
kept: { ...nextKept(failedOn, key.generation, options.keep), token: void 0 }
|
|
5469
|
+
};
|
|
5072
5470
|
if (record !== null && record.status === "active" && record.currentGen === key.generation) {
|
|
5073
|
-
return
|
|
5471
|
+
return REFUSED;
|
|
5074
5472
|
}
|
|
5075
5473
|
failedOn = void 0;
|
|
5076
5474
|
}
|
|
5077
5475
|
try {
|
|
5078
5476
|
if (options.expectFrom !== void 0 && record?.currentGen !== options.expectFrom) {
|
|
5079
|
-
return
|
|
5477
|
+
return REFUSED;
|
|
5080
5478
|
}
|
|
5081
5479
|
if (options.expectAbsent === true && record !== null) {
|
|
5082
|
-
return
|
|
5480
|
+
return REFUSED;
|
|
5083
5481
|
}
|
|
5482
|
+
throughChurn = false;
|
|
5084
5483
|
if (options.expectToken !== void 0 && record?.token !== options.expectToken) {
|
|
5085
|
-
|
|
5484
|
+
const held = options.expectRow;
|
|
5485
|
+
if (record !== null && held != null && record === waited && onlyLeasesDiffer(held, record)) {
|
|
5486
|
+
throughChurn = true;
|
|
5487
|
+
} else {
|
|
5488
|
+
const settled2 = record === waited ? void 0 : await churn.settle(held, record, () => registry.get(key));
|
|
5489
|
+
if (settled2 !== void 0) {
|
|
5490
|
+
waited = settled2.row;
|
|
5491
|
+
fresh = settled2.row;
|
|
5492
|
+
attempt -= 1;
|
|
5493
|
+
continue;
|
|
5494
|
+
}
|
|
5495
|
+
return REFUSED;
|
|
5496
|
+
}
|
|
5086
5497
|
}
|
|
5087
5498
|
if (record === null && options.summary !== void 0 && "sealed" in options.summary && options.wrappedDeks === void 0) {
|
|
5088
5499
|
if (reused) continue;
|
|
5089
|
-
return
|
|
5500
|
+
return REFUSED;
|
|
5090
5501
|
}
|
|
5091
5502
|
if (record === null) {
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5503
|
+
next = nextKept(null, key.generation, options.keep);
|
|
5504
|
+
wrote = (await registry.create(
|
|
5505
|
+
key,
|
|
5506
|
+
{
|
|
5507
|
+
currentGen: key.generation,
|
|
5508
|
+
wrappedDeks: options.wrappedDeks,
|
|
5509
|
+
...options.summary === void 0 ? {} : { summary: options.summary },
|
|
5510
|
+
...next.list === void 0 ? {} : { keptGens: next.list }
|
|
5511
|
+
},
|
|
5512
|
+
write
|
|
5513
|
+
)).token;
|
|
5097
5514
|
} else if (record.status === "destroyed") {
|
|
5098
5515
|
throw new ValidationError(
|
|
5099
5516
|
`segment "${key.segment}" was destroyed (crypto-shredded) while generation ${key.generation} was being written \u2014 refusing to publish it; the written object is unreadable. Use a new segment.`
|
|
5100
5517
|
);
|
|
5101
5518
|
} else if (record.currentGen === null) {
|
|
5102
5519
|
refuseCleartextOntoKey(key, record, options.cleartext);
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5520
|
+
next = nextKept(record, key.generation, options.keep);
|
|
5521
|
+
wrote = (await registry.compareAndSwap(
|
|
5522
|
+
key,
|
|
5523
|
+
record.token,
|
|
5524
|
+
{
|
|
5525
|
+
currentGen: key.generation,
|
|
5526
|
+
...options.wrappedDeks === void 0 ? {} : { wrappedDeks: options.wrappedDeks },
|
|
5527
|
+
...options.summary === void 0 ? {} : { summary: options.summary },
|
|
5528
|
+
...next.list === void 0 ? {} : { keptGens: next.list }
|
|
5529
|
+
},
|
|
5530
|
+
write
|
|
5531
|
+
)).token;
|
|
5108
5532
|
} else if (record.currentGen > key.generation) {
|
|
5109
|
-
return
|
|
5533
|
+
return REFUSED;
|
|
5110
5534
|
} else if (record.currentGen === key.generation) {
|
|
5111
5535
|
if (reused) continue;
|
|
5112
|
-
return true;
|
|
5536
|
+
return { published: true };
|
|
5113
5537
|
} else {
|
|
5114
5538
|
if (options.wrappedDeks !== void 0 && options.wrappedDeks.length > 0) {
|
|
5115
5539
|
throw new ValidationError(
|
|
@@ -5117,15 +5541,27 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5117
5541
|
);
|
|
5118
5542
|
}
|
|
5119
5543
|
refuseCleartextOntoKey(key, record, options.cleartext);
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5544
|
+
next = nextKept(record, key.generation, options.keep);
|
|
5545
|
+
wrote = (await registry.compareAndSwap(
|
|
5546
|
+
key,
|
|
5547
|
+
record.token,
|
|
5548
|
+
{
|
|
5549
|
+
currentGen: key.generation,
|
|
5550
|
+
...options.summary === void 0 ? {} : { summary: options.summary },
|
|
5551
|
+
...next.list === void 0 ? {} : { keptGens: next.list },
|
|
5552
|
+
...leasesPatch(record, options)
|
|
5553
|
+
},
|
|
5554
|
+
write
|
|
5555
|
+
)).token;
|
|
5124
5556
|
}
|
|
5125
|
-
return
|
|
5557
|
+
return {
|
|
5558
|
+
published: true,
|
|
5559
|
+
kept: { ...next ?? { list: void 0, evict: [] }, token: wrote }
|
|
5560
|
+
};
|
|
5126
5561
|
} catch (err) {
|
|
5127
5562
|
if (isWriteConflictError(err)) {
|
|
5128
5563
|
failedOn = record;
|
|
5564
|
+
if (throughChurn) attempt -= 1;
|
|
5129
5565
|
continue;
|
|
5130
5566
|
}
|
|
5131
5567
|
if (!isTransientError(err)) throw err;
|
|
@@ -5154,10 +5590,12 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5154
5590
|
}
|
|
5155
5591
|
const final = await registry.get(key);
|
|
5156
5592
|
if (final !== null && final.currentGen !== null && final.currentGen >= key.generation) {
|
|
5157
|
-
if (final.currentGen !== key.generation) return
|
|
5158
|
-
if (!sawUnanswered) return true;
|
|
5593
|
+
if (final.currentGen !== key.generation) return REFUSED;
|
|
5594
|
+
if (!sawUnanswered) return { published: true };
|
|
5159
5595
|
try {
|
|
5160
|
-
return
|
|
5596
|
+
return {
|
|
5597
|
+
published: await landedHere(final, lastRecord ?? null, key, options.holdsOwnObject)
|
|
5598
|
+
};
|
|
5161
5599
|
} catch (proofErr) {
|
|
5162
5600
|
throw outcomeUnknown(key, proofErr);
|
|
5163
5601
|
}
|
|
@@ -5167,6 +5605,10 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5167
5605
|
`publishGeneration: contention setting currentGen for ${key.segment}`
|
|
5168
5606
|
);
|
|
5169
5607
|
}
|
|
5608
|
+
var REFUSED = { published: false };
|
|
5609
|
+
async function publishGeneration(registry, key, options = {}) {
|
|
5610
|
+
return (await publishGenerationKept(registry, key, options)).published;
|
|
5611
|
+
}
|
|
5170
5612
|
async function landedHere(now, before, key, holdsOwnObject) {
|
|
5171
5613
|
if (now === null || now.status === "destroyed" || now.currentGen !== key.generation) return false;
|
|
5172
5614
|
if (before !== null && !sameIncarnation(before, now)) return false;
|
|
@@ -5206,6 +5648,8 @@ async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
|
|
|
5206
5648
|
await pause();
|
|
5207
5649
|
chunks = ids.bitmap.encodeChunks();
|
|
5208
5650
|
await pause();
|
|
5651
|
+
} else if (ids instanceof ChunkLoadInput) {
|
|
5652
|
+
chunks = encodeEach(rangeCheckedAtWrite(await collectChunks(ids, codec, options.clock)));
|
|
5209
5653
|
} else {
|
|
5210
5654
|
chunks = encodeEach(
|
|
5211
5655
|
await bucketIds(ids instanceof DecodedLoadInput ? ids.bitmap : ids, codec, options.clock)
|
|
@@ -5298,6 +5742,52 @@ async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
|
|
|
5298
5742
|
fingerprint
|
|
5299
5743
|
};
|
|
5300
5744
|
}
|
|
5745
|
+
function* rangeCheckedAtWrite(chunks) {
|
|
5746
|
+
for (const chunk of chunks) {
|
|
5747
|
+
const max = chunk.bitmap.maximum?.();
|
|
5748
|
+
if (max !== void 0 && max > MAX_REMAINDER) {
|
|
5749
|
+
throw new ValidationError(
|
|
5750
|
+
`chunk ${chunk.chunkKey} holds ${max}, outside the 16-bit range [0, ${MAX_REMAINDER}]`
|
|
5751
|
+
);
|
|
5752
|
+
}
|
|
5753
|
+
yield chunk;
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
async function collectChunks(input, codec, clock) {
|
|
5757
|
+
const chunks = [];
|
|
5758
|
+
const tick = yieldEvery(clock);
|
|
5759
|
+
let last = -1;
|
|
5760
|
+
for await (const chunk of input.chunks) {
|
|
5761
|
+
if (typeof chunk !== "object" || chunk === null) {
|
|
5762
|
+
throw new ValidationError(
|
|
5763
|
+
`a chunk must be an object with a chunkKey and a bitmap; got ${String(chunk)}`
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5766
|
+
const { chunkKey, bitmap } = chunk;
|
|
5767
|
+
if (codec.owns?.(bitmap) !== true) {
|
|
5768
|
+
throw new ValidationError(
|
|
5769
|
+
`chunk ${String(chunkKey)}: its bitmap is not one this load's codec made`
|
|
5770
|
+
);
|
|
5771
|
+
}
|
|
5772
|
+
if (!Number.isInteger(chunkKey) || chunkKey < 0 || chunkKey > MAX_REMAINDER) {
|
|
5773
|
+
throw new ValidationError(`a chunk's key must be an integer in [0, 65535]; got ${chunkKey}`);
|
|
5774
|
+
}
|
|
5775
|
+
if (chunkKey <= last) {
|
|
5776
|
+
throw new ValidationError(`chunk ${chunkKey} does not ascend: the last was ${last}`);
|
|
5777
|
+
}
|
|
5778
|
+
last = chunkKey;
|
|
5779
|
+
const max = bitmap.maximum?.();
|
|
5780
|
+
if (max !== void 0 && max > MAX_REMAINDER) {
|
|
5781
|
+
throw new ValidationError(
|
|
5782
|
+
`chunk ${chunkKey} holds ${max}, outside the 16-bit range [0, ${MAX_REMAINDER}]`
|
|
5783
|
+
);
|
|
5784
|
+
}
|
|
5785
|
+
chunks.push({ chunkKey, bitmap });
|
|
5786
|
+
const pause = tick();
|
|
5787
|
+
if (pause !== null) await pause;
|
|
5788
|
+
}
|
|
5789
|
+
return chunks;
|
|
5790
|
+
}
|
|
5301
5791
|
async function bucketIds(ids, codec, clock) {
|
|
5302
5792
|
const byChunk = /* @__PURE__ */ new Map();
|
|
5303
5793
|
const pendingByChunk = /* @__PURE__ */ new Map();
|
|
@@ -5575,7 +6065,25 @@ async function currentCardinality(ref, deps, record, unwrapped) {
|
|
|
5575
6065
|
for (const n of reader.cardinalities().values()) total += n;
|
|
5576
6066
|
return { cardinality: total, fromSummary: false };
|
|
5577
6067
|
}
|
|
5578
|
-
|
|
6068
|
+
function loadSegment(ref, input, deps, options = {}) {
|
|
6069
|
+
return runLoad(ref, (codec) => prepareLoadInput(input, codec), deps, options);
|
|
6070
|
+
}
|
|
6071
|
+
function loadSegmentChunks(ref, chunks, deps, options = {}) {
|
|
6072
|
+
return runLoad(
|
|
6073
|
+
ref,
|
|
6074
|
+
() => {
|
|
6075
|
+
if (typeof chunks !== "object" || chunks === null || !(Symbol.asyncIterator in chunks)) {
|
|
6076
|
+
throw new ValidationError(
|
|
6077
|
+
"loadSegmentChunks takes an async iterable of { chunkKey, bitmap }"
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
return new ChunkLoadInput(chunks);
|
|
6081
|
+
},
|
|
6082
|
+
deps,
|
|
6083
|
+
options
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
6086
|
+
async function runLoad(ref, prepare, deps, options) {
|
|
5579
6087
|
validateUserRef(ref);
|
|
5580
6088
|
const codec = requireCodec(deps.codec, "loadSegment");
|
|
5581
6089
|
const keep = options.keep ?? 1;
|
|
@@ -5601,7 +6109,7 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
5601
6109
|
throw new ValidationError(message);
|
|
5602
6110
|
});
|
|
5603
6111
|
const audit = safeAudit(options.audit ?? NOOP_AUDIT);
|
|
5604
|
-
const ids =
|
|
6112
|
+
const ids = prepare(codec);
|
|
5605
6113
|
assertRegistryCanWrite(deps.registry, "load");
|
|
5606
6114
|
const row = await deps.registry.get(ref);
|
|
5607
6115
|
const fromToken = row?.token;
|
|
@@ -5658,7 +6166,7 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
5658
6166
|
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
5659
6167
|
const reclaim = async () => {
|
|
5660
6168
|
const now = await deps.registry.get(ref);
|
|
5661
|
-
if (now !== null && (now.token === fromToken || now.status === "destroyed")) {
|
|
6169
|
+
if (now !== null && (now.token === fromToken || now.status === "destroyed" || row !== null && onlyLeasesDiffer(row, now))) {
|
|
5662
6170
|
await deps.storage.delete(key);
|
|
5663
6171
|
return;
|
|
5664
6172
|
}
|
|
@@ -5700,14 +6208,17 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
5700
6208
|
if (guard?.minRetained !== void 0 && before !== null) {
|
|
5701
6209
|
if (written.cardinality < before * guard.minRetained) return refuse("min-retained");
|
|
5702
6210
|
}
|
|
6211
|
+
const leasesNow = deps.clock === void 0 ? void 0 : () => deps.clock.now();
|
|
5703
6212
|
let published;
|
|
5704
6213
|
try {
|
|
5705
|
-
published = await
|
|
6214
|
+
published = await publishGenerationKept(deps.registry, key, {
|
|
6215
|
+
keep,
|
|
6216
|
+
leasesNow,
|
|
5706
6217
|
row,
|
|
5707
6218
|
wrappedDeks: written.wrappedDeks,
|
|
5708
6219
|
summary: written.summary,
|
|
5709
6220
|
cleartext: !written.encrypted,
|
|
5710
|
-
...fromToken === void 0 ? {} : { expectToken: fromToken },
|
|
6221
|
+
...fromToken === void 0 ? {} : { expectToken: fromToken, expectRow: row },
|
|
5711
6222
|
...needsBefore && fromGeneration !== void 0 ? { expectFrom: fromGeneration } : {},
|
|
5712
6223
|
// The third case, and the one the two fences above structurally cannot cover: the guard judged a segment
|
|
5713
6224
|
// that had NO ROW. Both `expectFrom` and `expectToken` compare against a value read from a row, so with no
|
|
@@ -5731,7 +6242,7 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
5731
6242
|
if (isValidationError(err) || err instanceof KeyUnavailableError) await reclaim();
|
|
5732
6243
|
throw err;
|
|
5733
6244
|
}
|
|
5734
|
-
if (!published) return refuse("superseded");
|
|
6245
|
+
if (!published.published) return refuse("superseded");
|
|
5735
6246
|
audit.onEvent({
|
|
5736
6247
|
kind: "segment.publish",
|
|
5737
6248
|
segment: ref.segment,
|
|
@@ -5742,7 +6253,10 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
5742
6253
|
const collected = await collectAfterLoad(ref, deps, {
|
|
5743
6254
|
generation,
|
|
5744
6255
|
keep,
|
|
5745
|
-
byName: checked &&
|
|
6256
|
+
byName: checked && deps.collectByListing !== true,
|
|
6257
|
+
currentGone: currentObjectGone,
|
|
6258
|
+
...leasesNow === void 0 ? {} : { leases: { now: leasesNow } },
|
|
6259
|
+
kept: published.kept,
|
|
5746
6260
|
...current.fromSummary && fromGeneration !== void 0 ? { proveCurrent: { ...ref, generation: fromGeneration } } : {}
|
|
5747
6261
|
});
|
|
5748
6262
|
return {
|
|
@@ -5841,10 +6355,25 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
|
|
|
5841
6355
|
{ cardinality: reader.count(), metadata: reader.metadata },
|
|
5842
6356
|
aead
|
|
5843
6357
|
);
|
|
5844
|
-
const
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
6358
|
+
const churn = leaseChurn(deps);
|
|
6359
|
+
let against = record.token;
|
|
6360
|
+
let token;
|
|
6361
|
+
for (; ; ) {
|
|
6362
|
+
try {
|
|
6363
|
+
({ token } = await deps.registry.compareAndSwap(ref, against, {
|
|
6364
|
+
currentGen: toGeneration,
|
|
6365
|
+
summary
|
|
6366
|
+
}));
|
|
6367
|
+
break;
|
|
6368
|
+
} catch (err) {
|
|
6369
|
+
if (!isWriteConflictError(err)) throw err;
|
|
6370
|
+
const seen = await deps.registry.get(ref);
|
|
6371
|
+
const settled2 = await churn.settle(record, seen, () => deps.registry.get(ref));
|
|
6372
|
+
const now = settled2?.row ?? null;
|
|
6373
|
+
if (settled2 === void 0 || now === null || !onlyLeasesDiffer(record, now)) throw err;
|
|
6374
|
+
against = now.token;
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
5848
6377
|
let stillThere = false;
|
|
5849
6378
|
for await (const key of deps.storage.list(ref)) {
|
|
5850
6379
|
if (key.generation === toGeneration) {
|
|
@@ -5867,13 +6396,31 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
|
|
|
5867
6396
|
}
|
|
5868
6397
|
if (!stillThere || replaced) {
|
|
5869
6398
|
let undone = false;
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
6399
|
+
let undoAgainst = token;
|
|
6400
|
+
for (; ; ) {
|
|
6401
|
+
try {
|
|
6402
|
+
await deps.registry.compareAndSwap(ref, undoAgainst, {
|
|
6403
|
+
currentGen: record.currentGen,
|
|
6404
|
+
summary: describingSummary(record)
|
|
6405
|
+
});
|
|
6406
|
+
undone = true;
|
|
6407
|
+
break;
|
|
6408
|
+
} catch (err) {
|
|
6409
|
+
if (!isWriteConflictError(err)) break;
|
|
6410
|
+
const moved = ["currentGen", "summary", "keptGens"];
|
|
6411
|
+
let now;
|
|
6412
|
+
try {
|
|
6413
|
+
const seen = await deps.registry.get(ref);
|
|
6414
|
+
if (seen === null || seen.currentGen !== toGeneration) break;
|
|
6415
|
+
const settled2 = await churn.settle(record, seen, () => deps.registry.get(ref), moved);
|
|
6416
|
+
now = settled2?.row ?? null;
|
|
6417
|
+
} catch {
|
|
6418
|
+
break;
|
|
6419
|
+
}
|
|
6420
|
+
if (now === null || now.currentGen !== toGeneration || !onlyLeasesDiffer(record, now, moved))
|
|
6421
|
+
break;
|
|
6422
|
+
undoAgainst = now.token;
|
|
6423
|
+
}
|
|
5877
6424
|
}
|
|
5878
6425
|
throw new NotFoundError(
|
|
5879
6426
|
`rollback: generation ${toGeneration} of "${ref.segment}" was ${replaced ? "replaced" : "collected"} while the pointer was moving` + (undone ? " \u2014 the pointer was put back" : `, and the move back failed, so the pointer may still name ${toGeneration} (a write that landed and lost its response reads as a failure here). Check which generation is current, then re-run a rollback to a generation that exists, or restore the object.`)
|
|
@@ -5937,7 +6484,7 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
5937
6484
|
if (row === null) return "absent";
|
|
5938
6485
|
if (row.status === "destroyed") return "destroyed";
|
|
5939
6486
|
if (row.currentGen === null) return "no-generation";
|
|
5940
|
-
return row.currentGen === from && row.token === fromToken ? null : "superseded";
|
|
6487
|
+
return row.currentGen === from && (row.token === fromToken || onlyLeasesDiffer(record, row)) ? null : "superseded";
|
|
5941
6488
|
};
|
|
5942
6489
|
const holds = async (generation2) => {
|
|
5943
6490
|
const key2 = { ...base, generation: generation2 };
|
|
@@ -6108,7 +6655,13 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
6108
6655
|
const published = await publishGeneration(deps.registry, key, {
|
|
6109
6656
|
expectFrom: from,
|
|
6110
6657
|
expectToken: fromToken,
|
|
6658
|
+
// A lease written while the rewrite streams moves the token and nothing it derived from: it does not refuse it.
|
|
6659
|
+
expectRow: record,
|
|
6111
6660
|
summary,
|
|
6661
|
+
// The row then records that no generation below the new pointer is kept, as the collection below leaves it.
|
|
6662
|
+
keep: 0,
|
|
6663
|
+
// Every generation below the new pointer is deleted whatever a lease says, so the row drops its leases.
|
|
6664
|
+
clearLeases: true,
|
|
6112
6665
|
// A write that ends without an answer is settled by reading the row, and a pointer at this number is this
|
|
6113
6666
|
// rewrite's only over the object it wrote: the footer proves it, so another incarnation's cannot pass for it.
|
|
6114
6667
|
holdsOwnObject: () => provesOwnObject(deps.storage, key, fingerprint),
|
|
@@ -6441,8 +6994,17 @@ async function listGenerations2(storage, ref) {
|
|
|
6441
6994
|
}
|
|
6442
6995
|
async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
6443
6996
|
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
6997
|
+
const churn = leaseChurn(deps);
|
|
6998
|
+
let lost;
|
|
6444
6999
|
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
6445
|
-
|
|
7000
|
+
let record = await deps.registry.get(ref);
|
|
7001
|
+
if (lost !== void 0) {
|
|
7002
|
+
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
7003
|
+
if (settled2 !== void 0) {
|
|
7004
|
+
record = settled2.row;
|
|
7005
|
+
attempt -= 1;
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
6446
7008
|
if (record === null) {
|
|
6447
7009
|
return { ...base, destroyed: false, cryptoShredded: false, reason: "absent" };
|
|
6448
7010
|
}
|
|
@@ -6461,11 +7023,14 @@ async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
|
6461
7023
|
keyId: void 0,
|
|
6462
7024
|
// The current generation's cached count and metadata go with it: sealed, they cannot be opened without
|
|
6463
7025
|
// the wrappings; clear, they would outlive the segment on its tombstone.
|
|
6464
|
-
summary: void 0
|
|
7026
|
+
summary: void 0,
|
|
7027
|
+
// A tombstone holds nothing: a lease names a generation of a segment that now resolves none.
|
|
7028
|
+
leases: void 0
|
|
6465
7029
|
});
|
|
6466
7030
|
return { ...base, destroyed: true, cryptoShredded: encrypted };
|
|
6467
7031
|
} catch (err) {
|
|
6468
7032
|
if (!isWriteConflictError(err)) throw err;
|
|
7033
|
+
lost = record;
|
|
6469
7034
|
}
|
|
6470
7035
|
}
|
|
6471
7036
|
throw new WriteConflictError(`${op}: contention shredding "${ref.segment}" \u2014 retry`);
|
|
@@ -6533,8 +7098,17 @@ async function setSegmentRetention(ref, deps, policy) {
|
|
|
6533
7098
|
validateRetentionPolicy(policy);
|
|
6534
7099
|
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
6535
7100
|
let previousExpiresAt;
|
|
7101
|
+
const churn = leaseChurn(deps);
|
|
7102
|
+
let lost;
|
|
6536
7103
|
for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
|
|
6537
|
-
|
|
7104
|
+
let record = await deps.registry.get(ref);
|
|
7105
|
+
if (lost !== void 0) {
|
|
7106
|
+
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
7107
|
+
if (settled2 !== void 0) {
|
|
7108
|
+
record = settled2.row;
|
|
7109
|
+
attempt -= 1;
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
6538
7112
|
try {
|
|
6539
7113
|
previousExpiresAt = readExpiresAt(record);
|
|
6540
7114
|
if (record === null) {
|
|
@@ -6561,6 +7135,7 @@ async function setSegmentRetention(ref, deps, policy) {
|
|
|
6561
7135
|
};
|
|
6562
7136
|
} catch (err) {
|
|
6563
7137
|
if (!isWriteConflictError(err)) throw err;
|
|
7138
|
+
lost = record ?? void 0;
|
|
6564
7139
|
}
|
|
6565
7140
|
}
|
|
6566
7141
|
throw new WriteConflictError(
|
|
@@ -6569,8 +7144,17 @@ async function setSegmentRetention(ref, deps, policy) {
|
|
|
6569
7144
|
}
|
|
6570
7145
|
async function clearSegmentRetention(ref, deps) {
|
|
6571
7146
|
validateUserRef(ref);
|
|
7147
|
+
const churn = leaseChurn(deps);
|
|
7148
|
+
let lost;
|
|
6572
7149
|
for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
|
|
6573
|
-
|
|
7150
|
+
let record = await deps.registry.get(ref);
|
|
7151
|
+
if (lost !== void 0) {
|
|
7152
|
+
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
7153
|
+
if (settled2 !== void 0) {
|
|
7154
|
+
record = settled2.row;
|
|
7155
|
+
attempt -= 1;
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
6574
7158
|
if (record === null) return false;
|
|
6575
7159
|
if (record.status === "destroyed") return false;
|
|
6576
7160
|
if (record.retention === void 0 || !(EXPIRES_AT in record.retention)) return false;
|
|
@@ -6584,6 +7168,7 @@ async function clearSegmentRetention(ref, deps) {
|
|
|
6584
7168
|
return true;
|
|
6585
7169
|
} catch (err) {
|
|
6586
7170
|
if (!isWriteConflictError(err)) throw err;
|
|
7171
|
+
lost = record;
|
|
6587
7172
|
}
|
|
6588
7173
|
}
|
|
6589
7174
|
throw new WriteConflictError(
|
|
@@ -6955,6 +7540,31 @@ function failureReason(err) {
|
|
|
6955
7540
|
return `failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
6956
7541
|
}
|
|
6957
7542
|
|
|
7543
|
+
// src/core/registry-reap.ts
|
|
7544
|
+
var DEFAULT_REAP_LIMIT = 1e3;
|
|
7545
|
+
async function reapRegistryTombstones(registry, options = {}) {
|
|
7546
|
+
const dryRun = options.dryRun === true;
|
|
7547
|
+
if (!dryRun && options.confirmNoLegacyWriters !== true) {
|
|
7548
|
+
throw new ValidationError(
|
|
7549
|
+
"reapRegistryTombstones: a real run needs `confirmNoLegacyWriters: true`, your statement that no process on a release before 0.12 writes this registry (one that did would issue a removed row\u2019s tokens again). Pass `dryRun: true` to see what would be removed."
|
|
7550
|
+
);
|
|
7551
|
+
}
|
|
7552
|
+
const limit = options.limit ?? DEFAULT_REAP_LIMIT;
|
|
7553
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
7554
|
+
throw new ValidationError(
|
|
7555
|
+
`reapRegistryTombstones: \`limit\` must be a positive integer; got ${String(limit)}`
|
|
7556
|
+
);
|
|
7557
|
+
}
|
|
7558
|
+
if (options.namespace !== void 0) validateUserNamespace(options.namespace);
|
|
7559
|
+
if (registry.reapLegacyTombstones === void 0) {
|
|
7560
|
+
throw new UnsupportedError(
|
|
7561
|
+
"reapRegistryTombstones: this registry keeps no deleted rows of a release before 0.12 to remove (only the object-store registries, S3, GCS and Azure Blob, do)"
|
|
7562
|
+
);
|
|
7563
|
+
}
|
|
7564
|
+
const request = options.namespace === void 0 ? { dryRun, limit } : { namespace: options.namespace, dryRun, limit };
|
|
7565
|
+
return registry.reapLegacyTombstones(request);
|
|
7566
|
+
}
|
|
7567
|
+
|
|
6958
7568
|
// src/core/consistency.ts
|
|
6959
7569
|
var DEFAULT_CHECK_CONCURRENCY = 8;
|
|
6960
7570
|
async function generationsPresent(storage, ref) {
|
|
@@ -7158,7 +7768,12 @@ export {
|
|
|
7158
7768
|
InProcessKeystore,
|
|
7159
7769
|
IntegrityError,
|
|
7160
7770
|
KeyUnavailableError,
|
|
7771
|
+
LEASE_SKEW_MS,
|
|
7772
|
+
LeaseExpiredError,
|
|
7773
|
+
LeaseLimitError,
|
|
7161
7774
|
LocalFsStorage,
|
|
7775
|
+
MAX_LEASES_PER_SEGMENT,
|
|
7776
|
+
MAX_LEASE_MS,
|
|
7162
7777
|
MIN_EXPIRES_AT_MS,
|
|
7163
7778
|
MemoryStorage,
|
|
7164
7779
|
NOOP_METRICS,
|
|
@@ -7177,6 +7792,7 @@ export {
|
|
|
7177
7792
|
checkBudget,
|
|
7178
7793
|
clearSegmentRetention,
|
|
7179
7794
|
collectWithinBudget,
|
|
7795
|
+
decodeSerialized,
|
|
7180
7796
|
destroySegment,
|
|
7181
7797
|
dropSegment,
|
|
7182
7798
|
eraseIdFromSegment,
|
|
@@ -7187,6 +7803,8 @@ export {
|
|
|
7187
7803
|
groundedReport,
|
|
7188
7804
|
isCloudRoaringError,
|
|
7189
7805
|
isIntegrityError,
|
|
7806
|
+
isLeaseExpiredError,
|
|
7807
|
+
isLeaseLimitError,
|
|
7190
7808
|
isNotFoundError,
|
|
7191
7809
|
isStorageBackend,
|
|
7192
7810
|
isTransientError,
|
|
@@ -7195,8 +7813,11 @@ export {
|
|
|
7195
7813
|
listGenerations,
|
|
7196
7814
|
listSegments,
|
|
7197
7815
|
loadSegment,
|
|
7816
|
+
loadSegmentChunks,
|
|
7198
7817
|
mapWithConcurrency,
|
|
7199
7818
|
readRetentionPolicy,
|
|
7819
|
+
reapRegistryTombstones,
|
|
7820
|
+
releaseLease,
|
|
7200
7821
|
resolveBudget,
|
|
7201
7822
|
resolvePerOpBudget,
|
|
7202
7823
|
retireExpired,
|
|
@@ -7208,6 +7829,7 @@ export {
|
|
|
7208
7829
|
segmentKey,
|
|
7209
7830
|
setSegmentRetention,
|
|
7210
7831
|
splitId,
|
|
7832
|
+
takeLease,
|
|
7211
7833
|
withRetry
|
|
7212
7834
|
};
|
|
7213
7835
|
//# sourceMappingURL=index.js.map
|