@cloudbitmaps/roaring 0.16.0 → 0.18.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/PRIVACY.md +26 -0
- package/README.md +4 -4
- package/dist/bin/export-segments.js +1058 -59
- package/dist/bin/export-segments.js.map +4 -4
- package/dist/index.d.ts +529 -36
- package/dist/index.js +1073 -60
- package/dist/index.js.map +4 -4
- package/dist/lease-guards.d.ts +34 -0
- package/dist/roaring-codec.d.ts +23 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
NOOP_METRICS,
|
|
12
12
|
RetryingStorageChunkSource,
|
|
13
13
|
SegmentEngine,
|
|
14
|
+
LEASE_SKEW_MS,
|
|
15
|
+
LeaseExpiredError,
|
|
16
|
+
MAX_LEASE_MS,
|
|
17
|
+
NotFoundError,
|
|
14
18
|
UnsupportedError,
|
|
15
19
|
ValidationError as ValidationError2,
|
|
16
20
|
WriteConflictError,
|
|
@@ -24,17 +28,24 @@ import {
|
|
|
24
28
|
groundedReport,
|
|
25
29
|
loadSegment,
|
|
26
30
|
loadSegmentChunks,
|
|
31
|
+
compileCombineMany,
|
|
32
|
+
prepareHeld,
|
|
33
|
+
rebindCombineMany,
|
|
34
|
+
runCombineMany,
|
|
27
35
|
mapWithConcurrency,
|
|
28
36
|
resolveBudget,
|
|
29
37
|
resolvePerOpBudget,
|
|
38
|
+
reapRegistryTombstones,
|
|
30
39
|
retireExpired,
|
|
31
40
|
runConsistencyCheck,
|
|
32
41
|
runExport,
|
|
33
42
|
setSegmentRetention,
|
|
34
43
|
clearSegmentRetention,
|
|
35
44
|
getSegmentRetention,
|
|
45
|
+
releaseLease,
|
|
36
46
|
safeMetrics,
|
|
37
|
-
splitId
|
|
47
|
+
splitId,
|
|
48
|
+
takeLease
|
|
38
49
|
} from "@cloudbitmaps/core";
|
|
39
50
|
import { validateSegmentRef } from "@cloudbitmaps/core/driver-kit";
|
|
40
51
|
|
|
@@ -94,7 +105,7 @@ function refuseReservedNamespace(namespace) {
|
|
|
94
105
|
|
|
95
106
|
// src/roaring-codec.ts
|
|
96
107
|
import roaring from "roaring";
|
|
97
|
-
import { IntegrityError as IntegrityError2 } from "@cloudbitmaps/core";
|
|
108
|
+
import { IntegrityError as IntegrityError2, decodeSerialized } from "@cloudbitmaps/core";
|
|
98
109
|
|
|
99
110
|
// src/portable/layout.ts
|
|
100
111
|
import { IntegrityError } from "@cloudbitmaps/core";
|
|
@@ -342,6 +353,10 @@ var SafeBitmap = class _SafeBitmap {
|
|
|
342
353
|
throw new IntegrityError2(`failed to deserialize bitmap: ${err.message}`);
|
|
343
354
|
}
|
|
344
355
|
}
|
|
356
|
+
/** The `roaring` bitmap this wraps, for a caller that was handed it once and keeps no wrapper. */
|
|
357
|
+
unwrap() {
|
|
358
|
+
return this.bitmap;
|
|
359
|
+
}
|
|
345
360
|
serialize() {
|
|
346
361
|
return this.bitmap.serialize(SerializationFormat.portable);
|
|
347
362
|
}
|
|
@@ -454,6 +469,9 @@ var roaringCodec = {
|
|
|
454
469
|
safeDeserialize: (bytes, maxBytes, options) => SafeBitmap.safeDeserialize(bytes, maxBytes, options),
|
|
455
470
|
owns: (bitmap) => bitmap instanceof SafeBitmap
|
|
456
471
|
};
|
|
472
|
+
function deserializePortable(bytes) {
|
|
473
|
+
return decodeSerialized(bytes, roaringCodec).unwrap();
|
|
474
|
+
}
|
|
457
475
|
|
|
458
476
|
// src/system-clock.ts
|
|
459
477
|
var SystemClock = class {
|
|
@@ -506,6 +524,86 @@ var OPTION_KEYS = {
|
|
|
506
524
|
seams: ["clock", "rng"]
|
|
507
525
|
};
|
|
508
526
|
|
|
527
|
+
// src/lease-guards.ts
|
|
528
|
+
var guardIdIterable = (inner, check) => ({
|
|
529
|
+
[Symbol.asyncIterator]: () => {
|
|
530
|
+
const it = inner[Symbol.asyncIterator]();
|
|
531
|
+
let chunk = -1;
|
|
532
|
+
let first = true;
|
|
533
|
+
const stop = async (err) => {
|
|
534
|
+
await it.return?.();
|
|
535
|
+
throw err;
|
|
536
|
+
};
|
|
537
|
+
return {
|
|
538
|
+
async next() {
|
|
539
|
+
if (first) {
|
|
540
|
+
first = false;
|
|
541
|
+
try {
|
|
542
|
+
check();
|
|
543
|
+
} catch (err) {
|
|
544
|
+
return stop(err);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const r = await it.next();
|
|
548
|
+
if (r.done === true) return r;
|
|
549
|
+
const key = r.value >>> 16;
|
|
550
|
+
if (key !== chunk) {
|
|
551
|
+
try {
|
|
552
|
+
check();
|
|
553
|
+
} catch (err) {
|
|
554
|
+
return stop(err);
|
|
555
|
+
}
|
|
556
|
+
chunk = key;
|
|
557
|
+
}
|
|
558
|
+
return r;
|
|
559
|
+
},
|
|
560
|
+
async return() {
|
|
561
|
+
await it.return?.();
|
|
562
|
+
return { done: true, value: void 0 };
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
var guardIds = (inner, check) => ({
|
|
568
|
+
[Symbol.asyncIterator]: () => guardIdIterable(inner, check)[Symbol.asyncIterator](),
|
|
569
|
+
batches: () => guardItems(inner.batches(), check)
|
|
570
|
+
});
|
|
571
|
+
var guardItems = (inner, check) => ({
|
|
572
|
+
[Symbol.asyncIterator]: () => {
|
|
573
|
+
const it = inner[Symbol.asyncIterator]();
|
|
574
|
+
let first = true;
|
|
575
|
+
const stop = async (err) => {
|
|
576
|
+
await it.return?.();
|
|
577
|
+
throw err;
|
|
578
|
+
};
|
|
579
|
+
return {
|
|
580
|
+
async next() {
|
|
581
|
+
if (first) {
|
|
582
|
+
first = false;
|
|
583
|
+
try {
|
|
584
|
+
check();
|
|
585
|
+
} catch (err) {
|
|
586
|
+
return stop(err);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const r = await it.next();
|
|
590
|
+
if (r.done === true) return r;
|
|
591
|
+
try {
|
|
592
|
+
check();
|
|
593
|
+
} catch (err) {
|
|
594
|
+
return stop(err);
|
|
595
|
+
}
|
|
596
|
+
return r;
|
|
597
|
+
},
|
|
598
|
+
async return() {
|
|
599
|
+
await it.return?.();
|
|
600
|
+
return { done: true, value: void 0 };
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
var guardChunks = (inner, check) => guardItems(inner, check);
|
|
606
|
+
|
|
509
607
|
// src/index.ts
|
|
510
608
|
import {
|
|
511
609
|
MemoryStorage,
|
|
@@ -516,6 +614,9 @@ import {
|
|
|
516
614
|
excludingReservedRows as excludingReservedRows2,
|
|
517
615
|
readRetentionPolicy,
|
|
518
616
|
MIN_EXPIRES_AT_MS as MIN_EXPIRES_AT_MS2,
|
|
617
|
+
LEASE_SKEW_MS as LEASE_SKEW_MS2,
|
|
618
|
+
MAX_LEASE_MS as MAX_LEASE_MS2,
|
|
619
|
+
MAX_LEASES_PER_SEGMENT,
|
|
519
620
|
InProcessKeystore,
|
|
520
621
|
NodeAead,
|
|
521
622
|
aadFor,
|
|
@@ -523,18 +624,24 @@ import {
|
|
|
523
624
|
ValidationError as ValidationError3,
|
|
524
625
|
WriteConflictError as WriteConflictError2,
|
|
525
626
|
IntegrityError as IntegrityError3,
|
|
526
|
-
NotFoundError,
|
|
627
|
+
NotFoundError as NotFoundError2,
|
|
527
628
|
UnsupportedError as UnsupportedError2,
|
|
528
629
|
CapabilityError,
|
|
529
630
|
TransientError,
|
|
530
631
|
KeyUnavailableError,
|
|
531
632
|
BudgetExceededError,
|
|
633
|
+
LeaseExpiredError as LeaseExpiredError2,
|
|
634
|
+
LeaseLimitError,
|
|
635
|
+
StaleOperandError,
|
|
532
636
|
isCloudRoaringError,
|
|
533
637
|
isWriteConflictError,
|
|
534
638
|
isTransientError,
|
|
535
639
|
isNotFoundError,
|
|
536
640
|
isIntegrityError,
|
|
537
641
|
isValidationError,
|
|
642
|
+
isLeaseExpiredError,
|
|
643
|
+
isLeaseLimitError,
|
|
644
|
+
isStaleOperandError,
|
|
538
645
|
CrbmReader,
|
|
539
646
|
BufferReader,
|
|
540
647
|
CountingMetricsSink,
|
|
@@ -565,6 +672,36 @@ function requireScope(options, op) {
|
|
|
565
672
|
);
|
|
566
673
|
}
|
|
567
674
|
}
|
|
675
|
+
var viewOf;
|
|
676
|
+
var MATERIALIZE_MANY_KEYS = [
|
|
677
|
+
"operands",
|
|
678
|
+
"outputs",
|
|
679
|
+
"keep",
|
|
680
|
+
"pin",
|
|
681
|
+
"maxBufferedBytes",
|
|
682
|
+
"budget",
|
|
683
|
+
"publishConcurrency",
|
|
684
|
+
"concurrency",
|
|
685
|
+
"allowAbsentOperands",
|
|
686
|
+
"after",
|
|
687
|
+
"through",
|
|
688
|
+
"feed",
|
|
689
|
+
"mayBeEmpty"
|
|
690
|
+
];
|
|
691
|
+
var MATERIALIZE_MANY_OUTPUT_KEYS = [
|
|
692
|
+
"dest",
|
|
693
|
+
"expr",
|
|
694
|
+
"exclude",
|
|
695
|
+
"guard",
|
|
696
|
+
"allowEmpty",
|
|
697
|
+
"metadata",
|
|
698
|
+
"audit",
|
|
699
|
+
"keep"
|
|
700
|
+
];
|
|
701
|
+
var DEFAULT_MAX_BUFFERED_BYTES = 256 * 1024 * 1024;
|
|
702
|
+
var DEFAULT_PUBLISH_CONCURRENCY = 8;
|
|
703
|
+
var DEFAULT_BATCH_CONCURRENCY = 1;
|
|
704
|
+
var PIN_PARALLELISM = 16;
|
|
568
705
|
function resolveStorageSource(options, clock) {
|
|
569
706
|
const storage = options.storage;
|
|
570
707
|
if (storage === null || typeof storage !== "object") {
|
|
@@ -677,6 +814,13 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
677
814
|
requireEncryption;
|
|
678
815
|
/** Resolved store-level per-op budget (null = disabled); the admin scans use it, with a per-op override. */
|
|
679
816
|
budget;
|
|
817
|
+
/**
|
|
818
|
+
* Moved at the start and at the end of each `eraseSubject` this store runs. A `materializeMany` call with a feed records
|
|
819
|
+
* it when it begins and is refused once it has moved: it reads caller memory, which no erasure can reach.
|
|
820
|
+
*/
|
|
821
|
+
erasureEpoch = 0;
|
|
822
|
+
/** How many `eraseSubject` calls of this store are running: while any is, every check of the counter counts as moved. */
|
|
823
|
+
erasuresRunning = 0;
|
|
680
824
|
/**
|
|
681
825
|
* Refuse any option key the store does not take, at the top level or inside a group, naming each one.
|
|
682
826
|
*
|
|
@@ -730,6 +874,9 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
730
874
|
clock
|
|
731
875
|
});
|
|
732
876
|
const resolved = resolveStorageSource(options, clock);
|
|
877
|
+
if (metrics !== NOOP_METRICS && isStorageBackend(options.storage)) {
|
|
878
|
+
options.storage.attachMetrics?.(metrics);
|
|
879
|
+
}
|
|
733
880
|
let storage = resolved.source;
|
|
734
881
|
if (options.retry !== false) {
|
|
735
882
|
const { onRetry: userOnRetry, ...ov } = options.retry ?? {};
|
|
@@ -827,7 +974,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
827
974
|
clock: this.clock,
|
|
828
975
|
metrics: this.metrics,
|
|
829
976
|
materialize: (dest, ids, op, options2) => this.materialize(dest, ids, op, options2),
|
|
830
|
-
pinned: (r, e) => this.pinSegment(r, e),
|
|
977
|
+
pinned: (r, e, l, at) => this.pinSegment(r, e, l, at),
|
|
831
978
|
combineEngine: (handles) => this.engineForCombine(handles),
|
|
832
979
|
expiresAt
|
|
833
980
|
});
|
|
@@ -913,6 +1060,440 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
913
1060
|
collected: result.collected
|
|
914
1061
|
};
|
|
915
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* **Hold a set of ids in memory**, to be an operand of {@link CloudRoaring.materializeMany} and of no other verb.
|
|
1065
|
+
*
|
|
1066
|
+
* ```ts
|
|
1067
|
+
* const vip = await store.memory([7, 42, 99_001]);
|
|
1068
|
+
* await store.materializeMany({ operands: { vip, engaged: store.segment('cond-engaged') }, outputs: [...], keep: 12 });
|
|
1069
|
+
* vip.release();
|
|
1070
|
+
* ```
|
|
1071
|
+
*
|
|
1072
|
+
* It takes what {@link CloudRoaring.load} takes as input (ids as a sync or async iterable or a typed array, `{ bitmap }`
|
|
1073
|
+
* or `{ serialized }`) and checks it the same way, with the same errors: the size cap, the structure, the safe
|
|
1074
|
+
* deserializer and each id's range. **Only a real `Uint32Array` skips the per-id range check**; any other typed array goes
|
|
1075
|
+
* through it, so `Int32Array([-1])` and `Float64Array([1.5])` throw {@link ValidationError}. An object with an `ascending`
|
|
1076
|
+
* key is refused: ids that arrive in key order are a `feed`. An empty set is accepted here, and refused by
|
|
1077
|
+
* `materializeMany` unless its name is in `mayBeEmpty`.
|
|
1078
|
+
*
|
|
1079
|
+
* The ids are held as the chunks a stored generation would hold, until {@link MemoryOperand.release}; between calls that
|
|
1080
|
+
* memory is the caller's, and during a call it counts against `maxBufferedBytes`. There is no size option: the load's
|
|
1081
|
+
* own cap applies to a serialized input.
|
|
1082
|
+
*
|
|
1083
|
+
* **Erasure.** Only `eraseSubject` moves the store's counter (at its start and at its end). One that started in this store
|
|
1084
|
+
* since the handle began to be made, or is still running, makes a call that reads the handle fail the outputs that read it with
|
|
1085
|
+
* {@link StaleOperandError} (`reason: 'erased'`), before any request and again just before each publish. An erasure in
|
|
1086
|
+
* another store or another process is not seen, nor are `dropSegment`, `eraseNamespace`, `destroySegment` and the free
|
|
1087
|
+
* function `eraseIdFromSegment`, which do not move it: the handle is the caller's copy, so build it from the source of truth at
|
|
1088
|
+
* the start of each refresh and release it at the end.
|
|
1089
|
+
*
|
|
1090
|
+
* @throws {ValidationError} where a load of the same input would.
|
|
1091
|
+
*/
|
|
1092
|
+
async memory(input) {
|
|
1093
|
+
const epochAt = this.epochNow();
|
|
1094
|
+
const chunks = await prepareHeld(input, roaringCodec, this.clock);
|
|
1095
|
+
return makeMemoryOperand({ store: this, chunks, epochAt });
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* **Many outputs, each operand read once per group.** Each output is an expression over named stored operands (`and`, `or`, `andNot`, nested
|
|
1099
|
+
* to depth 64) published as a new generation of its own `dest`, exactly as an `*Into` would publish it, but computed
|
|
1100
|
+
* together: each operand's chunks are read once for all the outputs that use them, instead of once per output and
|
|
1101
|
+
* once more for every scratch segment a nested expression would otherwise need.
|
|
1102
|
+
*
|
|
1103
|
+
* ```ts
|
|
1104
|
+
* const run = await store.materializeMany({
|
|
1105
|
+
* operands: { us: store.segment('us'), engaged: store.segment('engaged'), optOut: store.segment('opt-out') },
|
|
1106
|
+
* outputs: [
|
|
1107
|
+
* { dest: store.segment('send-1'), expr: { and: ['us', 'engaged'] }, exclude: ['optOut'] },
|
|
1108
|
+
* { dest: store.segment('send-2'), expr: { andNot: ['us', 'engaged'] } },
|
|
1109
|
+
* ],
|
|
1110
|
+
* keep: 3,
|
|
1111
|
+
* });
|
|
1112
|
+
* run.outputs[0]; // a MaterializeResult, or { published: false, error }
|
|
1113
|
+
* ```
|
|
1114
|
+
*
|
|
1115
|
+
* **Results.** `run.outputs[i]` is index-aligned with `outputs` and is exactly what output `i`'s `*Into` would have
|
|
1116
|
+
* returned (a {@link MaterializeResult}: published, or refused with its `reason`), or `{ published: false, error }` for
|
|
1117
|
+
* what it would have thrown. One output's refusal or error never stops another. The call itself throws only for bad
|
|
1118
|
+
* input ({@link ValidationError} naming the output index and path, before any request), an operand that names no
|
|
1119
|
+
* segment (unless `allowAbsentOperands`), and a `budget` its own plan exceeds, the last two before any chunk is read.
|
|
1120
|
+
*
|
|
1121
|
+
* **One generation per operand.** By default every stored operand is pinned for the call: read at the generation it had
|
|
1122
|
+
* when the call started, however long the call runs and whatever loads land meanwhile. That is one generation per
|
|
1123
|
+
* operand, not one instant across operands (two pins are two registry reads). `pin: false` reads live, as a combine does.
|
|
1124
|
+
*
|
|
1125
|
+
* **Every pinned operand an output subtracts is re-checked.** An output subtracts an operand when it is in its `exclude`
|
|
1126
|
+
* or under any entry of an `andNot` after the first, at any depth. Immediately before the publishes the call re-reads the
|
|
1127
|
+
* registry row of each such operand and compares its generation and incarnation with what was pinned (a name deleted and
|
|
1128
|
+
* created again is a replacement though it is generation 0 again). If one was replaced, each output that subtracts it is
|
|
1129
|
+
* not published and carries {@link StaleOperandError}; the others publish. It costs one registry read per such operand per
|
|
1130
|
+
* group, and `stats.operands` carries every operand's pinned, start and end generation and whether it moved.
|
|
1131
|
+
*
|
|
1132
|
+
* **Memory.** Everything the pass holds resident is counted against `maxBufferedBytes` (default 256 MiB): each output's
|
|
1133
|
+
* buffered chunks, the operand streams' ranges, the chunks being evaluated, and the plan itself, and a call whose plan alone
|
|
1134
|
+
* passes it is refused before any chunk is read. When the outputs do not fit, they run in groups, each reading its operands
|
|
1135
|
+
* once (so the operand reads grow with the total output size over the budget), and a group's buffers are released only as
|
|
1136
|
+
* its publishes settle. A chunk count the index understated does not grow a group past the budget: the largest buffer is
|
|
1137
|
+
* dropped and that output re-runs alone. An output that cannot fit alone has `BudgetExceededError` as its result. The count
|
|
1138
|
+
* is the pass's own: process memory is more, measured at 1.6 times the ledger's high water on Linux and 2.5 to 5 times on
|
|
1139
|
+
* macOS on the in-memory backend (the guide has the shapes and what is not counted).
|
|
1140
|
+
*
|
|
1141
|
+
* **Untrusted bytes.** Every operand chunk is decoded through the checks on untrusted tier data. A key an operand's index
|
|
1142
|
+
* lists whose bytes are missing is an error for the outputs that read the operand, never an empty chunk.
|
|
1143
|
+
*
|
|
1144
|
+
* **Handles.** An expired or released handle anywhere in the call (an operand or a `dest`) is refused before any request,
|
|
1145
|
+
* as on the `*Into` verbs. A leased operand's lease, and an operand's deadline, are checked before each chunk key
|
|
1146
|
+
* is read: a lapse fails only the outputs that read that operand, never reads empty. Each `dest`'s lease and deadline
|
|
1147
|
+
* are checked again just before its publish.
|
|
1148
|
+
*
|
|
1149
|
+
* **The chunk cache is bypassed:** the pass reads through the storage source and never writes the decoded-chunk cache, so
|
|
1150
|
+
* a batch does not evict other readers' hot chunks. Each publish still invalidates its `dest` as an `*Into` does.
|
|
1151
|
+
*
|
|
1152
|
+
* **A limit of the form.** An output cannot name another output of the same call: a `dest` that is also an operand is
|
|
1153
|
+
* refused. Chain two calls.
|
|
1154
|
+
*
|
|
1155
|
+
* **Held operands.** An operand may be a {@link MemoryOperand} from {@link CloudRoaring.memory}: ids the caller holds,
|
|
1156
|
+
* read beside the stored ones and giving the same output byte for byte. It is read from memory (no request, and the
|
|
1157
|
+
* shared chunk cache never sees it), counts against `maxBufferedBytes` while the call runs, and is refused when empty
|
|
1158
|
+
* unless its name is in `mayBeEmpty`. A handle from another store, or a released one, is a {@link ValidationError}. An
|
|
1159
|
+
* erasure in this store since the handle was made fails the outputs that read it with {@link StaleOperandError}
|
|
1160
|
+
* (`reason: 'erased'`).
|
|
1161
|
+
*
|
|
1162
|
+
* **A feed.** `feed: { names, records, counts }` adds operands that arrive as records in chunk-key order, for conditions
|
|
1163
|
+
* too many to hold or store. Each record is checked before the pass sees it (see {@link MaterializeManyFeedRecord}): a bad
|
|
1164
|
+
* feed is refused, never read as fewer members, and **a fed output is published only after the whole feed was read and every
|
|
1165
|
+
* end-of-feed check passed**, so a bad feed refuses every fed output and publishes none of them; outputs that name only
|
|
1166
|
+
* stored operands are unaffected. At the end of the feed `counts` must equal the ids the feed held for each name, and a
|
|
1167
|
+
* name that appeared in no record is refused unless it is in `mayBeEmpty`. A feed is read once, so the call runs its outputs
|
|
1168
|
+
* as one group and **`maxBufferedBytes` is required**: each record is converted to compressed bitmaps as it arrives, the key
|
|
1169
|
+
* in hand and its fed outputs are counted against the budget, and the call fails as soon as the count passes it, with
|
|
1170
|
+
* {@link BudgetExceededError} for every fed output. **Erasure:** a fed call is refused at its next record, and before each
|
|
1171
|
+
* fed publish, once `eraseSubject` has started in this store ({@link StaleOperandError}, `reason: 'erased'`).
|
|
1172
|
+
*
|
|
1173
|
+
* ```ts
|
|
1174
|
+
* await store.materializeMany({
|
|
1175
|
+
* operands: { unsub: store.segment('global-unsub') },
|
|
1176
|
+
* feed: {
|
|
1177
|
+
* names: ['us', 'engaged'],
|
|
1178
|
+
* records: warehouseRecords(), // yields { key, operands: { us: Uint32Array, engaged: Uint32Array } } by key
|
|
1179
|
+
* counts: () => warehouseCounts(),
|
|
1180
|
+
* },
|
|
1181
|
+
* maxBufferedBytes: 512 * 1024 * 1024,
|
|
1182
|
+
* outputs: [{ dest: store.segment('send-1'), expr: { and: ['us', 'engaged'] }, exclude: ['unsub'] }],
|
|
1183
|
+
* keep: 12,
|
|
1184
|
+
* });
|
|
1185
|
+
* ```
|
|
1186
|
+
*/
|
|
1187
|
+
async materializeMany(options) {
|
|
1188
|
+
if (typeof options !== "object" || options === null) {
|
|
1189
|
+
throw new ValidationError2(
|
|
1190
|
+
"materializeMany needs an options object with `operands`, `outputs` and `keep`"
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
const epochAt = options.feed === void 0 ? 0 : this.epochNow();
|
|
1194
|
+
const unknown = Object.keys(options).filter(
|
|
1195
|
+
(k) => !MATERIALIZE_MANY_KEYS.includes(k)
|
|
1196
|
+
);
|
|
1197
|
+
if (unknown.length > 0) {
|
|
1198
|
+
throw new ValidationError2(
|
|
1199
|
+
`materializeMany: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
const operandsIn = options.operands;
|
|
1203
|
+
const outputsIn = options.outputs;
|
|
1204
|
+
const keep = options.keep;
|
|
1205
|
+
const after = options.after;
|
|
1206
|
+
const through = options.through;
|
|
1207
|
+
const pinOption = options.pin ?? true;
|
|
1208
|
+
const feedIn = options.feed;
|
|
1209
|
+
const mayBeEmptyIn = options.mayBeEmpty;
|
|
1210
|
+
if (feedIn !== void 0 && options.maxBufferedBytes === void 0) {
|
|
1211
|
+
throw new ValidationError2(
|
|
1212
|
+
"materializeMany: maxBufferedBytes is required with a feed: the feed is read once, so the call runs as one group and the budget is what bounds it"
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
const maxBufferedBytes = options.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;
|
|
1216
|
+
const publishConcurrency = options.publishConcurrency ?? DEFAULT_PUBLISH_CONCURRENCY;
|
|
1217
|
+
const concurrency = options.concurrency ?? DEFAULT_BATCH_CONCURRENCY;
|
|
1218
|
+
const budgetOption = options.budget;
|
|
1219
|
+
const allowAbsentOperands = options.allowAbsentOperands;
|
|
1220
|
+
if (typeof pinOption !== "boolean")
|
|
1221
|
+
throw new ValidationError2("materializeMany: pin must be a boolean");
|
|
1222
|
+
if (allowAbsentOperands !== void 0 && typeof allowAbsentOperands !== "boolean") {
|
|
1223
|
+
throw new ValidationError2("materializeMany: allowAbsentOperands must be a boolean");
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof operandsIn !== "object" || operandsIn === null || Array.isArray(operandsIn)) {
|
|
1226
|
+
throw new ValidationError2("materializeMany: operands must be an object of name to segment");
|
|
1227
|
+
}
|
|
1228
|
+
if (!Array.isArray(outputsIn) || outputsIn.length === 0) {
|
|
1229
|
+
throw new ValidationError2("materializeMany: outputs must be a non-empty array");
|
|
1230
|
+
}
|
|
1231
|
+
const names = Object.keys(operandsIn);
|
|
1232
|
+
const handles = /* @__PURE__ */ new Map();
|
|
1233
|
+
const heldHandles = /* @__PURE__ */ new Map();
|
|
1234
|
+
for (const name of names) {
|
|
1235
|
+
const handle = operandsIn[name];
|
|
1236
|
+
if (handle instanceof MemoryOperand) {
|
|
1237
|
+
const view = heldOf(handle);
|
|
1238
|
+
if (view === void 0) {
|
|
1239
|
+
throw new ValidationError2(
|
|
1240
|
+
`materializeMany: operands["${name}"] is not a memory operand from store.memory()`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
if (view.store !== this) {
|
|
1244
|
+
throw new ValidationError2(
|
|
1245
|
+
`materializeMany: operands["${name}"] was made by another store: a memory operand belongs to the store that made it`
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
if (view.chunks.released) {
|
|
1249
|
+
throw new ValidationError2(
|
|
1250
|
+
`materializeMany: operands["${name}"] was released: make another with store.memory()`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
heldHandles.set(name, view);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if (!(handle instanceof Segment)) {
|
|
1257
|
+
throw new ValidationError2(
|
|
1258
|
+
`materializeMany: operands["${name}"] must be a segment from store.segment() or a memory operand from store.memory()`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
handles.set(name, handle);
|
|
1262
|
+
}
|
|
1263
|
+
const heldMayBeEmpty = /* @__PURE__ */ new Set();
|
|
1264
|
+
let fedMayBeEmpty = mayBeEmptyIn;
|
|
1265
|
+
if (mayBeEmptyIn !== void 0) {
|
|
1266
|
+
if (Array.isArray(mayBeEmptyIn)) {
|
|
1267
|
+
const rest = [];
|
|
1268
|
+
for (const name of mayBeEmptyIn) {
|
|
1269
|
+
if (typeof name === "string" && heldHandles.has(name)) heldMayBeEmpty.add(name);
|
|
1270
|
+
else rest.push(name);
|
|
1271
|
+
}
|
|
1272
|
+
fedMayBeEmpty = rest;
|
|
1273
|
+
}
|
|
1274
|
+
if (feedIn === void 0 && (heldHandles.size === 0 || !Array.isArray(fedMayBeEmpty) || fedMayBeEmpty.length > 0)) {
|
|
1275
|
+
throw new ValidationError2(
|
|
1276
|
+
"materializeMany: mayBeEmpty names fed operands, and the call has no feed"
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const operandKeys = new Set([...handles.values()].map((h) => h.key()));
|
|
1281
|
+
const outputs = [];
|
|
1282
|
+
const destKeys = /* @__PURE__ */ new Set();
|
|
1283
|
+
outputsIn.forEach((raw, i) => {
|
|
1284
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1285
|
+
throw new ValidationError2(`outputs[${i}] must be an object`);
|
|
1286
|
+
}
|
|
1287
|
+
const bad = Object.keys(raw).filter(
|
|
1288
|
+
(k) => !MATERIALIZE_MANY_OUTPUT_KEYS.includes(k)
|
|
1289
|
+
);
|
|
1290
|
+
if (bad.length > 0) {
|
|
1291
|
+
throw new ValidationError2(
|
|
1292
|
+
`outputs[${i}]: unknown option${bad.length > 1 ? "s" : ""} ${bad.map((k) => `"${k}"`).join(", ")}`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
const spec = raw;
|
|
1296
|
+
const dest = spec.dest;
|
|
1297
|
+
if (!(dest instanceof Segment)) {
|
|
1298
|
+
throw new ValidationError2(`outputs[${i}].dest must be a segment from store.segment()`);
|
|
1299
|
+
}
|
|
1300
|
+
if (operandKeys.has(dest.key())) {
|
|
1301
|
+
throw new ValidationError2(
|
|
1302
|
+
`outputs[${i}].dest is also an operand of the call: one call is one pass over operands that do not change under it, so chain two calls`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (destKeys.has(dest.key())) {
|
|
1306
|
+
throw new ValidationError2(
|
|
1307
|
+
`outputs[${i}].dest names a segment another output of the call writes`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
destKeys.add(dest.key());
|
|
1311
|
+
outputs.push({ spec, dest });
|
|
1312
|
+
});
|
|
1313
|
+
const storeCeiling = this.budget === null || this.budget.maxRequests !== DEFAULT_BUDGET.maxRequests ? this.budget : void 0;
|
|
1314
|
+
const budget = budgetOption === void 0 ? storeCeiling : resolvePerOpBudget(budgetOption, this.budget);
|
|
1315
|
+
const operandSpecs = (pins2) => names.map((name) => {
|
|
1316
|
+
const view = heldHandles.get(name);
|
|
1317
|
+
if (view !== void 0) {
|
|
1318
|
+
return {
|
|
1319
|
+
name,
|
|
1320
|
+
ref: { segment: name },
|
|
1321
|
+
held: {
|
|
1322
|
+
chunks: view.chunks,
|
|
1323
|
+
moved: () => this.epochMoved(view.epochAt),
|
|
1324
|
+
mayBeEmpty: heldMayBeEmpty.has(name)
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
const handle = handles.get(name);
|
|
1329
|
+
const seg = viewOf(handle);
|
|
1330
|
+
const pinnedAt = pins2.get(handle.key());
|
|
1331
|
+
const guarded = seg.expiresAt !== void 0 || handle.lease !== void 0;
|
|
1332
|
+
return {
|
|
1333
|
+
name,
|
|
1334
|
+
ref: seg.ref,
|
|
1335
|
+
...guarded ? { check: () => this.assertReadable("materializeMany", handle) } : {},
|
|
1336
|
+
...pinnedAt === void 0 ? {} : {
|
|
1337
|
+
pinnedGeneration: pinnedAt.generation,
|
|
1338
|
+
pinnedVersion: pinnedAt.version,
|
|
1339
|
+
pinnedFingerprint: pinnedAt.fingerprint ?? null
|
|
1340
|
+
},
|
|
1341
|
+
current: (read) => this.currentRowOf(seg.ref, read?.fingerprint === true)
|
|
1342
|
+
};
|
|
1343
|
+
});
|
|
1344
|
+
const request = (pins2) => ({
|
|
1345
|
+
operands: operandSpecs(pins2),
|
|
1346
|
+
outputs: outputs.map(({ spec, dest }, i) => ({
|
|
1347
|
+
expr: spec.expr,
|
|
1348
|
+
...spec.exclude === void 0 ? {} : { exclude: spec.exclude },
|
|
1349
|
+
...spec.allowEmpty === void 0 ? {} : { allowEmpty: spec.allowEmpty },
|
|
1350
|
+
...spec.guard === void 0 ? {} : { guard: spec.guard },
|
|
1351
|
+
...spec.metadata === void 0 ? {} : { metadata: spec.metadata },
|
|
1352
|
+
...spec.keep === void 0 ? {} : { keep: spec.keep },
|
|
1353
|
+
beforePublish: () => this.assertWritable(`outputs[${i}]`, dest),
|
|
1354
|
+
// What a settled publish proves it sent: a published one wrote its object and moved the pointer, a refused one
|
|
1355
|
+
// wrote its object and deleted it.
|
|
1356
|
+
requestsOf: (result) => ({ get: 0, put: result.published ? 2 : 1 }),
|
|
1357
|
+
publish: (chunks, write) => this.materialize(viewOf(dest).ref, new CombineChunks(chunks), "materializeMany", {
|
|
1358
|
+
...write,
|
|
1359
|
+
...spec.audit === void 0 ? {} : { audit: spec.audit }
|
|
1360
|
+
})
|
|
1361
|
+
})),
|
|
1362
|
+
keep,
|
|
1363
|
+
...after === void 0 ? {} : { after },
|
|
1364
|
+
...through === void 0 ? {} : { through },
|
|
1365
|
+
maxBufferedBytes,
|
|
1366
|
+
publishConcurrency,
|
|
1367
|
+
concurrency,
|
|
1368
|
+
...budget === void 0 ? {} : { budget },
|
|
1369
|
+
...allowAbsentOperands === void 0 ? {} : { allowAbsentOperands },
|
|
1370
|
+
...feedIn === void 0 ? {} : { feed: this.feedOf(feedIn, fedMayBeEmpty, epochAt) }
|
|
1371
|
+
});
|
|
1372
|
+
const compiled = compileCombineMany(request(/* @__PURE__ */ new Map()));
|
|
1373
|
+
for (const { dest } of outputs) this.assertWritable("materializeMany", dest);
|
|
1374
|
+
for (const handle of handles.values()) this.assertReadable("materializeMany", handle);
|
|
1375
|
+
this.lifecycleDeps("materializeMany");
|
|
1376
|
+
const crbm = this.crbmSource;
|
|
1377
|
+
if (crbm === void 0) {
|
|
1378
|
+
throw new UnsupportedError(
|
|
1379
|
+
"materializeMany needs the `.crbm` storage source \u2014 pass a backend or a raw IStorageDriver as `storage`"
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
const held = /* @__PURE__ */ new Map();
|
|
1383
|
+
for (const [name, handle] of handles) {
|
|
1384
|
+
const key = handle.key();
|
|
1385
|
+
const at = handle.pinnedAt;
|
|
1386
|
+
const prior = held.get(key);
|
|
1387
|
+
if (prior === void 0) {
|
|
1388
|
+
held.set(key, { at, handle });
|
|
1389
|
+
} else if (prior.at !== void 0 && at !== void 0) {
|
|
1390
|
+
if (!samePin(prior.at, at)) {
|
|
1391
|
+
throw new ValidationError2(
|
|
1392
|
+
`materializeMany: operand "${name}" holds a segment another operand holds, ${twoPins(prior.at, at)}: one call reads a segment at one generation`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
} else if (prior.at !== void 0 || at !== void 0) {
|
|
1396
|
+
if (!pinOption) {
|
|
1397
|
+
throw new ValidationError2(
|
|
1398
|
+
`materializeMany: operand "${name}" holds a segment another operand holds, ${twoPins(prior.at, at)}: one call reads a segment at one generation`
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
if (at !== void 0) held.set(key, { at, handle });
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
const pins = /* @__PURE__ */ new Map();
|
|
1405
|
+
const toPin = [];
|
|
1406
|
+
for (const [key, { at, handle }] of held) {
|
|
1407
|
+
if (at !== void 0) pins.set(key, at);
|
|
1408
|
+
else if (pinOption) toPin.push([key, handle]);
|
|
1409
|
+
else pins.set(key, void 0);
|
|
1410
|
+
}
|
|
1411
|
+
const taken = await mapWithConcurrency(
|
|
1412
|
+
toPin,
|
|
1413
|
+
PIN_PARALLELISM,
|
|
1414
|
+
async ([key, handle]) => [key, (await handle.pin()).pinnedAt]
|
|
1415
|
+
);
|
|
1416
|
+
for (const [key, at] of taken) pins.set(key, at);
|
|
1417
|
+
const live = new Map([...pins].filter((e) => e[1] !== void 0));
|
|
1418
|
+
const pinnedSource = new PinnedStorageChunkSource(crbm, live);
|
|
1419
|
+
const source = this.retryOptions === void 0 ? pinnedSource : new RetryingStorageChunkSource(pinnedSource, this.retryOptions);
|
|
1420
|
+
const final = rebindCombineMany(compiled, operandSpecs(pins));
|
|
1421
|
+
const run = await runCombineMany(final, { source, codec: roaringCodec, clock: this.clock });
|
|
1422
|
+
return {
|
|
1423
|
+
outputs: run.outputs.map(
|
|
1424
|
+
(o) => o.ok ? o.value : {
|
|
1425
|
+
published: false,
|
|
1426
|
+
error: o.error instanceof Error ? o.error : new Error(String(o.error))
|
|
1427
|
+
}
|
|
1428
|
+
),
|
|
1429
|
+
stats: run.stats
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
epochNow() {
|
|
1433
|
+
return this.erasureEpoch;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Whether an erasure has started in this store since `at` was read, or was running when it was: one that is still
|
|
1437
|
+
* running is seen by the count of those running, and one that has ended since by the counter, which its end moved.
|
|
1438
|
+
* The one check every call that reads caller memory uses.
|
|
1439
|
+
*/
|
|
1440
|
+
epochMoved(at) {
|
|
1441
|
+
return this.erasuresRunning > 0 || this.epochNow() !== at;
|
|
1442
|
+
}
|
|
1443
|
+
/** A call's feed with the store's erasure counter: read once here, checked by the combine before any record is read. */
|
|
1444
|
+
feedOf(feed, mayBeEmpty, epochAt) {
|
|
1445
|
+
if (typeof feed !== "object" || feed === null || Array.isArray(feed)) {
|
|
1446
|
+
throw new ValidationError2(
|
|
1447
|
+
"materializeMany: feed must be an object with names, records and counts"
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
const f = feed;
|
|
1451
|
+
return {
|
|
1452
|
+
names: f.names,
|
|
1453
|
+
records: f.records,
|
|
1454
|
+
counts: f.counts,
|
|
1455
|
+
...mayBeEmpty === void 0 ? {} : { mayBeEmpty },
|
|
1456
|
+
epoch: { moved: () => this.epochMoved(epochAt) }
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* The registry row of `ref`, read straight from the registry, not from a snapshot: a re-check must see a move. With
|
|
1461
|
+
* `fingerprint`, the current generation's object is opened too (one tail read, as a pin of it makes), and its
|
|
1462
|
+
* fingerprint is returned beside the row: a generation's number is identified by its row and its object, since a number
|
|
1463
|
+
* can be taken again once its object is gone.
|
|
1464
|
+
*/
|
|
1465
|
+
async currentRowOf(ref, fingerprint) {
|
|
1466
|
+
const registry = this.requireRegistry("materializeMany");
|
|
1467
|
+
if (fingerprint && this.crbmSource !== void 0) {
|
|
1468
|
+
const crbm = this.crbmSource;
|
|
1469
|
+
const at = await this.withRetries(() => crbm.pinGeneration(ref));
|
|
1470
|
+
const colon = at?.version.indexOf(":") ?? -1;
|
|
1471
|
+
if (at !== null && colon >= 0) {
|
|
1472
|
+
return {
|
|
1473
|
+
generation: at.generation,
|
|
1474
|
+
token: at.version.slice(colon + 1),
|
|
1475
|
+
fingerprint: at.fingerprint
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const row = await this.withRetries(() => registry.get(ref));
|
|
1480
|
+
return row === null ? null : { generation: row.currentGen, token: row.token };
|
|
1481
|
+
}
|
|
1482
|
+
/** Throw what a read of `handle` must throw now: its lease error, or a refusal for a deadline that has passed. */
|
|
1483
|
+
assertReadable(op, handle) {
|
|
1484
|
+
const view = viewOf(handle);
|
|
1485
|
+
const lease = view.leaseError();
|
|
1486
|
+
if (lease !== void 0) throw lease;
|
|
1487
|
+
if (view.expired()) {
|
|
1488
|
+
throw new ValidationError2(
|
|
1489
|
+
`${op}: refusing to read "${view.ref.segment}", whose handle has expired \u2014 an expired handle reads as empty, so a generation built from it would be short. Open the handle without \`expiresAt\` if you meant to materialise.`
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/** {@link assertReadable} for a destination, named by where it sits in the call. */
|
|
1494
|
+
assertWritable(where, dest) {
|
|
1495
|
+
this.assertReadable(where, dest);
|
|
1496
|
+
}
|
|
916
1497
|
/**
|
|
917
1498
|
* **Subject access (GDPR Art. 15 / CCPA right-to-know): which segments is this id a member of?**
|
|
918
1499
|
*
|
|
@@ -1021,6 +1602,16 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1021
1602
|
* is `dropSegment` / the `destroySegment`/`eraseNamespace` free functions.
|
|
1022
1603
|
*/
|
|
1023
1604
|
async eraseSubject(id, options = {}) {
|
|
1605
|
+
this.erasureEpoch++;
|
|
1606
|
+
this.erasuresRunning++;
|
|
1607
|
+
try {
|
|
1608
|
+
return await this.eraseSubjectRun(id, options);
|
|
1609
|
+
} finally {
|
|
1610
|
+
this.erasuresRunning--;
|
|
1611
|
+
this.erasureEpoch++;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
async eraseSubjectRun(id, options) {
|
|
1024
1615
|
const deps = this.lifecycleDeps("eraseSubject");
|
|
1025
1616
|
requireScope(options, "eraseSubject");
|
|
1026
1617
|
validateConcurrency(options.concurrency);
|
|
@@ -1252,24 +1843,26 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1252
1843
|
* A refused load deletes the object it wrote before returning — it sits above `currentGen`, where generation
|
|
1253
1844
|
* collection deliberately never looks — but only while the segment's registry row is unchanged or gone. Once
|
|
1254
1845
|
* another write has changed the row, the generation number it holds may name another incarnation's live object,
|
|
1255
|
-
* so it leaves the orphan rather than risk deleting live data.
|
|
1256
|
-
*
|
|
1846
|
+
* so it leaves the orphan rather than risk deleting live data. No row's list names it, so it takes no place in a
|
|
1847
|
+
* window, and a listing deletes it once a later generation is current above it.
|
|
1257
1848
|
*
|
|
1258
|
-
* **Collection is by name
|
|
1259
|
-
* pointer deletes the
|
|
1260
|
-
* objects on every sixteenth generation, and on any load that met an object above the
|
|
1261
|
-
* the current generation's object gone, to take what the name-only passes leave, such as
|
|
1262
|
-
*
|
|
1263
|
-
* the
|
|
1849
|
+
* **Collection is by name at any `keep` up to 64.** The segment's row records the generations a load keeps, so a load
|
|
1850
|
+
* that found nothing above the pointer deletes the generations its publish pushed out of the window and lists
|
|
1851
|
+
* nothing. It lists the segment's objects on every sixteenth generation, and on any load that met an object above the
|
|
1852
|
+
* pointer or whose check found the current generation's object gone, to take what the name-only passes leave, such as
|
|
1853
|
+
* an object a crashed load left below the pointer. A `keep` above 64 records no list and lists on every load, and so
|
|
1854
|
+
* does the first load of a row that records none, which then records it. {@link LoadResult.collected} then names
|
|
1855
|
+
* what the pass deleted by name, and one name may have been gone already.
|
|
1264
1856
|
*
|
|
1265
1857
|
* What it **throws** for is a fault rather than an outcome: invalid options or ids, and a crypto-shredded
|
|
1266
1858
|
* segment (`ValidationError`); a key the keystore cannot provide (`KeyUnavailableError`); a current generation
|
|
1267
1859
|
* that will not open when a guard has to read its size, which it does only when the row has no usable summary of it
|
|
1268
|
-
* (`IntegrityError`); a driver failure; and a collection
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1860
|
+
* (`IntegrityError`); a driver failure; and a fault in the collection's own reads, listing or deletes, or in the write
|
|
1861
|
+
* that records the window (anything but a lost race). A collection fault can be raised **after** the publish already
|
|
1862
|
+
* landed, so a throw does not by itself mean the load did not take effect — re-read the pointer rather than assuming.
|
|
1863
|
+
* A collection, by name or by listing, that finds another writer has moved the row since the publish (a load, a
|
|
1864
|
+
* rollback, a purge) stops and returns what it deleted in `collected`: a load that took effect does not throw for a
|
|
1865
|
+
* race it won.
|
|
1273
1866
|
*
|
|
1274
1867
|
* **A `TransientError` from the registry write can leave the publish unsettled, and deletes nothing.** The
|
|
1275
1868
|
* generation's object is sent again after a throttle where the backend allows it (a write id tells a first send
|
|
@@ -1341,7 +1934,11 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1341
1934
|
validateSegmentRef(ref);
|
|
1342
1935
|
const deps = this.lifecycleDeps("dropSegment");
|
|
1343
1936
|
try {
|
|
1344
|
-
return await dropSegment(
|
|
1937
|
+
return await dropSegment(
|
|
1938
|
+
ref,
|
|
1939
|
+
{ registry: deps.registry, storage: deps.storage, clock: deps.clock, rng: deps.rng },
|
|
1940
|
+
options
|
|
1941
|
+
);
|
|
1345
1942
|
} finally {
|
|
1346
1943
|
if (options.dryRun !== true) this.engine.invalidate(ref);
|
|
1347
1944
|
}
|
|
@@ -1374,7 +1971,11 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1374
1971
|
*/
|
|
1375
1972
|
async setRetention(ref, policy) {
|
|
1376
1973
|
validateSegmentRef(ref);
|
|
1377
|
-
return setSegmentRetention(
|
|
1974
|
+
return setSegmentRetention(
|
|
1975
|
+
ref,
|
|
1976
|
+
{ registry: this.requireRegistry("setRetention"), clock: this.clock, rng: this.rng },
|
|
1977
|
+
policy
|
|
1978
|
+
);
|
|
1378
1979
|
}
|
|
1379
1980
|
/**
|
|
1380
1981
|
* **The stored retention policy**, or `null` if the segment has none (or has no registry row, or is a
|
|
@@ -1393,7 +1994,11 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1393
1994
|
*/
|
|
1394
1995
|
async clearRetention(ref) {
|
|
1395
1996
|
validateSegmentRef(ref);
|
|
1396
|
-
return clearSegmentRetention(ref, {
|
|
1997
|
+
return clearSegmentRetention(ref, {
|
|
1998
|
+
registry: this.requireRegistry("clearRetention"),
|
|
1999
|
+
clock: this.clock,
|
|
2000
|
+
rng: this.rng
|
|
2001
|
+
});
|
|
1397
2002
|
}
|
|
1398
2003
|
/**
|
|
1399
2004
|
* **Run the retention sweep: retire every segment whose `expiresAt` has passed.** This is the call that acts on
|
|
@@ -1456,7 +2061,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1456
2061
|
async retireExpired(options = {}) {
|
|
1457
2062
|
const deps = this.lifecycleDeps("retireExpired");
|
|
1458
2063
|
const result = await retireExpired(
|
|
1459
|
-
{ registry: deps.registry, storage: deps.storage },
|
|
2064
|
+
{ registry: deps.registry, storage: deps.storage, clock: this.clock, rng: this.rng },
|
|
1460
2065
|
{ ...options, now: options.now ?? this.clock.now() }
|
|
1461
2066
|
);
|
|
1462
2067
|
if (options.dryRun !== true) {
|
|
@@ -1466,6 +2071,57 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1466
2071
|
}
|
|
1467
2072
|
return result;
|
|
1468
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* **Remove the registry rows that releases before 0.12.0 left as `deleted: true` envelopes.** Those releases never
|
|
2076
|
+
* removed a row they deleted, and no later call removes it either: it is invisible to every read, and every full
|
|
2077
|
+
* listing of the registry still reads it (one GET each). This is the one call that removes them. It is an admin call,
|
|
2078
|
+
* not a sweep, and nothing schedules it: run it once after the last process on a release before 0.12.0 is gone.
|
|
2079
|
+
*
|
|
2080
|
+
* ```ts
|
|
2081
|
+
* const preview = await store.reapRegistryTombstones({ dryRun: true });
|
|
2082
|
+
* console.log(`would remove ${preview.wouldReap} of ${preview.examined} rows read`);
|
|
2083
|
+
*
|
|
2084
|
+
* const done = await store.reapRegistryTombstones({ confirmNoLegacyWriters: true });
|
|
2085
|
+
* if (done.limited) runAgain(); // `limit` (default 1,000) stopped the run with rows unread
|
|
2086
|
+
* ```
|
|
2087
|
+
*
|
|
2088
|
+
* **What it removes.** Only a row that is `deleted: true` and whose token carries no incarnation id, which is what a
|
|
2089
|
+
* release before 0.12.0 left, or a 0.12.0 or later release left by deleting a row born before 0.12.0 (both are safe to
|
|
2090
|
+
* remove), whatever its `status`. It never touches a live row, a `destroyed` row that is not `deleted` (a crypto-shred's
|
|
2091
|
+
* or a `dropSegment`'s tombstone: the attestation of an erasure, which it cannot tell from another), or a deleted row
|
|
2092
|
+
* with an incarnation id, and it never touches a generation. **So it does not clean a bucket completely**: the
|
|
2093
|
+
* `destroyed` tombstones `dropSegment` leaves stay, so do live rows written before 0.12.0, and so does a tombstone a
|
|
2094
|
+
* 0.12.0 or later release wrote while `conditionalDelete` was off (GCS by default, S3 on a custom endpoint): every full
|
|
2095
|
+
* listing still reads it, and this call neither removes it nor runs on such a store.
|
|
2096
|
+
* It also covers the library's own bookkeeping rows when `namespace` is not set, by the same rule. The default
|
|
2097
|
+
* namespace cannot be scoped to: leave `namespace` out to cover it.
|
|
2098
|
+
*
|
|
2099
|
+
* **The two things it asks.** `confirmNoLegacyWriters: true` is required for a real run (without it, a
|
|
2100
|
+
* `ValidationError`, and nothing is requested): your statement that no process on a release before 0.12.0 writes
|
|
2101
|
+
* this registry, since one that did would, re-creating a removed name, issue the removed row's tokens again. And the
|
|
2102
|
+
* registry must apply a delete under a precondition: where its `conditionalDelete` capability is off (GCS by default,
|
|
2103
|
+
* S3 on a custom endpoint) it throws `CapabilityError` before any request, dry run included. A registry that keeps
|
|
2104
|
+
* no such rows (the in-memory and local-filesystem ones) throws `UnsupportedError`.
|
|
2105
|
+
*
|
|
2106
|
+
* **Each removal is fenced** on the version it read, so a `create` that lands over the envelope first wins, the
|
|
2107
|
+
* delete is refused, and the row is counted as `skipped.raced` (which also counts a row already gone). That holds where
|
|
2108
|
+
* the backend applies the precondition, which auto-detection checks (S3 only on an AWS host). On an endpoint that
|
|
2109
|
+
* ignores `If-Match` (MinIO, fake-gcs-server), a `conditionalDelete: true` you set makes this an unfenced delete: run
|
|
2110
|
+
* it with every writer stopped. On a versioned bucket a delete leaves a delete marker and the earlier version, record
|
|
2111
|
+
* included, so the name is not erased from the bucket. A `create` that had already read the envelope and
|
|
2112
|
+
* whose write meets the removal throws `WriteConflictError`, which this library does not retry: retry the create, and
|
|
2113
|
+
* it finds no row and writes one.
|
|
2114
|
+
*
|
|
2115
|
+
* **Cost** over R rows read and E removed: `ceil(R / 1000)` LIST requests, R GETs and E DELETEs (a dry run, no
|
|
2116
|
+
* DELETEs). `limit` bounds the removals, and the run stops listing once it is spent. **A run is not resumable**: each
|
|
2117
|
+
* lists and reads from the start (R GETs whatever the `limit`), a dry run with a `limit` reports the same first rows
|
|
2118
|
+
* every time, so `wouldReap` is a total only when `limited` is `false`, and `limited` is `true` whenever the limit was
|
|
2119
|
+
* spent with keys left, removable or not. An unreadable object stops the run with an error naming its key, and
|
|
2120
|
+
* removes nothing more: it may be a newer release's row. Scope the run with `namespace` to get past one.
|
|
2121
|
+
*/
|
|
2122
|
+
async reapRegistryTombstones(options = {}) {
|
|
2123
|
+
return reapRegistryTombstones(this.requireRegistry("reapRegistryTombstones"), options);
|
|
2124
|
+
}
|
|
1469
2125
|
/**
|
|
1470
2126
|
* **Forget everything this store has cached about a segment.** Use it after destroying or retiring a segment
|
|
1471
2127
|
* through a path this store did not perform itself.
|
|
@@ -1512,14 +2168,54 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1512
2168
|
* pinned view reports the version captured at pin time, marked as a pin's, so its decoded chunks are never
|
|
1513
2169
|
* those of a live read that fetched across a publish (see {@link PinnedStorageChunkSource.currentVersion}).
|
|
1514
2170
|
*/
|
|
1515
|
-
async pinSegment(ref, expiresAt) {
|
|
2171
|
+
async pinSegment(ref, expiresAt, leaseUntil, named) {
|
|
1516
2172
|
const crbm = this.crbmSource;
|
|
1517
2173
|
if (crbm === void 0) {
|
|
1518
2174
|
throw new UnsupportedError(
|
|
1519
2175
|
"pin() needs the `.crbm` storage source \u2014 pass a backend or a raw IStorageDriver as `storage` (a pre-built StorageChunkSource that cannot resolve a generation has nothing to pin)"
|
|
1520
2176
|
);
|
|
1521
2177
|
}
|
|
1522
|
-
|
|
2178
|
+
let hold;
|
|
2179
|
+
let take;
|
|
2180
|
+
if (leaseUntil !== void 0) {
|
|
2181
|
+
const registry = this.requireRegistry("pin({ leaseUntil })");
|
|
2182
|
+
if (registry.capabilities().canWrite === false) {
|
|
2183
|
+
throw new UnsupportedError(
|
|
2184
|
+
"pin({ leaseUntil }) needs a registry that can write a row, and this store's cannot in this runtime"
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
const holder = this.mintHolder();
|
|
2188
|
+
const deps = { registry, clock: this.clock, rng: this.rng };
|
|
2189
|
+
const generationOf = (row) => named?.generation ?? row.currentGen;
|
|
2190
|
+
hold = {
|
|
2191
|
+
holder,
|
|
2192
|
+
until: leaseUntil,
|
|
2193
|
+
released: false,
|
|
2194
|
+
done: false,
|
|
2195
|
+
release: () => releaseLease(ref, deps, holder)
|
|
2196
|
+
};
|
|
2197
|
+
take = {
|
|
2198
|
+
take: (row) => takeLease(ref, deps, {
|
|
2199
|
+
holder,
|
|
2200
|
+
generation: generationOf(row),
|
|
2201
|
+
until: leaseUntil,
|
|
2202
|
+
row,
|
|
2203
|
+
current: named === void 0
|
|
2204
|
+
})
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
let at;
|
|
2208
|
+
try {
|
|
2209
|
+
at = await this.withRetries(
|
|
2210
|
+
() => named === void 0 ? crbm.pinGeneration(ref, take) : crbm.pinGenerationAt(ref, named, take)
|
|
2211
|
+
);
|
|
2212
|
+
if (hold !== void 0 && at === null) {
|
|
2213
|
+
throw new NotFoundError(`segment "${ref.segment}" has no generation to lease`);
|
|
2214
|
+
}
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
if (hold !== void 0) await hold.release().catch(() => void 0);
|
|
2217
|
+
throw err;
|
|
2218
|
+
}
|
|
1523
2219
|
const pinnedAt = {
|
|
1524
2220
|
generation: at?.generation ?? null,
|
|
1525
2221
|
version: at?.version ?? null,
|
|
@@ -1532,12 +2228,20 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1532
2228
|
clock: this.clock,
|
|
1533
2229
|
metrics: this.metrics,
|
|
1534
2230
|
materialize: (dest, ids, op, options) => this.materialize(dest, ids, op, options),
|
|
1535
|
-
pinned: (r, e) => this.pinSegment(r, e),
|
|
2231
|
+
pinned: (r, e, l, at2) => this.pinSegment(r, e, l, at2),
|
|
1536
2232
|
combineEngine: (handles) => this.engineForCombine(handles),
|
|
1537
2233
|
expiresAt,
|
|
1538
|
-
pinnedAt
|
|
2234
|
+
pinnedAt,
|
|
2235
|
+
lease: hold
|
|
1539
2236
|
});
|
|
1540
2237
|
}
|
|
2238
|
+
/** 16 hex digits from the store's `Rng`, which names the holder of one lease: unique, not secret. */
|
|
2239
|
+
mintHolder() {
|
|
2240
|
+
let id = "";
|
|
2241
|
+
for (let i = 0; i < 4; i++)
|
|
2242
|
+
id += Math.floor(this.rng.next() * 65536).toString(16).padStart(4, "0");
|
|
2243
|
+
return id;
|
|
2244
|
+
}
|
|
1541
2245
|
/**
|
|
1542
2246
|
* An engine reading every segment in `pins` at its pinned generation and everything else live, with the store's
|
|
1543
2247
|
* retries, so a transient fault on a pinned read is retried as it would be on a live one.
|
|
@@ -1698,6 +2402,7 @@ var EMPTY_IDS = {
|
|
|
1698
2402
|
var AS_IDS = {
|
|
1699
2403
|
none: EMPTY_IDS,
|
|
1700
2404
|
failing,
|
|
2405
|
+
guard: guardIds,
|
|
1701
2406
|
intersect: (engine, refs, opts) => withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts)),
|
|
1702
2407
|
union: (engine, refs, opts) => withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts)),
|
|
1703
2408
|
andNot: (engine, base, refs, opts) => withBatches(engine.andNot(base, refs, opts), () => engine.andNotBatches(base, refs, opts))
|
|
@@ -1714,6 +2419,7 @@ var CombineChunks = class {
|
|
|
1714
2419
|
};
|
|
1715
2420
|
var AS_CHUNKS = {
|
|
1716
2421
|
none: NO_CHUNKS,
|
|
2422
|
+
guard: guardChunks,
|
|
1717
2423
|
failing: (err) => ({
|
|
1718
2424
|
[Symbol.asyncIterator]: () => ({ next: () => Promise.reject(err) })
|
|
1719
2425
|
}),
|
|
@@ -1722,6 +2428,39 @@ var AS_CHUNKS = {
|
|
|
1722
2428
|
andNot: (engine, base, refs, opts) => engine.andNotChunks(base, refs, opts)
|
|
1723
2429
|
};
|
|
1724
2430
|
var KEEP_EVERY_GENERATION = Number.MAX_SAFE_INTEGER;
|
|
2431
|
+
var heldOf;
|
|
2432
|
+
var makeMemoryOperand;
|
|
2433
|
+
var mintingMemory = false;
|
|
2434
|
+
var MemoryOperand = class _MemoryOperand {
|
|
2435
|
+
view;
|
|
2436
|
+
static {
|
|
2437
|
+
heldOf = (operand) => operand.view;
|
|
2438
|
+
makeMemoryOperand = (view) => {
|
|
2439
|
+
mintingMemory = true;
|
|
2440
|
+
try {
|
|
2441
|
+
return new _MemoryOperand(view);
|
|
2442
|
+
} finally {
|
|
2443
|
+
mintingMemory = false;
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
/** Not constructible: a handle comes from {@link CloudRoaring.memory}. */
|
|
2448
|
+
constructor(view) {
|
|
2449
|
+
if (!mintingMemory) {
|
|
2450
|
+
throw new ValidationError2(
|
|
2451
|
+
"a MemoryOperand is not constructed directly; call `await store.memory(ids)`"
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
this.view = view;
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Zero and drop the bytes it holds. Any later use throws {@link ValidationError}; releasing twice does nothing. A call
|
|
2458
|
+
* still reading it fails the outputs that read it.
|
|
2459
|
+
*/
|
|
2460
|
+
release() {
|
|
2461
|
+
this.view.chunks.release();
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
1725
2464
|
var makeSegment;
|
|
1726
2465
|
var minting = false;
|
|
1727
2466
|
var Segment = class _Segment {
|
|
@@ -1740,6 +2479,13 @@ var Segment = class _Segment {
|
|
|
1740
2479
|
* pinned handle passed as an **operand** is still read at its pin rather than live.
|
|
1741
2480
|
*/
|
|
1742
2481
|
pinnedAt;
|
|
2482
|
+
/**
|
|
2483
|
+
* The lease this handle holds, when it came from `pin({ leaseUntil })`; `undefined` otherwise. It reports what was
|
|
2484
|
+
* taken and does not change when the lease ends: a read after `lease.until` (or after {@link Segment.release}) throws
|
|
2485
|
+
* {@link LeaseExpiredError}.
|
|
2486
|
+
*/
|
|
2487
|
+
lease;
|
|
2488
|
+
leaseHold;
|
|
1743
2489
|
static {
|
|
1744
2490
|
makeSegment = (parts) => {
|
|
1745
2491
|
minting = true;
|
|
@@ -1749,6 +2495,13 @@ var Segment = class _Segment {
|
|
|
1749
2495
|
minting = false;
|
|
1750
2496
|
}
|
|
1751
2497
|
};
|
|
2498
|
+
viewOf = (segment) => ({
|
|
2499
|
+
ref: segment.ref,
|
|
2500
|
+
pinnedAt: segment.pinnedAt,
|
|
2501
|
+
expiresAt: segment.expiresAt,
|
|
2502
|
+
leaseError: () => segment.leaseError(),
|
|
2503
|
+
expired: () => segment.expired()
|
|
2504
|
+
});
|
|
1752
2505
|
}
|
|
1753
2506
|
/**
|
|
1754
2507
|
* Not constructible: a handle comes from {@link CloudRoaring.segment}, which wires it to the store's engine,
|
|
@@ -1769,6 +2522,10 @@ var Segment = class _Segment {
|
|
|
1769
2522
|
this.combineEngine = parts.combineEngine;
|
|
1770
2523
|
this.expiresAt = parts.expiresAt;
|
|
1771
2524
|
this.pinnedAt = parts.pinnedAt;
|
|
2525
|
+
this.leaseHold = parts.lease;
|
|
2526
|
+
if (parts.lease !== void 0) {
|
|
2527
|
+
this.lease = Object.freeze({ holder: parts.lease.holder, until: parts.lease.until });
|
|
2528
|
+
}
|
|
1772
2529
|
this.metricsOn = parts.metrics !== NOOP_METRICS;
|
|
1773
2530
|
}
|
|
1774
2531
|
/**
|
|
@@ -1797,11 +2554,20 @@ var Segment = class _Segment {
|
|
|
1797
2554
|
* 0, so a pin of the old segment never reads the new one: what it has already read still answers, as the
|
|
1798
2555
|
* instant it pinned, and anything it would have to fetch fails with `NotFoundError`, as a swept pin's does.
|
|
1799
2556
|
*
|
|
1800
|
-
* **It is a hold,
|
|
1801
|
-
* you: a pinned read deliberately does **not** heal forward, because silently
|
|
1802
|
-
* is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover
|
|
1803
|
-
* job — see [Generations and `keep`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#generations-and-keep) —
|
|
1804
|
-
* you are not collecting.
|
|
2557
|
+
* **It is a hold, and `pin({ leaseUntil })` makes it a bounded one.** Without a lease nothing stops a collection
|
|
2558
|
+
* deleting the generation underneath you: a pinned read deliberately does **not** heal forward, because silently
|
|
2559
|
+
* serving a different generation is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover
|
|
2560
|
+
* your longest pinned job — see [Generations and `keep`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#generations-and-keep) — take the pin on a segment
|
|
2561
|
+
* you are not collecting, or lease it.
|
|
2562
|
+
*
|
|
2563
|
+
* **A lease** (`leaseUntil`: epoch **milliseconds**, after now and at most 14 days out) keeps the generation out of
|
|
2564
|
+
* a load's collection until then, from any process, and the pin is taken with it: the lease is written to the segment's
|
|
2565
|
+
* row before the object is opened. A read of the handle after the lease, or after {@link Segment.release}, throws
|
|
2566
|
+
* {@link LeaseExpiredError} at every read site, including when the handle is an operand or an `exclude` of a combine
|
|
2567
|
+
* or the target of an `*Into`, and never reads empty. A stream checks the lease each time it reads a chunk. A segment
|
|
2568
|
+
* holds at most 64 live leases ({@link LeaseLimitError}). A collector holds a lease 60 seconds past its end, which covers
|
|
2569
|
+
* clocks that differ by that much. Erasure, shred, drop and retention expiry ignore a lease. See
|
|
2570
|
+
* [Hold a generation for a job](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/reading.md#hold-a-generation-for-a-job-a-lease).
|
|
1805
2571
|
*
|
|
1806
2572
|
* A segment with no current generation pins nothing and reads empty, exactly as it would unpinned. A pinned
|
|
1807
2573
|
* segment whose row is later dropped or destroyed fails once it must open its object again, rather than go empty
|
|
@@ -1841,8 +2607,144 @@ var Segment = class _Segment {
|
|
|
1841
2607
|
* Needs a store built on the `.crbm` storage source (the default when you pass a backend or a raw driver). Throws
|
|
1842
2608
|
* {@link UnsupportedError} on a store wired with a pre-built source that cannot pin.
|
|
1843
2609
|
*/
|
|
1844
|
-
async pin() {
|
|
1845
|
-
|
|
2610
|
+
async pin(options) {
|
|
2611
|
+
this.assertLeases([this]);
|
|
2612
|
+
const leaseUntil = this.leaseUntilOf("pin", options);
|
|
2613
|
+
return this.pinned(this.ref, this.expiresAt, leaseUntil);
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Hold what this pin names until `leaseUntil`, or `undefined` for a pin with no lease. Refuses an option it does not
|
|
2617
|
+
* know, a value that is not epoch-milliseconds in the future, one past the longest lease, and one past the handle's own
|
|
2618
|
+
* `expiresAt`, before any request is made.
|
|
2619
|
+
*/
|
|
2620
|
+
leaseUntilOf(op, options) {
|
|
2621
|
+
if (options === void 0 || options === null) return void 0;
|
|
2622
|
+
if (typeof options !== "object" || Array.isArray(options)) {
|
|
2623
|
+
throw new ValidationError2(`${op}: options must be an object such as { leaseUntil }`);
|
|
2624
|
+
}
|
|
2625
|
+
const unknown = Object.keys(options).filter((k) => k !== "leaseUntil");
|
|
2626
|
+
if (unknown.length > 0) {
|
|
2627
|
+
throw new ValidationError2(
|
|
2628
|
+
`${op}: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; this version takes { leaseUntil } only`
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
const until = options.leaseUntil;
|
|
2632
|
+
if (until === void 0) return void 0;
|
|
2633
|
+
const now = this.clock.now();
|
|
2634
|
+
if (typeof until !== "number" || !Number.isSafeInteger(until)) {
|
|
2635
|
+
throw new ValidationError2(
|
|
2636
|
+
`${op}: leaseUntil must be an integer epoch-MILLISECONDS; got ${String(until)}`
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
if (until <= now) {
|
|
2640
|
+
throw new ValidationError2(
|
|
2641
|
+
`${op}: leaseUntil (${until}) is not after now (${now}); it is epoch MILLISECONDS, so a value in seconds reads as long past. Use \`Date.now() + hours * 3_600_000\``
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
if (until > now + MAX_LEASE_MS) {
|
|
2645
|
+
throw new ValidationError2(
|
|
2646
|
+
`${op}: leaseUntil is more than 14 days away; the longest lease is ${MAX_LEASE_MS} ms. Take a new lease when a job outlasts it`
|
|
2647
|
+
);
|
|
2648
|
+
}
|
|
2649
|
+
if (this.expiresAt !== void 0 && until > this.expiresAt) {
|
|
2650
|
+
throw new ValidationError2(
|
|
2651
|
+
`${op}: leaseUntil outlasts this handle's own expiresAt, which would have it read empty while it still holds the generation`
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
return until;
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* End this handle's lease now, so a load's collection may take its generation. Idempotent: a handle with no lease, or
|
|
2658
|
+
* one already released, does nothing and makes no request. Every read of the handle after it throws
|
|
2659
|
+
* {@link LeaseExpiredError} (reason `'released'`), including one already under way at its next chunk. A lease that has
|
|
2660
|
+
* ended by its own time needs no release: this then makes no request either, and the registry row's entry is pruned
|
|
2661
|
+
* by a later write. Otherwise it is one registry read and one write; a failure of either is thrown as the registry's own
|
|
2662
|
+
* typed error, and the entry then holds until its time ends, `release()` being safe to call again.
|
|
2663
|
+
*/
|
|
2664
|
+
async release() {
|
|
2665
|
+
const hold = this.leaseHold;
|
|
2666
|
+
if (hold === void 0) return;
|
|
2667
|
+
hold.released = true;
|
|
2668
|
+
if (hold.done) return;
|
|
2669
|
+
if (this.clock.now() >= hold.until + LEASE_SKEW_MS) {
|
|
2670
|
+
hold.done = true;
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
await hold.release();
|
|
2674
|
+
hold.done = true;
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* The error a read of this handle must throw, or `undefined` when it holds no lease or the lease is live. One property
|
|
2678
|
+
* read on a handle with no lease. It is not {@link Segment.expired}: that rule reads empty, and a lease must never.
|
|
2679
|
+
*/
|
|
2680
|
+
leaseError() {
|
|
2681
|
+
const hold = this.leaseHold;
|
|
2682
|
+
if (hold === void 0) return void 0;
|
|
2683
|
+
const name = this.ref.namespace ? `${this.ref.namespace}/${this.ref.segment}` : this.ref.segment;
|
|
2684
|
+
if (hold.released) {
|
|
2685
|
+
return new LeaseExpiredError(
|
|
2686
|
+
`the lease on segment "${name}" was released`,
|
|
2687
|
+
hold.until,
|
|
2688
|
+
"released"
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
if (this.clock.now() >= hold.until) {
|
|
2692
|
+
return new LeaseExpiredError(
|
|
2693
|
+
`the lease on segment "${name}" ended; a read of a handle past its lease throws, never reads empty`,
|
|
2694
|
+
hold.until,
|
|
2695
|
+
"expired"
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
return void 0;
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Throw the first lease error among `handles`: this handle and every operand and exclude of a call. It runs before
|
|
2702
|
+
* any `expiresAt` rule and before the engine, so no operand, exclude or cached reader can answer past a lease.
|
|
2703
|
+
*/
|
|
2704
|
+
assertLeases(handles) {
|
|
2705
|
+
for (const h of handles) {
|
|
2706
|
+
const err = h.leaseError();
|
|
2707
|
+
if (err !== void 0) throw err;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Reopen a generation a pin named earlier, as a pinned handle like the one {@link Segment.pin} returns, held at
|
|
2712
|
+
* `at.generation` instead of the current one. Pass what the earlier pin recorded, `{ generation, fingerprint }`
|
|
2713
|
+
* from its {@link Segment.pinnedAt}. A generation number alone is not an identity, since a purged and re-created
|
|
2714
|
+
* name starts again at 0, so the fingerprint is required: a bare number, a malformed fingerprint or a key other
|
|
2715
|
+
* than `generation` and `fingerprint` is a {@link ValidationError}. Identify a pin by its `generation` and
|
|
2716
|
+
* `fingerprint`: the handle's `pinnedAt.version` can differ from the earlier pin's.
|
|
2717
|
+
*
|
|
2718
|
+
* Throws {@link NotFoundError} when the generation is gone (collected, purged, or on a crypto-shredded segment),
|
|
2719
|
+
* is above the row's pointer, or is another object than the fingerprint names, and never reads empty. A rollback
|
|
2720
|
+
* deletes nothing, so once a later load moves the pointer past a generation it rolled back from, that one can be
|
|
2721
|
+
* reopened while its object is stored. Nothing keeps a generation alive unless you lease it: a later load's
|
|
2722
|
+
* collection can delete it, which is sized by `keep` on every writer, or held by `options.leaseUntil` (see
|
|
2723
|
+
* {@link PinOptions} and {@link Segment.pin}). A leased `pinAt` writes the lease before it verifies the object, and
|
|
2724
|
+
* releases it if the verify fails; for a generation below the pointer, a collector that read the row before the lease
|
|
2725
|
+
* landed can still delete the generation inside its own round trip, and the handle then fails with `NotFoundError`
|
|
2726
|
+
* like any swept pin. A leased handle is checked at every read site as a leased `pin()` is. With a registry it costs one row read and one tail read,
|
|
2727
|
+
* and a leased `pinAt` one conditional write to the row besides; the handle
|
|
2728
|
+
* then reads as one from `pin()` does, including its failure on a chunk it has not cached once the generation is
|
|
2729
|
+
* swept. Works for an encrypted segment. The first argument is an object so that fields can join it later; the lease
|
|
2730
|
+
* is in `options`, the second.
|
|
2731
|
+
*/
|
|
2732
|
+
async pinAt(at, options) {
|
|
2733
|
+
this.assertLeases([this]);
|
|
2734
|
+
const { generation, fingerprint } = at ?? {};
|
|
2735
|
+
const unknown = typeof at === "object" && at !== null ? Object.keys(at).filter((k) => k !== "generation" && k !== "fingerprint") : [];
|
|
2736
|
+
if (unknown.length > 0) {
|
|
2737
|
+
throw new ValidationError2(
|
|
2738
|
+
`pinAt: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; this version takes { generation, fingerprint } only`
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
if (typeof generation !== "number" || !Number.isSafeInteger(generation) || generation < 0 || typeof fingerprint !== "string" || !/^\d+:\d+$/.test(fingerprint)) {
|
|
2742
|
+
throw new ValidationError2(
|
|
2743
|
+
"pinAt: pass { generation, fingerprint } as recorded by an earlier pin (its pinnedAt); a generation number without the fingerprint of its object does not identify it"
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
const leaseUntil = this.leaseUntilOf("pinAt", options);
|
|
2747
|
+
return this.pinned(this.ref, this.expiresAt, leaseUntil, { generation, fingerprint });
|
|
1846
2748
|
}
|
|
1847
2749
|
/**
|
|
1848
2750
|
* An opaque string that names this handle's segment, namespace included: two handles of one segment have the
|
|
@@ -1886,6 +2788,7 @@ var Segment = class _Segment {
|
|
|
1886
2788
|
* awaiting.
|
|
1887
2789
|
*/
|
|
1888
2790
|
refuseIfExpired(op, dest, operands) {
|
|
2791
|
+
this.assertLeases([this, dest, ...operands]);
|
|
1889
2792
|
const stale = [];
|
|
1890
2793
|
for (const seg of [this, dest, ...operands]) {
|
|
1891
2794
|
if (seg.expired())
|
|
@@ -1897,6 +2800,10 @@ var Segment = class _Segment {
|
|
|
1897
2800
|
);
|
|
1898
2801
|
}
|
|
1899
2802
|
}
|
|
2803
|
+
/** `result` as is, or, when a handle of the call holds a lease, checking that lease each time it reads a chunk. */
|
|
2804
|
+
guarded(out, handles, result) {
|
|
2805
|
+
return handles.some((h) => h.leaseHold !== void 0) ? out.guard(result, () => this.assertLeases(handles)) : result;
|
|
2806
|
+
}
|
|
1900
2807
|
/**
|
|
1901
2808
|
* Time an op with the injected clock and emit an `op` metric on completion (success or throw). Skipped
|
|
1902
2809
|
* entirely when no sink is wired, so the default path pays nothing.
|
|
@@ -1912,6 +2819,8 @@ var Segment = class _Segment {
|
|
|
1912
2819
|
}
|
|
1913
2820
|
/** Membership: one chunk — the cache, else one ranged GET. Throws {@link ValidationError} on a bad id. */
|
|
1914
2821
|
has(id) {
|
|
2822
|
+
const lease = this.leaseError();
|
|
2823
|
+
if (lease !== void 0) return Promise.reject(lease);
|
|
1915
2824
|
if (this.expired()) return Promise.resolve(false);
|
|
1916
2825
|
return this.timed("has", () => this.engine.has(this.ref, id));
|
|
1917
2826
|
}
|
|
@@ -1934,6 +2843,8 @@ var Segment = class _Segment {
|
|
|
1934
2843
|
* structure is checked.
|
|
1935
2844
|
*/
|
|
1936
2845
|
count() {
|
|
2846
|
+
const lease = this.leaseError();
|
|
2847
|
+
if (lease !== void 0) return Promise.reject(lease);
|
|
1937
2848
|
if (this.expired()) return Promise.resolve(0);
|
|
1938
2849
|
return this.timed("count", () => this.engine.count(this.ref));
|
|
1939
2850
|
}
|
|
@@ -1952,6 +2863,7 @@ var Segment = class _Segment {
|
|
|
1952
2863
|
* ```
|
|
1953
2864
|
*/
|
|
1954
2865
|
async stat() {
|
|
2866
|
+
this.assertLeases([this]);
|
|
1955
2867
|
if (this.expired()) return { generation: null, cardinality: 0 };
|
|
1956
2868
|
return this.engine.stat(this.ref);
|
|
1957
2869
|
}
|
|
@@ -1971,16 +2883,70 @@ var Segment = class _Segment {
|
|
|
1971
2883
|
* without checking its options, as every read of one does.
|
|
1972
2884
|
*/
|
|
1973
2885
|
iterate(options) {
|
|
2886
|
+
const lease = this.leaseError();
|
|
2887
|
+
if (lease !== void 0) return failing(lease);
|
|
1974
2888
|
if (this.expired()) return EMPTY_IDS;
|
|
1975
2889
|
const range = options == null ? void 0 : rangeOf(options);
|
|
1976
2890
|
const none = range === void 0 || range.after === void 0 && range.through === void 0;
|
|
1977
2891
|
const { engine, ref } = this;
|
|
1978
2892
|
const bounds = none ? void 0 : range;
|
|
1979
|
-
|
|
2893
|
+
const stream = withBatches(
|
|
2894
|
+
engine.iterate(ref, bounds),
|
|
2895
|
+
() => engine.iterateBatches(ref, bounds)
|
|
2896
|
+
);
|
|
2897
|
+
return this.leaseHold === void 0 ? stream : guardIds(stream, () => this.assertLeases([this]));
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* **Pinned handles only.** The ids at 1-based ranks `n`, `2n`, `3n` …, counted over the ids in `(after, through]`
|
|
2901
|
+
* when a range is given, ascending: `everyNth(1000)` yields the 1,000th id, then the 2,000th, and so on. A last
|
|
2902
|
+
* partial window yields nothing, so a caller cutting a send into windows of `n` appends its own final end.
|
|
2903
|
+
*
|
|
2904
|
+
* It places each boundary from the per-chunk counts the pin already holds and reads only the chunks that hold one,
|
|
2905
|
+
* each once however many boundaries it holds, so it reads at most one chunk per boundary and never more chunks
|
|
2906
|
+
* than the object has. A range with `after` inside a chunk reads that chunk too, to count the ids at or below
|
|
2907
|
+
* `after`, even when fewer than `n` ids remain above it; when the chunks in range hold fewer than `n` ids in all,
|
|
2908
|
+
* nothing is fetched. `through` stops the read and no chunk past it is read, though a `through` inside a chunk may read that last chunk and
|
|
2909
|
+
* yield nothing from it. The reads and the read-ahead window are
|
|
2910
|
+
* those of {@link Segment.iterate}, and the per-op budget is charged before the first fetch, with an upper bound on
|
|
2911
|
+
* the chunks the read can take. A pinned read after the generation is swept throws {@link NotFoundError}.
|
|
2912
|
+
*
|
|
2913
|
+
* The ranks come from the index's counts, which are trusted as {@link Segment.count} trusts them. A chunk that is
|
|
2914
|
+
* read must hold the number of ids its index says, or the read throws {@link IntegrityError}; a chunk that is not
|
|
2915
|
+
* read is not checked, so an index that is wrong about one, and, on an unencrypted object, still adds up to the footer's
|
|
2916
|
+
* total, moves the ids after it to the wrong ranks. An encrypted object's index is authenticated and bound to the
|
|
2917
|
+
* object, so a wrong count there needs the key.
|
|
2918
|
+
*
|
|
2919
|
+
* A live handle is refused with {@link UnsupportedError} at the first read, because its counts and its chunks could
|
|
2920
|
+
* come from two generations and name the wrong id. `n` that is not a positive integer throws
|
|
2921
|
+
* {@link ValidationError} at the first read, as does a bad bound. An expired handle reads empty.
|
|
2922
|
+
*
|
|
2923
|
+
* ```ts
|
|
2924
|
+
* const audience = await store.segment('audience').pin();
|
|
2925
|
+
* const ends: number[] = [];
|
|
2926
|
+
* for await (const id of audience.everyNth(1_000)) ends.push(id); // the end of each window of 1,000
|
|
2927
|
+
* ```
|
|
2928
|
+
*/
|
|
2929
|
+
everyNth(n, options) {
|
|
2930
|
+
const lease = this.leaseError();
|
|
2931
|
+
if (lease !== void 0) return failing(lease);
|
|
2932
|
+
if (this.expired()) return EMPTY_IDS;
|
|
2933
|
+
if (this.pinnedAt === void 0) {
|
|
2934
|
+
return failing(
|
|
2935
|
+
new UnsupportedError(
|
|
2936
|
+
"everyNth needs a pinned handle: a live handle can place a rank by one generation and read it from another. Call `pin()` first."
|
|
2937
|
+
)
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
const stream = this.engine.everyNth(
|
|
2941
|
+
this.ref,
|
|
2942
|
+
n,
|
|
2943
|
+
options == null ? void 0 : rangeOf(options)
|
|
2944
|
+
);
|
|
2945
|
+
return this.leaseHold === void 0 ? stream : guardIdIterable(stream, () => this.assertLeases([this]));
|
|
1980
2946
|
}
|
|
1981
2947
|
/**
|
|
1982
2948
|
* The options a combine hands the engine, read once, when it is called ({@link readOptions}), with the `exclude`
|
|
1983
|
-
* it was given
|
|
2949
|
+
* it was given ({@link excludesOf}) and mapped down to the plain refs
|
|
1984
2950
|
* `core` takes. A method rather than a module function because `ref` is class-private — the encapsulation is
|
|
1985
2951
|
* worth more than the free function.
|
|
1986
2952
|
*
|
|
@@ -1996,18 +2962,35 @@ var Segment = class _Segment {
|
|
|
1996
2962
|
};
|
|
1997
2963
|
}
|
|
1998
2964
|
/**
|
|
1999
|
-
* The handles in `options.exclude
|
|
2000
|
-
*
|
|
2001
|
-
* checked for absence, and never counted in the pin-consistency check. `exclude` is read once, so a getter sees
|
|
2002
|
-
* one call.
|
|
2003
|
-
*
|
|
2004
|
-
* With no `exclude` this is one property read and a shared empty list, and with none expired it returns the
|
|
2005
|
-
* caller's own array, so the common case allocates nothing.
|
|
2965
|
+
* The handles in `options.exclude`, read once so a getter sees one call. With none this is one property read and
|
|
2966
|
+
* a shared empty list; otherwise it is the caller's own array, so the common case allocates nothing.
|
|
2006
2967
|
*/
|
|
2007
|
-
|
|
2968
|
+
excludesOf(options) {
|
|
2008
2969
|
const exclude = options?.exclude;
|
|
2009
|
-
|
|
2010
|
-
|
|
2970
|
+
return exclude == null || exclude.length === 0 ? NO_SEGMENTS : exclude;
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* The refusal for a combine whose **exclusion** has expired, or `undefined` when none has. Unlike an expired
|
|
2974
|
+
* operand, which reads empty, an exclusion is a suppression or opt-out list: left out silently, the result
|
|
2975
|
+
* would include the very ids it was passed to remove. So it is refused with a {@link ValidationError} naming each
|
|
2976
|
+
* expired exclusion, as the `*Into` verbs refuse an expired handle.
|
|
2977
|
+
*
|
|
2978
|
+
* It is judged against the injected clock when the combine is called, before the engine is asked for anything:
|
|
2979
|
+
* no request is made, and an expired exclusion that names a segment that does not exist is refused as expired,
|
|
2980
|
+
* not as an absent operand. It runs ahead of the expired-operand shortcuts, so a combine that would read empty
|
|
2981
|
+
* anyway still refuses a lapsed exclusion. A stream already being read is not re-checked when an exclusion
|
|
2982
|
+
* expires part-way, as an operand expiring part-way does not stop it either.
|
|
2983
|
+
*/
|
|
2984
|
+
expiredExcludes(op, excludes) {
|
|
2985
|
+
let stale;
|
|
2986
|
+
for (const e of excludes) {
|
|
2987
|
+
if (!e.expired()) continue;
|
|
2988
|
+
(stale ??= []).push(e.ref.namespace ? `${e.ref.namespace}/${e.ref.segment}` : e.ref.segment);
|
|
2989
|
+
}
|
|
2990
|
+
if (stale === void 0) return void 0;
|
|
2991
|
+
return new ValidationError2(
|
|
2992
|
+
`${op}: refusing to read while these exclusions have expired \u2014 ${[...new Set(stale)].join(", ")}. An expired exclusion would subtract nothing, so the result would include the ids it was passed to remove. Renew the exclusion's \`expiresAt\`, open it without one, or leave it out of the call.`
|
|
2993
|
+
);
|
|
2011
2994
|
}
|
|
2012
2995
|
/**
|
|
2013
2996
|
* Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
|
|
@@ -2022,8 +3005,16 @@ var Segment = class _Segment {
|
|
|
2022
3005
|
return this.intersectAs(AS_IDS, others, options);
|
|
2023
3006
|
}
|
|
2024
3007
|
intersectAs(out, others, options) {
|
|
3008
|
+
const exclude = this.excludesOf(options);
|
|
3009
|
+
const handles = [this, ...others, ...exclude];
|
|
3010
|
+
try {
|
|
3011
|
+
this.assertLeases(handles);
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
return out.failing(err);
|
|
3014
|
+
}
|
|
3015
|
+
const refused = this.expiredExcludes("intersect", exclude);
|
|
3016
|
+
if (refused) return out.failing(refused);
|
|
2025
3017
|
if (this.expired() || others.some((o) => o.expired())) return out.none;
|
|
2026
|
-
const exclude = this.liveExcludes(options);
|
|
2027
3018
|
let engine;
|
|
2028
3019
|
try {
|
|
2029
3020
|
engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
|
|
@@ -2032,7 +3023,7 @@ var Segment = class _Segment {
|
|
|
2032
3023
|
}
|
|
2033
3024
|
const refs = [this.ref, ...others.map((o) => o.ref)];
|
|
2034
3025
|
const opts = this.refsIn(options, exclude);
|
|
2035
|
-
return out.intersect(engine, refs, opts);
|
|
3026
|
+
return this.guarded(out, handles, out.intersect(engine, refs, opts));
|
|
2036
3027
|
}
|
|
2037
3028
|
/**
|
|
2038
3029
|
* Materialize `this ∩ others…` (minus `exclude`) as a **new generation of `dest`** — `dest`'s previous contents
|
|
@@ -2081,17 +3072,24 @@ var Segment = class _Segment {
|
|
|
2081
3072
|
return this.unionAs(AS_IDS, others, options);
|
|
2082
3073
|
}
|
|
2083
3074
|
unionAs(out, others, options) {
|
|
3075
|
+
const exclude = this.excludesOf(options);
|
|
3076
|
+
const handles = [this, ...others, ...exclude];
|
|
3077
|
+
try {
|
|
3078
|
+
this.assertLeases(handles);
|
|
3079
|
+
} catch (err) {
|
|
3080
|
+
return out.failing(err);
|
|
3081
|
+
}
|
|
3082
|
+
const refused = this.expiredExcludes("union", exclude);
|
|
3083
|
+
if (refused) return out.failing(refused);
|
|
2084
3084
|
const live = others.filter((o) => !o.expired());
|
|
2085
3085
|
if (this.expired()) {
|
|
2086
3086
|
if (live.length === 0) return out.none;
|
|
2087
3087
|
return live[0].unionAs(out, live.slice(1), options);
|
|
2088
3088
|
}
|
|
2089
3089
|
if (live.length === 0 && others.length > 0) {
|
|
2090
|
-
|
|
2091
|
-
return exclude2.length > 0 ? this.andNotAs(out, [...exclude2], options) : this.unionAs(out, [], options);
|
|
3090
|
+
return exclude.length > 0 ? this.andNotAs(out, [...exclude], options) : this.unionAs(out, [], options);
|
|
2092
3091
|
}
|
|
2093
3092
|
if (live.length !== others.length) return this.unionAs(out, live, options);
|
|
2094
|
-
const exclude = this.liveExcludes(options);
|
|
2095
3093
|
let engine;
|
|
2096
3094
|
try {
|
|
2097
3095
|
engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
|
|
@@ -2100,7 +3098,7 @@ var Segment = class _Segment {
|
|
|
2100
3098
|
}
|
|
2101
3099
|
const refs = [this.ref, ...others.map((o) => o.ref)];
|
|
2102
3100
|
const opts = this.refsIn(options, exclude);
|
|
2103
|
-
return out.union(engine, refs, opts);
|
|
3101
|
+
return this.guarded(out, handles, out.union(engine, refs, opts));
|
|
2104
3102
|
}
|
|
2105
3103
|
/** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
2106
3104
|
async unionInto(dest, others, options) {
|
|
@@ -2124,9 +3122,9 @@ var Segment = class _Segment {
|
|
|
2124
3122
|
* suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
|
|
2125
3123
|
* opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
|
|
2126
3124
|
*
|
|
2127
|
-
* An expired handle in `excludes`
|
|
2128
|
-
*
|
|
2129
|
-
* the same rule.
|
|
3125
|
+
* An expired handle in `excludes` is refused, not skipped: the stream throws {@link ValidationError} naming it,
|
|
3126
|
+
* before any request is made, because a lapsed suppression list would otherwise let through the ids it was
|
|
3127
|
+
* passed to remove. An expired `this` is empty. `exclude` on `intersect` and `union` follows the same rule.
|
|
2130
3128
|
*
|
|
2131
3129
|
* To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
|
|
2132
3130
|
* the suppression folds into the same pass rather than materializing an intermediate segment first.
|
|
@@ -2135,12 +3133,15 @@ var Segment = class _Segment {
|
|
|
2135
3133
|
return this.andNotAs(AS_IDS, excludes, options);
|
|
2136
3134
|
}
|
|
2137
3135
|
andNotAs(out, excludes, options) {
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
3136
|
+
const handles = [this, ...excludes];
|
|
3137
|
+
try {
|
|
3138
|
+
this.assertLeases(handles);
|
|
3139
|
+
} catch (err) {
|
|
3140
|
+
return out.failing(err);
|
|
2142
3141
|
}
|
|
2143
|
-
|
|
3142
|
+
const refused = this.expiredExcludes("andNot", excludes);
|
|
3143
|
+
if (refused) return out.failing(refused);
|
|
3144
|
+
if (this.expired()) return out.none;
|
|
2144
3145
|
let engine;
|
|
2145
3146
|
try {
|
|
2146
3147
|
engine = this.combineEngine([this, ...excludes]) ?? this.engine;
|
|
@@ -2150,7 +3151,7 @@ var Segment = class _Segment {
|
|
|
2150
3151
|
const base = this.ref;
|
|
2151
3152
|
const refs = excludes.map((o) => o.ref);
|
|
2152
3153
|
const opts = options == null ? void 0 : readOptions(options);
|
|
2153
|
-
return out.andNot(engine, base, refs, opts);
|
|
3154
|
+
return this.guarded(out, handles, out.andNot(engine, base, refs, opts));
|
|
2154
3155
|
}
|
|
2155
3156
|
/** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
2156
3157
|
async andNotInto(dest, excludes, options) {
|
|
@@ -2174,6 +3175,7 @@ var Segment = class _Segment {
|
|
|
2174
3175
|
* the lose-zone).
|
|
2175
3176
|
*/
|
|
2176
3177
|
async costReport(options) {
|
|
3178
|
+
this.assertLeases([this]);
|
|
2177
3179
|
const canMeasure = this.engine.supportsStorageSize;
|
|
2178
3180
|
const size = canMeasure ? await this.engine.segmentSize(this.ref) : null;
|
|
2179
3181
|
const refreshMs = this.engine.pointerRefreshMs;
|
|
@@ -2202,25 +3204,36 @@ export {
|
|
|
2202
3204
|
InProcessKeystore,
|
|
2203
3205
|
IntegrityError3 as IntegrityError,
|
|
2204
3206
|
KeyUnavailableError,
|
|
3207
|
+
LEASE_SKEW_MS2 as LEASE_SKEW_MS,
|
|
3208
|
+
LeaseExpiredError2 as LeaseExpiredError,
|
|
3209
|
+
LeaseLimitError,
|
|
2205
3210
|
LocalFsStorage,
|
|
3211
|
+
MAX_LEASES_PER_SEGMENT,
|
|
3212
|
+
MAX_LEASE_MS2 as MAX_LEASE_MS,
|
|
2206
3213
|
MIN_EXPIRES_AT_MS2 as MIN_EXPIRES_AT_MS,
|
|
3214
|
+
MemoryOperand,
|
|
2207
3215
|
MemoryStorage,
|
|
2208
3216
|
NodeAead,
|
|
2209
|
-
NotFoundError,
|
|
3217
|
+
NotFoundError2 as NotFoundError,
|
|
2210
3218
|
ONE_REDIS_HA_CLUSTER,
|
|
2211
3219
|
RecordingAuditSink,
|
|
2212
3220
|
Segment,
|
|
3221
|
+
StaleOperandError,
|
|
2213
3222
|
TransientError,
|
|
2214
3223
|
UnsupportedError2 as UnsupportedError,
|
|
2215
3224
|
ValidationError3 as ValidationError,
|
|
2216
3225
|
WriteConflictError2 as WriteConflictError,
|
|
2217
3226
|
aadFor,
|
|
3227
|
+
deserializePortable,
|
|
2218
3228
|
destroySegment,
|
|
2219
3229
|
eraseNamespace,
|
|
2220
3230
|
excludingReservedRows2 as excludingReservedRows,
|
|
2221
3231
|
isCloudRoaringError,
|
|
2222
3232
|
isIntegrityError,
|
|
3233
|
+
isLeaseExpiredError,
|
|
3234
|
+
isLeaseLimitError,
|
|
2223
3235
|
isNotFoundError,
|
|
3236
|
+
isStaleOperandError,
|
|
2224
3237
|
isTransientError,
|
|
2225
3238
|
isValidationError,
|
|
2226
3239
|
isWriteConflictError,
|