@cloudbitmaps/core 0.13.0 → 0.15.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-B7IGS3W6.js} +8 -1
- package/dist/chunk-B7IGS3W6.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 +11 -0
- package/dist/core/cost.d.ts +12 -7
- 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 +49 -5
- package/dist/core/engine.d.ts +96 -14
- package/dist/core/item-pull.d.ts +29 -0
- package/dist/core/metrics.d.ts +13 -6
- package/dist/core/pinned-storage-source.d.ts +7 -1
- package/dist/core/ports.d.ts +69 -0
- package/dist/core/validate.d.ts +5 -0
- package/dist/driver-kit.js +2 -2
- package/dist/driver-kit.js.map +1 -1
- package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +951 -145
- 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-B7IGS3W6.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;
|
|
@@ -372,6 +414,65 @@ function keysWithin(keys, w) {
|
|
|
372
414
|
};
|
|
373
415
|
return keys.slice(firstAtLeast(w.loKey), firstAtLeast(w.hiKey + 1));
|
|
374
416
|
}
|
|
417
|
+
var CombineWindow = class {
|
|
418
|
+
constructor(keys, limit, read) {
|
|
419
|
+
this.keys = keys;
|
|
420
|
+
this.limit = limit;
|
|
421
|
+
this.read = read;
|
|
422
|
+
this.fill();
|
|
423
|
+
}
|
|
424
|
+
keys;
|
|
425
|
+
limit;
|
|
426
|
+
read;
|
|
427
|
+
inFlight = [];
|
|
428
|
+
next_ = 0;
|
|
429
|
+
taken = 0;
|
|
430
|
+
fill() {
|
|
431
|
+
const width = Math.min(
|
|
432
|
+
this.limit,
|
|
433
|
+
Math.max(COMBINE_WINDOW_START, 2 ** Math.min(this.taken, 30))
|
|
434
|
+
);
|
|
435
|
+
while (this.next_ < this.keys.length && this.inFlight.length < width) {
|
|
436
|
+
const key = this.keys[this.next_++];
|
|
437
|
+
this.inFlight.push(
|
|
438
|
+
this.read(key).then(
|
|
439
|
+
(result) => ({ key, result }),
|
|
440
|
+
(error) => ({ key, result: null, error })
|
|
441
|
+
)
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/** The next key's chunk in ascending key order, `undefined` when the keys are used up; throws that key's error. */
|
|
446
|
+
async next() {
|
|
447
|
+
if (this.inFlight.length === 0) return void 0;
|
|
448
|
+
const slot = await this.inFlight.shift();
|
|
449
|
+
this.taken += 1;
|
|
450
|
+
this.fill();
|
|
451
|
+
if (slot.error !== void 0) throw slot.error;
|
|
452
|
+
return slot;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
function chunkIds(chunk, chunkKey, w) {
|
|
456
|
+
let rem = chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
|
|
457
|
+
if (w !== null && isEdge(chunkKey, w)) {
|
|
458
|
+
const from = chunkKey === w.loKey ? w.loRem : 0;
|
|
459
|
+
const to = chunkKey === w.hiKey ? w.hiRem : MAX_REMAINDER;
|
|
460
|
+
rem = rem.slice(firstAbove(rem, from - 1), firstAbove(rem, to));
|
|
461
|
+
}
|
|
462
|
+
const base = chunkKey * CHUNK_COUNT;
|
|
463
|
+
for (let i = 0; i < rem.length; i++) rem[i] = base + (rem[i] & MAX_REMAINDER);
|
|
464
|
+
return rem;
|
|
465
|
+
}
|
|
466
|
+
function firstAbove(a, value) {
|
|
467
|
+
let lo = 0;
|
|
468
|
+
let hi = a.length;
|
|
469
|
+
while (lo < hi) {
|
|
470
|
+
const mid = lo + hi >>> 1;
|
|
471
|
+
if (a[mid] <= value) lo = mid + 1;
|
|
472
|
+
else hi = mid;
|
|
473
|
+
}
|
|
474
|
+
return lo;
|
|
475
|
+
}
|
|
375
476
|
var SegmentEngine = class {
|
|
376
477
|
storage;
|
|
377
478
|
cache;
|
|
@@ -388,6 +489,11 @@ var SegmentEngine = class {
|
|
|
388
489
|
* promise per distinct key in flight.
|
|
389
490
|
*/
|
|
390
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();
|
|
391
497
|
constructor(deps) {
|
|
392
498
|
this.storage = deps.storage;
|
|
393
499
|
this.cache = deps.cache;
|
|
@@ -468,9 +574,27 @@ var SegmentEngine = class {
|
|
|
468
574
|
);
|
|
469
575
|
}
|
|
470
576
|
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
577
|
+
* The chunks of `seg` at `chunkKeys`, taken in ascending key order by a read that may stop at any point: one stream of
|
|
578
|
+
* coalesced ranges when the source has `getChunks`, opening 1, 2, 4 … ranges wide up to {@link DEFAULT_INTERSECT_CONCURRENCY},
|
|
579
|
+
* else the chunk-by-chunk window of that many reads. `close` stops what is left of it.
|
|
580
|
+
*/
|
|
581
|
+
chunkSequence(seg, chunkKeys, gen) {
|
|
582
|
+
const streamed = this.openStreamed(seg, chunkKeys, gen, DEFAULT_INTERSECT_CONCURRENCY, 1);
|
|
583
|
+
if (streamed !== void 0) {
|
|
584
|
+
return {
|
|
585
|
+
take: (chunkKey) => this.streamedChunk(streamed, chunkKey),
|
|
586
|
+
close: () => this.closeStreamed(streamed)
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const window = this.chunkWindow(seg, chunkKeys, gen, true);
|
|
590
|
+
return { take: () => window.take(), close: () => void 0 };
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Every id, ascending, reading the chunks as one stream of coalesced ranges (chunks that sit near each other are one
|
|
594
|
+
* request) that opens 1, 2, 4 … 32 ranges wide, so a read that stops early has asked for only a few ranges past the
|
|
595
|
+
* one it stopped in (each at most 1 MiB); with `range`, only the ids in `(after, through]`, fetching only the
|
|
596
|
+
* chunks the range overlaps (see {@link IdRange}). A source that reads chunk by chunk is read through a window of up to
|
|
597
|
+
* 32 chunk fetches that opens 1, 2, 4 … 32 wide.
|
|
474
598
|
*
|
|
475
599
|
* Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
|
|
476
600
|
* every id of a read that asked for no range paid for it (measured at about 5% per id). The full read keeps the
|
|
@@ -483,11 +607,15 @@ var SegmentEngine = class {
|
|
|
483
607
|
const chunkKeys = await this.chunkKeys(seg);
|
|
484
608
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
485
609
|
const gen = await this.cacheVersion(seg);
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
610
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen);
|
|
611
|
+
try {
|
|
612
|
+
for (const chunkKey of chunkKeys) {
|
|
613
|
+
const chunk = await chunks.take(chunkKey);
|
|
614
|
+
if (chunk === null) continue;
|
|
615
|
+
for (const remainder of chunk) yield joinId(chunkKey, remainder);
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
chunks.close();
|
|
491
619
|
}
|
|
492
620
|
}
|
|
493
621
|
async *iterateRange(seg, range) {
|
|
@@ -496,15 +624,43 @@ var SegmentEngine = class {
|
|
|
496
624
|
const chunkKeys = keysWithin(await this.chunkKeys(seg), w);
|
|
497
625
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
498
626
|
const gen = await this.cacheVersion(seg);
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
627
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen);
|
|
628
|
+
try {
|
|
629
|
+
for (const chunkKey of chunkKeys) {
|
|
630
|
+
const chunk = await chunks.take(chunkKey);
|
|
631
|
+
if (chunk === null) continue;
|
|
632
|
+
if (isEdge(chunkKey, w)) {
|
|
633
|
+
for (const id of edgeIds(chunk, chunkKey, w)) yield id;
|
|
634
|
+
} else {
|
|
635
|
+
for (const remainder of chunk) yield joinId(chunkKey, remainder);
|
|
636
|
+
}
|
|
507
637
|
}
|
|
638
|
+
} finally {
|
|
639
|
+
chunks.close();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* {@link iterate}, one `Uint32Array` per chunk instead of one id at a time: the same chunks, in the same order, with
|
|
644
|
+
* the same ids (a range cut at its edges the same way), and no array for a chunk that holds none of them. The
|
|
645
|
+
* reads, the budget charge and the window are the per-id read's. Each array is at most 65,536 ids (256 KiB) and is
|
|
646
|
+
* the caller's to keep.
|
|
647
|
+
*/
|
|
648
|
+
async *iterateBatches(seg, range) {
|
|
649
|
+
const w = windowOf(range);
|
|
650
|
+
if (w === "empty") return;
|
|
651
|
+
const chunkKeys = w === null ? await this.chunkKeys(seg) : keysWithin(await this.chunkKeys(seg), w);
|
|
652
|
+
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
653
|
+
const gen = await this.cacheVersion(seg);
|
|
654
|
+
const chunks = this.chunkSequence(seg, chunkKeys, gen);
|
|
655
|
+
try {
|
|
656
|
+
for (const chunkKey of chunkKeys) {
|
|
657
|
+
const chunk = await chunks.take(chunkKey);
|
|
658
|
+
if (chunk === null) continue;
|
|
659
|
+
const ids = chunkIds(chunk, chunkKey, w);
|
|
660
|
+
if (ids.length > 0) yield ids;
|
|
661
|
+
}
|
|
662
|
+
} finally {
|
|
663
|
+
chunks.close();
|
|
508
664
|
}
|
|
509
665
|
}
|
|
510
666
|
/**
|
|
@@ -521,11 +677,11 @@ var SegmentEngine = class {
|
|
|
521
677
|
*
|
|
522
678
|
* Chunk-skipping intersection. Aligns each segment's chunk-key set (read from the `.crbm` index, no payload),
|
|
523
679
|
* keeps only keys present in **all** operands (a key missing from any operand can't contribute → its Storage
|
|
524
|
-
* chunks are never fetched — the core saving), then
|
|
525
|
-
*
|
|
680
|
+
* chunks are never fetched — the core saving), then reads each operand's surviving chunks as one stream of coalesced
|
|
681
|
+
* ranges and hands them, key by key, to the codec for the AND, streaming results through a bounded window.
|
|
526
682
|
*
|
|
527
|
-
* **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk
|
|
528
|
-
* segment size — that's the Lambda-friendly property.
|
|
683
|
+
* **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk`, a range being
|
|
684
|
+
* at most a chunk's cap of 1 MiB), not by segment size — that's the Lambda-friendly property.
|
|
529
685
|
*
|
|
530
686
|
* Generation-consistent within the call (normal case): each operand's current generation is resolved **once**
|
|
531
687
|
* up front (before the fan-out) and threaded into every chunk read, so a concurrent load can't corrupt or tear
|
|
@@ -536,7 +692,9 @@ var SegmentEngine = class {
|
|
|
536
692
|
* waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
|
|
537
693
|
* (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
|
|
538
694
|
* heals the read forward; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make
|
|
539
|
-
* and `invalidate()` makes on request. Still whole/immutable per read, never torn.
|
|
695
|
+
* and `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
|
|
696
|
+
* segment again before it serves each chunk, so each of those moves a running read as it would a read of one chunk, and
|
|
697
|
+
* the ranges it had requested of the earlier generation are dropped, not served.
|
|
540
698
|
*/
|
|
541
699
|
intersect(segs, options) {
|
|
542
700
|
return this.combine(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
@@ -561,7 +719,24 @@ var SegmentEngine = class {
|
|
|
561
719
|
andNot(seg, excludes, options) {
|
|
562
720
|
return this.combine([seg], excludes, "all", "andNot", options);
|
|
563
721
|
}
|
|
564
|
-
|
|
722
|
+
/** {@link intersect}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
|
|
723
|
+
intersectBatches(segs, options) {
|
|
724
|
+
return this.combineBatches(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
725
|
+
}
|
|
726
|
+
/** {@link union}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
|
|
727
|
+
unionBatches(segs, options) {
|
|
728
|
+
return this.combineBatches(segs, options?.exclude ?? [], "any", "union", options);
|
|
729
|
+
}
|
|
730
|
+
/** {@link andNot}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
|
|
731
|
+
andNotBatches(seg, excludes, options) {
|
|
732
|
+
return this.combineBatches([seg], excludes, "all", "andNot", options);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
|
|
736
|
+
* absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
|
|
737
|
+
* read and the batch read share it, so the two cannot disagree about what is read or charged.
|
|
738
|
+
*/
|
|
739
|
+
async combinePlan(segs, excludeSegs, mode, op, options) {
|
|
565
740
|
if (segs.length === 0) {
|
|
566
741
|
throw new ValidationError(`${op} requires at least one segment`);
|
|
567
742
|
}
|
|
@@ -576,7 +751,7 @@ var SegmentEngine = class {
|
|
|
576
751
|
}
|
|
577
752
|
const budget = resolvePerOpBudget(options?.budget, this.budget);
|
|
578
753
|
const w = windowOf(options);
|
|
579
|
-
if (w === "empty") return;
|
|
754
|
+
if (w === "empty") return null;
|
|
580
755
|
const extract = async (seg) => {
|
|
581
756
|
const all = await this.chunkKeys(seg);
|
|
582
757
|
const gen = await this.cacheVersion(seg);
|
|
@@ -621,31 +796,61 @@ var SegmentEngine = class {
|
|
|
621
796
|
skippedChunks: distinctKeys.size - common.length
|
|
622
797
|
});
|
|
623
798
|
}
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
799
|
+
const streams = [];
|
|
800
|
+
const streamExcludes = mode === "any" || operands.length === 1;
|
|
801
|
+
const open4 = (o, wanted) => {
|
|
802
|
+
o.streamed = this.openStreamed(
|
|
803
|
+
o.seg,
|
|
804
|
+
common.filter(wanted),
|
|
805
|
+
o.gen,
|
|
806
|
+
limit,
|
|
807
|
+
COMBINE_RANGE_START
|
|
808
|
+
);
|
|
809
|
+
if (o.streamed?.stream) streams.push(o.streamed);
|
|
635
810
|
};
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
811
|
+
for (const o of operands) open4(o, mode === "all" ? () => true : (k) => o.keys.has(k));
|
|
812
|
+
if (streamExcludes) for (const e of excludes) open4(e, (k) => e.keys.has(k));
|
|
813
|
+
return {
|
|
814
|
+
w,
|
|
815
|
+
window: new CombineWindow(
|
|
816
|
+
common,
|
|
817
|
+
limit,
|
|
818
|
+
(key) => this.combineChunk(operands, excludes, mode, key)
|
|
819
|
+
),
|
|
820
|
+
streams
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
async *combine(segs, excludeSegs, mode, op, options) {
|
|
824
|
+
const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
|
|
825
|
+
if (plan === null) return;
|
|
826
|
+
const { w, window } = plan;
|
|
827
|
+
try {
|
|
828
|
+
for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
|
|
829
|
+
if (slot.result && !slot.result.isEmpty) {
|
|
830
|
+
if (w !== null && isEdge(slot.key, w)) {
|
|
831
|
+
for (const id of edgeIds(slot.result, slot.key, w)) yield id;
|
|
832
|
+
} else {
|
|
833
|
+
for (const remainder of slot.result) yield joinId(slot.key, remainder);
|
|
834
|
+
}
|
|
647
835
|
}
|
|
648
836
|
}
|
|
837
|
+
} finally {
|
|
838
|
+
for (const stream of plan.streams) this.closeStreamed(stream);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async *combineBatches(segs, excludeSegs, mode, op, options) {
|
|
842
|
+
const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
|
|
843
|
+
if (plan === null) return;
|
|
844
|
+
const { w, window } = plan;
|
|
845
|
+
try {
|
|
846
|
+
for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
|
|
847
|
+
if (slot.result) {
|
|
848
|
+
const ids = chunkIds(slot.result, slot.key, w);
|
|
849
|
+
if (ids.length > 0) yield ids;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
} finally {
|
|
853
|
+
for (const stream of plan.streams) this.closeStreamed(stream);
|
|
649
854
|
}
|
|
650
855
|
}
|
|
651
856
|
/**
|
|
@@ -663,9 +868,7 @@ var SegmentEngine = class {
|
|
|
663
868
|
const relevant = excludes.filter((e) => e.keys.has(chunkKey));
|
|
664
869
|
const together = relevant.length > 0 && (mode === "any" || present.length === 1);
|
|
665
870
|
const reads = together ? [...present, ...relevant] : present;
|
|
666
|
-
const fetched = await Promise.all(
|
|
667
|
-
reads.map((op) => this.storageChunk({ ...op.seg, chunkKey }, op.gen))
|
|
668
|
-
);
|
|
871
|
+
const fetched = await Promise.all(reads.map((op) => this.operandChunk(op, chunkKey)));
|
|
669
872
|
const chunks = together ? fetched.slice(0, present.length) : fetched;
|
|
670
873
|
if (mode === "all" && chunks.some((c) => c === null)) return null;
|
|
671
874
|
const first = chunks[0];
|
|
@@ -691,6 +894,92 @@ var SegmentEngine = class {
|
|
|
691
894
|
}
|
|
692
895
|
return acc;
|
|
693
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* One operand's chunk at `key`: from its stream when it has one, else a read of that chunk on its own. Not `async`,
|
|
899
|
+
* so a stream's take is made before anything is awaited: concurrent takes of one stream must be made in key order.
|
|
900
|
+
*/
|
|
901
|
+
operandChunk(op, chunkKey) {
|
|
902
|
+
return op.streamed === void 0 ? this.storageChunk({ ...op.seg, chunkKey }, op.gen) : this.streamedChunk(op.streamed, chunkKey);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Open `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
|
|
906
|
+
* `getChunks`, a segment with no generation, or nothing to read. The chunks already decoded in the cache are taken out
|
|
907
|
+
* of the key list first and served from there, so a repeat read asks the source for nothing it holds. Nothing is
|
|
908
|
+
* requested until the first chunk is asked for.
|
|
909
|
+
*/
|
|
910
|
+
openStreamed(seg, keys, gen, concurrency, rampStart) {
|
|
911
|
+
const getChunks = this.storage.getChunks;
|
|
912
|
+
if (getChunks === void 0 || gen === null || keys.length === 0) return void 0;
|
|
913
|
+
let wanted = keys;
|
|
914
|
+
let inStream;
|
|
915
|
+
if (this.cache) {
|
|
916
|
+
const misses = [];
|
|
917
|
+
for (const chunkKey of keys) {
|
|
918
|
+
const hit = this.cache.get(this.chunkCacheKey({ ...seg, chunkKey }, gen));
|
|
919
|
+
if (!hit) misses.push(chunkKey);
|
|
920
|
+
if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
|
|
921
|
+
}
|
|
922
|
+
wanted = misses;
|
|
923
|
+
if (misses.length < keys.length) inStream = new Set(misses);
|
|
924
|
+
}
|
|
925
|
+
const onRequest = this.metricsOn ? (request) => {
|
|
926
|
+
this.metrics.onEvent({
|
|
927
|
+
kind: "storage.get",
|
|
928
|
+
namespace: seg.namespace,
|
|
929
|
+
segment: seg.segment,
|
|
930
|
+
bytes: request.bytes,
|
|
931
|
+
ms: request.ms
|
|
932
|
+
});
|
|
933
|
+
} : void 0;
|
|
934
|
+
const stream = wanted.length === 0 ? void 0 : new ChunkStream(
|
|
935
|
+
getChunks.call(this.storage, seg, wanted, {
|
|
936
|
+
concurrency,
|
|
937
|
+
ramp: rampStart,
|
|
938
|
+
...onRequest === void 0 ? {} : { onRequest }
|
|
939
|
+
})
|
|
940
|
+
);
|
|
941
|
+
const streamed = { seg, gen, inStream, stream, invalidated: false };
|
|
942
|
+
if (stream) this.openStreams.add(streamed);
|
|
943
|
+
return streamed;
|
|
944
|
+
}
|
|
945
|
+
closeStreamed(streamed) {
|
|
946
|
+
streamed.stream?.close();
|
|
947
|
+
this.openStreams.delete(streamed);
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* The decoded chunk at `chunkKey` of a streamed read: the next chunk of its stream, or, for a chunk the cache held
|
|
951
|
+
* when the read opened, the cached instance while it is still there, and a read of that chunk alone once it is not.
|
|
952
|
+
*/
|
|
953
|
+
streamedChunk(streamed, chunkKey) {
|
|
954
|
+
if (streamed.inStream !== void 0 && !streamed.inStream.has(chunkKey)) {
|
|
955
|
+
const ref = { ...streamed.seg, chunkKey };
|
|
956
|
+
const hit = this.cache?.get(this.chunkCacheKey(ref, streamed.gen));
|
|
957
|
+
return hit ? Promise.resolve(hit) : this.storageChunk(ref, streamed.gen);
|
|
958
|
+
}
|
|
959
|
+
return streamed.stream.take(chunkKey).then((read) => {
|
|
960
|
+
if (read.bytes === null) return null;
|
|
961
|
+
const bitmap = this.decodeChunk(read.bytes, chunkKey);
|
|
962
|
+
if (this.cache && !streamed.invalidated) {
|
|
963
|
+
const version = this.streamedVersion(streamed.gen, read.version);
|
|
964
|
+
if (version !== null) {
|
|
965
|
+
this.cache.set(this.chunkCacheKey({ ...streamed.seg, chunkKey }, version), bitmap);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return bitmap;
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* The version a streamed chunk is cached under, or `null` for one not to cache: the version its read reports when the
|
|
973
|
+
* source names versions as its lookups do (`currentVersion`), and otherwise the one the read planned under, since a
|
|
974
|
+
* lookup by generation number would never find a chunk cached under a stream's version.
|
|
975
|
+
*/
|
|
976
|
+
streamedVersion(planned, read) {
|
|
977
|
+
return planned === void 0 || this.storage.currentVersion === void 0 ? planned : read;
|
|
978
|
+
}
|
|
979
|
+
/** The cache key of a chunk: by the version it was read under, or by segment and key alone for a source with none. */
|
|
980
|
+
chunkCacheKey(ref, version) {
|
|
981
|
+
return version === void 0 ? chunkRefKey(ref) : chunkGenKey(ref, version);
|
|
982
|
+
}
|
|
694
983
|
/** The segment's chunk keys, ascending — a shape read off the index, no payload. Keys are untrusted (invariant 5). */
|
|
695
984
|
async chunkKeys(seg) {
|
|
696
985
|
const keys = [...await this.storage.listChunkKeys(seg)];
|
|
@@ -785,6 +1074,9 @@ var SegmentEngine = class {
|
|
|
785
1074
|
*/
|
|
786
1075
|
invalidate(ref) {
|
|
787
1076
|
const prefix = segmentPrefix(ref);
|
|
1077
|
+
for (const streamed of this.openStreams) {
|
|
1078
|
+
if (segmentPrefix(streamed.seg) === prefix) streamed.invalidated = true;
|
|
1079
|
+
}
|
|
788
1080
|
this.cache?.deleteWhere((key) => key.startsWith(prefix));
|
|
789
1081
|
for (const key of this.openReads.keys()) if (key.startsWith(prefix)) this.openReads.delete(key);
|
|
790
1082
|
this.storage.invalidate?.(ref);
|
|
@@ -826,7 +1118,7 @@ var SegmentEngine = class {
|
|
|
826
1118
|
*/
|
|
827
1119
|
async storageChunk(ref, gen) {
|
|
828
1120
|
if (gen === null) return null;
|
|
829
|
-
const cacheKey =
|
|
1121
|
+
const cacheKey = this.chunkCacheKey(ref, gen);
|
|
830
1122
|
if (this.cache) {
|
|
831
1123
|
const cached = this.cache.get(cacheKey);
|
|
832
1124
|
if (cached) {
|
|
@@ -838,31 +1130,71 @@ var SegmentEngine = class {
|
|
|
838
1130
|
const open4 = this.openReads.get(cacheKey);
|
|
839
1131
|
if (open4) return open4.read;
|
|
840
1132
|
const token = {};
|
|
841
|
-
const entry = { token, read: this.fetchChunk(ref, cacheKey, token) };
|
|
1133
|
+
const entry = { token, read: this.fetchChunk(ref, gen, cacheKey, token) };
|
|
842
1134
|
this.openReads.set(cacheKey, entry);
|
|
843
|
-
const
|
|
1135
|
+
const settled2 = () => {
|
|
844
1136
|
if (this.openReads.get(cacheKey) === entry) this.openReads.delete(cacheKey);
|
|
845
1137
|
};
|
|
846
|
-
entry.read.then(
|
|
1138
|
+
entry.read.then(settled2, settled2);
|
|
847
1139
|
return entry.read;
|
|
848
1140
|
}
|
|
849
|
-
/**
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
* A chunk's stored bytes decoded under the size cap (invariant 5) and range-checked: the one place a chunk becomes a
|
|
1143
|
+
* bitmap, whether its bytes came from a request of its own or from a stream of coalesced ranges.
|
|
1144
|
+
*/
|
|
1145
|
+
decodeChunk(bytes, chunkKey) {
|
|
1146
|
+
const bitmap = this.codec.safeDeserialize(bytes, this.maxBitmapBytes);
|
|
1147
|
+
this.assertChunkPayloadInRange(bitmap, chunkKey);
|
|
1148
|
+
return bitmap;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* One chunk, decoded, range-checked and cached. A source with `getChunks` is read through it, as a one-key stream, so
|
|
1152
|
+
* a `storage.get` is reported only for a request that was sent (a chunk the source holds in memory sends none);
|
|
1153
|
+
* the chunk is cached under the version it came from, as a stream's chunk is. A source without it is read by
|
|
1154
|
+
* `getChunk`, one event a call.
|
|
1155
|
+
*/
|
|
1156
|
+
async fetchChunk(ref, gen, cacheKey, token) {
|
|
1157
|
+
const getChunks = this.storage.getChunks;
|
|
1158
|
+
let bytes;
|
|
1159
|
+
let key = cacheKey;
|
|
1160
|
+
if (getChunks === void 0) {
|
|
1161
|
+
const startedAt = this.metricsOn ? this.clock.now() : 0;
|
|
1162
|
+
bytes = await this.storage.getChunk(ref);
|
|
1163
|
+
if (this.metricsOn) {
|
|
1164
|
+
this.metrics.onEvent({
|
|
1165
|
+
kind: "storage.get",
|
|
1166
|
+
namespace: ref.namespace,
|
|
1167
|
+
segment: ref.segment,
|
|
1168
|
+
bytes: bytes ? bytes.length : 0,
|
|
1169
|
+
ms: Math.max(0, this.clock.now() - startedAt)
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
} else {
|
|
1173
|
+
const stream = new ChunkStream(
|
|
1174
|
+
getChunks.call(this.storage, ref, [ref.chunkKey], {
|
|
1175
|
+
...this.metricsOn ? {
|
|
1176
|
+
onRequest: (request) => this.metrics.onEvent({
|
|
1177
|
+
kind: "storage.get",
|
|
1178
|
+
namespace: ref.namespace,
|
|
1179
|
+
segment: ref.segment,
|
|
1180
|
+
bytes: request.bytes,
|
|
1181
|
+
ms: request.ms
|
|
1182
|
+
})
|
|
1183
|
+
} : {}
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
try {
|
|
1187
|
+
const read = await stream.take(ref.chunkKey);
|
|
1188
|
+
bytes = read.bytes;
|
|
1189
|
+
const version = this.streamedVersion(gen, read.version);
|
|
1190
|
+
key = version === null ? "" : this.chunkCacheKey(ref, version);
|
|
1191
|
+
} finally {
|
|
1192
|
+
stream.close();
|
|
1193
|
+
}
|
|
861
1194
|
}
|
|
862
1195
|
if (!bytes) return null;
|
|
863
|
-
const bitmap = this.
|
|
864
|
-
this.
|
|
865
|
-
if (this.openReads.get(cacheKey)?.token === token) this.cache?.set(cacheKey, bitmap);
|
|
1196
|
+
const bitmap = this.decodeChunk(bytes, ref.chunkKey);
|
|
1197
|
+
if (key !== "" && this.openReads.get(cacheKey)?.token === token) this.cache?.set(key, bitmap);
|
|
866
1198
|
return bitmap;
|
|
867
1199
|
}
|
|
868
1200
|
};
|
|
@@ -1443,7 +1775,7 @@ function buildReport(input) {
|
|
|
1443
1775
|
"Request cost is from the supplied workload rates (live-metrics-derived request cost is a later phase).",
|
|
1444
1776
|
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.",
|
|
1445
1777
|
...intersects > 0 ? [
|
|
1446
|
-
`Intersections priced cold: ${intersectGets} GETs each, ${getsPerColdOperand} for each of ${operandsPerIntersect} operand(s) plus ${chunksPerIntersect} chunk
|
|
1778
|
+
`Intersections priced cold: ${intersectGets} GETs each, ${getsPerColdOperand} for each of ${operandsPerIntersect} operand(s) plus ${chunksPerIntersect} chunk range request(s); cacheHitRate does not apply.`
|
|
1447
1779
|
] : [],
|
|
1448
1780
|
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,
|
|
1449
1781
|
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,
|
|
@@ -2419,6 +2751,40 @@ function crc32c(bytes, seed = 0) {
|
|
|
2419
2751
|
return (crc ^ 4294967295) >>> 0;
|
|
2420
2752
|
}
|
|
2421
2753
|
|
|
2754
|
+
// src/core/crbm/plan-reads.ts
|
|
2755
|
+
var MAX_COALESCE_GAP_BYTES = 256 * 1024;
|
|
2756
|
+
var MAX_COALESCED_READ_BYTES = DEFAULT_MAX_BITMAP_BYTES;
|
|
2757
|
+
var MAX_RANGES_IN_FLIGHT = 32;
|
|
2758
|
+
function planChunkReads(extents, region) {
|
|
2759
|
+
const reads = [];
|
|
2760
|
+
let chunks = [];
|
|
2761
|
+
let start = 0;
|
|
2762
|
+
let end = 0;
|
|
2763
|
+
const flush = () => {
|
|
2764
|
+
if (chunks.length > 0) reads.push({ offset: start, length: end - start, chunks });
|
|
2765
|
+
chunks = [];
|
|
2766
|
+
};
|
|
2767
|
+
let previousEnd = region.start;
|
|
2768
|
+
for (const extent of extents) {
|
|
2769
|
+
const extentEnd = extent.offset + extent.length;
|
|
2770
|
+
if (!(Number.isSafeInteger(extent.offset) && Number.isSafeInteger(extent.length) && extent.length >= 1 && extent.offset >= previousEnd && extentEnd <= region.end)) {
|
|
2771
|
+
throw new IntegrityError(`chunk ${extent.key} is not inside the chunk region, in order`);
|
|
2772
|
+
}
|
|
2773
|
+
previousEnd = extentEnd;
|
|
2774
|
+
if (chunks.length > 0 && extent.offset - end <= MAX_COALESCE_GAP_BYTES && extentEnd - start <= MAX_COALESCED_READ_BYTES) {
|
|
2775
|
+
chunks.push(extent);
|
|
2776
|
+
end = extentEnd;
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
flush();
|
|
2780
|
+
chunks.push(extent);
|
|
2781
|
+
start = extent.offset;
|
|
2782
|
+
end = extentEnd;
|
|
2783
|
+
}
|
|
2784
|
+
flush();
|
|
2785
|
+
return reads;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2422
2788
|
// src/core/crbm/varint.ts
|
|
2423
2789
|
var U32_CEIL = 4294967296;
|
|
2424
2790
|
function writeVarint(out, value) {
|
|
@@ -2499,8 +2865,14 @@ function readU64(view, offset, field) {
|
|
|
2499
2865
|
}
|
|
2500
2866
|
return Number(big);
|
|
2501
2867
|
}
|
|
2868
|
+
var keepLimits = /* @__PURE__ */ new WeakMap();
|
|
2869
|
+
function openCrbmReaderKeeping(blob, options, keepChunkBytesUpTo) {
|
|
2870
|
+
const asked = { ...options };
|
|
2871
|
+
if (keepChunkBytesUpTo !== void 0) keepLimits.set(asked, keepChunkBytesUpTo);
|
|
2872
|
+
return CrbmReader.open(blob, asked);
|
|
2873
|
+
}
|
|
2502
2874
|
var CrbmReader = class _CrbmReader {
|
|
2503
|
-
constructor(blob, objectSize, generation, lineage, totalCardinality, index, servedFromTail, crypto, footerCrc, metadata, metadataWeight) {
|
|
2875
|
+
constructor(blob, objectSize, generation, lineage, totalCardinality, index, servedFromTail, crypto, footerCrc, metadata, metadataWeight, payloadEnd, kept) {
|
|
2504
2876
|
this.blob = blob;
|
|
2505
2877
|
this.objectSize = objectSize;
|
|
2506
2878
|
this.generation = generation;
|
|
@@ -2512,6 +2884,8 @@ var CrbmReader = class _CrbmReader {
|
|
|
2512
2884
|
this.footerCrc = footerCrc;
|
|
2513
2885
|
this.metadata = metadata;
|
|
2514
2886
|
this.metadataWeight = metadataWeight;
|
|
2887
|
+
this.payloadEnd = payloadEnd;
|
|
2888
|
+
this.kept = kept;
|
|
2515
2889
|
}
|
|
2516
2890
|
blob;
|
|
2517
2891
|
objectSize;
|
|
@@ -2524,6 +2898,8 @@ var CrbmReader = class _CrbmReader {
|
|
|
2524
2898
|
footerCrc;
|
|
2525
2899
|
metadata;
|
|
2526
2900
|
metadataWeight;
|
|
2901
|
+
payloadEnd;
|
|
2902
|
+
kept;
|
|
2527
2903
|
/**
|
|
2528
2904
|
* Which object this reader opened, as its size and its footer's CRC. The footer covers the index's CRC, so two
|
|
2529
2905
|
* objects with the same fingerprint hold the same chunks, as far as a CRC can tell; a pin compares it to know that
|
|
@@ -2548,11 +2924,12 @@ var CrbmReader = class _CrbmReader {
|
|
|
2548
2924
|
/**
|
|
2549
2925
|
* What this reader holds, the weight the storage reader cache bounds on: its parsed index
|
|
2550
2926
|
* ({@link retainedIndexBytes}), plus a weight for the metadata it decoded, from its canonical length and its number
|
|
2551
|
-
* of keys
|
|
2552
|
-
* how the metadata is weighed may
|
|
2927
|
+
* of keys, plus the chunk bytes kept from the open, when it kept any. Equal to {@link retainedIndexBytes} for a
|
|
2928
|
+
* generation with no metadata and nothing kept. A cache heuristic, not a contract: how the metadata is weighed may
|
|
2929
|
+
* change in any release.
|
|
2553
2930
|
*/
|
|
2554
2931
|
get retainedBytes() {
|
|
2555
|
-
return this.retainedIndexBytes + this.metadataWeight;
|
|
2932
|
+
return this.retainedIndexBytes + this.metadataWeight + (this.kept?.length ?? 0);
|
|
2556
2933
|
}
|
|
2557
2934
|
/** Per-chunk cardinality (`chunkKey → count`) from the parsed index — no payload reads. */
|
|
2558
2935
|
cardinalities() {
|
|
@@ -2750,7 +3127,11 @@ var CrbmReader = class _CrbmReader {
|
|
|
2750
3127
|
options.crypto,
|
|
2751
3128
|
storedFooterCrc,
|
|
2752
3129
|
metadata,
|
|
2753
|
-
metadata === void 0 ? 0 : RETAINED_BYTES_PER_METADATA_BYTE * metadataLength + RETAINED_BYTES_PER_METADATA_KEY * Object.keys(metadata).length
|
|
3130
|
+
metadata === void 0 ? 0 : RETAINED_BYTES_PER_METADATA_BYTE * metadataLength + RETAINED_BYTES_PER_METADATA_KEY * Object.keys(metadata).length,
|
|
3131
|
+
payloadEnd,
|
|
3132
|
+
// Only a tail that began at the front of the object holds the chunks, and only a region the caller's limit
|
|
3133
|
+
// allows is kept: a copy of just that region, so the tail buffer is not held.
|
|
3134
|
+
tailCoversFrom === 0 && payloadEnd - PAYLOAD_START <= (keepLimits.get(options) ?? 0) ? tail.slice(PAYLOAD_START, payloadEnd) : void 0
|
|
2754
3135
|
);
|
|
2755
3136
|
}
|
|
2756
3137
|
/** Chunk keys present in this generation, ascending. */
|
|
@@ -2792,7 +3173,30 @@ var CrbmReader = class _CrbmReader {
|
|
|
2792
3173
|
if (offset < PAYLOAD_START || offset + length > this.objectSize - FOOTER_BYTES) {
|
|
2793
3174
|
throw new IntegrityError(`chunk ${chunkKey} payload out of bounds`);
|
|
2794
3175
|
}
|
|
2795
|
-
const
|
|
3176
|
+
const held = this.keptRange(offset, length);
|
|
3177
|
+
return this.openChunk(slot, chunkKey, held ?? await this.blob.getRange(offset, length));
|
|
3178
|
+
}
|
|
3179
|
+
/**
|
|
3180
|
+
* `[offset, offset + length)` of the object from the kept chunk region, or `undefined` when it is not wholly in it
|
|
3181
|
+
* (or nothing was kept). The bounds checks are defence on untrusted bytes: the index's validation at open keeps every
|
|
3182
|
+
* extent inside the region, so they do not fire for a reader that opened.
|
|
3183
|
+
*
|
|
3184
|
+
* The range is returned as a copy, so nothing a caller or an AEAD does with it changes the next read.
|
|
3185
|
+
*/
|
|
3186
|
+
keptRange(offset, length) {
|
|
3187
|
+
const kept = this.kept;
|
|
3188
|
+
if (kept === void 0 || offset < PAYLOAD_START || offset + length > PAYLOAD_START + kept.length) {
|
|
3189
|
+
return void 0;
|
|
3190
|
+
}
|
|
3191
|
+
const from = offset - PAYLOAD_START;
|
|
3192
|
+
return kept.slice(from, from + length);
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Check one chunk's stored bytes against the index, and open them: the CRC32C first, then, on an encrypted object,
|
|
3196
|
+
* the AEAD with the chunk's own associated data. The one check every read of a chunk goes through, whether the
|
|
3197
|
+
* bytes came from a range of their own or from a slice of a larger one.
|
|
3198
|
+
*/
|
|
3199
|
+
openChunk(slot, chunkKey, bytes) {
|
|
2796
3200
|
if (crc32c(bytes) !== this.index.crcs[slot]) {
|
|
2797
3201
|
throw new IntegrityError(`chunk ${chunkKey} payload CRC mismatch`);
|
|
2798
3202
|
}
|
|
@@ -2809,6 +3213,154 @@ var CrbmReader = class _CrbmReader {
|
|
|
2809
3213
|
this.crypto.aadFor(chunkKey)
|
|
2810
3214
|
);
|
|
2811
3215
|
}
|
|
3216
|
+
/**
|
|
3217
|
+
* The next needed chunk of the range in `cur`, checked and opened, as a stream hands it out; the range is let go of
|
|
3218
|
+
* when this is its last chunk.
|
|
3219
|
+
*/
|
|
3220
|
+
nextChunk(cur, slot, key) {
|
|
3221
|
+
const range = cur.range;
|
|
3222
|
+
const chunk = range.read.chunks[cur.at++];
|
|
3223
|
+
if (cur.at === range.read.chunks.length) cur.range = void 0;
|
|
3224
|
+
const from = chunk.offset - range.read.offset;
|
|
3225
|
+
const bytes = this.openChunk(slot, chunk.key, range.bytes.subarray(from, from + chunk.length));
|
|
3226
|
+
return { key, bytes };
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Read several chunks as a stream, one storage request per merged range ({@link planChunkReads}) rather than one per
|
|
3230
|
+
* chunk. `chunkKeys` are in ascending order, a key may repeat, and anything else is a {@link ValidationError}. One
|
|
3231
|
+
* item comes out per position of `chunkKeys`, in order: the chunk's bytes, or `null` where this generation has none,
|
|
3232
|
+
* as {@link getChunk} answers for an absent key. A key given twice is read once and answered at both positions with
|
|
3233
|
+
* the same view.
|
|
3234
|
+
*
|
|
3235
|
+
* The whole key list is planned once, up front, against this one generation's index. The ranges then run through a
|
|
3236
|
+
* window of `options.concurrency` reads ({@link MAX_RANGES_IN_FLIGHT} by default) that counts a range until the
|
|
3237
|
+
* consumer has taken it, so a stream never holds more than that many ranges, in flight or landed, however many keys
|
|
3238
|
+
* 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
|
|
3239
|
+
* unless it is one chunk, which is at most the payload cap, so the bound is that many times the per-chunk cap
|
|
3240
|
+
* (1 MiB, and 28 bytes more for an encrypted chunk). Nothing is read until the first item is asked for, and a
|
|
3241
|
+
* consumer that stops launches no more: ranges already in flight finish and are dropped, and a failure of one of
|
|
3242
|
+
* those is never raised.
|
|
3243
|
+
*
|
|
3244
|
+
* Every chunk is held to the check a read of it alone gets: its slice of the range must match its index entry's
|
|
3245
|
+
* CRC32C, and on an encrypted object it opens under its own associated data, so a chunk moved to another place in
|
|
3246
|
+
* the object, or another key's bytes, is refused. The bytes in a gap between needed chunks are never parsed. A
|
|
3247
|
+
* range that comes back shorter than asked is an {@link IntegrityError}, and a chunk that fails its check ends the
|
|
3248
|
+
* stream with that error when its turn comes, as a read of it alone would fail.
|
|
3249
|
+
*
|
|
3250
|
+
* `options.readRange`, when given, runs each range read, so a caller can retry one request without repeating the
|
|
3251
|
+
* others; `options.now` times each request for `options.onRequest`, which hears of every range request that was sent, once, when
|
|
3252
|
+
* it settles: taken, abandoned after the consumer stopped, or failed (a reader has no clock of its own, so `ms` is 0
|
|
3253
|
+
* without one).
|
|
3254
|
+
* A plain chunk is a writable view into the range it was read in: copy a chunk to keep it.
|
|
3255
|
+
*
|
|
3256
|
+
* A stream that fails (a range that errors, a chunk that fails its check) raises at once and sends nothing further:
|
|
3257
|
+
* the ranges it still had in flight finish in the background, are not retried and never raise. Its `settled()`
|
|
3258
|
+
* resolves when those have finished, for a caller that is about to read again and does not want them beside its next
|
|
3259
|
+
* window (the source awaits it before a heal, and only then). It waits as long as the slowest of them takes to
|
|
3260
|
+
* answer, which only the store's own read timeout bounds, so a read that never answers delays a heal and never an
|
|
3261
|
+
* error.
|
|
3262
|
+
*/
|
|
3263
|
+
readChunks(chunkKeys, options = {}) {
|
|
3264
|
+
const open4 = { window: void 0 };
|
|
3265
|
+
const stream = this.streamChunks(chunkKeys, options, open4);
|
|
3266
|
+
return Object.assign(stream, {
|
|
3267
|
+
settled: () => open4.window?.settle() ?? Promise.resolve()
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
async *streamChunks(chunkKeys, options, open4) {
|
|
3271
|
+
const width = options.concurrency ?? MAX_RANGES_IN_FLIGHT;
|
|
3272
|
+
if (!Number.isInteger(width) || width < 1) {
|
|
3273
|
+
throw new ValidationError(
|
|
3274
|
+
`concurrency must be a positive integer; got ${options.concurrency}`
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
validateChunkKeyOrder(chunkKeys);
|
|
3278
|
+
const ramp = options.ramp ?? false;
|
|
3279
|
+
if (typeof ramp === "number" && (!Number.isInteger(ramp) || ramp < 1)) {
|
|
3280
|
+
throw new ValidationError(`ramp must be a boolean or a positive integer; got ${ramp}`);
|
|
3281
|
+
}
|
|
3282
|
+
const readRange = options.readRange ?? ((read) => read());
|
|
3283
|
+
const now = options.now ?? (() => 0);
|
|
3284
|
+
const slots = chunkKeys.map((key) => this.slotOf(key));
|
|
3285
|
+
const needed = [...new Set(slots.filter((slot) => slot >= 0))];
|
|
3286
|
+
const extents = needed.map((slot) => ({
|
|
3287
|
+
key: this.index.keys[slot],
|
|
3288
|
+
offset: this.index.offsets[slot],
|
|
3289
|
+
length: this.index.lengths[slot]
|
|
3290
|
+
}));
|
|
3291
|
+
const reads = planChunkReads(extents, { start: PAYLOAD_START, end: this.payloadEnd });
|
|
3292
|
+
let closed = false;
|
|
3293
|
+
const report = (bytes, ms) => {
|
|
3294
|
+
try {
|
|
3295
|
+
options.onRequest?.({ bytes, ms });
|
|
3296
|
+
} catch {
|
|
3297
|
+
}
|
|
3298
|
+
};
|
|
3299
|
+
const window = new ChunkWindow(
|
|
3300
|
+
reads.map((_, i) => i),
|
|
3301
|
+
async (i) => {
|
|
3302
|
+
const read = reads[i];
|
|
3303
|
+
const held = this.keptRange(read.offset, read.length);
|
|
3304
|
+
if (held !== void 0) return { bytes: held };
|
|
3305
|
+
const startedAt = now();
|
|
3306
|
+
let sent = false;
|
|
3307
|
+
let moved = 0;
|
|
3308
|
+
try {
|
|
3309
|
+
const bytes = await readRange(() => {
|
|
3310
|
+
if (closed) throw new StreamClosed();
|
|
3311
|
+
sent = true;
|
|
3312
|
+
return this.blob.getRange(read.offset, read.length);
|
|
3313
|
+
});
|
|
3314
|
+
moved = bytes.length;
|
|
3315
|
+
if (bytes.length !== read.length) {
|
|
3316
|
+
throw new IntegrityError(
|
|
3317
|
+
`.crbm range [${read.offset}, +${read.length}) read short (${bytes.length} bytes)`
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
return { bytes };
|
|
3321
|
+
} finally {
|
|
3322
|
+
if (sent) report(moved, Math.max(0, now() - startedAt));
|
|
3323
|
+
}
|
|
3324
|
+
},
|
|
3325
|
+
width,
|
|
3326
|
+
ramp !== false,
|
|
3327
|
+
typeof ramp === "number" ? ramp : 1
|
|
3328
|
+
);
|
|
3329
|
+
open4.window = window;
|
|
3330
|
+
let next = 0;
|
|
3331
|
+
const cur = { range: void 0, at: 0 };
|
|
3332
|
+
let last;
|
|
3333
|
+
try {
|
|
3334
|
+
for (const [i, key] of chunkKeys.entries()) {
|
|
3335
|
+
if (slots[i] < 0) {
|
|
3336
|
+
yield { key, bytes: null };
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
if (last !== void 0 && last.key === key) {
|
|
3340
|
+
yield { key, bytes: last.bytes };
|
|
3341
|
+
if (chunkKeys[i + 1] !== key) last = void 0;
|
|
3342
|
+
continue;
|
|
3343
|
+
}
|
|
3344
|
+
if (cur.range === void 0) {
|
|
3345
|
+
cur.range = { read: reads[next++], ...await window.take() };
|
|
3346
|
+
cur.at = 0;
|
|
3347
|
+
}
|
|
3348
|
+
if (chunkKeys[i + 1] === key) {
|
|
3349
|
+
last = this.nextChunk(cur, slots[i], key);
|
|
3350
|
+
yield last;
|
|
3351
|
+
} else {
|
|
3352
|
+
yield this.nextChunk(cur, slots[i], key);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
} finally {
|
|
3356
|
+
closed = true;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
var StreamClosed = class extends Error {
|
|
3361
|
+
constructor() {
|
|
3362
|
+
super("the chunk stream was closed");
|
|
3363
|
+
}
|
|
2812
3364
|
};
|
|
2813
3365
|
function within(bytes, start, from, to) {
|
|
2814
3366
|
if (from < start || to > start + bytes.length) {
|
|
@@ -2938,6 +3490,33 @@ function parseIndex(indexBytes, payloadEnd, maxPayloadBytes, minPayloadBytes = 1
|
|
|
2938
3490
|
};
|
|
2939
3491
|
}
|
|
2940
3492
|
|
|
3493
|
+
// src/core/item-pull.ts
|
|
3494
|
+
var ItemPull = class {
|
|
3495
|
+
iterator;
|
|
3496
|
+
item;
|
|
3497
|
+
constructor(source) {
|
|
3498
|
+
this.iterator = source[Symbol.asyncIterator]();
|
|
3499
|
+
}
|
|
3500
|
+
/** Waits for the next item and keeps it inside; `false` once the source has ended. A failure of the source throws. */
|
|
3501
|
+
async advance() {
|
|
3502
|
+
const step = await this.iterator.next();
|
|
3503
|
+
if (step.done === true) return false;
|
|
3504
|
+
this.item = step.value;
|
|
3505
|
+
return true;
|
|
3506
|
+
}
|
|
3507
|
+
/** Hands over the item {@link advance} fetched, through `map`, and forgets it. */
|
|
3508
|
+
take(map) {
|
|
3509
|
+
const item = this.item;
|
|
3510
|
+
this.item = void 0;
|
|
3511
|
+
return map(item);
|
|
3512
|
+
}
|
|
3513
|
+
/** Ends the source early (a consumer that stopped), as leaving a `for await` does. */
|
|
3514
|
+
async close() {
|
|
3515
|
+
this.item = void 0;
|
|
3516
|
+
await this.iterator.return?.();
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
|
|
2941
3520
|
// src/core/crbm/writer.ts
|
|
2942
3521
|
var CrbmWriter = class {
|
|
2943
3522
|
constructor(sink, options) {
|
|
@@ -3472,6 +4051,10 @@ var CrbmStorageChunkSource = class {
|
|
|
3472
4051
|
this.keystore = keystore;
|
|
3473
4052
|
this.requireEncryption = requireEncryption ?? false;
|
|
3474
4053
|
this.readerOptions = readerOptions;
|
|
4054
|
+
const refreshes = registry !== void 0 && clock !== void 0 && (currentGenTtlMs ?? DEFAULT_CURRENT_GEN_TTL_MS) > 0;
|
|
4055
|
+
this.keepChunkBytesUpTo = refreshes ? Math.floor(
|
|
4056
|
+
(maxOpenIndexBytes ?? DEFAULT_MAX_OPEN_INDEX_BYTES) / (maxOpenSegments ?? DEFAULT_MAX_OPEN_SEGMENTS)
|
|
4057
|
+
) : 0;
|
|
3475
4058
|
this.clock = clock;
|
|
3476
4059
|
this.currentGenTtlMs = currentGenTtlMs ?? DEFAULT_CURRENT_GEN_TTL_MS;
|
|
3477
4060
|
this.snapshots = new BoundedLru({
|
|
@@ -3535,6 +4118,8 @@ var CrbmStorageChunkSource = class {
|
|
|
3535
4118
|
keystore;
|
|
3536
4119
|
requireEncryption;
|
|
3537
4120
|
readerOptions;
|
|
4121
|
+
/** 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. */
|
|
4122
|
+
keepChunkBytesUpTo;
|
|
3538
4123
|
clock;
|
|
3539
4124
|
currentGenTtlMs;
|
|
3540
4125
|
/**
|
|
@@ -3979,11 +4564,12 @@ var CrbmStorageChunkSource = class {
|
|
|
3979
4564
|
segment: ref.segment,
|
|
3980
4565
|
generation: target.generation
|
|
3981
4566
|
};
|
|
3982
|
-
return openChecked(
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
lineage: target.lineage
|
|
3986
|
-
|
|
4567
|
+
return openChecked(
|
|
4568
|
+
this.driver,
|
|
4569
|
+
genKey,
|
|
4570
|
+
{ ...this.readerOptions, crypto, lineage: target.lineage },
|
|
4571
|
+
this.keepChunkBytesUpTo
|
|
4572
|
+
);
|
|
3987
4573
|
}
|
|
3988
4574
|
/**
|
|
3989
4575
|
* Open the generation a resolution found, and hold the row's summary against the object while it is open anyway:
|
|
@@ -4070,6 +4656,128 @@ var CrbmStorageChunkSource = class {
|
|
|
4070
4656
|
validateChunkRef(ref);
|
|
4071
4657
|
return this.withFreshSnapshot(ref, (reader) => reader.getChunk(ref.chunkKey), null);
|
|
4072
4658
|
}
|
|
4659
|
+
/**
|
|
4660
|
+
* Several chunks as a stream, in as few storage requests as {@link CrbmReader.readChunks} can make them (see
|
|
4661
|
+
* {@link StorageChunkSource.getChunks}). The stream is served from one resolved generation, and every chunk says
|
|
4662
|
+
* which version it came from. A generation swept, or an object replaced under its number, while the stream runs
|
|
4663
|
+
* re-resolves the segment and carries on with the keys not yet yielded from whatever is current then, so each chunk
|
|
4664
|
+
* is whole and from one generation and the stream may describe two; a second failure with no chunk yielded in between
|
|
4665
|
+
* propagates. `options.retry` runs the resolution, each range request and the check that a failed read's object was
|
|
4666
|
+
* replaced, so a transient fault repeats that step alone. Nothing is resolved until the first chunk is asked for.
|
|
4667
|
+
*/
|
|
4668
|
+
async *getChunks(ref, keys, options) {
|
|
4669
|
+
validateUserRef(ref);
|
|
4670
|
+
for (const chunkKey of keys) validateChunkRef({ ...ref, chunkKey });
|
|
4671
|
+
validateChunkKeyOrder(keys);
|
|
4672
|
+
const retry = options?.retry;
|
|
4673
|
+
let yielded = 0;
|
|
4674
|
+
let healed = false;
|
|
4675
|
+
for (; ; ) {
|
|
4676
|
+
let snap = this.liveSnapshot(ref);
|
|
4677
|
+
let pending = snap.reader;
|
|
4678
|
+
let chunks;
|
|
4679
|
+
try {
|
|
4680
|
+
let tried = false;
|
|
4681
|
+
const reader = await (retry === void 0 ? pending : retry(() => {
|
|
4682
|
+
if (tried) {
|
|
4683
|
+
snap = this.liveSnapshot(ref);
|
|
4684
|
+
pending = snap.reader;
|
|
4685
|
+
}
|
|
4686
|
+
tried = true;
|
|
4687
|
+
return pending;
|
|
4688
|
+
}));
|
|
4689
|
+
if (reader === null) {
|
|
4690
|
+
for (; yielded < keys.length; yielded++) {
|
|
4691
|
+
yield { key: keys[yielded], bytes: null, version: null };
|
|
4692
|
+
}
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
const version = versionOf(reader.generation, reader.lineage);
|
|
4696
|
+
chunks = reader.readChunks(keys.slice(yielded), {
|
|
4697
|
+
...options?.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
4698
|
+
...options?.ramp === void 0 ? {} : { ramp: options.ramp },
|
|
4699
|
+
...retry === void 0 ? {} : { readRange: retry },
|
|
4700
|
+
...options?.onRequest === void 0 ? {} : { onRequest: options.onRequest },
|
|
4701
|
+
now: () => this.now()
|
|
4702
|
+
});
|
|
4703
|
+
const pull = new ItemPull(chunks);
|
|
4704
|
+
let movedOn = false;
|
|
4705
|
+
try {
|
|
4706
|
+
while (await pull.advance()) {
|
|
4707
|
+
const live = this.liveSnapshot(ref);
|
|
4708
|
+
if (live !== snap) {
|
|
4709
|
+
const now = await live.reader;
|
|
4710
|
+
if (now === null || versionOf(now.generation, now.lineage) !== version) {
|
|
4711
|
+
movedOn = true;
|
|
4712
|
+
break;
|
|
4713
|
+
}
|
|
4714
|
+
snap = live;
|
|
4715
|
+
}
|
|
4716
|
+
yield pull.take((chunk) => {
|
|
4717
|
+
yielded += 1;
|
|
4718
|
+
healed = false;
|
|
4719
|
+
return { ...chunk, version };
|
|
4720
|
+
});
|
|
4721
|
+
}
|
|
4722
|
+
} finally {
|
|
4723
|
+
await pull.close();
|
|
4724
|
+
}
|
|
4725
|
+
if (!movedOn) return;
|
|
4726
|
+
await chunks.settled();
|
|
4727
|
+
healed = false;
|
|
4728
|
+
continue;
|
|
4729
|
+
} catch (err) {
|
|
4730
|
+
if (healed) throw err;
|
|
4731
|
+
if (!isNotFoundError(err) && !await this.replacedUnder(ref, pending, err, retry))
|
|
4732
|
+
throw err;
|
|
4733
|
+
await chunks?.settled();
|
|
4734
|
+
this.dropStale(segmentKey(ref), snap);
|
|
4735
|
+
healed = true;
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
/**
|
|
4740
|
+
* {@link getChunks} of a specific generation, the pinned shape read. It yields the chunks without a version: the
|
|
4741
|
+
* caller that pinned the generation knows it. It never moves to another generation; `held` as for
|
|
4742
|
+
* {@link getChunkAt}.
|
|
4743
|
+
*/
|
|
4744
|
+
async *getChunksAt(ref, generation, keys, held, options) {
|
|
4745
|
+
validateUserRef(ref);
|
|
4746
|
+
for (const chunkKey of keys) validateChunkRef({ ...ref, chunkKey });
|
|
4747
|
+
validateChunkKeyOrder(keys);
|
|
4748
|
+
const retry = options?.retry;
|
|
4749
|
+
const open4 = () => this.readerAt(ref, generation, held?.version, held?.fingerprint);
|
|
4750
|
+
const reader = await (retry === void 0 ? open4() : retry(open4));
|
|
4751
|
+
if (reader === null) {
|
|
4752
|
+
for (const key of keys) yield { key, bytes: null };
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
const fingerprint = held?.fingerprint;
|
|
4756
|
+
if (held !== void 0 && fingerprint !== void 0 && this.replacedPins.get(this.heldKey(ref, held.version, fingerprint)) !== void 0)
|
|
4757
|
+
throw notThePinned(ref, generation);
|
|
4758
|
+
try {
|
|
4759
|
+
const pull = new ItemPull(
|
|
4760
|
+
reader.readChunks(keys, {
|
|
4761
|
+
...options?.concurrency === void 0 ? {} : { concurrency: options.concurrency },
|
|
4762
|
+
...options?.ramp === void 0 ? {} : { ramp: options.ramp },
|
|
4763
|
+
...retry === void 0 ? {} : { readRange: retry },
|
|
4764
|
+
...options?.onRequest === void 0 ? {} : { onRequest: options.onRequest },
|
|
4765
|
+
now: () => this.now()
|
|
4766
|
+
})
|
|
4767
|
+
);
|
|
4768
|
+
try {
|
|
4769
|
+
while (await pull.advance()) yield pull.take((chunk) => chunk);
|
|
4770
|
+
} finally {
|
|
4771
|
+
await pull.close();
|
|
4772
|
+
}
|
|
4773
|
+
} catch (err) {
|
|
4774
|
+
if (held === void 0 || fingerprint === void 0) throw err;
|
|
4775
|
+
if (!(isIntegrityError(err) || isValidationError(err))) throw err;
|
|
4776
|
+
const check = () => this.throwIfReplaced(ref, generation, held.version, fingerprint);
|
|
4777
|
+
await (retry === void 0 ? check() : retry(check));
|
|
4778
|
+
throw err;
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4073
4781
|
async listChunkKeys(ref) {
|
|
4074
4782
|
validateUserRef(ref);
|
|
4075
4783
|
return this.withFreshSnapshot(ref, (reader) => reader.chunkKeys(), []);
|
|
@@ -4156,17 +4864,26 @@ var CrbmStorageChunkSource = class {
|
|
|
4156
4864
|
* tight loop on a segment whose pointer names an object that is permanently absent — and an N-way
|
|
4157
4865
|
* `intersect` would do it N times. Gated in `tests/core/storage-source-heal-open.test.ts` by counting calls.
|
|
4158
4866
|
*/
|
|
4159
|
-
async withFreshSnapshot(ref, read, ifGone) {
|
|
4867
|
+
async withFreshSnapshot(ref, read, ifGone, retry) {
|
|
4160
4868
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4161
|
-
|
|
4162
|
-
|
|
4869
|
+
let snap = this.liveSnapshot(ref);
|
|
4870
|
+
let pending = snap.reader;
|
|
4163
4871
|
try {
|
|
4164
|
-
|
|
4872
|
+
let tried = false;
|
|
4873
|
+
const reader = await (retry === void 0 ? pending : retry(() => {
|
|
4874
|
+
if (tried) {
|
|
4875
|
+
snap = this.liveSnapshot(ref);
|
|
4876
|
+
pending = snap.reader;
|
|
4877
|
+
}
|
|
4878
|
+
tried = true;
|
|
4879
|
+
return pending;
|
|
4880
|
+
}));
|
|
4165
4881
|
if (reader === null) return ifGone;
|
|
4166
4882
|
return await read(reader);
|
|
4167
4883
|
} catch (err) {
|
|
4168
4884
|
if (attempt === 1) throw err;
|
|
4169
|
-
if (!isNotFoundError(err) && !await this.replacedUnder(ref, pending, err))
|
|
4885
|
+
if (!isNotFoundError(err) && !await this.replacedUnder(ref, pending, err, retry))
|
|
4886
|
+
throw err;
|
|
4170
4887
|
this.dropStale(segmentKey(ref), snap);
|
|
4171
4888
|
}
|
|
4172
4889
|
}
|
|
@@ -4182,7 +4899,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4182
4899
|
* the error is real, and it stands. Only these two errors pay for the footer read, never a read that succeeded. A
|
|
4183
4900
|
* transient fault in the check is rethrown; a footer that cannot be read says nothing, and the error stands.
|
|
4184
4901
|
*/
|
|
4185
|
-
async replacedUnder(ref, pending, err) {
|
|
4902
|
+
async replacedUnder(ref, pending, err, retry) {
|
|
4186
4903
|
if (!(isIntegrityError(err) || isValidationError(err))) return false;
|
|
4187
4904
|
let reader;
|
|
4188
4905
|
try {
|
|
@@ -4197,7 +4914,8 @@ var CrbmStorageChunkSource = class {
|
|
|
4197
4914
|
generation: reader.generation
|
|
4198
4915
|
};
|
|
4199
4916
|
try {
|
|
4200
|
-
|
|
4917
|
+
const check = () => CrbmReader.sameObject(storageBlobReader(this.driver, at), reader.fingerprint);
|
|
4918
|
+
return !await (retry === void 0 ? check() : retry(check));
|
|
4201
4919
|
} catch (checkErr) {
|
|
4202
4920
|
if (isNotFoundError(checkErr)) return true;
|
|
4203
4921
|
if (isTransientError(checkErr)) throw checkErr;
|
|
@@ -4477,7 +5195,7 @@ async function provesOwnObject(storage, key, fingerprint) {
|
|
|
4477
5195
|
throw err;
|
|
4478
5196
|
}
|
|
4479
5197
|
}
|
|
4480
|
-
async function
|
|
5198
|
+
async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
|
|
4481
5199
|
if (options.keystore === void 0 && options.requireEncryption === true) {
|
|
4482
5200
|
throw new ValidationError("requireEncryption: a load needs a keystore to write encrypted");
|
|
4483
5201
|
}
|
|
@@ -4497,27 +5215,30 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
4497
5215
|
throw new ValidationError("an encrypted load requires a registry to store the wrapped DEK");
|
|
4498
5216
|
}
|
|
4499
5217
|
const passed = options.row;
|
|
4500
|
-
const existing = passed !== void 0 && passed !== null && (passed.wrappedDeks === void 0 || passed.wrappedDeks.length === 0) ? passed : options.registry !== void 0 ? await options.registry.get(
|
|
5218
|
+
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;
|
|
4501
5219
|
if (existing?.status === "destroyed") {
|
|
4502
5220
|
throw new ValidationError(
|
|
4503
|
-
`segment "${
|
|
5221
|
+
`segment "${ref.segment}" is destroyed (crypto-shredded) \u2014 refusing to write; use a new segment`
|
|
4504
5222
|
);
|
|
4505
5223
|
}
|
|
4506
5224
|
if (existing?.wrappedDeks !== void 0 && existing.wrappedDeks.length > 0 && options.keystore === void 0) {
|
|
4507
5225
|
throw new KeyUnavailableError(
|
|
4508
|
-
`segment "${
|
|
5226
|
+
`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.`
|
|
4509
5227
|
);
|
|
4510
5228
|
}
|
|
5229
|
+
const generation = await ahead.generation;
|
|
5230
|
+
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
4511
5231
|
let crypto;
|
|
4512
5232
|
let newWrapped;
|
|
4513
5233
|
if (options.keystore !== void 0) {
|
|
4514
5234
|
if (existing?.wrappedDeks !== void 0 && existing.wrappedDeks.length > 0) {
|
|
4515
|
-
const
|
|
5235
|
+
const early = ahead.unwrapped;
|
|
5236
|
+
const aead = early !== void 0 && sameKeys(early.wrapped, existing.wrappedDeks) ? await early.aead : await options.keystore.openDek(existing.wrappedDeks);
|
|
4516
5237
|
crypto = { aead, aadFor: (scope) => aadFor(key, key.generation, scope) };
|
|
4517
5238
|
} else if (existing !== null && existing.currentGen !== null) {
|
|
4518
5239
|
if (options.requireEncryption === true) {
|
|
4519
5240
|
throw new ValidationError(
|
|
4520
|
-
`requireEncryption: segment "${
|
|
5241
|
+
`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.`
|
|
4521
5242
|
);
|
|
4522
5243
|
}
|
|
4523
5244
|
} else {
|
|
@@ -4624,8 +5345,12 @@ async function bucketIds(ids, codec, clock) {
|
|
|
4624
5345
|
function openGenerationReader(storage, key, crypto, options = {}) {
|
|
4625
5346
|
return openChecked(storage, key, { ...options, crypto });
|
|
4626
5347
|
}
|
|
4627
|
-
async function openChecked(storage, key, options) {
|
|
4628
|
-
const reader = await
|
|
5348
|
+
async function openChecked(storage, key, options, keepChunkBytesUpTo) {
|
|
5349
|
+
const reader = await openCrbmReaderKeeping(
|
|
5350
|
+
storageBlobReader(storage, key),
|
|
5351
|
+
options,
|
|
5352
|
+
keepChunkBytesUpTo
|
|
5353
|
+
);
|
|
4629
5354
|
if (reader.generation !== key.generation) {
|
|
4630
5355
|
throw new IntegrityError(
|
|
4631
5356
|
`segment "${key.segment}" generation ${key.generation}: its footer says generation ${reader.generation}`
|
|
@@ -4686,6 +5411,30 @@ var PinnedStorageChunkSource = class {
|
|
|
4686
5411
|
if (pin === void 0) return this.inner.getChunk(ref);
|
|
4687
5412
|
return pin.generation === null ? Promise.resolve(null) : this.inner.getChunkAt(ref, pin.generation, heldBy(pin));
|
|
4688
5413
|
}
|
|
5414
|
+
/**
|
|
5415
|
+
* A pinned segment's chunks come from the pinned generation alone, under the version a pinned read keys by
|
|
5416
|
+
* ({@link currentVersion}); another segment's go to the live source, which answers with its own.
|
|
5417
|
+
*/
|
|
5418
|
+
getChunks(ref, keys, options) {
|
|
5419
|
+
const pin = this.pinFor(ref);
|
|
5420
|
+
if (pin === void 0) return this.inner.getChunks(ref, keys, options);
|
|
5421
|
+
return this.pinnedChunks(ref, pin, keys, options);
|
|
5422
|
+
}
|
|
5423
|
+
async *pinnedChunks(ref, pin, keys, options) {
|
|
5424
|
+
if (pin.generation === null) {
|
|
5425
|
+
for (const key of keys) yield { key, bytes: null, version: null };
|
|
5426
|
+
return;
|
|
5427
|
+
}
|
|
5428
|
+
const version = await this.currentVersion(ref);
|
|
5429
|
+
const pull = new ItemPull(
|
|
5430
|
+
this.inner.getChunksAt(ref, pin.generation, keys, heldBy(pin), options)
|
|
5431
|
+
);
|
|
5432
|
+
try {
|
|
5433
|
+
while (await pull.advance()) yield pull.take((chunk) => ({ ...chunk, version }));
|
|
5434
|
+
} finally {
|
|
5435
|
+
await pull.close();
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
4689
5438
|
listChunkKeys(ref) {
|
|
4690
5439
|
const pin = this.pinFor(ref);
|
|
4691
5440
|
if (pin === void 0) return this.inner.listChunkKeys(ref);
|
|
@@ -4788,7 +5537,11 @@ async function withRetry(op, policy, deps) {
|
|
|
4788
5537
|
}
|
|
4789
5538
|
|
|
4790
5539
|
// src/core/load.ts
|
|
4791
|
-
|
|
5540
|
+
function settled(promise) {
|
|
5541
|
+
promise.catch(() => void 0);
|
|
5542
|
+
return promise;
|
|
5543
|
+
}
|
|
5544
|
+
async function currentCardinality(ref, deps, record, unwrapped) {
|
|
4792
5545
|
if (record === null || record.currentGen === null || record.status === "destroyed") {
|
|
4793
5546
|
return { cardinality: null, fromSummary: false };
|
|
4794
5547
|
}
|
|
@@ -4802,7 +5555,7 @@ async function currentCardinality(ref, deps, record) {
|
|
|
4802
5555
|
`segment "${ref.segment}" is encrypted but load was given no keystore`
|
|
4803
5556
|
);
|
|
4804
5557
|
}
|
|
4805
|
-
aead = await deps.keystore.openDek(wrapped);
|
|
5558
|
+
aead = await (unwrapped ?? deps.keystore.openDek(wrapped));
|
|
4806
5559
|
const opened = aead;
|
|
4807
5560
|
crypto = { aead: opened, aadFor: (scope) => aadFor(ref, generation, scope) };
|
|
4808
5561
|
}
|
|
@@ -4853,35 +5606,44 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
4853
5606
|
const row = await deps.registry.get(ref);
|
|
4854
5607
|
const fromToken = row?.token;
|
|
4855
5608
|
const fromGeneration = row?.currentGen ?? void 0;
|
|
5609
|
+
const wrappedKeys = row?.wrappedDeks;
|
|
5610
|
+
const keystore = deps.keystore;
|
|
5611
|
+
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;
|
|
4856
5612
|
const needsBefore = guard?.minRetained !== void 0 || options.allowEmpty !== true;
|
|
4857
|
-
const current = needsBefore ? await currentCardinality(ref, deps, row) : { cardinality: null, fromSummary: false };
|
|
5613
|
+
const current = needsBefore ? await currentCardinality(ref, deps, row, unwrapped?.aead) : { cardinality: null, fromSummary: false };
|
|
4858
5614
|
const before = current.cardinality;
|
|
4859
|
-
const
|
|
4860
|
-
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
5615
|
+
const numbering = settled(nextLoadGeneration(ref, deps, row));
|
|
4861
5616
|
let written;
|
|
4862
5617
|
try {
|
|
4863
|
-
written = await
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
5618
|
+
written = await bulkLoadAhead(
|
|
5619
|
+
deps.storage,
|
|
5620
|
+
ref,
|
|
5621
|
+
{ generation: settled(numbering.then((n) => n.generation)), unwrapped },
|
|
5622
|
+
ids,
|
|
5623
|
+
{
|
|
5624
|
+
registry: deps.registry,
|
|
5625
|
+
publish: false,
|
|
5626
|
+
keystore: deps.keystore,
|
|
5627
|
+
requireEncryption: deps.requireEncryption,
|
|
5628
|
+
codec: deps.codec,
|
|
5629
|
+
clock: deps.clock,
|
|
5630
|
+
metadata,
|
|
5631
|
+
row
|
|
5632
|
+
}
|
|
5633
|
+
);
|
|
4873
5634
|
} catch (err) {
|
|
4874
5635
|
if (!isWriteConflictError(err)) throw err;
|
|
5636
|
+
const { generation: generation2 } = await numbering;
|
|
4875
5637
|
audit.onEvent({
|
|
4876
5638
|
kind: "segment.load-refused",
|
|
4877
5639
|
segment: ref.segment,
|
|
4878
5640
|
namespace: ref.namespace,
|
|
4879
|
-
generation,
|
|
5641
|
+
generation: generation2,
|
|
4880
5642
|
reason: "superseded",
|
|
4881
5643
|
cardinality: 0
|
|
4882
5644
|
});
|
|
4883
5645
|
return {
|
|
4884
|
-
generation,
|
|
5646
|
+
generation: generation2,
|
|
4885
5647
|
published: false,
|
|
4886
5648
|
reason: "superseded",
|
|
4887
5649
|
size: 0,
|
|
@@ -4892,6 +5654,8 @@ async function loadSegment(ref, input, deps, options = {}) {
|
|
|
4892
5654
|
collected: []
|
|
4893
5655
|
};
|
|
4894
5656
|
}
|
|
5657
|
+
const { generation, checked } = await numbering;
|
|
5658
|
+
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
4895
5659
|
const reclaim = async () => {
|
|
4896
5660
|
const now = await deps.registry.get(ref);
|
|
4897
5661
|
if (now !== null && (now.token === fromToken || now.status === "destroyed")) {
|
|
@@ -5132,6 +5896,7 @@ function describingSummary(record) {
|
|
|
5132
5896
|
}
|
|
5133
5897
|
|
|
5134
5898
|
// src/core/erase-id.ts
|
|
5899
|
+
var HOLDS_CONCURRENCY = 4;
|
|
5135
5900
|
async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
5136
5901
|
validateUserRef(ref);
|
|
5137
5902
|
const { chunkKey, remainder } = splitId(id);
|
|
@@ -5193,13 +5958,37 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
5193
5958
|
throw err;
|
|
5194
5959
|
}
|
|
5195
5960
|
};
|
|
5961
|
+
const holdsEach = async (generations, stopAtHolder) => {
|
|
5962
|
+
let firstHolder = Infinity;
|
|
5963
|
+
const settled2 = await mapWithConcurrency(
|
|
5964
|
+
generations,
|
|
5965
|
+
HOLDS_CONCURRENCY,
|
|
5966
|
+
async (generation2, index) => {
|
|
5967
|
+
if (stopAtHolder && index > firstHolder) {
|
|
5968
|
+
return { generation: generation2, held: null, fault: void 0 };
|
|
5969
|
+
}
|
|
5970
|
+
try {
|
|
5971
|
+
const held = await holds(generation2);
|
|
5972
|
+
if (held === true && index < firstHolder) firstHolder = index;
|
|
5973
|
+
return { generation: generation2, held, fault: void 0 };
|
|
5974
|
+
} catch (fault) {
|
|
5975
|
+
return { generation: generation2, held: null, fault: { error: fault } };
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
);
|
|
5979
|
+
const outcomes = [];
|
|
5980
|
+
for (const { generation: generation2, held, fault } of settled2) {
|
|
5981
|
+
if (fault !== void 0) throw fault.error;
|
|
5982
|
+
outcomes.push({ generation: generation2, held });
|
|
5983
|
+
if (stopAtHolder && held === true) break;
|
|
5984
|
+
}
|
|
5985
|
+
return outcomes;
|
|
5986
|
+
};
|
|
5196
5987
|
const holderLeft = async (clean) => {
|
|
5197
5988
|
const present = /* @__PURE__ */ new Set();
|
|
5198
5989
|
for await (const key2 of deps.storage.list(ref)) present.add(key2.generation);
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
}
|
|
5202
|
-
return void 0;
|
|
5990
|
+
const toRead = [...present].sort((a, b) => b - a).filter((generation2) => !clean.has(generation2));
|
|
5991
|
+
return (await holdsEach(toRead, true)).find((outcome) => outcome.held === true)?.generation;
|
|
5203
5992
|
};
|
|
5204
5993
|
const cannotRemove = (generation2) => new WriteConflictError(
|
|
5205
5994
|
`erasure of segment ${ref.segment} could not remove generation ${generation2}, which still holds the id and is still in the bucket; re-run`
|
|
@@ -5220,20 +6009,21 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
5220
6009
|
const newestFirst = [...others].sort((a, b) => b - a);
|
|
5221
6010
|
const clean = /* @__PURE__ */ new Set([from]);
|
|
5222
6011
|
const holdersAbove = [];
|
|
5223
|
-
for (const
|
|
5224
|
-
|
|
6012
|
+
for (const { generation: generation2, held } of await holdsEach(
|
|
6013
|
+
newestFirst.filter((g) => g > from),
|
|
6014
|
+
false
|
|
6015
|
+
)) {
|
|
5225
6016
|
if (held === true) holdersAbove.push(generation2);
|
|
5226
6017
|
else if (held === false) clean.add(generation2);
|
|
5227
6018
|
}
|
|
5228
6019
|
let holderBelow;
|
|
5229
6020
|
if (holdersAbove.length === 0) {
|
|
5230
|
-
for (const
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
if (held === false) clean.add(generation2);
|
|
6021
|
+
for (const { generation: generation2, held } of await holdsEach(
|
|
6022
|
+
newestFirst.filter((g) => g < from),
|
|
6023
|
+
true
|
|
6024
|
+
)) {
|
|
6025
|
+
if (held === true) holderBelow = generation2;
|
|
6026
|
+
else if (held === false) clean.add(generation2);
|
|
5237
6027
|
}
|
|
5238
6028
|
}
|
|
5239
6029
|
const newest = holdersAbove[0] ?? holderBelow;
|
|
@@ -5348,25 +6138,31 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
5348
6138
|
if (left !== void 0) throw cannotRemove(left);
|
|
5349
6139
|
return { ...base, erased: true, fromGeneration: from, generation, collected };
|
|
5350
6140
|
}
|
|
5351
|
-
var
|
|
6141
|
+
var REWRITE_RANGES_AHEAD = 4;
|
|
5352
6142
|
async function* rewrite(reader, chunkKey, replacement, codec, maxBytes, read) {
|
|
5353
6143
|
const keys = [...reader.chunkKeys()].sort((a, b) => a - b);
|
|
5354
|
-
const
|
|
6144
|
+
const stream = reader.readChunks(
|
|
5355
6145
|
keys.filter((k) => k !== chunkKey),
|
|
5356
|
-
|
|
5357
|
-
REWRITE_READ_AHEAD,
|
|
5358
|
-
false
|
|
6146
|
+
{ concurrency: REWRITE_RANGES_AHEAD, readRange: read }
|
|
5359
6147
|
);
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
6148
|
+
try {
|
|
6149
|
+
for (const k of keys) {
|
|
6150
|
+
if (k === chunkKey) {
|
|
6151
|
+
yield { chunkKey: k, bitmap: replacement };
|
|
6152
|
+
continue;
|
|
6153
|
+
}
|
|
6154
|
+
const item = await stream.next();
|
|
6155
|
+
if (item.done === true || item.value.key !== k) {
|
|
6156
|
+
throw new IntegrityError(`the chunk stream ended before chunk ${k}`);
|
|
6157
|
+
}
|
|
6158
|
+
const bytes = item.value.bytes;
|
|
6159
|
+
if (bytes === null) continue;
|
|
6160
|
+
const bitmap = codec.safeDeserialize(bytes, maxBytes);
|
|
6161
|
+
assertRemaindersInRange(bitmap, k);
|
|
6162
|
+
yield { chunkKey: k, bitmap };
|
|
5364
6163
|
}
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
const bitmap = codec.safeDeserialize(bytes, maxBytes);
|
|
5368
|
-
assertRemaindersInRange(bitmap, k);
|
|
5369
|
-
yield { chunkKey: k, bitmap };
|
|
6164
|
+
} finally {
|
|
6165
|
+
await stream.return(void 0);
|
|
5370
6166
|
}
|
|
5371
6167
|
}
|
|
5372
6168
|
function assertRemaindersInRange(bitmap, chunkKey) {
|
|
@@ -5521,9 +6317,8 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
5521
6317
|
segment: rec.segment
|
|
5522
6318
|
}));
|
|
5523
6319
|
const audit = safeAudit(options.audit ?? NOOP_AUDIT);
|
|
5524
|
-
const destroyed = [];
|
|
5525
6320
|
let segmentsShredded = 0;
|
|
5526
|
-
|
|
6321
|
+
const destroyed = await mapWithConcurrency(refs, ERASE_CONCURRENCY, async (ref) => {
|
|
5527
6322
|
let result;
|
|
5528
6323
|
try {
|
|
5529
6324
|
result = await shredSegment(ref, deps, options.allowCleartext ?? false);
|
|
@@ -5536,12 +6331,12 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
5536
6331
|
reason: isWriteConflictError(err) ? "contended" : `failed: ${err instanceof Error ? err.message : String(err)}`
|
|
5537
6332
|
};
|
|
5538
6333
|
}
|
|
5539
|
-
destroyed.push(result);
|
|
5540
6334
|
if (result.cryptoShredded) {
|
|
5541
6335
|
segmentsShredded += 1;
|
|
5542
6336
|
audit.onEvent({ kind: "segment.erase", namespace: ref.namespace, segment: ref.segment });
|
|
5543
6337
|
}
|
|
5544
|
-
|
|
6338
|
+
return result;
|
|
6339
|
+
});
|
|
5545
6340
|
audit.onEvent({ kind: "namespace.erase", namespace, segmentsShredded });
|
|
5546
6341
|
return { destroyed };
|
|
5547
6342
|
}
|
|
@@ -6280,6 +7075,7 @@ var RetryingStorageChunkSource = class {
|
|
|
6280
7075
|
deps;
|
|
6281
7076
|
/** Present only when the inner source supports it — so capability detection stays honest. */
|
|
6282
7077
|
sizeOf;
|
|
7078
|
+
getChunks;
|
|
6283
7079
|
cardinalities;
|
|
6284
7080
|
summary;
|
|
6285
7081
|
currentGeneration;
|
|
@@ -6302,6 +7098,16 @@ var RetryingStorageChunkSource = class {
|
|
|
6302
7098
|
if (innerSizeOf) {
|
|
6303
7099
|
this.sizeOf = (ref) => withRetry(() => innerSizeOf.call(inner, ref), this.policy, this.deps);
|
|
6304
7100
|
}
|
|
7101
|
+
const innerGetChunks = inner.getChunks;
|
|
7102
|
+
if (innerGetChunks) {
|
|
7103
|
+
this.getChunks = (ref, keys, options) => {
|
|
7104
|
+
const outer = options?.retry;
|
|
7105
|
+
return innerGetChunks.call(inner, ref, keys, {
|
|
7106
|
+
...options,
|
|
7107
|
+
retry: (request) => withRetry(outer === void 0 ? request : () => outer(request), this.policy, this.deps)
|
|
7108
|
+
});
|
|
7109
|
+
};
|
|
7110
|
+
}
|
|
6305
7111
|
const innerCardinalities = inner.cardinalities;
|
|
6306
7112
|
if (innerCardinalities) {
|
|
6307
7113
|
this.cardinalities = (ref) => withRetry(() => innerCardinalities.call(inner, ref), this.policy, this.deps);
|