@cloudbitmaps/core 0.14.0 → 0.16.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-5W3XS2IU.js → chunk-LFUNPMXZ.js} +8 -1
- package/dist/chunk-LFUNPMXZ.js.map +7 -0
- package/dist/core/chunk-stream.d.ts +22 -0
- package/dist/core/chunk-window.d.ts +8 -2
- package/dist/core/codec.d.ts +10 -0
- package/dist/core/cost.d.ts +13 -8
- package/dist/core/crbm/plan-reads.d.ts +44 -0
- package/dist/core/crbm/reader.d.ts +84 -3
- package/dist/core/crbm-storage-source.d.ts +51 -7
- package/dist/core/engine.d.ts +106 -14
- package/dist/core/item-pull.d.ts +29 -0
- package/dist/core/load-input.d.ts +9 -0
- package/dist/core/load.d.ts +20 -1
- package/dist/core/metrics.d.ts +13 -6
- package/dist/core/pinned-storage-source.d.ts +7 -1
- package/dist/core/ports.d.ts +98 -4
- package/dist/core/validate.d.ts +5 -0
- package/dist/driver-kit.js +44 -11
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +16 -3
- package/dist/drivers/localfs/registry.d.ts +4 -0
- package/dist/drivers/memory.d.ts +4 -0
- package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1040 -156
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-5W3XS2IU.js.map +0 -7
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
recordFromNew,
|
|
45
45
|
sameIncarnation,
|
|
46
46
|
serializeRegistryEnvelope,
|
|
47
|
+
validateChunkKeyOrder,
|
|
47
48
|
validateChunkRef,
|
|
48
49
|
validateNewRegistryRecord,
|
|
49
50
|
validateRegistryPatch,
|
|
@@ -51,7 +52,7 @@ import {
|
|
|
51
52
|
validateUserNamespace,
|
|
52
53
|
validateUserRef,
|
|
53
54
|
webCryptoEntropy
|
|
54
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-LFUNPMXZ.js";
|
|
55
56
|
|
|
56
57
|
// src/core/bit-route.ts
|
|
57
58
|
var U32_MAX = 4294967295;
|
|
@@ -102,24 +103,64 @@ async function collectWithinBudget(source, budget, op) {
|
|
|
102
103
|
return out;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
// src/core/chunk-stream.ts
|
|
107
|
+
var ChunkStream = class {
|
|
108
|
+
iterator;
|
|
109
|
+
failure;
|
|
110
|
+
constructor(stream) {
|
|
111
|
+
this.iterator = stream[Symbol.asyncIterator]();
|
|
112
|
+
}
|
|
113
|
+
/** The chunk for `key`, which must be the next key the stream was opened over. */
|
|
114
|
+
take(key) {
|
|
115
|
+
if (this.failure !== void 0) return Promise.reject(this.failure.error);
|
|
116
|
+
return this.iterator.next().then(
|
|
117
|
+
(step) => {
|
|
118
|
+
if (step.done === true) {
|
|
119
|
+
throw this.failure?.error ?? new IntegrityError(`chunk stream ended before chunk ${key}`);
|
|
120
|
+
}
|
|
121
|
+
if (step.value.key !== key) {
|
|
122
|
+
throw new IntegrityError(
|
|
123
|
+
`chunk stream answered chunk ${step.value.key} where ${key} was due`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return step.value;
|
|
127
|
+
},
|
|
128
|
+
(error) => {
|
|
129
|
+
this.failure ??= { error };
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
/** Stop the stream: no further range is started, and ranges already in flight finish and are dropped. */
|
|
135
|
+
close() {
|
|
136
|
+
this.iterator.return?.(void 0).catch(() => void 0);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
105
140
|
// src/core/chunk-window.ts
|
|
106
141
|
var ChunkWindow = class {
|
|
107
|
-
constructor(keys, fetch, max, ramp) {
|
|
142
|
+
constructor(keys, fetch, max, ramp, rampStart = 1) {
|
|
108
143
|
this.keys = keys;
|
|
109
144
|
this.fetch = fetch;
|
|
110
145
|
this.max = max;
|
|
111
146
|
this.ramp = ramp;
|
|
147
|
+
this.rampStart = rampStart;
|
|
112
148
|
}
|
|
113
149
|
keys;
|
|
114
150
|
fetch;
|
|
115
151
|
max;
|
|
116
152
|
ramp;
|
|
153
|
+
rampStart;
|
|
117
154
|
open = [];
|
|
118
155
|
launched = 0;
|
|
119
156
|
taken = 0;
|
|
157
|
+
/** Resolves once every read launched and not yet taken has settled; it never rejects, as the reads never do. */
|
|
158
|
+
async settle() {
|
|
159
|
+
await Promise.all(this.open);
|
|
160
|
+
}
|
|
120
161
|
/** The next read's result in key order, or the error that read raised. */
|
|
121
162
|
async take() {
|
|
122
|
-
const width = this.ramp ? Math.min(this.max, 2 ** Math.min(this.taken, 30)) : this.max;
|
|
163
|
+
const width = this.ramp ? Math.min(this.max, Math.max(this.rampStart, 2 ** Math.min(this.taken, 30))) : this.max;
|
|
123
164
|
while (this.launched < this.keys.length && this.launched - this.taken < width) {
|
|
124
165
|
this.open.push(
|
|
125
166
|
this.fetch(this.keys[this.launched++]).then(
|
|
@@ -320,6 +361,7 @@ var CountingMetricsSink = class _CountingMetricsSink {
|
|
|
320
361
|
// src/core/engine.ts
|
|
321
362
|
var DEFAULT_INTERSECT_CONCURRENCY = 32;
|
|
322
363
|
var COMBINE_WINDOW_START = 8;
|
|
364
|
+
var COMBINE_RANGE_START = 4;
|
|
323
365
|
var ZERO_CLOCK = { now: () => 0 };
|
|
324
366
|
function windowOf(range) {
|
|
325
367
|
const after = range?.after;
|
|
@@ -447,6 +489,13 @@ var SegmentEngine = class {
|
|
|
447
489
|
* promise per distinct key in flight.
|
|
448
490
|
*/
|
|
449
491
|
openReads = /* @__PURE__ */ new Map();
|
|
492
|
+
/**
|
|
493
|
+
* The chunk streams open right now. An {@link invalidate} of a segment marks its streams, which then cache no chunk
|
|
494
|
+
* they deliver, as a read whose entry an invalidation dropped does not (streams hold no `openReads` entry).
|
|
495
|
+
*/
|
|
496
|
+
openStreams = /* @__PURE__ */ new Set();
|
|
497
|
+
/** How many times {@link invalidate} has been called: a read compares it with the count it began under. */
|
|
498
|
+
invalidations = 0;
|
|
450
499
|
constructor(deps) {
|
|
451
500
|
this.storage = deps.storage;
|
|
452
501
|
this.cache = deps.cache;
|
|
@@ -527,9 +576,34 @@ var SegmentEngine = class {
|
|
|
527
576
|
);
|
|
528
577
|
}
|
|
529
578
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
579
|
+
* The chunks of `seg` at `chunkKeys`, taken in ascending key order by a read that may stop at any point: one stream of
|
|
580
|
+
* coalesced ranges when the source has `getChunks`, opening 1, 2, 4 … ranges wide up to {@link DEFAULT_INTERSECT_CONCURRENCY},
|
|
581
|
+
* else the chunk-by-chunk window of that many reads. `close` stops what is left of it.
|
|
582
|
+
*/
|
|
583
|
+
chunkSequence(seg, chunkKeys, gen, epoch) {
|
|
584
|
+
const streamed = this.openStreamed(
|
|
585
|
+
seg,
|
|
586
|
+
chunkKeys,
|
|
587
|
+
gen,
|
|
588
|
+
epoch,
|
|
589
|
+
DEFAULT_INTERSECT_CONCURRENCY,
|
|
590
|
+
1
|
|
591
|
+
);
|
|
592
|
+
if (streamed !== void 0) {
|
|
593
|
+
return {
|
|
594
|
+
take: (chunkKey) => this.streamedChunk(streamed, chunkKey),
|
|
595
|
+
close: () => this.closeStreamed(streamed)
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const window = this.chunkWindow(seg, chunkKeys, gen, true);
|
|
599
|
+
return { take: () => window.take(), close: () => void 0 };
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Every id, ascending, reading the chunks as one stream of coalesced ranges (chunks that sit near each other are one
|
|
603
|
+
* request) that opens 1, 2, 4 … 32 ranges wide, so a read that stops early has asked for only a few ranges past the
|
|
604
|
+
* one it stopped in (each at most 1 MiB); with `range`, only the ids in `(after, through]`, fetching only the
|
|
605
|
+
* chunks the range overlaps (see {@link IdRange}). A source that reads chunk by chunk is read through a window of up to
|
|
606
|
+
* 32 chunk fetches that opens 1, 2, 4 … 32 wide.
|
|
533
607
|
*
|
|
534
608
|
* Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
|
|
535
609
|
* every id of a read that asked for no range paid for it (measured at about 5% per id). The full read keeps the
|
|
@@ -539,31 +613,41 @@ var SegmentEngine = class {
|
|
|
539
613
|
return range === void 0 ? this.iterateAll(seg) : this.iterateRange(seg, range);
|
|
540
614
|
}
|
|
541
615
|
async *iterateAll(seg) {
|
|
616
|
+
const epoch = this.invalidations;
|
|
542
617
|
const chunkKeys = await this.chunkKeys(seg);
|
|
543
618
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
544
619
|
const gen = await this.cacheVersion(seg);
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
620
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
|
|
621
|
+
try {
|
|
622
|
+
for (const chunkKey of chunkKeys) {
|
|
623
|
+
const chunk = await chunks.take(chunkKey);
|
|
624
|
+
if (chunk === null) continue;
|
|
625
|
+
for (const remainder of chunk) yield joinId(chunkKey, remainder);
|
|
626
|
+
}
|
|
627
|
+
} finally {
|
|
628
|
+
chunks.close();
|
|
550
629
|
}
|
|
551
630
|
}
|
|
552
631
|
async *iterateRange(seg, range) {
|
|
632
|
+
const epoch = this.invalidations;
|
|
553
633
|
const w = windowOf(range) ?? WHOLE_ID_SPACE;
|
|
554
634
|
if (w === "empty") return;
|
|
555
635
|
const chunkKeys = keysWithin(await this.chunkKeys(seg), w);
|
|
556
636
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
557
637
|
const gen = await this.cacheVersion(seg);
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
638
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
|
|
639
|
+
try {
|
|
640
|
+
for (const chunkKey of chunkKeys) {
|
|
641
|
+
const chunk = await chunks.take(chunkKey);
|
|
642
|
+
if (chunk === null) continue;
|
|
643
|
+
if (isEdge(chunkKey, w)) {
|
|
644
|
+
for (const id of edgeIds(chunk, chunkKey, w)) yield id;
|
|
645
|
+
} else {
|
|
646
|
+
for (const remainder of chunk) yield joinId(chunkKey, remainder);
|
|
647
|
+
}
|
|
566
648
|
}
|
|
649
|
+
} finally {
|
|
650
|
+
chunks.close();
|
|
567
651
|
}
|
|
568
652
|
}
|
|
569
653
|
/**
|
|
@@ -573,17 +657,22 @@ var SegmentEngine = class {
|
|
|
573
657
|
* the caller's to keep.
|
|
574
658
|
*/
|
|
575
659
|
async *iterateBatches(seg, range) {
|
|
660
|
+
const epoch = this.invalidations;
|
|
576
661
|
const w = windowOf(range);
|
|
577
662
|
if (w === "empty") return;
|
|
578
663
|
const chunkKeys = w === null ? await this.chunkKeys(seg) : keysWithin(await this.chunkKeys(seg), w);
|
|
579
664
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
580
665
|
const gen = await this.cacheVersion(seg);
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
666
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
|
|
667
|
+
try {
|
|
668
|
+
for (const chunkKey of chunkKeys) {
|
|
669
|
+
const chunk = await chunks.take(chunkKey);
|
|
670
|
+
if (chunk === null) continue;
|
|
671
|
+
const ids = chunkIds(chunk, chunkKey, w);
|
|
672
|
+
if (ids.length > 0) yield ids;
|
|
673
|
+
}
|
|
674
|
+
} finally {
|
|
675
|
+
chunks.close();
|
|
587
676
|
}
|
|
588
677
|
}
|
|
589
678
|
/**
|
|
@@ -600,11 +689,11 @@ var SegmentEngine = class {
|
|
|
600
689
|
*
|
|
601
690
|
* Chunk-skipping intersection. Aligns each segment's chunk-key set (read from the `.crbm` index, no payload),
|
|
602
691
|
* keeps only keys present in **all** operands (a key missing from any operand can't contribute → its Storage
|
|
603
|
-
* chunks are never fetched — the core saving), then
|
|
604
|
-
*
|
|
692
|
+
* chunks are never fetched — the core saving), then reads each operand's surviving chunks as one stream of coalesced
|
|
693
|
+
* ranges and hands them, key by key, to the codec for the AND, streaming results through a bounded window.
|
|
605
694
|
*
|
|
606
|
-
* **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk
|
|
607
|
-
* segment size — that's the Lambda-friendly property.
|
|
695
|
+
* **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk`, a range being
|
|
696
|
+
* at most a chunk's cap of 1 MiB), not by segment size — that's the Lambda-friendly property.
|
|
608
697
|
*
|
|
609
698
|
* Generation-consistent within the call (normal case): each operand's current generation is resolved **once**
|
|
610
699
|
* up front (before the fan-out) and threaded into every chunk read, so a concurrent load can't corrupt or tear
|
|
@@ -615,7 +704,9 @@ var SegmentEngine = class {
|
|
|
615
704
|
* waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
|
|
616
705
|
* (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
|
|
617
706
|
* heals the read forward; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make
|
|
618
|
-
* and `invalidate()` makes on request. Still whole/immutable per read, never torn.
|
|
707
|
+
* and `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
|
|
708
|
+
* segment again before it serves each chunk, so each of those moves a running read as it would a read of one chunk, and
|
|
709
|
+
* the ranges it had requested of the earlier generation are dropped, not served.
|
|
619
710
|
*/
|
|
620
711
|
intersect(segs, options) {
|
|
621
712
|
return this.combine(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
@@ -652,6 +743,26 @@ var SegmentEngine = class {
|
|
|
652
743
|
andNotBatches(seg, excludes, options) {
|
|
653
744
|
return this.combineBatches([seg], excludes, "all", "andNot", options);
|
|
654
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* {@link intersect}, as the chunks the result is made of: `{ chunkKey, bitmap }` in ascending key order, each a
|
|
748
|
+
* non-empty bitmap of the chunk's 16-bit remainders, cut to the read's range. What a materialisation writes
|
|
749
|
+
* straight into its new generation, with no id built on the way.
|
|
750
|
+
*
|
|
751
|
+
* **Each bitmap belongs to the caller, who may change it:** it is the combine's own accumulator or a bitmap built
|
|
752
|
+
* for the range's edge, never a chunk the cache holds or another operand's. The reads, the budget, the metric and the
|
|
753
|
+
* errors are the id reads' own.
|
|
754
|
+
*/
|
|
755
|
+
intersectChunks(segs, options) {
|
|
756
|
+
return this.combineChunks(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
757
|
+
}
|
|
758
|
+
/** {@link union}, as chunks: see {@link intersectChunks}. */
|
|
759
|
+
unionChunks(segs, options) {
|
|
760
|
+
return this.combineChunks(segs, options?.exclude ?? [], "any", "union", options);
|
|
761
|
+
}
|
|
762
|
+
/** {@link andNot}, as chunks: see {@link intersectChunks}. */
|
|
763
|
+
andNotChunks(seg, excludes, options) {
|
|
764
|
+
return this.combineChunks([seg], excludes, "all", "andNot", options);
|
|
765
|
+
}
|
|
655
766
|
/**
|
|
656
767
|
* Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
|
|
657
768
|
* absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
|
|
@@ -674,10 +785,11 @@ var SegmentEngine = class {
|
|
|
674
785
|
const w = windowOf(options);
|
|
675
786
|
if (w === "empty") return null;
|
|
676
787
|
const extract = async (seg) => {
|
|
788
|
+
const epoch = this.invalidations;
|
|
677
789
|
const all = await this.chunkKeys(seg);
|
|
678
790
|
const gen = await this.cacheVersion(seg);
|
|
679
791
|
const keys = w === null ? all : keysWithin(all, w);
|
|
680
|
-
return { seg, keys: new Set(keys), chunkless: all.length === 0, gen };
|
|
792
|
+
return { seg, keys: new Set(keys), chunkless: all.length === 0, gen, epoch };
|
|
681
793
|
};
|
|
682
794
|
const [operands, excludes] = await Promise.all([
|
|
683
795
|
Promise.all(segs.map(extract)),
|
|
@@ -717,40 +829,86 @@ var SegmentEngine = class {
|
|
|
717
829
|
skippedChunks: distinctKeys.size - common.length
|
|
718
830
|
});
|
|
719
831
|
}
|
|
832
|
+
const streams = [];
|
|
833
|
+
const streamExcludes = mode === "any" || operands.length === 1;
|
|
834
|
+
const open4 = (o, wanted) => {
|
|
835
|
+
o.streamed = this.openStreamed(
|
|
836
|
+
o.seg,
|
|
837
|
+
wanted === void 0 ? common : common.filter(wanted),
|
|
838
|
+
o.gen,
|
|
839
|
+
o.epoch,
|
|
840
|
+
limit,
|
|
841
|
+
COMBINE_RANGE_START
|
|
842
|
+
);
|
|
843
|
+
if (o.streamed) streams.push(o.streamed);
|
|
844
|
+
};
|
|
845
|
+
for (const o of operands) open4(o, mode === "all" ? void 0 : (k) => o.keys.has(k));
|
|
846
|
+
if (streamExcludes) for (const e of excludes) open4(e, (k) => e.keys.has(k));
|
|
720
847
|
return {
|
|
721
848
|
w,
|
|
722
849
|
window: new CombineWindow(
|
|
723
850
|
common,
|
|
724
851
|
limit,
|
|
725
852
|
(key) => this.combineChunk(operands, excludes, mode, key)
|
|
726
|
-
)
|
|
853
|
+
),
|
|
854
|
+
streams
|
|
727
855
|
};
|
|
728
856
|
}
|
|
729
857
|
async *combine(segs, excludeSegs, mode, op, options) {
|
|
730
858
|
const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
|
|
731
859
|
if (plan === null) return;
|
|
732
860
|
const { w, window } = plan;
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
861
|
+
try {
|
|
862
|
+
for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
|
|
863
|
+
if (slot.result && !slot.result.isEmpty) {
|
|
864
|
+
if (w !== null && isEdge(slot.key, w)) {
|
|
865
|
+
for (const id of edgeIds(slot.result, slot.key, w)) yield id;
|
|
866
|
+
} else {
|
|
867
|
+
for (const remainder of slot.result) yield joinId(slot.key, remainder);
|
|
868
|
+
}
|
|
739
869
|
}
|
|
740
870
|
}
|
|
871
|
+
} finally {
|
|
872
|
+
for (const stream of plan.streams) this.closeStreamed(stream);
|
|
741
873
|
}
|
|
742
874
|
}
|
|
743
875
|
async *combineBatches(segs, excludeSegs, mode, op, options) {
|
|
744
876
|
const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
|
|
745
877
|
if (plan === null) return;
|
|
746
878
|
const { w, window } = plan;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
879
|
+
try {
|
|
880
|
+
for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
|
|
881
|
+
if (slot.result) {
|
|
882
|
+
const ids = chunkIds(slot.result, slot.key, w);
|
|
883
|
+
if (ids.length > 0) yield ids;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
} finally {
|
|
887
|
+
for (const stream of plan.streams) this.closeStreamed(stream);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
|
|
891
|
+
async *combineChunks(segs, excludeSegs, mode, op, options) {
|
|
892
|
+
const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
|
|
893
|
+
if (plan === null) return;
|
|
894
|
+
const { w, window } = plan;
|
|
895
|
+
try {
|
|
896
|
+
for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
|
|
897
|
+
if (!slot.result || slot.result.isEmpty) continue;
|
|
898
|
+
const bitmap = w !== null && isEdge(slot.key, w) ? this.cutToWindow(slot.result, slot.key, w) : slot.result;
|
|
899
|
+
if (!bitmap.isEmpty) yield { chunkKey: slot.key, bitmap };
|
|
751
900
|
}
|
|
901
|
+
} finally {
|
|
902
|
+
for (const stream of plan.streams) this.closeStreamed(stream);
|
|
752
903
|
}
|
|
753
904
|
}
|
|
905
|
+
/** An edge chunk's bitmap cut to the window, as a new bitmap: the remainders `edgeIds` would yield. */
|
|
906
|
+
cutToWindow(chunk, chunkKey, w) {
|
|
907
|
+
const from = chunkKey === w.loKey ? w.loRem : 0;
|
|
908
|
+
const to = chunkKey === w.hiKey ? w.hiRem : MAX_REMAINDER;
|
|
909
|
+
const rem = chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
|
|
910
|
+
return this.codec.fromValues(rem.subarray(firstAbove(rem, from - 1), firstAbove(rem, to)));
|
|
911
|
+
}
|
|
754
912
|
/**
|
|
755
913
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
756
914
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -766,9 +924,7 @@ var SegmentEngine = class {
|
|
|
766
924
|
const relevant = excludes.filter((e) => e.keys.has(chunkKey));
|
|
767
925
|
const together = relevant.length > 0 && (mode === "any" || present.length === 1);
|
|
768
926
|
const reads = together ? [...present, ...relevant] : present;
|
|
769
|
-
const fetched = await Promise.all(
|
|
770
|
-
reads.map((op) => this.storageChunk({ ...op.seg, chunkKey }, op.gen))
|
|
771
|
-
);
|
|
927
|
+
const fetched = await Promise.all(reads.map((op) => this.operandChunk(op, chunkKey)));
|
|
772
928
|
const chunks = together ? fetched.slice(0, present.length) : fetched;
|
|
773
929
|
if (mode === "all" && chunks.some((c) => c === null)) return null;
|
|
774
930
|
const first = chunks[0];
|
|
@@ -794,6 +950,119 @@ var SegmentEngine = class {
|
|
|
794
950
|
}
|
|
795
951
|
return acc;
|
|
796
952
|
}
|
|
953
|
+
/**
|
|
954
|
+
* One operand's chunk at `key`: from its stream when it has one, else a read of that chunk on its own. Not `async`,
|
|
955
|
+
* so a stream's take is made before anything is awaited: concurrent takes of one stream must be made in key order.
|
|
956
|
+
*/
|
|
957
|
+
operandChunk(op, chunkKey) {
|
|
958
|
+
return op.streamed === void 0 ? this.storageChunk({ ...op.seg, chunkKey }, op.gen) : this.streamedChunk(op.streamed, chunkKey);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
|
|
962
|
+
* `getChunks`, a segment with no generation, or nothing to read. Nothing is requested, and no cache lookup made, until
|
|
963
|
+
* the first chunk is asked for (see {@link StreamedChunks}).
|
|
964
|
+
*/
|
|
965
|
+
openStreamed(seg, keys, gen, epoch, concurrency, rampStart) {
|
|
966
|
+
if (this.storage.getChunks === void 0 || gen === null || keys.length === 0) return void 0;
|
|
967
|
+
return {
|
|
968
|
+
seg,
|
|
969
|
+
gen,
|
|
970
|
+
epoch,
|
|
971
|
+
keys,
|
|
972
|
+
concurrency,
|
|
973
|
+
rampStart,
|
|
974
|
+
opened: false,
|
|
975
|
+
inStream: void 0,
|
|
976
|
+
stream: void 0,
|
|
977
|
+
invalidated: false
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Open the stream of `streamed`, at its first uncached chunk `from`: over that chunk and the later keys the cache does
|
|
982
|
+
* 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.
|
|
983
|
+
*/
|
|
984
|
+
startStream(streamed, from) {
|
|
985
|
+
const { seg, gen } = streamed;
|
|
986
|
+
const getChunks = this.storage.getChunks;
|
|
987
|
+
let wanted = streamed.keys.slice(streamed.keys.indexOf(from));
|
|
988
|
+
if (this.cache) {
|
|
989
|
+
const misses = [from];
|
|
990
|
+
for (let i = 1; i < wanted.length; i++) {
|
|
991
|
+
const chunkKey = wanted[i];
|
|
992
|
+
const hit = this.cache.get(this.chunkCacheKey({ ...seg, chunkKey }, gen));
|
|
993
|
+
if (!hit) misses.push(chunkKey);
|
|
994
|
+
if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
|
|
995
|
+
}
|
|
996
|
+
if (misses.length < wanted.length) streamed.inStream = new Set(misses);
|
|
997
|
+
wanted = misses;
|
|
998
|
+
}
|
|
999
|
+
const onRequest = this.metricsOn ? (request) => {
|
|
1000
|
+
this.metrics.onEvent({
|
|
1001
|
+
kind: "storage.get",
|
|
1002
|
+
namespace: seg.namespace,
|
|
1003
|
+
segment: seg.segment,
|
|
1004
|
+
bytes: request.bytes,
|
|
1005
|
+
ms: request.ms
|
|
1006
|
+
});
|
|
1007
|
+
} : void 0;
|
|
1008
|
+
streamed.stream = new ChunkStream(
|
|
1009
|
+
getChunks.call(this.storage, seg, wanted, {
|
|
1010
|
+
concurrency: streamed.concurrency,
|
|
1011
|
+
ramp: streamed.rampStart,
|
|
1012
|
+
...onRequest === void 0 ? {} : { onRequest }
|
|
1013
|
+
})
|
|
1014
|
+
);
|
|
1015
|
+
streamed.opened = true;
|
|
1016
|
+
streamed.invalidated = this.storage.currentVersion === void 0 && this.invalidations !== streamed.epoch;
|
|
1017
|
+
this.openStreams.add(streamed);
|
|
1018
|
+
}
|
|
1019
|
+
closeStreamed(streamed) {
|
|
1020
|
+
streamed.stream?.close();
|
|
1021
|
+
this.openStreams.delete(streamed);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* The decoded chunk at `chunkKey` of a streamed read: from the cache while the stream is unopened and the cache holds
|
|
1025
|
+
* it; else the next chunk of its stream, or, for a chunk the cache held when the stream opened, the cached instance
|
|
1026
|
+
* while it is still there, and a read of that chunk alone once it is not.
|
|
1027
|
+
*/
|
|
1028
|
+
streamedChunk(streamed, chunkKey) {
|
|
1029
|
+
if (!streamed.opened) {
|
|
1030
|
+
const cache = this.cache;
|
|
1031
|
+
if (cache) {
|
|
1032
|
+
const hit = cache.get(this.chunkCacheKey({ ...streamed.seg, chunkKey }, streamed.gen));
|
|
1033
|
+
if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
|
|
1034
|
+
if (hit) return Promise.resolve(hit);
|
|
1035
|
+
}
|
|
1036
|
+
this.startStream(streamed, chunkKey);
|
|
1037
|
+
} else if (streamed.inStream !== void 0 && !streamed.inStream.has(chunkKey)) {
|
|
1038
|
+
const ref = { ...streamed.seg, chunkKey };
|
|
1039
|
+
const hit = this.cache?.get(this.chunkCacheKey(ref, streamed.gen));
|
|
1040
|
+
return hit ? Promise.resolve(hit) : this.storageChunk(ref, streamed.gen);
|
|
1041
|
+
}
|
|
1042
|
+
return streamed.stream.take(chunkKey).then((read) => {
|
|
1043
|
+
if (read.bytes === null) return null;
|
|
1044
|
+
const bitmap = this.decodeChunk(read.bytes, chunkKey);
|
|
1045
|
+
if (this.cache && !streamed.invalidated) {
|
|
1046
|
+
const version = this.streamedVersion(streamed.gen, read.version);
|
|
1047
|
+
if (version !== null) {
|
|
1048
|
+
this.cache.set(this.chunkCacheKey({ ...streamed.seg, chunkKey }, version), bitmap);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return bitmap;
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* The version a streamed chunk is cached under, or `null` for one not to cache: the version its read reports when the
|
|
1056
|
+
* source names versions as its lookups do (`currentVersion`), and otherwise the one the read planned under, since a
|
|
1057
|
+
* lookup by generation number would never find a chunk cached under a stream's version.
|
|
1058
|
+
*/
|
|
1059
|
+
streamedVersion(planned, read) {
|
|
1060
|
+
return planned === void 0 || this.storage.currentVersion === void 0 ? planned : read;
|
|
1061
|
+
}
|
|
1062
|
+
/** The cache key of a chunk: by the version it was read under, or by segment and key alone for a source with none. */
|
|
1063
|
+
chunkCacheKey(ref, version) {
|
|
1064
|
+
return version === void 0 ? chunkRefKey(ref) : chunkGenKey(ref, version);
|
|
1065
|
+
}
|
|
797
1066
|
/** The segment's chunk keys, ascending — a shape read off the index, no payload. Keys are untrusted (invariant 5). */
|
|
798
1067
|
async chunkKeys(seg) {
|
|
799
1068
|
const keys = [...await this.storage.listChunkKeys(seg)];
|
|
@@ -888,6 +1157,10 @@ var SegmentEngine = class {
|
|
|
888
1157
|
*/
|
|
889
1158
|
invalidate(ref) {
|
|
890
1159
|
const prefix = segmentPrefix(ref);
|
|
1160
|
+
this.invalidations += 1;
|
|
1161
|
+
for (const streamed of this.openStreams) {
|
|
1162
|
+
if (segmentPrefix(streamed.seg) === prefix) streamed.invalidated = true;
|
|
1163
|
+
}
|
|
891
1164
|
this.cache?.deleteWhere((key) => key.startsWith(prefix));
|
|
892
1165
|
for (const key of this.openReads.keys()) if (key.startsWith(prefix)) this.openReads.delete(key);
|
|
893
1166
|
this.storage.invalidate?.(ref);
|
|
@@ -929,7 +1202,7 @@ var SegmentEngine = class {
|
|
|
929
1202
|
*/
|
|
930
1203
|
async storageChunk(ref, gen) {
|
|
931
1204
|
if (gen === null) return null;
|
|
932
|
-
const cacheKey =
|
|
1205
|
+
const cacheKey = this.chunkCacheKey(ref, gen);
|
|
933
1206
|
if (this.cache) {
|
|
934
1207
|
const cached = this.cache.get(cacheKey);
|
|
935
1208
|
if (cached) {
|
|
@@ -941,31 +1214,71 @@ var SegmentEngine = class {
|
|
|
941
1214
|
const open4 = this.openReads.get(cacheKey);
|
|
942
1215
|
if (open4) return open4.read;
|
|
943
1216
|
const token = {};
|
|
944
|
-
const entry = { token, read: this.fetchChunk(ref, cacheKey, token) };
|
|
1217
|
+
const entry = { token, read: this.fetchChunk(ref, gen, cacheKey, token) };
|
|
945
1218
|
this.openReads.set(cacheKey, entry);
|
|
946
|
-
const
|
|
1219
|
+
const settled2 = () => {
|
|
947
1220
|
if (this.openReads.get(cacheKey) === entry) this.openReads.delete(cacheKey);
|
|
948
1221
|
};
|
|
949
|
-
entry.read.then(
|
|
1222
|
+
entry.read.then(settled2, settled2);
|
|
950
1223
|
return entry.read;
|
|
951
1224
|
}
|
|
952
|
-
/**
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1225
|
+
/**
|
|
1226
|
+
* A chunk's stored bytes decoded under the size cap (invariant 5) and range-checked: the one place a chunk becomes a
|
|
1227
|
+
* bitmap, whether its bytes came from a request of its own or from a stream of coalesced ranges.
|
|
1228
|
+
*/
|
|
1229
|
+
decodeChunk(bytes, chunkKey) {
|
|
1230
|
+
const bitmap = this.codec.safeDeserialize(bytes, this.maxBitmapBytes);
|
|
1231
|
+
this.assertChunkPayloadInRange(bitmap, chunkKey);
|
|
1232
|
+
return bitmap;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* One chunk, decoded, range-checked and cached. A source with `getChunks` is read through it, as a one-key stream, so
|
|
1236
|
+
* a `storage.get` is reported only for a request that was sent (a chunk the source holds in memory sends none);
|
|
1237
|
+
* the chunk is cached under the version it came from, as a stream's chunk is. A source without it is read by
|
|
1238
|
+
* `getChunk`, one event a call.
|
|
1239
|
+
*/
|
|
1240
|
+
async fetchChunk(ref, gen, cacheKey, token) {
|
|
1241
|
+
const getChunks = this.storage.getChunks;
|
|
1242
|
+
let bytes;
|
|
1243
|
+
let key = cacheKey;
|
|
1244
|
+
if (getChunks === void 0) {
|
|
1245
|
+
const startedAt = this.metricsOn ? this.clock.now() : 0;
|
|
1246
|
+
bytes = await this.storage.getChunk(ref);
|
|
1247
|
+
if (this.metricsOn) {
|
|
1248
|
+
this.metrics.onEvent({
|
|
1249
|
+
kind: "storage.get",
|
|
1250
|
+
namespace: ref.namespace,
|
|
1251
|
+
segment: ref.segment,
|
|
1252
|
+
bytes: bytes ? bytes.length : 0,
|
|
1253
|
+
ms: Math.max(0, this.clock.now() - startedAt)
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
} else {
|
|
1257
|
+
const stream = new ChunkStream(
|
|
1258
|
+
getChunks.call(this.storage, ref, [ref.chunkKey], {
|
|
1259
|
+
...this.metricsOn ? {
|
|
1260
|
+
onRequest: (request) => this.metrics.onEvent({
|
|
1261
|
+
kind: "storage.get",
|
|
1262
|
+
namespace: ref.namespace,
|
|
1263
|
+
segment: ref.segment,
|
|
1264
|
+
bytes: request.bytes,
|
|
1265
|
+
ms: request.ms
|
|
1266
|
+
})
|
|
1267
|
+
} : {}
|
|
1268
|
+
})
|
|
1269
|
+
);
|
|
1270
|
+
try {
|
|
1271
|
+
const read = await stream.take(ref.chunkKey);
|
|
1272
|
+
bytes = read.bytes;
|
|
1273
|
+
const version = this.streamedVersion(gen, read.version);
|
|
1274
|
+
key = version === null ? "" : this.chunkCacheKey(ref, version);
|
|
1275
|
+
} finally {
|
|
1276
|
+
stream.close();
|
|
1277
|
+
}
|
|
964
1278
|
}
|
|
965
1279
|
if (!bytes) return null;
|
|
966
|
-
const bitmap = this.
|
|
967
|
-
this.
|
|
968
|
-
if (this.openReads.get(cacheKey)?.token === token) this.cache?.set(cacheKey, bitmap);
|
|
1280
|
+
const bitmap = this.decodeChunk(bytes, ref.chunkKey);
|
|
1281
|
+
if (key !== "" && this.openReads.get(cacheKey)?.token === token) this.cache?.set(key, bitmap);
|
|
969
1282
|
return bitmap;
|
|
970
1283
|
}
|
|
971
1284
|
};
|
|
@@ -1262,7 +1575,7 @@ var HOURS_PER_MONTH = 730;
|
|
|
1262
1575
|
var SECONDS_PER_MONTH = HOURS_PER_MONTH * 3600;
|
|
1263
1576
|
var GIB = 1024 ** 3;
|
|
1264
1577
|
var STORE_LOAD_PUT_CLASS = 1 + 1 / LIST_COLLECTION_CADENCE;
|
|
1265
|
-
var STORE_LOAD_POINTER_READS =
|
|
1578
|
+
var STORE_LOAD_POINTER_READS = 2 + 2 / LIST_COLLECTION_CADENCE;
|
|
1266
1579
|
var STORE_LOAD_EXISTENCE_CHECKS = 2 - 1 / LIST_COLLECTION_CADENCE;
|
|
1267
1580
|
var RETENTION_SWEEP_REQUESTS = deepFreeze({
|
|
1268
1581
|
conditionalDelete: {
|
|
@@ -1546,7 +1859,7 @@ function buildReport(input) {
|
|
|
1546
1859
|
"Request cost is from the supplied workload rates (live-metrics-derived request cost is a later phase).",
|
|
1547
1860
|
loadsPerMonth > 0 ? `Loads modeled: ${loadsPerMonth}/mo, each ${requestsPerLoad} PUT-class request(s) for the object plus ${shown(STORE_LOAD_PUT_CLASS)} PUT-class and ${shown(loadGets)} GETs that store.load() adds (the pointer, a check that the next generation number is free, a check that the current generation is there, and a listing every ${LIST_COLLECTION_CADENCE}th load).` : "Loads are NOT modeled \u2014 set workload.loadsPerMonth (+ requestsPerLoad for multipart) to include them.",
|
|
1548
1861
|
...intersects > 0 ? [
|
|
1549
|
-
`Intersections priced cold: ${intersectGets} GETs each, ${getsPerColdOperand} for each of ${operandsPerIntersect} operand(s) plus ${chunksPerIntersect} chunk
|
|
1862
|
+
`Intersections priced cold: ${intersectGets} GETs each, ${getsPerColdOperand} for each of ${operandsPerIntersect} operand(s) plus ${chunksPerIntersect} chunk range request(s); cacheHitRate does not apply.`
|
|
1550
1863
|
] : [],
|
|
1551
1864
|
hotSegments > 0 ? genTtlMs > 0 ? `Pointer refresh modeled: ${hotSegments} hot segment(s) in each of ${readerProcesses} reader process(es), each re-reading its pointer at most every ${genTtlMs} ms, and at most once a point read.` : "Pointer refresh: none \u2014 genTtlMs 0 turns the timed refresh off. A reader still re-reads a pointer on an eviction, a swept generation or an invalidation, which this does not price." : readsPerSec > 0 ? "The pointer refresh is NOT modeled \u2014 set workload.hotSegments to the segments each long-lived reader keeps reading, and workload.readerProcesses to how many readers there are." : null,
|
|
1552
1865
|
hotSegments > DEFAULT_MAX_OPEN_SEGMENTS ? `More hot segments in a reader than a store keeps open by default (${DEFAULT_MAX_OPEN_SEGMENTS}): a read of a segment the reader evicted opens it again, a pointer and a tail read, which this does not price. Raise cache.readerMax, and cache.readerMaxBytes, to keep them open.` : null,
|
|
@@ -1879,6 +2192,10 @@ var MemoryRegistryDriver = class {
|
|
|
1879
2192
|
this.rows.set(key, structuredClone(recordFromNew(ref, checked, this.now(), token)));
|
|
1880
2193
|
return { token };
|
|
1881
2194
|
}
|
|
2195
|
+
/**
|
|
2196
|
+
* 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
|
|
2197
|
+
* as the write, in memory, so a stale one fails exactly as a lost race does.
|
|
2198
|
+
*/
|
|
1882
2199
|
async compareAndSwap(ref, expected, patch) {
|
|
1883
2200
|
validateSegmentRef(ref);
|
|
1884
2201
|
const checked = validateRegistryPatch(patch);
|
|
@@ -2234,6 +2551,10 @@ var LocalFsRegistryDriver = class {
|
|
|
2234
2551
|
return { token };
|
|
2235
2552
|
});
|
|
2236
2553
|
}
|
|
2554
|
+
/**
|
|
2555
|
+
* 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
|
|
2556
|
+
* there, a local read and no request, so a stale one fails exactly as a lost race does.
|
|
2557
|
+
*/
|
|
2237
2558
|
async compareAndSwap(ref, expected, patch) {
|
|
2238
2559
|
const checked = validateRegistryPatch(patch);
|
|
2239
2560
|
const path = registryRowPath(this.root, ref);
|
|
@@ -2522,6 +2843,40 @@ function crc32c(bytes, seed = 0) {
|
|
|
2522
2843
|
return (crc ^ 4294967295) >>> 0;
|
|
2523
2844
|
}
|
|
2524
2845
|
|
|
2846
|
+
// src/core/crbm/plan-reads.ts
|
|
2847
|
+
var MAX_COALESCE_GAP_BYTES = 256 * 1024;
|
|
2848
|
+
var MAX_COALESCED_READ_BYTES = DEFAULT_MAX_BITMAP_BYTES;
|
|
2849
|
+
var MAX_RANGES_IN_FLIGHT = 32;
|
|
2850
|
+
function planChunkReads(extents, region) {
|
|
2851
|
+
const reads = [];
|
|
2852
|
+
let chunks = [];
|
|
2853
|
+
let start = 0;
|
|
2854
|
+
let end = 0;
|
|
2855
|
+
const flush = () => {
|
|
2856
|
+
if (chunks.length > 0) reads.push({ offset: start, length: end - start, chunks });
|
|
2857
|
+
chunks = [];
|
|
2858
|
+
};
|
|
2859
|
+
let previousEnd = region.start;
|
|
2860
|
+
for (const extent of extents) {
|
|
2861
|
+
const extentEnd = extent.offset + extent.length;
|
|
2862
|
+
if (!(Number.isSafeInteger(extent.offset) && Number.isSafeInteger(extent.length) && extent.length >= 1 && extent.offset >= previousEnd && extentEnd <= region.end)) {
|
|
2863
|
+
throw new IntegrityError(`chunk ${extent.key} is not inside the chunk region, in order`);
|
|
2864
|
+
}
|
|
2865
|
+
previousEnd = extentEnd;
|
|
2866
|
+
if (chunks.length > 0 && extent.offset - end <= MAX_COALESCE_GAP_BYTES && extentEnd - start <= MAX_COALESCED_READ_BYTES) {
|
|
2867
|
+
chunks.push(extent);
|
|
2868
|
+
end = extentEnd;
|
|
2869
|
+
continue;
|
|
2870
|
+
}
|
|
2871
|
+
flush();
|
|
2872
|
+
chunks.push(extent);
|
|
2873
|
+
start = extent.offset;
|
|
2874
|
+
end = extentEnd;
|
|
2875
|
+
}
|
|
2876
|
+
flush();
|
|
2877
|
+
return reads;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2525
2880
|
// src/core/crbm/varint.ts
|
|
2526
2881
|
var U32_CEIL = 4294967296;
|
|
2527
2882
|
function writeVarint(out, value) {
|
|
@@ -2602,8 +2957,14 @@ function readU64(view, offset, field) {
|
|
|
2602
2957
|
}
|
|
2603
2958
|
return Number(big);
|
|
2604
2959
|
}
|
|
2960
|
+
var keepLimits = /* @__PURE__ */ new WeakMap();
|
|
2961
|
+
function openCrbmReaderKeeping(blob, options, keepChunkBytesUpTo) {
|
|
2962
|
+
const asked = { ...options };
|
|
2963
|
+
if (keepChunkBytesUpTo !== void 0) keepLimits.set(asked, keepChunkBytesUpTo);
|
|
2964
|
+
return CrbmReader.open(blob, asked);
|
|
2965
|
+
}
|
|
2605
2966
|
var CrbmReader = class _CrbmReader {
|
|
2606
|
-
constructor(blob, objectSize, generation, lineage, totalCardinality, index, servedFromTail, crypto, footerCrc, metadata, metadataWeight) {
|
|
2967
|
+
constructor(blob, objectSize, generation, lineage, totalCardinality, index, servedFromTail, crypto, footerCrc, metadata, metadataWeight, payloadEnd, kept) {
|
|
2607
2968
|
this.blob = blob;
|
|
2608
2969
|
this.objectSize = objectSize;
|
|
2609
2970
|
this.generation = generation;
|
|
@@ -2615,6 +2976,8 @@ var CrbmReader = class _CrbmReader {
|
|
|
2615
2976
|
this.footerCrc = footerCrc;
|
|
2616
2977
|
this.metadata = metadata;
|
|
2617
2978
|
this.metadataWeight = metadataWeight;
|
|
2979
|
+
this.payloadEnd = payloadEnd;
|
|
2980
|
+
this.kept = kept;
|
|
2618
2981
|
}
|
|
2619
2982
|
blob;
|
|
2620
2983
|
objectSize;
|
|
@@ -2627,6 +2990,8 @@ var CrbmReader = class _CrbmReader {
|
|
|
2627
2990
|
footerCrc;
|
|
2628
2991
|
metadata;
|
|
2629
2992
|
metadataWeight;
|
|
2993
|
+
payloadEnd;
|
|
2994
|
+
kept;
|
|
2630
2995
|
/**
|
|
2631
2996
|
* Which object this reader opened, as its size and its footer's CRC. The footer covers the index's CRC, so two
|
|
2632
2997
|
* objects with the same fingerprint hold the same chunks, as far as a CRC can tell; a pin compares it to know that
|
|
@@ -2651,11 +3016,12 @@ var CrbmReader = class _CrbmReader {
|
|
|
2651
3016
|
/**
|
|
2652
3017
|
* What this reader holds, the weight the storage reader cache bounds on: its parsed index
|
|
2653
3018
|
* ({@link retainedIndexBytes}), plus a weight for the metadata it decoded, from its canonical length and its number
|
|
2654
|
-
* of keys
|
|
2655
|
-
* how the metadata is weighed may
|
|
3019
|
+
* of keys, plus the chunk bytes kept from the open, when it kept any. Equal to {@link retainedIndexBytes} for a
|
|
3020
|
+
* generation with no metadata and nothing kept. A cache heuristic, not a contract: how the metadata is weighed may
|
|
3021
|
+
* change in any release.
|
|
2656
3022
|
*/
|
|
2657
3023
|
get retainedBytes() {
|
|
2658
|
-
return this.retainedIndexBytes + this.metadataWeight;
|
|
3024
|
+
return this.retainedIndexBytes + this.metadataWeight + (this.kept?.length ?? 0);
|
|
2659
3025
|
}
|
|
2660
3026
|
/** Per-chunk cardinality (`chunkKey → count`) from the parsed index — no payload reads. */
|
|
2661
3027
|
cardinalities() {
|
|
@@ -2853,7 +3219,11 @@ var CrbmReader = class _CrbmReader {
|
|
|
2853
3219
|
options.crypto,
|
|
2854
3220
|
storedFooterCrc,
|
|
2855
3221
|
metadata,
|
|
2856
|
-
metadata === void 0 ? 0 : RETAINED_BYTES_PER_METADATA_BYTE * metadataLength + RETAINED_BYTES_PER_METADATA_KEY * Object.keys(metadata).length
|
|
3222
|
+
metadata === void 0 ? 0 : RETAINED_BYTES_PER_METADATA_BYTE * metadataLength + RETAINED_BYTES_PER_METADATA_KEY * Object.keys(metadata).length,
|
|
3223
|
+
payloadEnd,
|
|
3224
|
+
// Only a tail that began at the front of the object holds the chunks, and only a region the caller's limit
|
|
3225
|
+
// allows is kept: a copy of just that region, so the tail buffer is not held.
|
|
3226
|
+
tailCoversFrom === 0 && payloadEnd - PAYLOAD_START <= (keepLimits.get(options) ?? 0) ? tail.slice(PAYLOAD_START, payloadEnd) : void 0
|
|
2857
3227
|
);
|
|
2858
3228
|
}
|
|
2859
3229
|
/** Chunk keys present in this generation, ascending. */
|
|
@@ -2895,7 +3265,30 @@ var CrbmReader = class _CrbmReader {
|
|
|
2895
3265
|
if (offset < PAYLOAD_START || offset + length > this.objectSize - FOOTER_BYTES) {
|
|
2896
3266
|
throw new IntegrityError(`chunk ${chunkKey} payload out of bounds`);
|
|
2897
3267
|
}
|
|
2898
|
-
const
|
|
3268
|
+
const held = this.keptRange(offset, length);
|
|
3269
|
+
return this.openChunk(slot, chunkKey, held ?? await this.blob.getRange(offset, length));
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* `[offset, offset + length)` of the object from the kept chunk region, or `undefined` when it is not wholly in it
|
|
3273
|
+
* (or nothing was kept). The bounds checks are defence on untrusted bytes: the index's validation at open keeps every
|
|
3274
|
+
* extent inside the region, so they do not fire for a reader that opened.
|
|
3275
|
+
*
|
|
3276
|
+
* The range is returned as a copy, so nothing a caller or an AEAD does with it changes the next read.
|
|
3277
|
+
*/
|
|
3278
|
+
keptRange(offset, length) {
|
|
3279
|
+
const kept = this.kept;
|
|
3280
|
+
if (kept === void 0 || offset < PAYLOAD_START || offset + length > PAYLOAD_START + kept.length) {
|
|
3281
|
+
return void 0;
|
|
3282
|
+
}
|
|
3283
|
+
const from = offset - PAYLOAD_START;
|
|
3284
|
+
return kept.slice(from, from + length);
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Check one chunk's stored bytes against the index, and open them: the CRC32C first, then, on an encrypted object,
|
|
3288
|
+
* the AEAD with the chunk's own associated data. The one check every read of a chunk goes through, whether the
|
|
3289
|
+
* bytes came from a range of their own or from a slice of a larger one.
|
|
3290
|
+
*/
|
|
3291
|
+
openChunk(slot, chunkKey, bytes) {
|
|
2899
3292
|
if (crc32c(bytes) !== this.index.crcs[slot]) {
|
|
2900
3293
|
throw new IntegrityError(`chunk ${chunkKey} payload CRC mismatch`);
|
|
2901
3294
|
}
|
|
@@ -2912,6 +3305,154 @@ var CrbmReader = class _CrbmReader {
|
|
|
2912
3305
|
this.crypto.aadFor(chunkKey)
|
|
2913
3306
|
);
|
|
2914
3307
|
}
|
|
3308
|
+
/**
|
|
3309
|
+
* The next needed chunk of the range in `cur`, checked and opened, as a stream hands it out; the range is let go of
|
|
3310
|
+
* when this is its last chunk.
|
|
3311
|
+
*/
|
|
3312
|
+
nextChunk(cur, slot, key) {
|
|
3313
|
+
const range = cur.range;
|
|
3314
|
+
const chunk = range.read.chunks[cur.at++];
|
|
3315
|
+
if (cur.at === range.read.chunks.length) cur.range = void 0;
|
|
3316
|
+
const from = chunk.offset - range.read.offset;
|
|
3317
|
+
const bytes = this.openChunk(slot, chunk.key, range.bytes.subarray(from, from + chunk.length));
|
|
3318
|
+
return { key, bytes };
|
|
3319
|
+
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Read several chunks as a stream, one storage request per merged range ({@link planChunkReads}) rather than one per
|
|
3322
|
+
* chunk. `chunkKeys` are in ascending order, a key may repeat, and anything else is a {@link ValidationError}. One
|
|
3323
|
+
* item comes out per position of `chunkKeys`, in order: the chunk's bytes, or `null` where this generation has none,
|
|
3324
|
+
* as {@link getChunk} answers for an absent key. A key given twice is read once and answered at both positions with
|
|
3325
|
+
* the same view.
|
|
3326
|
+
*
|
|
3327
|
+
* The whole key list is planned once, up front, against this one generation's index. The ranges then run through a
|
|
3328
|
+
* window of `options.concurrency` reads ({@link MAX_RANGES_IN_FLIGHT} by default) that counts a range until the
|
|
3329
|
+
* consumer has taken it, so a stream never holds more than that many ranges, in flight or landed, however many keys
|
|
3330
|
+
* it was given or how slowly it is read; with `options.ramp` the window opens 1, 2, 4 … wide. A range is at most 1 MiB
|
|
3331
|
+
* unless it is one chunk, which is at most the payload cap, so the bound is that many times the per-chunk cap
|
|
3332
|
+
* (1 MiB, and 28 bytes more for an encrypted chunk). Nothing is read until the first item is asked for, and a
|
|
3333
|
+
* consumer that stops launches no more: ranges already in flight finish and are dropped, and a failure of one of
|
|
3334
|
+
* those is never raised.
|
|
3335
|
+
*
|
|
3336
|
+
* Every chunk is held to the check a read of it alone gets: its slice of the range must match its index entry's
|
|
3337
|
+
* CRC32C, and on an encrypted object it opens under its own associated data, so a chunk moved to another place in
|
|
3338
|
+
* the object, or another key's bytes, is refused. The bytes in a gap between needed chunks are never parsed. A
|
|
3339
|
+
* range that comes back shorter than asked is an {@link IntegrityError}, and a chunk that fails its check ends the
|
|
3340
|
+
* stream with that error when its turn comes, as a read of it alone would fail.
|
|
3341
|
+
*
|
|
3342
|
+
* `options.readRange`, when given, runs each range read, so a caller can retry one request without repeating the
|
|
3343
|
+
* others; `options.now` times each request for `options.onRequest`, which hears of every range request that was sent, once, when
|
|
3344
|
+
* it settles: taken, abandoned after the consumer stopped, or failed (a reader has no clock of its own, so `ms` is 0
|
|
3345
|
+
* without one).
|
|
3346
|
+
* A plain chunk is a writable view into the range it was read in: copy a chunk to keep it.
|
|
3347
|
+
*
|
|
3348
|
+
* A stream that fails (a range that errors, a chunk that fails its check) raises at once and sends nothing further:
|
|
3349
|
+
* the ranges it still had in flight finish in the background, are not retried and never raise. Its `settled()`
|
|
3350
|
+
* resolves when those have finished, for a caller that is about to read again and does not want them beside its next
|
|
3351
|
+
* window (the source awaits it before a heal, and only then). It waits as long as the slowest of them takes to
|
|
3352
|
+
* answer, which only the store's own read timeout bounds, so a read that never answers delays a heal and never an
|
|
3353
|
+
* error.
|
|
3354
|
+
*/
|
|
3355
|
+
readChunks(chunkKeys, options = {}) {
|
|
3356
|
+
const open4 = { window: void 0 };
|
|
3357
|
+
const stream = this.streamChunks(chunkKeys, options, open4);
|
|
3358
|
+
return Object.assign(stream, {
|
|
3359
|
+
settled: () => open4.window?.settle() ?? Promise.resolve()
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
async *streamChunks(chunkKeys, options, open4) {
|
|
3363
|
+
const width = options.concurrency ?? MAX_RANGES_IN_FLIGHT;
|
|
3364
|
+
if (!Number.isInteger(width) || width < 1) {
|
|
3365
|
+
throw new ValidationError(
|
|
3366
|
+
`concurrency must be a positive integer; got ${options.concurrency}`
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
validateChunkKeyOrder(chunkKeys);
|
|
3370
|
+
const ramp = options.ramp ?? false;
|
|
3371
|
+
if (typeof ramp === "number" && (!Number.isInteger(ramp) || ramp < 1)) {
|
|
3372
|
+
throw new ValidationError(`ramp must be a boolean or a positive integer; got ${ramp}`);
|
|
3373
|
+
}
|
|
3374
|
+
const readRange = options.readRange ?? ((read) => read());
|
|
3375
|
+
const now = options.now ?? (() => 0);
|
|
3376
|
+
const slots = chunkKeys.map((key) => this.slotOf(key));
|
|
3377
|
+
const needed = [...new Set(slots.filter((slot) => slot >= 0))];
|
|
3378
|
+
const extents = needed.map((slot) => ({
|
|
3379
|
+
key: this.index.keys[slot],
|
|
3380
|
+
offset: this.index.offsets[slot],
|
|
3381
|
+
length: this.index.lengths[slot]
|
|
3382
|
+
}));
|
|
3383
|
+
const reads = planChunkReads(extents, { start: PAYLOAD_START, end: this.payloadEnd });
|
|
3384
|
+
let closed = false;
|
|
3385
|
+
const report = (bytes, ms) => {
|
|
3386
|
+
try {
|
|
3387
|
+
options.onRequest?.({ bytes, ms });
|
|
3388
|
+
} catch {
|
|
3389
|
+
}
|
|
3390
|
+
};
|
|
3391
|
+
const window = new ChunkWindow(
|
|
3392
|
+
reads.map((_, i) => i),
|
|
3393
|
+
async (i) => {
|
|
3394
|
+
const read = reads[i];
|
|
3395
|
+
const held = this.keptRange(read.offset, read.length);
|
|
3396
|
+
if (held !== void 0) return { bytes: held };
|
|
3397
|
+
const startedAt = now();
|
|
3398
|
+
let sent = false;
|
|
3399
|
+
let moved = 0;
|
|
3400
|
+
try {
|
|
3401
|
+
const bytes = await readRange(() => {
|
|
3402
|
+
if (closed) throw new StreamClosed();
|
|
3403
|
+
sent = true;
|
|
3404
|
+
return this.blob.getRange(read.offset, read.length);
|
|
3405
|
+
});
|
|
3406
|
+
moved = bytes.length;
|
|
3407
|
+
if (bytes.length !== read.length) {
|
|
3408
|
+
throw new IntegrityError(
|
|
3409
|
+
`.crbm range [${read.offset}, +${read.length}) read short (${bytes.length} bytes)`
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
return { bytes };
|
|
3413
|
+
} finally {
|
|
3414
|
+
if (sent) report(moved, Math.max(0, now() - startedAt));
|
|
3415
|
+
}
|
|
3416
|
+
},
|
|
3417
|
+
width,
|
|
3418
|
+
ramp !== false,
|
|
3419
|
+
typeof ramp === "number" ? ramp : 1
|
|
3420
|
+
);
|
|
3421
|
+
open4.window = window;
|
|
3422
|
+
let next = 0;
|
|
3423
|
+
const cur = { range: void 0, at: 0 };
|
|
3424
|
+
let last;
|
|
3425
|
+
try {
|
|
3426
|
+
for (const [i, key] of chunkKeys.entries()) {
|
|
3427
|
+
if (slots[i] < 0) {
|
|
3428
|
+
yield { key, bytes: null };
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
if (last !== void 0 && last.key === key) {
|
|
3432
|
+
yield { key, bytes: last.bytes };
|
|
3433
|
+
if (chunkKeys[i + 1] !== key) last = void 0;
|
|
3434
|
+
continue;
|
|
3435
|
+
}
|
|
3436
|
+
if (cur.range === void 0) {
|
|
3437
|
+
cur.range = { read: reads[next++], ...await window.take() };
|
|
3438
|
+
cur.at = 0;
|
|
3439
|
+
}
|
|
3440
|
+
if (chunkKeys[i + 1] === key) {
|
|
3441
|
+
last = this.nextChunk(cur, slots[i], key);
|
|
3442
|
+
yield last;
|
|
3443
|
+
} else {
|
|
3444
|
+
yield this.nextChunk(cur, slots[i], key);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
} finally {
|
|
3448
|
+
closed = true;
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
};
|
|
3452
|
+
var StreamClosed = class extends Error {
|
|
3453
|
+
constructor() {
|
|
3454
|
+
super("the chunk stream was closed");
|
|
3455
|
+
}
|
|
2915
3456
|
};
|
|
2916
3457
|
function within(bytes, start, from, to) {
|
|
2917
3458
|
if (from < start || to > start + bytes.length) {
|
|
@@ -3041,6 +3582,33 @@ function parseIndex(indexBytes, payloadEnd, maxPayloadBytes, minPayloadBytes = 1
|
|
|
3041
3582
|
};
|
|
3042
3583
|
}
|
|
3043
3584
|
|
|
3585
|
+
// src/core/item-pull.ts
|
|
3586
|
+
var ItemPull = class {
|
|
3587
|
+
iterator;
|
|
3588
|
+
item;
|
|
3589
|
+
constructor(source) {
|
|
3590
|
+
this.iterator = source[Symbol.asyncIterator]();
|
|
3591
|
+
}
|
|
3592
|
+
/** Waits for the next item and keeps it inside; `false` once the source has ended. A failure of the source throws. */
|
|
3593
|
+
async advance() {
|
|
3594
|
+
const step = await this.iterator.next();
|
|
3595
|
+
if (step.done === true) return false;
|
|
3596
|
+
this.item = step.value;
|
|
3597
|
+
return true;
|
|
3598
|
+
}
|
|
3599
|
+
/** Hands over the item {@link advance} fetched, through `map`, and forgets it. */
|
|
3600
|
+
take(map) {
|
|
3601
|
+
const item = this.item;
|
|
3602
|
+
this.item = void 0;
|
|
3603
|
+
return map(item);
|
|
3604
|
+
}
|
|
3605
|
+
/** Ends the source early (a consumer that stopped), as leaving a `for await` does. */
|
|
3606
|
+
async close() {
|
|
3607
|
+
this.item = void 0;
|
|
3608
|
+
await this.iterator.return?.();
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
|
|
3044
3612
|
// src/core/crbm/writer.ts
|
|
3045
3613
|
var CrbmWriter = class {
|
|
3046
3614
|
constructor(sink, options) {
|
|
@@ -3241,6 +3809,12 @@ var DecodedLoadInput = class {
|
|
|
3241
3809
|
}
|
|
3242
3810
|
bitmap;
|
|
3243
3811
|
};
|
|
3812
|
+
var ChunkLoadInput = class {
|
|
3813
|
+
constructor(chunks) {
|
|
3814
|
+
this.chunks = chunks;
|
|
3815
|
+
}
|
|
3816
|
+
chunks;
|
|
3817
|
+
};
|
|
3244
3818
|
function tagOf(value) {
|
|
3245
3819
|
return Object.prototype.toString.call(value).slice(8, -1);
|
|
3246
3820
|
}
|
|
@@ -3575,6 +4149,10 @@ var CrbmStorageChunkSource = class {
|
|
|
3575
4149
|
this.keystore = keystore;
|
|
3576
4150
|
this.requireEncryption = requireEncryption ?? false;
|
|
3577
4151
|
this.readerOptions = readerOptions;
|
|
4152
|
+
const refreshes = registry !== void 0 && clock !== void 0 && (currentGenTtlMs ?? DEFAULT_CURRENT_GEN_TTL_MS) > 0;
|
|
4153
|
+
this.keepChunkBytesUpTo = refreshes ? Math.floor(
|
|
4154
|
+
(maxOpenIndexBytes ?? DEFAULT_MAX_OPEN_INDEX_BYTES) / (maxOpenSegments ?? DEFAULT_MAX_OPEN_SEGMENTS)
|
|
4155
|
+
) : 0;
|
|
3578
4156
|
this.clock = clock;
|
|
3579
4157
|
this.currentGenTtlMs = currentGenTtlMs ?? DEFAULT_CURRENT_GEN_TTL_MS;
|
|
3580
4158
|
this.snapshots = new BoundedLru({
|
|
@@ -3638,6 +4216,8 @@ var CrbmStorageChunkSource = class {
|
|
|
3638
4216
|
keystore;
|
|
3639
4217
|
requireEncryption;
|
|
3640
4218
|
readerOptions;
|
|
4219
|
+
/** The most chunk bytes a reader keeps from its open: the reader cache's byte bound over its count bound, per reader; 0 without a timed pointer refresh. */
|
|
4220
|
+
keepChunkBytesUpTo;
|
|
3641
4221
|
clock;
|
|
3642
4222
|
currentGenTtlMs;
|
|
3643
4223
|
/**
|
|
@@ -4082,11 +4662,12 @@ var CrbmStorageChunkSource = class {
|
|
|
4082
4662
|
segment: ref.segment,
|
|
4083
4663
|
generation: target.generation
|
|
4084
4664
|
};
|
|
4085
|
-
return openChecked(
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
lineage: target.lineage
|
|
4089
|
-
|
|
4665
|
+
return openChecked(
|
|
4666
|
+
this.driver,
|
|
4667
|
+
genKey,
|
|
4668
|
+
{ ...this.readerOptions, crypto, lineage: target.lineage },
|
|
4669
|
+
this.keepChunkBytesUpTo
|
|
4670
|
+
);
|
|
4090
4671
|
}
|
|
4091
4672
|
/**
|
|
4092
4673
|
* Open the generation a resolution found, and hold the row's summary against the object while it is open anyway:
|
|
@@ -4173,6 +4754,128 @@ var CrbmStorageChunkSource = class {
|
|
|
4173
4754
|
validateChunkRef(ref);
|
|
4174
4755
|
return this.withFreshSnapshot(ref, (reader) => reader.getChunk(ref.chunkKey), null);
|
|
4175
4756
|
}
|
|
4757
|
+
/**
|
|
4758
|
+
* Several chunks as a stream, in as few storage requests as {@link CrbmReader.readChunks} can make them (see
|
|
4759
|
+
* {@link StorageChunkSource.getChunks}). The stream is served from one resolved generation, and every chunk says
|
|
4760
|
+
* which version it came from. A generation swept, or an object replaced under its number, while the stream runs
|
|
4761
|
+
* re-resolves the segment and carries on with the keys not yet yielded from whatever is current then, so each chunk
|
|
4762
|
+
* is whole and from one generation and the stream may describe two; a second failure with no chunk yielded in between
|
|
4763
|
+
* propagates. `options.retry` runs the resolution, each range request and the check that a failed read's object was
|
|
4764
|
+
* replaced, so a transient fault repeats that step alone. Nothing is resolved until the first chunk is asked for.
|
|
4765
|
+
*/
|
|
4766
|
+
async *getChunks(ref, keys, options) {
|
|
4767
|
+
validateUserRef(ref);
|
|
4768
|
+
for (const chunkKey of keys) validateChunkRef({ ...ref, chunkKey });
|
|
4769
|
+
validateChunkKeyOrder(keys);
|
|
4770
|
+
const retry = options?.retry;
|
|
4771
|
+
let yielded = 0;
|
|
4772
|
+
let healed = false;
|
|
4773
|
+
for (; ; ) {
|
|
4774
|
+
let snap = this.liveSnapshot(ref);
|
|
4775
|
+
let pending = snap.reader;
|
|
4776
|
+
let chunks;
|
|
4777
|
+
try {
|
|
4778
|
+
let tried = false;
|
|
4779
|
+
const reader = await (retry === void 0 ? pending : retry(() => {
|
|
4780
|
+
if (tried) {
|
|
4781
|
+
snap = this.liveSnapshot(ref);
|
|
4782
|
+
pending = snap.reader;
|
|
4783
|
+
}
|
|
4784
|
+
tried = true;
|
|
4785
|
+
return pending;
|
|
4786
|
+
}));
|
|
4787
|
+
if (reader === null) {
|
|
4788
|
+
for (; yielded < keys.length; yielded++) {
|
|
4789
|
+
yield { key: keys[yielded], bytes: null, version: null };
|
|
4790
|
+
}
|
|
4791
|
+
return;
|
|
4792
|
+
}
|
|
4793
|
+
const version = versionOf(reader.generation, reader.lineage);
|
|
4794
|
+
chunks = reader.readChunks(keys.slice(yielded), {
|
|
4795
|
+
...options?.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
4796
|
+
...options?.ramp === void 0 ? {} : { ramp: options.ramp },
|
|
4797
|
+
...retry === void 0 ? {} : { readRange: retry },
|
|
4798
|
+
...options?.onRequest === void 0 ? {} : { onRequest: options.onRequest },
|
|
4799
|
+
now: () => this.now()
|
|
4800
|
+
});
|
|
4801
|
+
const pull = new ItemPull(chunks);
|
|
4802
|
+
let movedOn = false;
|
|
4803
|
+
try {
|
|
4804
|
+
while (await pull.advance()) {
|
|
4805
|
+
const live = this.liveSnapshot(ref);
|
|
4806
|
+
if (live !== snap) {
|
|
4807
|
+
const now = await live.reader;
|
|
4808
|
+
if (now === null || versionOf(now.generation, now.lineage) !== version) {
|
|
4809
|
+
movedOn = true;
|
|
4810
|
+
break;
|
|
4811
|
+
}
|
|
4812
|
+
snap = live;
|
|
4813
|
+
}
|
|
4814
|
+
yield pull.take((chunk) => {
|
|
4815
|
+
yielded += 1;
|
|
4816
|
+
healed = false;
|
|
4817
|
+
return { ...chunk, version };
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
} finally {
|
|
4821
|
+
await pull.close();
|
|
4822
|
+
}
|
|
4823
|
+
if (!movedOn) return;
|
|
4824
|
+
await chunks.settled();
|
|
4825
|
+
healed = false;
|
|
4826
|
+
continue;
|
|
4827
|
+
} catch (err) {
|
|
4828
|
+
if (healed) throw err;
|
|
4829
|
+
if (!isNotFoundError(err) && !await this.replacedUnder(ref, pending, err, retry))
|
|
4830
|
+
throw err;
|
|
4831
|
+
await chunks?.settled();
|
|
4832
|
+
this.dropStale(segmentKey(ref), snap);
|
|
4833
|
+
healed = true;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
/**
|
|
4838
|
+
* {@link getChunks} of a specific generation, the pinned shape read. It yields the chunks without a version: the
|
|
4839
|
+
* caller that pinned the generation knows it. It never moves to another generation; `held` as for
|
|
4840
|
+
* {@link getChunkAt}.
|
|
4841
|
+
*/
|
|
4842
|
+
async *getChunksAt(ref, generation, keys, held, options) {
|
|
4843
|
+
validateUserRef(ref);
|
|
4844
|
+
for (const chunkKey of keys) validateChunkRef({ ...ref, chunkKey });
|
|
4845
|
+
validateChunkKeyOrder(keys);
|
|
4846
|
+
const retry = options?.retry;
|
|
4847
|
+
const open4 = () => this.readerAt(ref, generation, held?.version, held?.fingerprint);
|
|
4848
|
+
const reader = await (retry === void 0 ? open4() : retry(open4));
|
|
4849
|
+
if (reader === null) {
|
|
4850
|
+
for (const key of keys) yield { key, bytes: null };
|
|
4851
|
+
return;
|
|
4852
|
+
}
|
|
4853
|
+
const fingerprint = held?.fingerprint;
|
|
4854
|
+
if (held !== void 0 && fingerprint !== void 0 && this.replacedPins.get(this.heldKey(ref, held.version, fingerprint)) !== void 0)
|
|
4855
|
+
throw notThePinned(ref, generation);
|
|
4856
|
+
try {
|
|
4857
|
+
const pull = new ItemPull(
|
|
4858
|
+
reader.readChunks(keys, {
|
|
4859
|
+
...options?.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
4860
|
+
...options?.ramp === void 0 ? {} : { ramp: options.ramp },
|
|
4861
|
+
...retry === void 0 ? {} : { readRange: retry },
|
|
4862
|
+
...options?.onRequest === void 0 ? {} : { onRequest: options.onRequest },
|
|
4863
|
+
now: () => this.now()
|
|
4864
|
+
})
|
|
4865
|
+
);
|
|
4866
|
+
try {
|
|
4867
|
+
while (await pull.advance()) yield pull.take((chunk) => chunk);
|
|
4868
|
+
} finally {
|
|
4869
|
+
await pull.close();
|
|
4870
|
+
}
|
|
4871
|
+
} catch (err) {
|
|
4872
|
+
if (held === void 0 || fingerprint === void 0) throw err;
|
|
4873
|
+
if (!(isIntegrityError(err) || isValidationError(err))) throw err;
|
|
4874
|
+
const check = () => this.throwIfReplaced(ref, generation, held.version, fingerprint);
|
|
4875
|
+
await (retry === void 0 ? check() : retry(check));
|
|
4876
|
+
throw err;
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4176
4879
|
async listChunkKeys(ref) {
|
|
4177
4880
|
validateUserRef(ref);
|
|
4178
4881
|
return this.withFreshSnapshot(ref, (reader) => reader.chunkKeys(), []);
|
|
@@ -4259,17 +4962,26 @@ var CrbmStorageChunkSource = class {
|
|
|
4259
4962
|
* tight loop on a segment whose pointer names an object that is permanently absent — and an N-way
|
|
4260
4963
|
* `intersect` would do it N times. Gated in `tests/core/storage-source-heal-open.test.ts` by counting calls.
|
|
4261
4964
|
*/
|
|
4262
|
-
async withFreshSnapshot(ref, read, ifGone) {
|
|
4965
|
+
async withFreshSnapshot(ref, read, ifGone, retry) {
|
|
4263
4966
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4264
|
-
|
|
4265
|
-
|
|
4967
|
+
let snap = this.liveSnapshot(ref);
|
|
4968
|
+
let pending = snap.reader;
|
|
4266
4969
|
try {
|
|
4267
|
-
|
|
4970
|
+
let tried = false;
|
|
4971
|
+
const reader = await (retry === void 0 ? pending : retry(() => {
|
|
4972
|
+
if (tried) {
|
|
4973
|
+
snap = this.liveSnapshot(ref);
|
|
4974
|
+
pending = snap.reader;
|
|
4975
|
+
}
|
|
4976
|
+
tried = true;
|
|
4977
|
+
return pending;
|
|
4978
|
+
}));
|
|
4268
4979
|
if (reader === null) return ifGone;
|
|
4269
4980
|
return await read(reader);
|
|
4270
4981
|
} catch (err) {
|
|
4271
4982
|
if (attempt === 1) throw err;
|
|
4272
|
-
if (!isNotFoundError(err) && !await this.replacedUnder(ref, pending, err))
|
|
4983
|
+
if (!isNotFoundError(err) && !await this.replacedUnder(ref, pending, err, retry))
|
|
4984
|
+
throw err;
|
|
4273
4985
|
this.dropStale(segmentKey(ref), snap);
|
|
4274
4986
|
}
|
|
4275
4987
|
}
|
|
@@ -4285,7 +4997,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4285
4997
|
* the error is real, and it stands. Only these two errors pay for the footer read, never a read that succeeded. A
|
|
4286
4998
|
* transient fault in the check is rethrown; a footer that cannot be read says nothing, and the error stands.
|
|
4287
4999
|
*/
|
|
4288
|
-
async replacedUnder(ref, pending, err) {
|
|
5000
|
+
async replacedUnder(ref, pending, err, retry) {
|
|
4289
5001
|
if (!(isIntegrityError(err) || isValidationError(err))) return false;
|
|
4290
5002
|
let reader;
|
|
4291
5003
|
try {
|
|
@@ -4300,7 +5012,8 @@ var CrbmStorageChunkSource = class {
|
|
|
4300
5012
|
generation: reader.generation
|
|
4301
5013
|
};
|
|
4302
5014
|
try {
|
|
4303
|
-
|
|
5015
|
+
const check = () => CrbmReader.sameObject(storageBlobReader(this.driver, at), reader.fingerprint);
|
|
5016
|
+
return !await (retry === void 0 ? check() : retry(check));
|
|
4304
5017
|
} catch (checkErr) {
|
|
4305
5018
|
if (isNotFoundError(checkErr)) return true;
|
|
4306
5019
|
if (isTransientError(checkErr)) throw checkErr;
|
|
@@ -4442,7 +5155,9 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
4442
5155
|
let lastRecord;
|
|
4443
5156
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
4444
5157
|
const reused = attempt === 0 && options.row !== void 0;
|
|
5158
|
+
const holdsRow = fresh === void 0;
|
|
4445
5159
|
const record = fresh !== void 0 ? fresh : reused ? options.row ?? null : await registry.get(key);
|
|
5160
|
+
const write = holdsRow ? { held: record } : void 0;
|
|
4446
5161
|
fresh = void 0;
|
|
4447
5162
|
unanswered = void 0;
|
|
4448
5163
|
lastRecord = record;
|
|
@@ -4474,22 +5189,31 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
4474
5189
|
return false;
|
|
4475
5190
|
}
|
|
4476
5191
|
if (record === null) {
|
|
4477
|
-
await registry.create(
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
5192
|
+
await registry.create(
|
|
5193
|
+
key,
|
|
5194
|
+
{
|
|
5195
|
+
currentGen: key.generation,
|
|
5196
|
+
wrappedDeks: options.wrappedDeks,
|
|
5197
|
+
...options.summary === void 0 ? {} : { summary: options.summary }
|
|
5198
|
+
},
|
|
5199
|
+
write
|
|
5200
|
+
);
|
|
4482
5201
|
} else if (record.status === "destroyed") {
|
|
4483
5202
|
throw new ValidationError(
|
|
4484
5203
|
`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.`
|
|
4485
5204
|
);
|
|
4486
5205
|
} else if (record.currentGen === null) {
|
|
4487
5206
|
refuseCleartextOntoKey(key, record, options.cleartext);
|
|
4488
|
-
await registry.compareAndSwap(
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
5207
|
+
await registry.compareAndSwap(
|
|
5208
|
+
key,
|
|
5209
|
+
record.token,
|
|
5210
|
+
{
|
|
5211
|
+
currentGen: key.generation,
|
|
5212
|
+
...options.wrappedDeks === void 0 ? {} : { wrappedDeks: options.wrappedDeks },
|
|
5213
|
+
...options.summary === void 0 ? {} : { summary: options.summary }
|
|
5214
|
+
},
|
|
5215
|
+
write
|
|
5216
|
+
);
|
|
4493
5217
|
} else if (record.currentGen > key.generation) {
|
|
4494
5218
|
return false;
|
|
4495
5219
|
} else if (record.currentGen === key.generation) {
|
|
@@ -4502,10 +5226,15 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
4502
5226
|
);
|
|
4503
5227
|
}
|
|
4504
5228
|
refuseCleartextOntoKey(key, record, options.cleartext);
|
|
4505
|
-
await registry.compareAndSwap(
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
5229
|
+
await registry.compareAndSwap(
|
|
5230
|
+
key,
|
|
5231
|
+
record.token,
|
|
5232
|
+
{
|
|
5233
|
+
currentGen: key.generation,
|
|
5234
|
+
...options.summary === void 0 ? {} : { summary: options.summary }
|
|
5235
|
+
},
|
|
5236
|
+
write
|
|
5237
|
+
);
|
|
4509
5238
|
}
|
|
4510
5239
|
return true;
|
|
4511
5240
|
} catch (err) {
|
|
@@ -4580,7 +5309,7 @@ async function provesOwnObject(storage, key, fingerprint) {
|
|
|
4580
5309
|
throw err;
|
|
4581
5310
|
}
|
|
4582
5311
|
}
|
|
4583
|
-
async function
|
|
5312
|
+
async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
|
|
4584
5313
|
if (options.keystore === void 0 && options.requireEncryption === true) {
|
|
4585
5314
|
throw new ValidationError("requireEncryption: a load needs a keystore to write encrypted");
|
|
4586
5315
|
}
|
|
@@ -4591,6 +5320,8 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
4591
5320
|
await pause();
|
|
4592
5321
|
chunks = ids.bitmap.encodeChunks();
|
|
4593
5322
|
await pause();
|
|
5323
|
+
} else if (ids instanceof ChunkLoadInput) {
|
|
5324
|
+
chunks = encodeEach(rangeCheckedAtWrite(await collectChunks(ids, codec, options.clock)));
|
|
4594
5325
|
} else {
|
|
4595
5326
|
chunks = encodeEach(
|
|
4596
5327
|
await bucketIds(ids instanceof DecodedLoadInput ? ids.bitmap : ids, codec, options.clock)
|
|
@@ -4600,27 +5331,30 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
4600
5331
|
throw new ValidationError("an encrypted load requires a registry to store the wrapped DEK");
|
|
4601
5332
|
}
|
|
4602
5333
|
const passed = options.row;
|
|
4603
|
-
const existing = passed !== void 0 && passed !== null && (passed.wrappedDeks === void 0 || passed.wrappedDeks.length === 0) ? passed : options.registry !== void 0 ? await options.registry.get(
|
|
5334
|
+
const existing = passed !== void 0 && passed !== null && (passed.wrappedDeks === void 0 || passed.wrappedDeks.length === 0) ? passed : options.registry !== void 0 ? await options.registry.get(ref) : null;
|
|
4604
5335
|
if (existing?.status === "destroyed") {
|
|
4605
5336
|
throw new ValidationError(
|
|
4606
|
-
`segment "${
|
|
5337
|
+
`segment "${ref.segment}" is destroyed (crypto-shredded) \u2014 refusing to write; use a new segment`
|
|
4607
5338
|
);
|
|
4608
5339
|
}
|
|
4609
5340
|
if (existing?.wrappedDeks !== void 0 && existing.wrappedDeks.length > 0 && options.keystore === void 0) {
|
|
4610
5341
|
throw new KeyUnavailableError(
|
|
4611
|
-
`segment "${
|
|
5342
|
+
`segment "${ref.segment}" is encrypted but this load has no keystore \u2014 refusing to write a cleartext generation onto an encrypted segment. Pass the keystore holding its DEK.`
|
|
4612
5343
|
);
|
|
4613
5344
|
}
|
|
5345
|
+
const generation = await ahead.generation;
|
|
5346
|
+
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
4614
5347
|
let crypto;
|
|
4615
5348
|
let newWrapped;
|
|
4616
5349
|
if (options.keystore !== void 0) {
|
|
4617
5350
|
if (existing?.wrappedDeks !== void 0 && existing.wrappedDeks.length > 0) {
|
|
4618
|
-
const
|
|
5351
|
+
const early = ahead.unwrapped;
|
|
5352
|
+
const aead = early !== void 0 && sameKeys(early.wrapped, existing.wrappedDeks) ? await early.aead : await options.keystore.openDek(existing.wrappedDeks);
|
|
4619
5353
|
crypto = { aead, aadFor: (scope) => aadFor(key, key.generation, scope) };
|
|
4620
5354
|
} else if (existing !== null && existing.currentGen !== null) {
|
|
4621
5355
|
if (options.requireEncryption === true) {
|
|
4622
5356
|
throw new ValidationError(
|
|
4623
|
-
`requireEncryption: segment "${
|
|
5357
|
+
`requireEncryption: segment "${ref.segment}" already has generation ${existing.currentGen} in cleartext, so this load cannot be encrypted \u2014 encryption is chosen when a segment is first loaded. Load into a new segment with the keystore wired, then drop this one.`
|
|
4624
5358
|
);
|
|
4625
5359
|
}
|
|
4626
5360
|
} else {
|
|
@@ -4680,6 +5414,52 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
4680
5414
|
fingerprint
|
|
4681
5415
|
};
|
|
4682
5416
|
}
|
|
5417
|
+
function* rangeCheckedAtWrite(chunks) {
|
|
5418
|
+
for (const chunk of chunks) {
|
|
5419
|
+
const max = chunk.bitmap.maximum?.();
|
|
5420
|
+
if (max !== void 0 && max > MAX_REMAINDER) {
|
|
5421
|
+
throw new ValidationError(
|
|
5422
|
+
`chunk ${chunk.chunkKey} holds ${max}, outside the 16-bit range [0, ${MAX_REMAINDER}]`
|
|
5423
|
+
);
|
|
5424
|
+
}
|
|
5425
|
+
yield chunk;
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
async function collectChunks(input, codec, clock) {
|
|
5429
|
+
const chunks = [];
|
|
5430
|
+
const tick = yieldEvery(clock);
|
|
5431
|
+
let last = -1;
|
|
5432
|
+
for await (const chunk of input.chunks) {
|
|
5433
|
+
if (typeof chunk !== "object" || chunk === null) {
|
|
5434
|
+
throw new ValidationError(
|
|
5435
|
+
`a chunk must be an object with a chunkKey and a bitmap; got ${String(chunk)}`
|
|
5436
|
+
);
|
|
5437
|
+
}
|
|
5438
|
+
const { chunkKey, bitmap } = chunk;
|
|
5439
|
+
if (codec.owns?.(bitmap) !== true) {
|
|
5440
|
+
throw new ValidationError(
|
|
5441
|
+
`chunk ${String(chunkKey)}: its bitmap is not one this load's codec made`
|
|
5442
|
+
);
|
|
5443
|
+
}
|
|
5444
|
+
if (!Number.isInteger(chunkKey) || chunkKey < 0 || chunkKey > MAX_REMAINDER) {
|
|
5445
|
+
throw new ValidationError(`a chunk's key must be an integer in [0, 65535]; got ${chunkKey}`);
|
|
5446
|
+
}
|
|
5447
|
+
if (chunkKey <= last) {
|
|
5448
|
+
throw new ValidationError(`chunk ${chunkKey} does not ascend: the last was ${last}`);
|
|
5449
|
+
}
|
|
5450
|
+
last = chunkKey;
|
|
5451
|
+
const max = bitmap.maximum?.();
|
|
5452
|
+
if (max !== void 0 && max > MAX_REMAINDER) {
|
|
5453
|
+
throw new ValidationError(
|
|
5454
|
+
`chunk ${chunkKey} holds ${max}, outside the 16-bit range [0, ${MAX_REMAINDER}]`
|
|
5455
|
+
);
|
|
5456
|
+
}
|
|
5457
|
+
chunks.push({ chunkKey, bitmap });
|
|
5458
|
+
const pause = tick();
|
|
5459
|
+
if (pause !== null) await pause;
|
|
5460
|
+
}
|
|
5461
|
+
return chunks;
|
|
5462
|
+
}
|
|
4683
5463
|
async function bucketIds(ids, codec, clock) {
|
|
4684
5464
|
const byChunk = /* @__PURE__ */ new Map();
|
|
4685
5465
|
const pendingByChunk = /* @__PURE__ */ new Map();
|
|
@@ -4727,8 +5507,12 @@ async function bucketIds(ids, codec, clock) {
|
|
|
4727
5507
|
function openGenerationReader(storage, key, crypto, options = {}) {
|
|
4728
5508
|
return openChecked(storage, key, { ...options, crypto });
|
|
4729
5509
|
}
|
|
4730
|
-
async function openChecked(storage, key, options) {
|
|
4731
|
-
const reader = await
|
|
5510
|
+
async function openChecked(storage, key, options, keepChunkBytesUpTo) {
|
|
5511
|
+
const reader = await openCrbmReaderKeeping(
|
|
5512
|
+
storageBlobReader(storage, key),
|
|
5513
|
+
options,
|
|
5514
|
+
keepChunkBytesUpTo
|
|
5515
|
+
);
|
|
4732
5516
|
if (reader.generation !== key.generation) {
|
|
4733
5517
|
throw new IntegrityError(
|
|
4734
5518
|
`segment "${key.segment}" generation ${key.generation}: its footer says generation ${reader.generation}`
|
|
@@ -4789,6 +5573,30 @@ var PinnedStorageChunkSource = class {
|
|
|
4789
5573
|
if (pin === void 0) return this.inner.getChunk(ref);
|
|
4790
5574
|
return pin.generation === null ? Promise.resolve(null) : this.inner.getChunkAt(ref, pin.generation, heldBy(pin));
|
|
4791
5575
|
}
|
|
5576
|
+
/**
|
|
5577
|
+
* A pinned segment's chunks come from the pinned generation alone, under the version a pinned read keys by
|
|
5578
|
+
* ({@link currentVersion}); another segment's go to the live source, which answers with its own.
|
|
5579
|
+
*/
|
|
5580
|
+
getChunks(ref, keys, options) {
|
|
5581
|
+
const pin = this.pinFor(ref);
|
|
5582
|
+
if (pin === void 0) return this.inner.getChunks(ref, keys, options);
|
|
5583
|
+
return this.pinnedChunks(ref, pin, keys, options);
|
|
5584
|
+
}
|
|
5585
|
+
async *pinnedChunks(ref, pin, keys, options) {
|
|
5586
|
+
if (pin.generation === null) {
|
|
5587
|
+
for (const key of keys) yield { key, bytes: null, version: null };
|
|
5588
|
+
return;
|
|
5589
|
+
}
|
|
5590
|
+
const version = await this.currentVersion(ref);
|
|
5591
|
+
const pull = new ItemPull(
|
|
5592
|
+
this.inner.getChunksAt(ref, pin.generation, keys, heldBy(pin), options)
|
|
5593
|
+
);
|
|
5594
|
+
try {
|
|
5595
|
+
while (await pull.advance()) yield pull.take((chunk) => ({ ...chunk, version }));
|
|
5596
|
+
} finally {
|
|
5597
|
+
await pull.close();
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
4792
5600
|
listChunkKeys(ref) {
|
|
4793
5601
|
const pin = this.pinFor(ref);
|
|
4794
5602
|
if (pin === void 0) return this.inner.listChunkKeys(ref);
|
|
@@ -4891,7 +5699,11 @@ async function withRetry(op, policy, deps) {
|
|
|
4891
5699
|
}
|
|
4892
5700
|
|
|
4893
5701
|
// src/core/load.ts
|
|
4894
|
-
|
|
5702
|
+
function settled(promise) {
|
|
5703
|
+
promise.catch(() => void 0);
|
|
5704
|
+
return promise;
|
|
5705
|
+
}
|
|
5706
|
+
async function currentCardinality(ref, deps, record, unwrapped) {
|
|
4895
5707
|
if (record === null || record.currentGen === null || record.status === "destroyed") {
|
|
4896
5708
|
return { cardinality: null, fromSummary: false };
|
|
4897
5709
|
}
|
|
@@ -4905,7 +5717,7 @@ async function currentCardinality(ref, deps, record) {
|
|
|
4905
5717
|
`segment "${ref.segment}" is encrypted but load was given no keystore`
|
|
4906
5718
|
);
|
|
4907
5719
|
}
|
|
4908
|
-
aead = await deps.keystore.openDek(wrapped);
|
|
5720
|
+
aead = await (unwrapped ?? deps.keystore.openDek(wrapped));
|
|
4909
5721
|
const opened = aead;
|
|
4910
5722
|
crypto = { aead: opened, aadFor: (scope) => aadFor(ref, generation, scope) };
|
|
4911
5723
|
}
|
|
@@ -4925,7 +5737,25 @@ async function currentCardinality(ref, deps, record) {
|
|
|
4925
5737
|
for (const n of reader.cardinalities().values()) total += n;
|
|
4926
5738
|
return { cardinality: total, fromSummary: false };
|
|
4927
5739
|
}
|
|
4928
|
-
|
|
5740
|
+
function loadSegment(ref, input, deps, options = {}) {
|
|
5741
|
+
return runLoad(ref, (codec) => prepareLoadInput(input, codec), deps, options);
|
|
5742
|
+
}
|
|
5743
|
+
function loadSegmentChunks(ref, chunks, deps, options = {}) {
|
|
5744
|
+
return runLoad(
|
|
5745
|
+
ref,
|
|
5746
|
+
() => {
|
|
5747
|
+
if (typeof chunks !== "object" || chunks === null || !(Symbol.asyncIterator in chunks)) {
|
|
5748
|
+
throw new ValidationError(
|
|
5749
|
+
"loadSegmentChunks takes an async iterable of { chunkKey, bitmap }"
|
|
5750
|
+
);
|
|
5751
|
+
}
|
|
5752
|
+
return new ChunkLoadInput(chunks);
|
|
5753
|
+
},
|
|
5754
|
+
deps,
|
|
5755
|
+
options
|
|
5756
|
+
);
|
|
5757
|
+
}
|
|
5758
|
+
async function runLoad(ref, prepare, deps, options) {
|
|
4929
5759
|
validateUserRef(ref);
|
|
4930
5760
|
const codec = requireCodec(deps.codec, "loadSegment");
|
|
4931
5761
|
const keep = options.keep ?? 1;
|
|
@@ -4951,40 +5781,49 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
4951
5781
|
throw new ValidationError(message);
|
|
4952
5782
|
});
|
|
4953
5783
|
const audit = safeAudit(options.audit ?? NOOP_AUDIT);
|
|
4954
|
-
const ids =
|
|
5784
|
+
const ids = prepare(codec);
|
|
4955
5785
|
assertRegistryCanWrite(deps.registry, "load");
|
|
4956
5786
|
const row = await deps.registry.get(ref);
|
|
4957
5787
|
const fromToken = row?.token;
|
|
4958
5788
|
const fromGeneration = row?.currentGen ?? void 0;
|
|
5789
|
+
const wrappedKeys = row?.wrappedDeks;
|
|
5790
|
+
const keystore = deps.keystore;
|
|
5791
|
+
const unwrapped = row !== null && row.status !== "destroyed" && wrappedKeys !== void 0 && wrappedKeys.length > 0 && keystore !== void 0 ? { wrapped: wrappedKeys, aead: settled((async () => keystore.openDek(wrappedKeys))()) } : void 0;
|
|
4959
5792
|
const needsBefore = guard?.minRetained !== void 0 || options.allowEmpty !== true;
|
|
4960
|
-
const current = needsBefore ? await currentCardinality(ref, deps, row) : { cardinality: null, fromSummary: false };
|
|
5793
|
+
const current = needsBefore ? await currentCardinality(ref, deps, row, unwrapped?.aead) : { cardinality: null, fromSummary: false };
|
|
4961
5794
|
const before = current.cardinality;
|
|
4962
|
-
const
|
|
4963
|
-
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
5795
|
+
const numbering = settled(nextLoadGeneration(ref, deps, row));
|
|
4964
5796
|
let written;
|
|
4965
5797
|
try {
|
|
4966
|
-
written = await
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
5798
|
+
written = await bulkLoadAhead(
|
|
5799
|
+
deps.storage,
|
|
5800
|
+
ref,
|
|
5801
|
+
{ generation: settled(numbering.then((n) => n.generation)), unwrapped },
|
|
5802
|
+
ids,
|
|
5803
|
+
{
|
|
5804
|
+
registry: deps.registry,
|
|
5805
|
+
publish: false,
|
|
5806
|
+
keystore: deps.keystore,
|
|
5807
|
+
requireEncryption: deps.requireEncryption,
|
|
5808
|
+
codec: deps.codec,
|
|
5809
|
+
clock: deps.clock,
|
|
5810
|
+
metadata,
|
|
5811
|
+
row
|
|
5812
|
+
}
|
|
5813
|
+
);
|
|
4976
5814
|
} catch (err) {
|
|
4977
5815
|
if (!isWriteConflictError(err)) throw err;
|
|
5816
|
+
const { generation: generation2 } = await numbering;
|
|
4978
5817
|
audit.onEvent({
|
|
4979
5818
|
kind: "segment.load-refused",
|
|
4980
5819
|
segment: ref.segment,
|
|
4981
5820
|
namespace: ref.namespace,
|
|
4982
|
-
generation,
|
|
5821
|
+
generation: generation2,
|
|
4983
5822
|
reason: "superseded",
|
|
4984
5823
|
cardinality: 0
|
|
4985
5824
|
});
|
|
4986
5825
|
return {
|
|
4987
|
-
generation,
|
|
5826
|
+
generation: generation2,
|
|
4988
5827
|
published: false,
|
|
4989
5828
|
reason: "superseded",
|
|
4990
5829
|
size: 0,
|
|
@@ -4995,6 +5834,8 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
4995
5834
|
collected: []
|
|
4996
5835
|
};
|
|
4997
5836
|
}
|
|
5837
|
+
const { generation, checked } = await numbering;
|
|
5838
|
+
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
4998
5839
|
const reclaim = async () => {
|
|
4999
5840
|
const now = await deps.registry.get(ref);
|
|
5000
5841
|
if (now !== null && (now.token === fromToken || now.status === "destroyed")) {
|
|
@@ -5235,6 +6076,7 @@ function describingSummary(record) {
|
|
|
5235
6076
|
}
|
|
5236
6077
|
|
|
5237
6078
|
// src/core/erase-id.ts
|
|
6079
|
+
var HOLDS_CONCURRENCY = 4;
|
|
5238
6080
|
async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
5239
6081
|
validateUserRef(ref);
|
|
5240
6082
|
const { chunkKey, remainder } = splitId(id);
|
|
@@ -5296,13 +6138,37 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
5296
6138
|
throw err;
|
|
5297
6139
|
}
|
|
5298
6140
|
};
|
|
6141
|
+
const holdsEach = async (generations, stopAtHolder) => {
|
|
6142
|
+
let firstHolder = Infinity;
|
|
6143
|
+
const settled2 = await mapWithConcurrency(
|
|
6144
|
+
generations,
|
|
6145
|
+
HOLDS_CONCURRENCY,
|
|
6146
|
+
async (generation2, index) => {
|
|
6147
|
+
if (stopAtHolder && index > firstHolder) {
|
|
6148
|
+
return { generation: generation2, held: null, fault: void 0 };
|
|
6149
|
+
}
|
|
6150
|
+
try {
|
|
6151
|
+
const held = await holds(generation2);
|
|
6152
|
+
if (held === true && index < firstHolder) firstHolder = index;
|
|
6153
|
+
return { generation: generation2, held, fault: void 0 };
|
|
6154
|
+
} catch (fault) {
|
|
6155
|
+
return { generation: generation2, held: null, fault: { error: fault } };
|
|
6156
|
+
}
|
|
6157
|
+
}
|
|
6158
|
+
);
|
|
6159
|
+
const outcomes = [];
|
|
6160
|
+
for (const { generation: generation2, held, fault } of settled2) {
|
|
6161
|
+
if (fault !== void 0) throw fault.error;
|
|
6162
|
+
outcomes.push({ generation: generation2, held });
|
|
6163
|
+
if (stopAtHolder && held === true) break;
|
|
6164
|
+
}
|
|
6165
|
+
return outcomes;
|
|
6166
|
+
};
|
|
5299
6167
|
const holderLeft = async (clean) => {
|
|
5300
6168
|
const present = /* @__PURE__ */ new Set();
|
|
5301
6169
|
for await (const key2 of deps.storage.list(ref)) present.add(key2.generation);
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
}
|
|
5305
|
-
return void 0;
|
|
6170
|
+
const toRead = [...present].sort((a, b) => b - a).filter((generation2) => !clean.has(generation2));
|
|
6171
|
+
return (await holdsEach(toRead, true)).find((outcome) => outcome.held === true)?.generation;
|
|
5306
6172
|
};
|
|
5307
6173
|
const cannotRemove = (generation2) => new WriteConflictError(
|
|
5308
6174
|
`erasure of segment ${ref.segment} could not remove generation ${generation2}, which still holds the id and is still in the bucket; re-run`
|
|
@@ -5323,20 +6189,21 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
5323
6189
|
const newestFirst = [...others].sort((a, b) => b - a);
|
|
5324
6190
|
const clean = /* @__PURE__ */ new Set([from]);
|
|
5325
6191
|
const holdersAbove = [];
|
|
5326
|
-
for (const
|
|
5327
|
-
|
|
6192
|
+
for (const { generation: generation2, held } of await holdsEach(
|
|
6193
|
+
newestFirst.filter((g) => g > from),
|
|
6194
|
+
false
|
|
6195
|
+
)) {
|
|
5328
6196
|
if (held === true) holdersAbove.push(generation2);
|
|
5329
6197
|
else if (held === false) clean.add(generation2);
|
|
5330
6198
|
}
|
|
5331
6199
|
let holderBelow;
|
|
5332
6200
|
if (holdersAbove.length === 0) {
|
|
5333
|
-
for (const
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
if (held === false) clean.add(generation2);
|
|
6201
|
+
for (const { generation: generation2, held } of await holdsEach(
|
|
6202
|
+
newestFirst.filter((g) => g < from),
|
|
6203
|
+
true
|
|
6204
|
+
)) {
|
|
6205
|
+
if (held === true) holderBelow = generation2;
|
|
6206
|
+
else if (held === false) clean.add(generation2);
|
|
5340
6207
|
}
|
|
5341
6208
|
}
|
|
5342
6209
|
const newest = holdersAbove[0] ?? holderBelow;
|
|
@@ -5451,25 +6318,31 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
5451
6318
|
if (left !== void 0) throw cannotRemove(left);
|
|
5452
6319
|
return { ...base, erased: true, fromGeneration: from, generation, collected };
|
|
5453
6320
|
}
|
|
5454
|
-
var
|
|
6321
|
+
var REWRITE_RANGES_AHEAD = 4;
|
|
5455
6322
|
async function* rewrite(reader, chunkKey, replacement, codec, maxBytes, read) {
|
|
5456
6323
|
const keys = [...reader.chunkKeys()].sort((a, b) => a - b);
|
|
5457
|
-
const
|
|
6324
|
+
const stream = reader.readChunks(
|
|
5458
6325
|
keys.filter((k) => k !== chunkKey),
|
|
5459
|
-
|
|
5460
|
-
REWRITE_READ_AHEAD,
|
|
5461
|
-
false
|
|
6326
|
+
{ concurrency: REWRITE_RANGES_AHEAD, readRange: read }
|
|
5462
6327
|
);
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
6328
|
+
try {
|
|
6329
|
+
for (const k of keys) {
|
|
6330
|
+
if (k === chunkKey) {
|
|
6331
|
+
yield { chunkKey: k, bitmap: replacement };
|
|
6332
|
+
continue;
|
|
6333
|
+
}
|
|
6334
|
+
const item = await stream.next();
|
|
6335
|
+
if (item.done === true || item.value.key !== k) {
|
|
6336
|
+
throw new IntegrityError(`the chunk stream ended before chunk ${k}`);
|
|
6337
|
+
}
|
|
6338
|
+
const bytes = item.value.bytes;
|
|
6339
|
+
if (bytes === null) continue;
|
|
6340
|
+
const bitmap = codec.safeDeserialize(bytes, maxBytes);
|
|
6341
|
+
assertRemaindersInRange(bitmap, k);
|
|
6342
|
+
yield { chunkKey: k, bitmap };
|
|
5467
6343
|
}
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
const bitmap = codec.safeDeserialize(bytes, maxBytes);
|
|
5471
|
-
assertRemaindersInRange(bitmap, k);
|
|
5472
|
-
yield { chunkKey: k, bitmap };
|
|
6344
|
+
} finally {
|
|
6345
|
+
await stream.return(void 0);
|
|
5473
6346
|
}
|
|
5474
6347
|
}
|
|
5475
6348
|
function assertRemaindersInRange(bitmap, chunkKey) {
|
|
@@ -5624,9 +6497,8 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
5624
6497
|
segment: rec.segment
|
|
5625
6498
|
}));
|
|
5626
6499
|
const audit = safeAudit(options.audit ?? NOOP_AUDIT);
|
|
5627
|
-
const destroyed = [];
|
|
5628
6500
|
let segmentsShredded = 0;
|
|
5629
|
-
|
|
6501
|
+
const destroyed = await mapWithConcurrency(refs, ERASE_CONCURRENCY, async (ref) => {
|
|
5630
6502
|
let result;
|
|
5631
6503
|
try {
|
|
5632
6504
|
result = await shredSegment(ref, deps, options.allowCleartext ?? false);
|
|
@@ -5639,12 +6511,12 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
5639
6511
|
reason: isWriteConflictError(err) ? "contended" : `failed: ${err instanceof Error ? err.message : String(err)}`
|
|
5640
6512
|
};
|
|
5641
6513
|
}
|
|
5642
|
-
destroyed.push(result);
|
|
5643
6514
|
if (result.cryptoShredded) {
|
|
5644
6515
|
segmentsShredded += 1;
|
|
5645
6516
|
audit.onEvent({ kind: "segment.erase", namespace: ref.namespace, segment: ref.segment });
|
|
5646
6517
|
}
|
|
5647
|
-
|
|
6518
|
+
return result;
|
|
6519
|
+
});
|
|
5648
6520
|
audit.onEvent({ kind: "namespace.erase", namespace, segmentsShredded });
|
|
5649
6521
|
return { destroyed };
|
|
5650
6522
|
}
|
|
@@ -6383,6 +7255,7 @@ var RetryingStorageChunkSource = class {
|
|
|
6383
7255
|
deps;
|
|
6384
7256
|
/** Present only when the inner source supports it — so capability detection stays honest. */
|
|
6385
7257
|
sizeOf;
|
|
7258
|
+
getChunks;
|
|
6386
7259
|
cardinalities;
|
|
6387
7260
|
summary;
|
|
6388
7261
|
currentGeneration;
|
|
@@ -6405,6 +7278,16 @@ var RetryingStorageChunkSource = class {
|
|
|
6405
7278
|
if (innerSizeOf) {
|
|
6406
7279
|
this.sizeOf = (ref) => withRetry(() => innerSizeOf.call(inner, ref), this.policy, this.deps);
|
|
6407
7280
|
}
|
|
7281
|
+
const innerGetChunks = inner.getChunks;
|
|
7282
|
+
if (innerGetChunks) {
|
|
7283
|
+
this.getChunks = (ref, keys, options) => {
|
|
7284
|
+
const outer = options?.retry;
|
|
7285
|
+
return innerGetChunks.call(inner, ref, keys, {
|
|
7286
|
+
...options,
|
|
7287
|
+
retry: (request) => withRetry(outer === void 0 ? request : () => outer(request), this.policy, this.deps)
|
|
7288
|
+
});
|
|
7289
|
+
};
|
|
7290
|
+
}
|
|
6408
7291
|
const innerCardinalities = inner.cardinalities;
|
|
6409
7292
|
if (innerCardinalities) {
|
|
6410
7293
|
this.cardinalities = (ref) => withRetry(() => innerCardinalities.call(inner, ref), this.policy, this.deps);
|
|
@@ -6492,6 +7375,7 @@ export {
|
|
|
6492
7375
|
listGenerations,
|
|
6493
7376
|
listSegments,
|
|
6494
7377
|
loadSegment,
|
|
7378
|
+
loadSegmentChunks,
|
|
6495
7379
|
mapWithConcurrency,
|
|
6496
7380
|
readRetentionPolicy,
|
|
6497
7381
|
resolveBudget,
|