@cloudbitmaps/core 0.25.0 → 0.26.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-BUDAVGBI.js → chunk-7VNEBKC4.js} +2 -2
- package/dist/{chunk-45ML6YU4.js → chunk-LJLZRVYN.js} +2 -2
- package/dist/{chunk-WPER7R2R.js → chunk-MNXTZJZ6.js} +2 -2
- package/dist/{chunk-2B2QF4FU.js → chunk-UFOMYONZ.js} +1 -1
- package/dist/chunk-UFOMYONZ.js.map +7 -0
- package/dist/core/crbm-storage-source.d.ts +7 -1
- package/dist/core/engine.d.ts +53 -0
- package/dist/core/pinned-storage-source.d.ts +2 -0
- package/dist/core/ports.d.ts +16 -1
- package/dist/driver-kit.js +3 -3
- package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +245 -6
- package/dist/index.js.map +2 -2
- package/dist/node.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-2B2QF4FU.js.map +0 -7
- /package/dist/{chunk-BUDAVGBI.js.map → chunk-7VNEBKC4.js.map} +0 -0
- /package/dist/{chunk-45ML6YU4.js.map → chunk-LJLZRVYN.js.map} +0 -0
- /package/dist/{chunk-WPER7R2R.js.map → chunk-MNXTZJZ6.js.map} +0 -0
|
@@ -405,9 +405,15 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
405
405
|
* ({@link replacedPins}), and a check under way is shared by the reads that ask at once; neither "gone" nor "the
|
|
406
406
|
* same" is kept, since either can change. A transient fault is rethrown, for the store's retries to see for what it
|
|
407
407
|
* is. A footer that cannot be read otherwise says nothing about which object is there, so this returns, and the
|
|
408
|
-
* caller's own error stands.
|
|
408
|
+
* caller's own error stands; a caller with no error of its own passes `strict`, and that fault is raised.
|
|
409
409
|
*/
|
|
410
410
|
private throwIfReplaced;
|
|
411
|
+
/**
|
|
412
|
+
* Throws the pin's `NotFoundError` when the object under `generation`'s key is no longer the one `held` names: one
|
|
413
|
+
* footer read, shared with a check already under way for the same pin. It fails closed: a footer that cannot be read,
|
|
414
|
+
* or an object that is not a generation object of this store, raises its own fault rather than passing.
|
|
415
|
+
*/
|
|
416
|
+
verifyHeld(ref: SegmentRef, generation: number, held: PinnedObject): Promise<void>;
|
|
411
417
|
/** Chunk keys of a specific generation — the pinned shape read. `held` as for {@link getChunkAt}. */
|
|
412
418
|
listChunkKeysAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<number[]>;
|
|
413
419
|
/**
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -101,6 +101,14 @@ export interface CombinedChunk {
|
|
|
101
101
|
readonly chunkKey: number;
|
|
102
102
|
readonly bitmap: CodecBitmap;
|
|
103
103
|
}
|
|
104
|
+
/** Options of {@link SegmentEngine.combineCount}: a combine's, and a check the count runs at its start and at each look. */
|
|
105
|
+
export interface CountOptions extends CombineOptions {
|
|
106
|
+
/**
|
|
107
|
+
* Runs before the first key and at each look, and may throw to stop the count (a lease that has ended). A count
|
|
108
|
+
* answered from the index fetches no chunk, so this is where such a handle is held to its terms.
|
|
109
|
+
*/
|
|
110
|
+
readonly guard?: () => void;
|
|
111
|
+
}
|
|
104
112
|
export declare class SegmentEngine {
|
|
105
113
|
private readonly storage;
|
|
106
114
|
private readonly cache;
|
|
@@ -397,6 +405,51 @@ export declare class SegmentEngine {
|
|
|
397
405
|
private combineBatches;
|
|
398
406
|
/** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
|
|
399
407
|
private combineChunks;
|
|
408
|
+
/**
|
|
409
|
+
* How many ids `segs[0] ∩ …` (`mode` 'all') or `segs[0] ∪ …` ('any') holds, minus `excludeSegs`, without streaming
|
|
410
|
+
* them: the size of what {@link combine} would stream. Each chunk key is taken one of two ways. A key exactly one
|
|
411
|
+
* include holds, that no exclude holds and the range does not cut, is answered from the index's count for it, with
|
|
412
|
+
* no chunk read. Every other key is decoded and counted as the combine reads it, with the same plan, the same ranges
|
|
413
|
+
* and the same window, over those keys alone. So an intersection reads what its stream reads, and a union or a
|
|
414
|
+
* difference reads no more than its stream.
|
|
415
|
+
*
|
|
416
|
+
* The keys answered from the index are trusted as {@link count} trusts the index, and checked no further than the
|
|
417
|
+
* reader checks it when it opens the object; a decoded key is exact. A source that reports no per-chunk counts has
|
|
418
|
+
* every key decoded. A union or a difference of one operand with no exclude and no range is {@link count}.
|
|
419
|
+
*
|
|
420
|
+
* A running union or difference, or an intersection with an `exclude`, looks at its operands' versions every
|
|
421
|
+
* `concurrency` keys, a key answered from the index being one taken, and any count looks before the first key after
|
|
422
|
+
* an invalidation, and runs `options.guard` at each look. On a move it plans again above the last key taken, as a
|
|
423
|
+
* combine does, so the bound on what it serves of an earlier generation is the combine's. A pinned operand's object
|
|
424
|
+
* is looked up before the first key and at each look there is (one footer read each, a fault other than a transient
|
|
425
|
+
* one raised as it is), so an erasure of what it pinned stops the count there with the pin's `NotFoundError`. An
|
|
426
|
+
* intersection of two or more operands with no exclude decodes every key and so shows a move without a version
|
|
427
|
+
* look; its guard still runs every `concurrency` keys.
|
|
428
|
+
*/
|
|
429
|
+
combineCount(segs: readonly SegmentRef[], excludeSegs: readonly SegmentRef[], mode: 'all' | 'any', op: 'intersect' | 'union' | 'andNot', options?: Omit<CountOptions, 'exclude'>): Promise<number>;
|
|
430
|
+
/** Take every step of `run`: the index's figure for one, the size of the next decoded chunk for another. */
|
|
431
|
+
private runCount;
|
|
432
|
+
/** Run the guard every `limit` keys when the version looks are off (an AND of two or more includes). */
|
|
433
|
+
private guardAtCadence;
|
|
434
|
+
/**
|
|
435
|
+
* Look up each pinned operand's generation object, as a pinned read finds it: one footer read per pinned operand, and
|
|
436
|
+
* the pin's `NotFoundError` when the object is gone or is another. An operand that is not pinned costs nothing.
|
|
437
|
+
*/
|
|
438
|
+
private verifyPinned;
|
|
439
|
+
/** Look at every operand's version, as {@link nextSlotAfterLook} does, and plan the keys above `last` again on a move. */
|
|
440
|
+
private countLook;
|
|
441
|
+
/**
|
|
442
|
+
* {@link combineOperands} for a count: each include also carries the per-chunk counts of the generation its keys were
|
|
443
|
+
* listed from. The counts are asked for after the keys, and the two must name the same keys, or the operand is listed
|
|
444
|
+
* again (a move between the two reads), and refused if a source keeps disagreeing with itself. A source with no
|
|
445
|
+
* per-chunk counts gives none, and every key of that operand is decoded.
|
|
446
|
+
*/
|
|
447
|
+
private countOperands;
|
|
448
|
+
/**
|
|
449
|
+
* The per-chunk counts of `seg`, checked as untrusted tier data; `undefined` when the source reports none, and `'moved'`
|
|
450
|
+
* when they name other keys than `listed`, which the segment's generation changing between the two reads explains.
|
|
451
|
+
*/
|
|
452
|
+
private chunkCounts;
|
|
400
453
|
/**
|
|
401
454
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
402
455
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -112,5 +112,7 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
|
|
|
112
112
|
*/
|
|
113
113
|
currentVersion(ref: SegmentRef): Promise<string | null>;
|
|
114
114
|
exists(ref: SegmentRef): Promise<boolean>;
|
|
115
|
+
/** Whether the object this segment's pin holds is still there: see {@link HeldObjectLookup.verifyPinned}. */
|
|
116
|
+
verifyPinned(ref: SegmentRef): Promise<void>;
|
|
115
117
|
invalidate(ref: SegmentRef): void;
|
|
116
118
|
}
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -147,7 +147,9 @@ export interface StorageChunkSource {
|
|
|
147
147
|
* already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.
|
|
148
148
|
* Powers the free `count()` of a source with no {@link StorageChunkSource.summary} — the engine sums the index
|
|
149
149
|
* instead of fetching a single chunk. A source with no
|
|
150
|
-
* index (e.g. the in-memory source) omits this, and `count()` falls back to fetching every chunk.
|
|
150
|
+
* index (e.g. the in-memory source) omits this, and `count()` falls back to fetching every chunk. A count of a
|
|
151
|
+
* combine also asks for it, and takes its keys to be the ones `listChunkKeys` names for the same generation (an emptied
|
|
152
|
+
* chunk is named by both or by neither), listing again when they differ and refusing a source that keeps disagreeing.
|
|
151
153
|
*/
|
|
152
154
|
cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
|
|
153
155
|
/**
|
|
@@ -227,6 +229,19 @@ export interface StorageChunkSource {
|
|
|
227
229
|
*/
|
|
228
230
|
currentVersion?(ref: SegmentRef): Promise<string | null>;
|
|
229
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* What a source that reads pinned generations also offers the engine, apart from {@link StorageChunkSource}: a lookup of
|
|
234
|
+
* the object a pin holds. Not part of the port a source implements; the engine asks for it when it is there.
|
|
235
|
+
*/
|
|
236
|
+
export interface HeldObjectLookup {
|
|
237
|
+
/**
|
|
238
|
+
* Resolves when the segment is not pinned here, or when the object its pin holds is still under its key; rejects with
|
|
239
|
+
* the pin's `NotFoundError` when it is gone or is another object, and with the fault itself when the footer cannot be
|
|
240
|
+
* read or is not a generation object's (a transient fault is for the caller's retries). One footer read for a pinned
|
|
241
|
+
* segment, none for any other.
|
|
242
|
+
*/
|
|
243
|
+
verifyPinned(ref: SegmentRef): Promise<void>;
|
|
244
|
+
}
|
|
230
245
|
/** Capabilities a Storage driver advertises; validated at wiring time, fail-fast. */
|
|
231
246
|
export interface StorageCaps {
|
|
232
247
|
/** REQUIRED — the format relies on byte-range reads. */
|
package/dist/driver-kit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mapWithConcurrency
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7VNEBKC4.js";
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_NAMESPACE,
|
|
6
6
|
applyRegistryPatch,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
validateNewRegistryRecord,
|
|
19
19
|
validateRegistryPatch,
|
|
20
20
|
webCryptoEntropy
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-LJLZRVYN.js";
|
|
22
22
|
import {
|
|
23
23
|
CloudRoaringError,
|
|
24
24
|
IntegrityError,
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
requireNonEmptyString,
|
|
46
46
|
unsettledConflict,
|
|
47
47
|
validateSegmentRef
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-UFOMYONZ.js";
|
|
49
49
|
|
|
50
50
|
// src/drivers/_shared/object-registry-keys.ts
|
|
51
51
|
var REGISTRY_SUFFIX = ".reg";
|
|
@@ -51,6 +51,7 @@ export declare class RetryingStorageChunkSource implements StorageChunkSource {
|
|
|
51
51
|
readonly invalidate?: (ref: SegmentRef) => void;
|
|
52
52
|
readonly exists?: (ref: SegmentRef) => Promise<boolean>;
|
|
53
53
|
readonly currentVersion?: (ref: SegmentRef) => Promise<string | null>;
|
|
54
|
+
readonly verifyPinned?: (ref: SegmentRef) => Promise<void>;
|
|
54
55
|
constructor(inner: StorageChunkSource, opts: RetryingOptions);
|
|
55
56
|
getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
|
|
56
57
|
listChunkKeys(ref: SegmentRef): Promise<number[]>;
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* publishable on its own and every codec reuses one driver set with zero duplication.
|
|
20
20
|
*/
|
|
21
21
|
export { SegmentEngine } from './core/engine.js';
|
|
22
|
-
export type { EngineDeps, CombineOptions as EngineCombineOptions, IdRange } from './core/engine.js';
|
|
22
|
+
export type { EngineDeps, CombineOptions as EngineCombineOptions, CountOptions as EngineCountOptions, IdRange, } from './core/engine.js';
|
|
23
23
|
export { BoundedLru } from './core/lru.js';
|
|
24
24
|
export { safeMetrics } from './core/metrics.js';
|
|
25
25
|
export { runExport } from './export/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
boundedAll,
|
|
3
3
|
mapWithConcurrency
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-7VNEBKC4.js";
|
|
5
5
|
import {
|
|
6
6
|
BufferReader,
|
|
7
7
|
KeptSegmentKeys,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
segmentKey,
|
|
10
10
|
segmentPrefix,
|
|
11
11
|
shardOf
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-MNXTZJZ6.js";
|
|
13
13
|
import {
|
|
14
14
|
BudgetExceededError,
|
|
15
15
|
CHUNK_COUNT,
|
|
@@ -116,7 +116,7 @@ import {
|
|
|
116
116
|
validateUserRef,
|
|
117
117
|
windowOf,
|
|
118
118
|
withRetry
|
|
119
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-UFOMYONZ.js";
|
|
120
120
|
|
|
121
121
|
// src/core/budget.ts
|
|
122
122
|
var DEFAULT_BUDGET = { maxRequests: 1e6 };
|
|
@@ -674,6 +674,7 @@ var COMBINE_WINDOW_START = 8;
|
|
|
674
674
|
var COMBINE_RANGE_START = 4;
|
|
675
675
|
var OPERANDS_AT_ONCE = 16;
|
|
676
676
|
var BATCH_QUIET_HOPS = 16;
|
|
677
|
+
var COUNT_RELIST_ATTEMPTS = 4;
|
|
677
678
|
var ZERO_CLOCK = { now: () => 0 };
|
|
678
679
|
var CombineWindow = class {
|
|
679
680
|
constructor(keys, limit, read) {
|
|
@@ -797,6 +798,19 @@ function combineReads(keys, operands, excludes, mode, through) {
|
|
|
797
798
|
}
|
|
798
799
|
return units;
|
|
799
800
|
}
|
|
801
|
+
function countSteps(operands, excludes, mode, w) {
|
|
802
|
+
const steps = [];
|
|
803
|
+
const decode = [];
|
|
804
|
+
for (const key of alignKeys(operands, mode)) {
|
|
805
|
+
const holders = mode === "all" ? operands : operands.filter((o) => o.keys.has(key));
|
|
806
|
+
const only = holders.length === 1 ? holders[0] : void 0;
|
|
807
|
+
const n = only?.cards?.get(key);
|
|
808
|
+
const viaIndex = n !== void 0 && !excludes.some((e) => e.keys.has(key)) && !(w !== null && isEdge(key, w));
|
|
809
|
+
steps.push({ key, n: viaIndex ? n : -1 });
|
|
810
|
+
if (!viaIndex) decode.push(key);
|
|
811
|
+
}
|
|
812
|
+
return { steps, decode };
|
|
813
|
+
}
|
|
800
814
|
var SegmentEngine = class _SegmentEngine {
|
|
801
815
|
storage;
|
|
802
816
|
cache;
|
|
@@ -1514,6 +1528,209 @@ var SegmentEngine = class _SegmentEngine {
|
|
|
1514
1528
|
this.closeCombine(run.plan);
|
|
1515
1529
|
}
|
|
1516
1530
|
}
|
|
1531
|
+
/**
|
|
1532
|
+
* How many ids `segs[0] ∩ …` (`mode` 'all') or `segs[0] ∪ …` ('any') holds, minus `excludeSegs`, without streaming
|
|
1533
|
+
* them: the size of what {@link combine} would stream. Each chunk key is taken one of two ways. A key exactly one
|
|
1534
|
+
* include holds, that no exclude holds and the range does not cut, is answered from the index's count for it, with
|
|
1535
|
+
* no chunk read. Every other key is decoded and counted as the combine reads it, with the same plan, the same ranges
|
|
1536
|
+
* and the same window, over those keys alone. So an intersection reads what its stream reads, and a union or a
|
|
1537
|
+
* difference reads no more than its stream.
|
|
1538
|
+
*
|
|
1539
|
+
* The keys answered from the index are trusted as {@link count} trusts the index, and checked no further than the
|
|
1540
|
+
* reader checks it when it opens the object; a decoded key is exact. A source that reports no per-chunk counts has
|
|
1541
|
+
* every key decoded. A union or a difference of one operand with no exclude and no range is {@link count}.
|
|
1542
|
+
*
|
|
1543
|
+
* A running union or difference, or an intersection with an `exclude`, looks at its operands' versions every
|
|
1544
|
+
* `concurrency` keys, a key answered from the index being one taken, and any count looks before the first key after
|
|
1545
|
+
* an invalidation, and runs `options.guard` at each look. On a move it plans again above the last key taken, as a
|
|
1546
|
+
* combine does, so the bound on what it serves of an earlier generation is the combine's. A pinned operand's object
|
|
1547
|
+
* is looked up before the first key and at each look there is (one footer read each, a fault other than a transient
|
|
1548
|
+
* one raised as it is), so an erasure of what it pinned stops the count there with the pin's `NotFoundError`. An
|
|
1549
|
+
* intersection of two or more operands with no exclude decodes every key and so shows a move without a version
|
|
1550
|
+
* look; its guard still runs every `concurrency` keys.
|
|
1551
|
+
*/
|
|
1552
|
+
async combineCount(segs, excludeSegs, mode, op, options) {
|
|
1553
|
+
const label = `${op}Count`;
|
|
1554
|
+
if (segs.length === 0) throw new ValidationError(`${label} requires at least one segment`);
|
|
1555
|
+
const limit = options?.concurrency === void 0 ? DEFAULT_INTERSECT_CONCURRENCY : options.concurrency;
|
|
1556
|
+
requirePositiveInteger(limit, `${label}: concurrency`);
|
|
1557
|
+
const budget = resolvePerOpBudget(options?.budget, this.budget);
|
|
1558
|
+
const w = windowOf2(options, label);
|
|
1559
|
+
options?.guard?.();
|
|
1560
|
+
if (w === "empty") return 0;
|
|
1561
|
+
if (op !== "intersect" && segs.length === 1 && excludeSegs.length === 0 && w === null) {
|
|
1562
|
+
const n = await this.count(segs[0]);
|
|
1563
|
+
if (n === 0) {
|
|
1564
|
+
const none = {
|
|
1565
|
+
seg: segs[0],
|
|
1566
|
+
keys: /* @__PURE__ */ new Set(),
|
|
1567
|
+
chunkless: true,
|
|
1568
|
+
gen: void 0,
|
|
1569
|
+
epoch: 0
|
|
1570
|
+
};
|
|
1571
|
+
await this.refuseAbsentOperands([none], options?.allowAbsentOperands, label);
|
|
1572
|
+
}
|
|
1573
|
+
return n;
|
|
1574
|
+
}
|
|
1575
|
+
const epoch = this.scope.epoch;
|
|
1576
|
+
const [operands, excludes] = await this.countOperands(segs, excludeSegs, mode, w, -1);
|
|
1577
|
+
await this.refuseAbsentOperands(
|
|
1578
|
+
[...operands, ...excludes],
|
|
1579
|
+
options?.allowAbsentOperands,
|
|
1580
|
+
label
|
|
1581
|
+
);
|
|
1582
|
+
await this.verifyPinned([...operands, ...excludes]);
|
|
1583
|
+
const { steps, decode } = countSteps(operands, excludes, mode, w);
|
|
1584
|
+
checkBudget(budget, combineReads(decode, operands, excludes, mode, Infinity), label);
|
|
1585
|
+
if (this.metricsOn) {
|
|
1586
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
1587
|
+
for (const o of [...operands, ...excludes]) for (const k of o.keys) distinct.add(k);
|
|
1588
|
+
this.metrics.onEvent({
|
|
1589
|
+
kind: "intersect",
|
|
1590
|
+
op,
|
|
1591
|
+
operands: segs.length + excludes.length,
|
|
1592
|
+
fetchedChunks: decode.length,
|
|
1593
|
+
skippedChunks: distinct.size - decode.length
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
const everyKey = mode === "all" && segs.length > 1 && excludeSegs.length === 0;
|
|
1597
|
+
const run = {
|
|
1598
|
+
segs,
|
|
1599
|
+
excludeSegs,
|
|
1600
|
+
mode,
|
|
1601
|
+
op,
|
|
1602
|
+
limit,
|
|
1603
|
+
budget,
|
|
1604
|
+
w,
|
|
1605
|
+
plan: this.openCombine(operands, excludes, mode, decode, limit),
|
|
1606
|
+
looks: this.looks(everyKey ? Infinity : limit, epoch),
|
|
1607
|
+
spent: 0,
|
|
1608
|
+
steps,
|
|
1609
|
+
at: 0,
|
|
1610
|
+
guard: options?.guard,
|
|
1611
|
+
...everyKey && options?.guard !== void 0 ? { guardEvery: { since: 0 } } : {}
|
|
1612
|
+
};
|
|
1613
|
+
try {
|
|
1614
|
+
return await this.runCount(run);
|
|
1615
|
+
} finally {
|
|
1616
|
+
this.closeCombine(run.plan);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
/** Take every step of `run`: the index's figure for one, the size of the next decoded chunk for another. */
|
|
1620
|
+
async runCount(run) {
|
|
1621
|
+
let total = 0;
|
|
1622
|
+
let last = -1;
|
|
1623
|
+
while (run.at < run.steps.length) {
|
|
1624
|
+
run.looks.left -= 1;
|
|
1625
|
+
if (run.guardEvery !== void 0) this.guardAtCadence(run);
|
|
1626
|
+
if (run.plan.moves.moved || this.lookDue(run.looks)) {
|
|
1627
|
+
await this.countLook(run, last);
|
|
1628
|
+
if (run.at >= run.steps.length) break;
|
|
1629
|
+
}
|
|
1630
|
+
const step = run.steps[run.at++];
|
|
1631
|
+
last = step.key;
|
|
1632
|
+
if (step.n >= 0) {
|
|
1633
|
+
total += step.n;
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
const slot = await run.plan.window.next();
|
|
1637
|
+
const chunk = slot?.result;
|
|
1638
|
+
if (chunk === null || chunk === void 0 || chunk.isEmpty) continue;
|
|
1639
|
+
total += run.w !== null && isEdge(step.key, run.w) ? cutToWindow(this.codec, chunk, step.key, run.w).size : chunk.size;
|
|
1640
|
+
}
|
|
1641
|
+
return total;
|
|
1642
|
+
}
|
|
1643
|
+
/** Run the guard every `limit` keys when the version looks are off (an AND of two or more includes). */
|
|
1644
|
+
guardAtCadence(run) {
|
|
1645
|
+
const cadence = run.guardEvery;
|
|
1646
|
+
if (cadence.since >= run.limit) {
|
|
1647
|
+
cadence.since = 0;
|
|
1648
|
+
run.guard();
|
|
1649
|
+
}
|
|
1650
|
+
cadence.since += 1;
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Look up each pinned operand's generation object, as a pinned read finds it: one footer read per pinned operand, and
|
|
1654
|
+
* the pin's `NotFoundError` when the object is gone or is another. An operand that is not pinned costs nothing.
|
|
1655
|
+
*/
|
|
1656
|
+
async verifyPinned(operands) {
|
|
1657
|
+
const verify = this.storage.verifyPinned;
|
|
1658
|
+
if (verify === void 0) return;
|
|
1659
|
+
await boundedAll(operands, OPERANDS_AT_ONCE, (o) => verify.call(this.storage, o.seg));
|
|
1660
|
+
}
|
|
1661
|
+
/** Look at every operand's version, as {@link nextSlotAfterLook} does, and plan the keys above `last` again on a move. */
|
|
1662
|
+
async countLook(run, last) {
|
|
1663
|
+
const plan = run.plan;
|
|
1664
|
+
plan.moves.moved = false;
|
|
1665
|
+
this.looked(run.looks);
|
|
1666
|
+
run.guard?.();
|
|
1667
|
+
const all = [...plan.operands, ...plan.excludes];
|
|
1668
|
+
await this.verifyPinned(all);
|
|
1669
|
+
const now = await boundedAll(all, OPERANDS_AT_ONCE, (o) => this.cacheVersion(o.seg));
|
|
1670
|
+
if (now.every((version, i) => version === all[i].gen)) return;
|
|
1671
|
+
this.closeCombine(plan);
|
|
1672
|
+
const spent = run.spent + combineReads(plan.keys, plan.operands, plan.excludes, run.mode, last);
|
|
1673
|
+
const [operands, excludes] = await this.countOperands(
|
|
1674
|
+
run.segs,
|
|
1675
|
+
run.excludeSegs,
|
|
1676
|
+
run.mode,
|
|
1677
|
+
run.w,
|
|
1678
|
+
last
|
|
1679
|
+
);
|
|
1680
|
+
const { steps, decode } = countSteps(operands, excludes, run.mode, run.w);
|
|
1681
|
+
checkBudget(
|
|
1682
|
+
run.budget,
|
|
1683
|
+
spent + combineReads(decode, operands, excludes, run.mode, Infinity),
|
|
1684
|
+
`${run.op}Count`
|
|
1685
|
+
);
|
|
1686
|
+
run.plan = this.openCombine(operands, excludes, run.mode, decode, run.limit);
|
|
1687
|
+
run.spent = spent;
|
|
1688
|
+
run.steps = steps;
|
|
1689
|
+
run.at = 0;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* {@link combineOperands} for a count: each include also carries the per-chunk counts of the generation its keys were
|
|
1693
|
+
* listed from. The counts are asked for after the keys, and the two must name the same keys, or the operand is listed
|
|
1694
|
+
* again (a move between the two reads), and refused if a source keeps disagreeing with itself. A source with no
|
|
1695
|
+
* per-chunk counts gives none, and every key of that operand is decoded.
|
|
1696
|
+
*/
|
|
1697
|
+
async countOperands(segs, excludeSegs, mode, w, after) {
|
|
1698
|
+
const wantCounts = !(mode === "all" && segs.length > 1);
|
|
1699
|
+
const extract = async (seg, withCounts) => {
|
|
1700
|
+
for (let attempt = 0; attempt < COUNT_RELIST_ATTEMPTS; attempt++) {
|
|
1701
|
+
const { listed: listed2, gen, epoch } = await this.listKeys(seg);
|
|
1702
|
+
const cards = withCounts ? await this.chunkCounts(seg, listed2) : void 0;
|
|
1703
|
+
if (cards === "moved") continue;
|
|
1704
|
+
const keys = after < 0 && w === null ? listed2 : keysAbove(listed2, w, after);
|
|
1705
|
+
return { seg, keys: new Set(keys), chunkless: listed2.length === 0, gen, epoch, cards };
|
|
1706
|
+
}
|
|
1707
|
+
throw new IntegrityError("a segment's index and its chunk keys name different chunks");
|
|
1708
|
+
};
|
|
1709
|
+
const all = await boundedAll(
|
|
1710
|
+
[
|
|
1711
|
+
...segs.map((s) => [s, wantCounts]),
|
|
1712
|
+
...excludeSegs.map((s) => [s, false])
|
|
1713
|
+
],
|
|
1714
|
+
OPERANDS_AT_ONCE,
|
|
1715
|
+
([seg, withCounts]) => extract(seg, withCounts)
|
|
1716
|
+
);
|
|
1717
|
+
return [all.slice(0, segs.length), all.slice(segs.length)];
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* The per-chunk counts of `seg`, checked as untrusted tier data; `undefined` when the source reports none, and `'moved'`
|
|
1721
|
+
* when they name other keys than `listed`, which the segment's generation changing between the two reads explains.
|
|
1722
|
+
*/
|
|
1723
|
+
async chunkCounts(seg, listed2) {
|
|
1724
|
+
const counts = this.storage.cardinalities ? await this.storage.cardinalities(seg) : null;
|
|
1725
|
+
if (counts === null) return void 0;
|
|
1726
|
+
if (counts.size !== listed2.length) return "moved";
|
|
1727
|
+
for (const key of listed2) {
|
|
1728
|
+
const n = counts.get(key);
|
|
1729
|
+
if (n === void 0) return "moved";
|
|
1730
|
+
if (n !== 0) assertChunkCardinalityInRange(n);
|
|
1731
|
+
}
|
|
1732
|
+
return counts;
|
|
1733
|
+
}
|
|
1517
1734
|
/**
|
|
1518
1735
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
1519
1736
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -4461,9 +4678,9 @@ var CrbmStorageChunkSource = class {
|
|
|
4461
4678
|
* ({@link replacedPins}), and a check under way is shared by the reads that ask at once; neither "gone" nor "the
|
|
4462
4679
|
* same" is kept, since either can change. A transient fault is rethrown, for the store's retries to see for what it
|
|
4463
4680
|
* is. A footer that cannot be read otherwise says nothing about which object is there, so this returns, and the
|
|
4464
|
-
* caller's own error stands.
|
|
4681
|
+
* caller's own error stands; a caller with no error of its own passes `strict`, and that fault is raised.
|
|
4465
4682
|
*/
|
|
4466
|
-
async throwIfReplaced(ref, generation, version, fingerprint) {
|
|
4683
|
+
async throwIfReplaced(ref, generation, version, fingerprint, strict = false) {
|
|
4467
4684
|
const held = this.heldKey(ref, version, fingerprint);
|
|
4468
4685
|
if (this.replacedPins.get(held) !== void 0) throw notThePinned(ref, generation);
|
|
4469
4686
|
let check = this.checking.get(held);
|
|
@@ -4491,11 +4708,20 @@ var CrbmStorageChunkSource = class {
|
|
|
4491
4708
|
try {
|
|
4492
4709
|
replaced = await check;
|
|
4493
4710
|
} catch (err) {
|
|
4494
|
-
if (isTransientError(err)) throw err;
|
|
4711
|
+
if (strict || isTransientError(err)) throw err;
|
|
4495
4712
|
return;
|
|
4496
4713
|
}
|
|
4497
4714
|
if (replaced) throw notThePinned(ref, generation);
|
|
4498
4715
|
}
|
|
4716
|
+
/**
|
|
4717
|
+
* Throws the pin's `NotFoundError` when the object under `generation`'s key is no longer the one `held` names: one
|
|
4718
|
+
* footer read, shared with a check already under way for the same pin. It fails closed: a footer that cannot be read,
|
|
4719
|
+
* or an object that is not a generation object of this store, raises its own fault rather than passing.
|
|
4720
|
+
*/
|
|
4721
|
+
async verifyHeld(ref, generation, held) {
|
|
4722
|
+
validateUserRef(ref, "read", PORT_KEYS);
|
|
4723
|
+
await this.throwIfReplaced(ref, generation, held.version, held.fingerprint, true);
|
|
4724
|
+
}
|
|
4499
4725
|
/** Chunk keys of a specific generation — the pinned shape read. `held` as for {@link getChunkAt}. */
|
|
4500
4726
|
async listChunkKeysAt(ref, generation, held) {
|
|
4501
4727
|
validateUserRef(ref, "read", PORT_KEYS);
|
|
@@ -5171,6 +5397,14 @@ var PinnedStorageChunkSource = class {
|
|
|
5171
5397
|
exists(ref) {
|
|
5172
5398
|
return this.inner.exists(ref);
|
|
5173
5399
|
}
|
|
5400
|
+
/** Whether the object this segment's pin holds is still there: see {@link HeldObjectLookup.verifyPinned}. */
|
|
5401
|
+
verifyPinned(ref) {
|
|
5402
|
+
const pin = this.pinFor(ref);
|
|
5403
|
+
const held = pin === void 0 ? void 0 : heldBy(pin);
|
|
5404
|
+
if (pin === void 0 || held === void 0 || pin.generation === null)
|
|
5405
|
+
return Promise.resolve();
|
|
5406
|
+
return this.inner.verifyHeld(ref, pin.generation, held);
|
|
5407
|
+
}
|
|
5174
5408
|
invalidate(ref) {
|
|
5175
5409
|
this.inner.invalidate(ref);
|
|
5176
5410
|
}
|
|
@@ -12023,6 +12257,7 @@ var RetryingStorageChunkSource = class {
|
|
|
12023
12257
|
invalidate;
|
|
12024
12258
|
exists;
|
|
12025
12259
|
currentVersion;
|
|
12260
|
+
verifyPinned;
|
|
12026
12261
|
constructor(inner, opts) {
|
|
12027
12262
|
this.inner = inner;
|
|
12028
12263
|
const r = toRetry(opts);
|
|
@@ -12068,6 +12303,10 @@ var RetryingStorageChunkSource = class {
|
|
|
12068
12303
|
if (innerExists) {
|
|
12069
12304
|
this.exists = (ref) => withRetry(() => innerExists.call(inner, ref), this.policy, this.deps);
|
|
12070
12305
|
}
|
|
12306
|
+
const innerVerify = inner.verifyPinned;
|
|
12307
|
+
if (innerVerify) {
|
|
12308
|
+
this.verifyPinned = (ref) => withRetry(() => innerVerify.call(inner, ref), this.policy, this.deps);
|
|
12309
|
+
}
|
|
12071
12310
|
const innerCurrentVersion = inner.currentVersion;
|
|
12072
12311
|
if (innerCurrentVersion) {
|
|
12073
12312
|
this.currentVersion = (ref) => withRetry(() => innerCurrentVersion.call(inner, ref), this.policy, this.deps);
|