@cloudbitmaps/roaring 0.18.3 → 0.19.1
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 +10 -7
- package/README.md +3 -3
- package/dist/bin/export-segments.js +452 -473
- package/dist/bin/export-segments.js.map +3 -3
- package/dist/index.d.ts +134 -220
- package/dist/index.js +440 -473
- package/dist/index.js.map +3 -3
- package/dist/testing/conformance.d.ts +29 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
UnsupportedError,
|
|
19
19
|
ValidationError as ValidationError2,
|
|
20
20
|
WriteConflictError,
|
|
21
|
-
MIN_EXPIRES_AT_MS,
|
|
22
21
|
checkBudget,
|
|
23
22
|
collectWithinBudget,
|
|
24
23
|
excludingReservedRows,
|
|
@@ -26,16 +25,15 @@ import {
|
|
|
26
25
|
eraseIdFromSegment,
|
|
27
26
|
estimateCost,
|
|
28
27
|
groundedReport,
|
|
28
|
+
judgeLoad,
|
|
29
29
|
loadSegment,
|
|
30
30
|
loadSegmentChunks,
|
|
31
31
|
compileCombineMany,
|
|
32
|
-
prepareHeld,
|
|
33
32
|
rebindCombineMany,
|
|
34
33
|
runCombineMany,
|
|
35
34
|
mapWithConcurrency,
|
|
36
35
|
resolveBudget,
|
|
37
36
|
resolvePerOpBudget,
|
|
38
|
-
reapRegistryTombstones,
|
|
39
37
|
retireExpired,
|
|
40
38
|
runConsistencyCheck,
|
|
41
39
|
runExport,
|
|
@@ -613,7 +611,7 @@ import {
|
|
|
613
611
|
eraseNamespace,
|
|
614
612
|
excludingReservedRows as excludingReservedRows2,
|
|
615
613
|
readRetentionPolicy,
|
|
616
|
-
MIN_EXPIRES_AT_MS
|
|
614
|
+
MIN_EXPIRES_AT_MS,
|
|
617
615
|
LEASE_SKEW_MS as LEASE_SKEW_MS2,
|
|
618
616
|
MAX_LEASE_MS as MAX_LEASE_MS2,
|
|
619
617
|
MAX_LEASES_PER_SEGMENT,
|
|
@@ -642,6 +640,10 @@ import {
|
|
|
642
640
|
isLeaseExpiredError,
|
|
643
641
|
isLeaseLimitError,
|
|
644
642
|
isStaleOperandError,
|
|
643
|
+
isUnsupportedError,
|
|
644
|
+
isCapabilityError,
|
|
645
|
+
isBudgetExceededError,
|
|
646
|
+
isKeyUnavailableError,
|
|
645
647
|
CrbmReader,
|
|
646
648
|
BufferReader,
|
|
647
649
|
CountingMetricsSink,
|
|
@@ -659,12 +661,25 @@ var SystemRng = class {
|
|
|
659
661
|
};
|
|
660
662
|
var DEFAULT_CACHE_MAX_CHUNKS = 1024;
|
|
661
663
|
var DEFAULT_ADMIN_CONCURRENCY = 8;
|
|
664
|
+
function rowVersionOfLive(version) {
|
|
665
|
+
if (version === null) return null;
|
|
666
|
+
const cut = version.lastIndexOf("#");
|
|
667
|
+
return cut < 0 ? version : version.slice(0, cut);
|
|
668
|
+
}
|
|
662
669
|
function validateConcurrency(concurrency) {
|
|
663
670
|
if (concurrency !== void 0 && (!Number.isInteger(concurrency) || concurrency < 1)) {
|
|
664
671
|
throw new ValidationError2(`concurrency must be a positive integer; got ${concurrency}`);
|
|
665
672
|
}
|
|
666
673
|
}
|
|
667
674
|
function requireScope(options, op) {
|
|
675
|
+
if (options.namespace !== void 0) {
|
|
676
|
+
if (typeof options.namespace !== "string") {
|
|
677
|
+
throw new ValidationError2(
|
|
678
|
+
`${op}: namespace must be a string; got ${kindOf(options.namespace)}`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
validateSegmentRef({ segment: "x", namespace: options.namespace });
|
|
682
|
+
}
|
|
668
683
|
refuseReservedNamespace(options.namespace);
|
|
669
684
|
if (options.namespace === void 0 && options.allNamespaces !== true) {
|
|
670
685
|
throw new ValidationError2(
|
|
@@ -672,9 +687,154 @@ function requireScope(options, op) {
|
|
|
672
687
|
);
|
|
673
688
|
}
|
|
674
689
|
}
|
|
690
|
+
var keysOf = () => (keys) => keys;
|
|
691
|
+
var CALL_KEYS = {
|
|
692
|
+
range: keysOf()(["after", "through"]),
|
|
693
|
+
andNot: keysOf()([
|
|
694
|
+
"after",
|
|
695
|
+
"through",
|
|
696
|
+
"concurrency",
|
|
697
|
+
"budget",
|
|
698
|
+
"allowAbsentOperands"
|
|
699
|
+
]),
|
|
700
|
+
combine: keysOf()([
|
|
701
|
+
"after",
|
|
702
|
+
"through",
|
|
703
|
+
"concurrency",
|
|
704
|
+
"budget",
|
|
705
|
+
"allowAbsentOperands",
|
|
706
|
+
"exclude"
|
|
707
|
+
]),
|
|
708
|
+
into: keysOf()([
|
|
709
|
+
"after",
|
|
710
|
+
"through",
|
|
711
|
+
"concurrency",
|
|
712
|
+
"budget",
|
|
713
|
+
"allowAbsentOperands",
|
|
714
|
+
"exclude",
|
|
715
|
+
"audit",
|
|
716
|
+
"allowEmpty",
|
|
717
|
+
"guard",
|
|
718
|
+
"metadata",
|
|
719
|
+
"keep"
|
|
720
|
+
]),
|
|
721
|
+
andNotInto: keysOf()([
|
|
722
|
+
"after",
|
|
723
|
+
"through",
|
|
724
|
+
"concurrency",
|
|
725
|
+
"budget",
|
|
726
|
+
"allowAbsentOperands",
|
|
727
|
+
"audit",
|
|
728
|
+
"allowEmpty",
|
|
729
|
+
"guard",
|
|
730
|
+
"metadata",
|
|
731
|
+
"keep"
|
|
732
|
+
]),
|
|
733
|
+
load: keysOf()(["allowEmpty", "guard", "keep", "metadata", "audit"]),
|
|
734
|
+
segments: keysOf()(["namespace"]),
|
|
735
|
+
subjectReport: keysOf()([
|
|
736
|
+
"namespace",
|
|
737
|
+
"allNamespaces",
|
|
738
|
+
"concurrency",
|
|
739
|
+
"budget"
|
|
740
|
+
]),
|
|
741
|
+
eraseSubject: keysOf()([
|
|
742
|
+
"namespace",
|
|
743
|
+
"allNamespaces",
|
|
744
|
+
"audit",
|
|
745
|
+
"concurrency",
|
|
746
|
+
"budget"
|
|
747
|
+
]),
|
|
748
|
+
rollback: keysOf()([
|
|
749
|
+
"audit",
|
|
750
|
+
"allowForward"
|
|
751
|
+
]),
|
|
752
|
+
dropSegment: keysOf()([
|
|
753
|
+
"confirmSegment",
|
|
754
|
+
"dryRun",
|
|
755
|
+
"audit"
|
|
756
|
+
]),
|
|
757
|
+
retireExpired: keysOf()([
|
|
758
|
+
"namespace",
|
|
759
|
+
"now",
|
|
760
|
+
"limit",
|
|
761
|
+
"dryRun",
|
|
762
|
+
"audit",
|
|
763
|
+
"maxScanSegments",
|
|
764
|
+
"scan",
|
|
765
|
+
"shards",
|
|
766
|
+
"totalShards",
|
|
767
|
+
"lookbackBuckets",
|
|
768
|
+
"purgeTombstones",
|
|
769
|
+
"tombstoneGraceMs"
|
|
770
|
+
]),
|
|
771
|
+
checkConsistency: keysOf()([
|
|
772
|
+
"namespace",
|
|
773
|
+
"concurrency",
|
|
774
|
+
"summaries"
|
|
775
|
+
]),
|
|
776
|
+
exportSegments: keysOf()(["format", "codec", "namespace", "ndjsonBatchBytes"]),
|
|
777
|
+
costReport: keysOf()(["pricing", "workload"]),
|
|
778
|
+
workload: keysOf()([
|
|
779
|
+
"readsPerSec",
|
|
780
|
+
"intersectsPerSec",
|
|
781
|
+
"cacheHitRate",
|
|
782
|
+
"chunksPerIntersect",
|
|
783
|
+
"operandsPerIntersect",
|
|
784
|
+
"loadsPerMonth",
|
|
785
|
+
"requestsPerLoad",
|
|
786
|
+
"hotSegments",
|
|
787
|
+
"readerProcesses",
|
|
788
|
+
"genTtlMs",
|
|
789
|
+
"retirementsPerMonth",
|
|
790
|
+
"purgesPerMonth",
|
|
791
|
+
"conditionalDelete"
|
|
792
|
+
])
|
|
793
|
+
};
|
|
794
|
+
var kindOf = (v) => v === null ? "null" : Array.isArray(v) ? "an array" : `a ${typeof v}`;
|
|
795
|
+
function optionsError(options, op, known, booleans = []) {
|
|
796
|
+
if (options === void 0 || options === null) return void 0;
|
|
797
|
+
if (typeof options !== "object" || Array.isArray(options)) {
|
|
798
|
+
return new ValidationError2(`${op}: options must be an object; got ${kindOf(options)}`);
|
|
799
|
+
}
|
|
800
|
+
const bag = options;
|
|
801
|
+
const unknown = Object.keys(bag).filter((k) => !known.includes(k) && bag[k] !== void 0);
|
|
802
|
+
if (unknown.length > 0) {
|
|
803
|
+
return new ValidationError2(
|
|
804
|
+
`${op}: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; it takes { ${known.join(", ")} }`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const audit = bag.audit;
|
|
808
|
+
if (audit !== void 0 && audit !== null && typeof audit.onEvent !== "function") {
|
|
809
|
+
return new ValidationError2(
|
|
810
|
+
`${op}: audit must be a sink with an onEvent(event) method, such as a RecordingAuditSink`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
for (const name of booleans) {
|
|
814
|
+
const v = bag[name];
|
|
815
|
+
if (v !== void 0 && typeof v !== "boolean") {
|
|
816
|
+
return new ValidationError2(`${op}: \`${name}\` must be a boolean; got ${kindOf(v)}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
function picked(options, keys) {
|
|
822
|
+
const out = {};
|
|
823
|
+
if (options === void 0 || options === null) return out;
|
|
824
|
+
for (const key of keys) {
|
|
825
|
+
const value = options[key];
|
|
826
|
+
if (value !== void 0) out[key] = value;
|
|
827
|
+
}
|
|
828
|
+
return out;
|
|
829
|
+
}
|
|
830
|
+
function checkOptions(options, op, known, booleans) {
|
|
831
|
+
const err = optionsError(options, op, known, booleans);
|
|
832
|
+
if (err !== void 0) throw err;
|
|
833
|
+
}
|
|
675
834
|
var viewOf;
|
|
676
835
|
var MATERIALIZE_MANY_KEYS = [
|
|
677
836
|
"operands",
|
|
837
|
+
"dryRun",
|
|
678
838
|
"outputs",
|
|
679
839
|
"keep",
|
|
680
840
|
"pin",
|
|
@@ -863,8 +1023,29 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
863
1023
|
).join(" ") + " https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/api-reference.md#build-a-store--new-cloudroaringoptions"
|
|
864
1024
|
);
|
|
865
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* A seam the store calls through must have the methods it calls. Without this a `clock` with no `now`, an `rng` with
|
|
1028
|
+
* no `next` or a keystore with no `createDek` builds a store that throws a raw `TypeError` at its first load or read.
|
|
1029
|
+
*/
|
|
1030
|
+
static rejectHollowSeams(options) {
|
|
1031
|
+
const seams = [
|
|
1032
|
+
[options.seams?.clock, "seams.clock", ["now", "sleep"]],
|
|
1033
|
+
[options.seams?.rng, "seams.rng", ["next"]],
|
|
1034
|
+
[options.encryption?.keystore, "encryption.keystore", ["createDek", "openDek"]]
|
|
1035
|
+
];
|
|
1036
|
+
for (const [seam, name, needs] of seams) {
|
|
1037
|
+
if (seam === void 0 || seam === null) continue;
|
|
1038
|
+
const has = (m) => typeof seam[m] === "function";
|
|
1039
|
+
if (typeof seam !== "object" || !needs.every(has)) {
|
|
1040
|
+
throw new ValidationError2(
|
|
1041
|
+
`CloudRoaring's \`${name}\` must be an object with ${needs.map((m) => `\`${m}()\``).join(" and ")}`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
866
1046
|
constructor(options) {
|
|
867
1047
|
_CloudRoaring.rejectUnknownOptions(options);
|
|
1048
|
+
_CloudRoaring.rejectHollowSeams(options);
|
|
868
1049
|
const clock = options.seams?.clock ?? new SystemClock();
|
|
869
1050
|
const rng = options.seams?.rng ?? new SystemRng();
|
|
870
1051
|
const metrics = safeMetrics(options.metrics ?? NOOP_METRICS);
|
|
@@ -880,6 +1061,9 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
880
1061
|
let storage = resolved.source;
|
|
881
1062
|
if (options.retry !== false) {
|
|
882
1063
|
const { onRetry: userOnRetry, ...ov } = options.retry ?? {};
|
|
1064
|
+
if (userOnRetry !== void 0 && typeof userOnRetry !== "function") {
|
|
1065
|
+
throw new ValidationError2("retry.onRetry must be a function");
|
|
1066
|
+
}
|
|
883
1067
|
const policy = {
|
|
884
1068
|
maxAttempts: ov.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts,
|
|
885
1069
|
baseDelayMs: ov.baseDelayMs ?? DEFAULT_RETRY_POLICY.baseDelayMs,
|
|
@@ -955,37 +1139,51 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
955
1139
|
readRetry: this.retryOptions
|
|
956
1140
|
};
|
|
957
1141
|
}
|
|
958
|
-
/**
|
|
1142
|
+
/**
|
|
1143
|
+
* Get a handle to a segment. Validates the name (non-empty, well-formed, within the encoded-length cap). The options
|
|
1144
|
+
* are `{ namespace }` and nothing else: anything else is refused with {@link ValidationError}, by name, since a
|
|
1145
|
+
* misspelt `namespace` would otherwise address the default namespace. An `expiresAt` among them is refused with its own
|
|
1146
|
+
* message: a handle carries no deadline, and one left unread would serve the data past it. A key whose value is
|
|
1147
|
+
* `undefined` is read as absent, so a spread of options keeps working.
|
|
1148
|
+
*/
|
|
959
1149
|
segment(name, options) {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1150
|
+
if (options !== void 0 && options !== null) {
|
|
1151
|
+
if (typeof options !== "object" || Array.isArray(options)) {
|
|
1152
|
+
throw new ValidationError2("segment: options must be an object such as { namespace }");
|
|
1153
|
+
}
|
|
1154
|
+
if (options.expiresAt !== void 0) {
|
|
1155
|
+
throw new ValidationError2(
|
|
1156
|
+
"segment: `expiresAt` is not an option of a handle; check a deadline where you read, or record it with `store.setRetention(ref, { expiresAt })` and run `store.retireExpired()`"
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
const unknown = Object.keys(options).filter(
|
|
1160
|
+
(k) => k !== "namespace" && options[k] !== void 0
|
|
1161
|
+
);
|
|
1162
|
+
if (unknown.length > 0) {
|
|
966
1163
|
throw new ValidationError2(
|
|
967
|
-
`segment:
|
|
1164
|
+
`segment: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; a handle takes { namespace } only`
|
|
968
1165
|
);
|
|
969
1166
|
}
|
|
970
1167
|
}
|
|
1168
|
+
const ref = { segment: name, namespace: options?.namespace };
|
|
1169
|
+
validateSegmentRef(ref);
|
|
1170
|
+
refuseReservedNamespace(ref.namespace);
|
|
971
1171
|
return makeSegment({
|
|
972
1172
|
engine: this.engine,
|
|
973
1173
|
ref,
|
|
974
1174
|
clock: this.clock,
|
|
975
1175
|
metrics: this.metrics,
|
|
976
1176
|
materialize: (dest, ids, op, options2) => this.materialize(dest, ids, op, options2),
|
|
977
|
-
pinned: (r,
|
|
978
|
-
combineEngine: (handles) => this.engineForCombine(handles)
|
|
979
|
-
expiresAt
|
|
1177
|
+
pinned: (r, l, at) => this.pinSegment(r, l, at),
|
|
1178
|
+
combineEngine: (handles) => this.engineForCombine(handles)
|
|
980
1179
|
});
|
|
981
1180
|
}
|
|
982
1181
|
/**
|
|
983
1182
|
* Write `ids` as a **new generation of `dest`** and publish it — the shared body of the `*Into` verbs. An `*Into`
|
|
984
1183
|
* materialisation is a load, and publishes the same way (hard invariant 1): every load that finds a row fences its
|
|
985
|
-
* publish on the row's token
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
* forward-only. The destination's previous generation stays readable until the publish lands (readers re-resolve
|
|
1184
|
+
* publish on the row's token, and every load that finds none on that absence, guarded or not; a guarded load (the
|
|
1185
|
+
* default, since the empty refusal needs the size of the current generation) also fences on the pointer it judged
|
|
1186
|
+
* (`expectFrom`). The destination's previous generation stays readable until the publish lands (readers re-resolve
|
|
989
1187
|
* within `cache.genTtlMs`).
|
|
990
1188
|
*
|
|
991
1189
|
* This routes through `loadSegment` rather than writing the generation itself, and that is the whole point
|
|
@@ -1012,9 +1210,10 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1012
1210
|
* **A `WriteConflictError` does not by itself mean nothing was published**, and that is worth knowing
|
|
1013
1211
|
* before you write the retry. `'superseded'` covers five different causes — the write-once PUT collided,
|
|
1014
1212
|
* the pointer moved, a row appeared where the load found none, the row's token changed, the row was purged —
|
|
1015
|
-
* and only the first
|
|
1016
|
-
* that is not a supersession at all
|
|
1017
|
-
*
|
|
1213
|
+
* and only the first two are always the "somebody beat us" the name suggests. A row can appear, and a token can
|
|
1214
|
+
* change, on a write that is not a supersession at all: a `setRetention` on the destination creates its row when it
|
|
1215
|
+
* has none, and rewrites it when it has one. On top of that, the collection pass that runs AFTER a successful
|
|
1216
|
+
* publish can raise the same error. So: treat it as "re-read the destination and decide",
|
|
1018
1217
|
* never as "the write did not happen".
|
|
1019
1218
|
*
|
|
1020
1219
|
* **And by default it collects nothing**, unlike `load()`. See the `keep` default below.
|
|
@@ -1046,7 +1245,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1046
1245
|
if (reason === "superseded") {
|
|
1047
1246
|
const wrote = result.size > 0 ? `generation ${result.generation} was written and is not current` : `nothing was written \u2014 another writer took generation ${result.generation} first`;
|
|
1048
1247
|
throw new WriteConflictError(
|
|
1049
|
-
`${op}: the destination "${dest.segment}" changed while this materialisation was in flight, so its result is not the destination's current generation: ${wrote}. The pointer may have moved, the row may have been rewritten (a retention policy does
|
|
1248
|
+
`${op}: the destination "${dest.segment}" changed while this materialisation was in flight, so its result is not the destination's current generation: ${wrote}. The pointer may have moved, the row may have been created or rewritten (a retention policy does either), dropped or purged. Re-read the destination and re-run.`
|
|
1050
1249
|
);
|
|
1051
1250
|
}
|
|
1052
1251
|
return {
|
|
@@ -1060,131 +1259,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1060
1259
|
collected: result.collected
|
|
1061
1260
|
};
|
|
1062
1261
|
}
|
|
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, and the fingerprint of the current
|
|
1128
|
-
* object where both reads carry one, with what was pinned (a name deleted and created again is a replacement though it
|
|
1129
|
-
* is generation 0 again). If one was replaced, each output that subtracts it is
|
|
1130
|
-
* not published and carries {@link StaleOperandError}; the others publish. It costs one registry read per such operand per
|
|
1131
|
-
* group, and `stats.operands` carries every operand's pinned, start and end generation and whether it moved.
|
|
1132
|
-
*
|
|
1133
|
-
* **Memory.** Everything the pass holds resident is counted against `maxBufferedBytes` (default 256 MiB): each output's
|
|
1134
|
-
* buffered chunks, the operand streams' ranges, the chunks being evaluated, and the plan itself, and a call whose plan alone
|
|
1135
|
-
* passes it is refused before any chunk is read. When the outputs do not fit, they run in groups, each reading its operands
|
|
1136
|
-
* once (so the operand reads grow with the total output size over the budget), and a group's buffers are released only as
|
|
1137
|
-
* its publishes settle. A chunk count the index understated does not grow a group past the budget: the largest buffer is
|
|
1138
|
-
* dropped and that output re-runs alone. An output that cannot fit alone has `BudgetExceededError` as its result. The count
|
|
1139
|
-
* 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
|
|
1140
|
-
* macOS on the in-memory backend (the guide has the shapes and what is not counted).
|
|
1141
|
-
*
|
|
1142
|
-
* **Untrusted bytes.** Every operand chunk is decoded through the checks on untrusted tier data. A key an operand's index
|
|
1143
|
-
* lists whose bytes are missing is an error for the outputs that read the operand, never an empty chunk.
|
|
1144
|
-
*
|
|
1145
|
-
* **Handles.** An expired or released handle anywhere in the call (an operand or a `dest`) is refused before any request,
|
|
1146
|
-
* as on the `*Into` verbs. A leased operand's lease, and an operand's deadline, are checked before each chunk key
|
|
1147
|
-
* is read: a lapse fails only the outputs that read that operand, never reads empty. Each `dest`'s lease and deadline
|
|
1148
|
-
* are checked again just before its publish.
|
|
1149
|
-
*
|
|
1150
|
-
* **The chunk cache is bypassed:** the pass reads through the storage source and never writes the decoded-chunk cache, so
|
|
1151
|
-
* a batch does not evict other readers' hot chunks. Each publish still invalidates its `dest` as an `*Into` does.
|
|
1152
|
-
*
|
|
1153
|
-
* **A limit of the form.** An output cannot name another output of the same call: a `dest` that is also an operand is
|
|
1154
|
-
* refused. Chain two calls.
|
|
1155
|
-
*
|
|
1156
|
-
* **Held operands.** An operand may be a {@link MemoryOperand} from {@link CloudRoaring.memory}: ids the caller holds,
|
|
1157
|
-
* read beside the stored ones and giving the same output byte for byte. It is read from memory (no request, and the
|
|
1158
|
-
* shared chunk cache never sees it), counts against `maxBufferedBytes` while the call runs, and is refused when empty
|
|
1159
|
-
* unless its name is in `mayBeEmpty`. A handle from another store, or a released one, is a {@link ValidationError}. An
|
|
1160
|
-
* erasure in this store since the handle was made fails the outputs that read it with {@link StaleOperandError}
|
|
1161
|
-
* (`reason: 'erased'`).
|
|
1162
|
-
*
|
|
1163
|
-
* **A feed.** `feed: { names, records, counts }` adds operands that arrive as records in chunk-key order, for conditions
|
|
1164
|
-
* too many to hold or store. Each record is checked before the pass sees it (see {@link MaterializeManyFeedRecord}): a bad
|
|
1165
|
-
* feed is refused, never read as fewer members, and **a fed output is published only after the whole feed was read and every
|
|
1166
|
-
* end-of-feed check passed**, so a bad feed refuses every fed output and publishes none of them; outputs that name only
|
|
1167
|
-
* stored operands are unaffected. At the end of the feed `counts` must equal the ids the feed held for each name, and a
|
|
1168
|
-
* name that appeared in no record is refused unless it is in `mayBeEmpty`. A feed is read once, so the call runs its outputs
|
|
1169
|
-
* as one group and **`maxBufferedBytes` is required**: each record is converted to compressed bitmaps as it arrives, the key
|
|
1170
|
-
* in hand and its fed outputs are counted against the budget, and the call fails as soon as the count passes it, with
|
|
1171
|
-
* {@link BudgetExceededError} for every fed output. **Erasure:** a fed call is refused at its next record, and before each
|
|
1172
|
-
* fed publish, once `eraseSubject` has started in this store ({@link StaleOperandError}, `reason: 'erased'`).
|
|
1173
|
-
*
|
|
1174
|
-
* ```ts
|
|
1175
|
-
* await store.materializeMany({
|
|
1176
|
-
* operands: { unsub: store.segment('global-unsub') },
|
|
1177
|
-
* feed: {
|
|
1178
|
-
* names: ['us', 'engaged'],
|
|
1179
|
-
* records: warehouseRecords(), // yields { key, operands: { us: Uint32Array, engaged: Uint32Array } } by key
|
|
1180
|
-
* counts: () => warehouseCounts(),
|
|
1181
|
-
* },
|
|
1182
|
-
* maxBufferedBytes: 512 * 1024 * 1024,
|
|
1183
|
-
* outputs: [{ dest: store.segment('send-1'), expr: { and: ['us', 'engaged'] }, exclude: ['unsub'] }],
|
|
1184
|
-
* keep: 12,
|
|
1185
|
-
* });
|
|
1186
|
-
* ```
|
|
1187
|
-
*/
|
|
1262
|
+
/** The one body of the forms above. */
|
|
1188
1263
|
async materializeMany(options) {
|
|
1189
1264
|
if (typeof options !== "object" || options === null) {
|
|
1190
1265
|
throw new ValidationError2(
|
|
@@ -1200,12 +1275,17 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1200
1275
|
`materializeMany: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}`
|
|
1201
1276
|
);
|
|
1202
1277
|
}
|
|
1278
|
+
const dryRunIn = options.dryRun;
|
|
1279
|
+
if (dryRunIn !== void 0 && typeof dryRunIn !== "boolean") {
|
|
1280
|
+
throw new ValidationError2("materializeMany: dryRun must be a boolean");
|
|
1281
|
+
}
|
|
1282
|
+
const dryRun = dryRunIn === true;
|
|
1203
1283
|
const operandsIn = options.operands;
|
|
1204
1284
|
const outputsIn = options.outputs;
|
|
1205
1285
|
const keep = options.keep;
|
|
1206
1286
|
const after = options.after;
|
|
1207
1287
|
const through = options.through;
|
|
1208
|
-
const pinOption = options.pin
|
|
1288
|
+
const pinOption = options.pin === void 0 ? true : options.pin;
|
|
1209
1289
|
const feedIn = options.feed;
|
|
1210
1290
|
const mayBeEmptyIn = options.mayBeEmpty;
|
|
1211
1291
|
if (feedIn !== void 0 && options.maxBufferedBytes === void 0) {
|
|
@@ -1231,50 +1311,26 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1231
1311
|
}
|
|
1232
1312
|
const names = Object.keys(operandsIn);
|
|
1233
1313
|
const handles = /* @__PURE__ */ new Map();
|
|
1234
|
-
const heldHandles = /* @__PURE__ */ new Map();
|
|
1235
1314
|
for (const name of names) {
|
|
1236
1315
|
const handle = operandsIn[name];
|
|
1237
|
-
if (handle instanceof MemoryOperand) {
|
|
1238
|
-
const view = heldOf(handle);
|
|
1239
|
-
if (view === void 0) {
|
|
1240
|
-
throw new ValidationError2(
|
|
1241
|
-
`materializeMany: operands["${name}"] is not a memory operand from store.memory()`
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
if (view.store !== this) {
|
|
1245
|
-
throw new ValidationError2(
|
|
1246
|
-
`materializeMany: operands["${name}"] was made by another store: a memory operand belongs to the store that made it`
|
|
1247
|
-
);
|
|
1248
|
-
}
|
|
1249
|
-
if (view.chunks.released) {
|
|
1250
|
-
throw new ValidationError2(
|
|
1251
|
-
`materializeMany: operands["${name}"] was released: make another with store.memory()`
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
heldHandles.set(name, view);
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
1316
|
if (!(handle instanceof Segment)) {
|
|
1258
1317
|
throw new ValidationError2(
|
|
1259
|
-
`materializeMany: operands["${name}"] must be a segment from store.segment()
|
|
1318
|
+
`materializeMany: operands["${name}"] must be a segment from store.segment()`
|
|
1260
1319
|
);
|
|
1261
1320
|
}
|
|
1262
1321
|
handles.set(name, handle);
|
|
1263
1322
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
for (const name of mayBeEmptyIn) {
|
|
1270
|
-
if (typeof name === "string" && heldHandles.has(name)) heldMayBeEmpty.add(name);
|
|
1271
|
-
else rest.push(name);
|
|
1272
|
-
}
|
|
1273
|
-
fedMayBeEmpty = rest;
|
|
1323
|
+
if (mayBeEmptyIn !== void 0 && feedIn === void 0) {
|
|
1324
|
+
if (!Array.isArray(mayBeEmptyIn)) {
|
|
1325
|
+
throw new ValidationError2(
|
|
1326
|
+
"materializeMany: mayBeEmpty must be an array of fed operand names"
|
|
1327
|
+
);
|
|
1274
1328
|
}
|
|
1275
|
-
if (
|
|
1329
|
+
if (mayBeEmptyIn.length > 0) {
|
|
1330
|
+
const first = mayBeEmptyIn[0];
|
|
1331
|
+
const what = typeof first !== "string" ? "something that is not a name" : handles.has(first) ? `"${first}", a stored operand` : `"${first}", which is not an operand of this call`;
|
|
1276
1332
|
throw new ValidationError2(
|
|
1277
|
-
|
|
1333
|
+
`materializeMany: mayBeEmpty names ${what}; it names fed operands, and this call has no feed`
|
|
1278
1334
|
);
|
|
1279
1335
|
}
|
|
1280
1336
|
}
|
|
@@ -1314,26 +1370,13 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1314
1370
|
const storeCeiling = this.budget === null || this.budget.maxRequests !== DEFAULT_BUDGET.maxRequests ? this.budget : void 0;
|
|
1315
1371
|
const budget = budgetOption === void 0 ? storeCeiling : resolvePerOpBudget(budgetOption, this.budget);
|
|
1316
1372
|
const operandSpecs = (pins2) => names.map((name) => {
|
|
1317
|
-
const view = heldHandles.get(name);
|
|
1318
|
-
if (view !== void 0) {
|
|
1319
|
-
return {
|
|
1320
|
-
name,
|
|
1321
|
-
ref: { segment: name },
|
|
1322
|
-
held: {
|
|
1323
|
-
chunks: view.chunks,
|
|
1324
|
-
moved: () => this.epochMoved(view.epochAt),
|
|
1325
|
-
mayBeEmpty: heldMayBeEmpty.has(name)
|
|
1326
|
-
}
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
1373
|
const handle = handles.get(name);
|
|
1330
1374
|
const seg = viewOf(handle);
|
|
1331
1375
|
const pinnedAt = pins2.get(handle.key());
|
|
1332
|
-
const guarded = seg.expiresAt !== void 0 || handle.lease !== void 0;
|
|
1333
1376
|
return {
|
|
1334
1377
|
name,
|
|
1335
1378
|
ref: seg.ref,
|
|
1336
|
-
...
|
|
1379
|
+
...handle.lease === void 0 ? {} : { check: () => this.assertLease(handle) },
|
|
1337
1380
|
...pinnedAt === void 0 ? {} : {
|
|
1338
1381
|
pinnedGeneration: pinnedAt.generation,
|
|
1339
1382
|
pinnedVersion: pinnedAt.version,
|
|
@@ -1344,18 +1387,18 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1344
1387
|
});
|
|
1345
1388
|
const request = (pins2) => ({
|
|
1346
1389
|
operands: operandSpecs(pins2),
|
|
1347
|
-
outputs: outputs.map(({ spec, dest }
|
|
1390
|
+
outputs: outputs.map(({ spec, dest }) => ({
|
|
1348
1391
|
expr: spec.expr,
|
|
1349
1392
|
...spec.exclude === void 0 ? {} : { exclude: spec.exclude },
|
|
1350
1393
|
...spec.allowEmpty === void 0 ? {} : { allowEmpty: spec.allowEmpty },
|
|
1351
1394
|
...spec.guard === void 0 ? {} : { guard: spec.guard },
|
|
1352
1395
|
...spec.metadata === void 0 ? {} : { metadata: spec.metadata },
|
|
1353
1396
|
...spec.keep === void 0 ? {} : { keep: spec.keep },
|
|
1354
|
-
beforePublish: () => this.
|
|
1397
|
+
beforePublish: () => this.assertLease(dest),
|
|
1355
1398
|
// What a settled publish proves it sent: a published one wrote its object and moved the pointer, a refused one
|
|
1356
|
-
// wrote its object and deleted it.
|
|
1357
|
-
requestsOf: (result) =>
|
|
1358
|
-
publish: (chunks, write) => this.materialize(viewOf(dest).ref, new CombineChunks(chunks), "materializeMany", {
|
|
1399
|
+
// wrote its object and deleted it. A dry run's judgement wrote nothing, and read the row and maybe the object.
|
|
1400
|
+
requestsOf: (result) => "dryRun" in result ? { get: result.reads, put: 0 } : { get: 0, put: result.published ? 2 : 1 },
|
|
1401
|
+
publish: (chunks, write) => dryRun ? this.judgeOutput(viewOf(dest).ref, chunks, write) : this.materialize(viewOf(dest).ref, new CombineChunks(chunks), "materializeMany", {
|
|
1359
1402
|
...write,
|
|
1360
1403
|
...spec.audit === void 0 ? {} : { audit: spec.audit }
|
|
1361
1404
|
})
|
|
@@ -1368,11 +1411,12 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1368
1411
|
concurrency,
|
|
1369
1412
|
...budget === void 0 ? {} : { budget },
|
|
1370
1413
|
...allowAbsentOperands === void 0 ? {} : { allowAbsentOperands },
|
|
1371
|
-
...feedIn === void 0 ? {} : { feed: this.feedOf(feedIn,
|
|
1414
|
+
...feedIn === void 0 ? {} : { feed: this.feedOf(feedIn, mayBeEmptyIn, epochAt) },
|
|
1415
|
+
...dryRun ? { dryRun: true } : {}
|
|
1372
1416
|
});
|
|
1373
1417
|
const compiled = compileCombineMany(request(/* @__PURE__ */ new Map()));
|
|
1374
|
-
for (const { dest } of outputs) this.
|
|
1375
|
-
for (const handle of handles.values()) this.
|
|
1418
|
+
for (const { dest } of outputs) this.assertLease(dest);
|
|
1419
|
+
for (const handle of handles.values()) this.assertLease(handle);
|
|
1376
1420
|
this.lifecycleDeps("materializeMany");
|
|
1377
1421
|
const crbm = this.crbmSource;
|
|
1378
1422
|
if (crbm === void 0) {
|
|
@@ -1409,25 +1453,64 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1409
1453
|
else if (pinOption) toPin.push([key, handle]);
|
|
1410
1454
|
else pins.set(key, void 0);
|
|
1411
1455
|
}
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1456
|
+
const metricsOn = this.metrics !== NOOP_METRICS;
|
|
1457
|
+
const work = async () => {
|
|
1458
|
+
const taken = await mapWithConcurrency(
|
|
1459
|
+
toPin,
|
|
1460
|
+
PIN_PARALLELISM,
|
|
1461
|
+
async ([key, handle]) => [key, (await handle.pin()).pinnedAt]
|
|
1462
|
+
);
|
|
1463
|
+
for (const [key, at] of taken) pins.set(key, at);
|
|
1464
|
+
const live = new Map([...pins].filter((e) => e[1] !== void 0));
|
|
1465
|
+
const pinnedSource = new PinnedStorageChunkSource(crbm, live);
|
|
1466
|
+
const source = this.retryOptions === void 0 ? pinnedSource : new RetryingStorageChunkSource(pinnedSource, this.retryOptions);
|
|
1467
|
+
const final = rebindCombineMany(compiled, operandSpecs(pins));
|
|
1468
|
+
return runCombineMany(final, {
|
|
1469
|
+
source,
|
|
1470
|
+
codec: roaringCodec,
|
|
1471
|
+
clock: this.clock,
|
|
1472
|
+
...metricsOn ? { metrics: this.metrics } : {}
|
|
1473
|
+
});
|
|
1474
|
+
};
|
|
1475
|
+
const run = metricsOn ? await timeOp(this.metrics, this.clock, "materializeMany", work) : await work();
|
|
1476
|
+
const results = run.outputs.map((o) => {
|
|
1477
|
+
if (!o.ok) {
|
|
1478
|
+
return {
|
|
1426
1479
|
published: false,
|
|
1427
1480
|
error: o.error instanceof Error ? o.error : new Error(String(o.error))
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
if (!("dryRun" in o.value)) return o.value;
|
|
1484
|
+
const v = o.value;
|
|
1485
|
+
const judged = {
|
|
1486
|
+
dryRun: true,
|
|
1487
|
+
published: false,
|
|
1488
|
+
cardinality: v.cardinality,
|
|
1489
|
+
cardinalityBefore: v.cardinalityBefore,
|
|
1490
|
+
...v.wouldRefuse === void 0 ? {} : { wouldRefuse: v.wouldRefuse }
|
|
1491
|
+
};
|
|
1492
|
+
return judged;
|
|
1493
|
+
});
|
|
1494
|
+
return dryRun ? { outputs: results, stats: run.stats } : { outputs: results, stats: run.stats };
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* A dry run's publish of one output: count what the finished chunks hold, and judge that size against `dest` as a
|
|
1498
|
+
* load would, writing nothing.
|
|
1499
|
+
*/
|
|
1500
|
+
async judgeOutput(dest, chunks, write) {
|
|
1501
|
+
let cardinality = 0;
|
|
1502
|
+
for await (const chunk of chunks) cardinality += chunk.bitmap.size;
|
|
1503
|
+
const judged = await judgeLoad(dest, cardinality, this.lifecycleDeps("materializeMany"), {
|
|
1504
|
+
...write.allowEmpty === void 0 ? {} : { allowEmpty: write.allowEmpty },
|
|
1505
|
+
...write.guard === void 0 ? {} : { guard: write.guard }
|
|
1506
|
+
});
|
|
1507
|
+
return {
|
|
1508
|
+
dryRun: true,
|
|
1509
|
+
published: false,
|
|
1510
|
+
cardinality,
|
|
1511
|
+
cardinalityBefore: judged.cardinalityBefore,
|
|
1512
|
+
...judged.wouldRefuse === void 0 ? {} : { wouldRefuse: judged.wouldRefuse },
|
|
1513
|
+
reads: judged.reads
|
|
1431
1514
|
};
|
|
1432
1515
|
}
|
|
1433
1516
|
epochNow() {
|
|
@@ -1480,20 +1563,10 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1480
1563
|
const row = await this.withRetries(() => registry.get(ref));
|
|
1481
1564
|
return row === null ? null : { generation: row.currentGen, token: row.token };
|
|
1482
1565
|
}
|
|
1483
|
-
/** Throw what a read of `handle` must throw now: its lease error,
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
const lease = view.leaseError();
|
|
1566
|
+
/** Throw what a read of, or a publish to, `handle` must throw now: its lease error, if it has one. */
|
|
1567
|
+
assertLease(handle) {
|
|
1568
|
+
const lease = viewOf(handle).leaseError();
|
|
1487
1569
|
if (lease !== void 0) throw lease;
|
|
1488
|
-
if (view.expired()) {
|
|
1489
|
-
throw new ValidationError2(
|
|
1490
|
-
`${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.`
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
/** {@link assertReadable} for a destination, named by where it sits in the call. */
|
|
1495
|
-
assertWritable(where, dest) {
|
|
1496
|
-
this.assertReadable(where, dest);
|
|
1497
1570
|
}
|
|
1498
1571
|
/**
|
|
1499
1572
|
* **Subject access (GDPR Art. 15 / CCPA right-to-know): which segments is this id a member of?**
|
|
@@ -1511,6 +1584,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1511
1584
|
* generation 0 while this store held its old generation 0 is told apart too.
|
|
1512
1585
|
*/
|
|
1513
1586
|
async subjectReport(id, options = {}) {
|
|
1587
|
+
checkOptions(options, "subjectReport", CALL_KEYS.subjectReport, ["allNamespaces"]);
|
|
1588
|
+
options ??= {};
|
|
1514
1589
|
const registry = this.requireRegistry("subjectReport");
|
|
1515
1590
|
requireScope(options, "subjectReport");
|
|
1516
1591
|
validateConcurrency(options.concurrency);
|
|
@@ -1532,7 +1607,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1532
1607
|
if (this.crbmSource !== void 0) {
|
|
1533
1608
|
const held = await this.crbmSource.currentVersion(ref);
|
|
1534
1609
|
const listed = rec.currentGen === null ? null : `${rec.currentGen}:${String(rec.token)}`;
|
|
1535
|
-
if (held !== listed) this.engine.invalidate(ref);
|
|
1610
|
+
if (rowVersionOfLive(held) !== listed) this.engine.invalidate(ref);
|
|
1536
1611
|
}
|
|
1537
1612
|
return await this.engine.has(ref, id) ? { segment: rec.segment, namespace: rec.namespace } : null;
|
|
1538
1613
|
}
|
|
@@ -1574,8 +1649,9 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1574
1649
|
* every generation below its new pointer — the one this rewrite is streaming, and the object this rewrite
|
|
1575
1650
|
* just wrote — so the loser can find its own inputs deleted mid-flight. That surfaces as an outcome rather than
|
|
1576
1651
|
* an error, read off the row: a moved pointer is a `'superseded'` entry, which a re-run settles, and a segment
|
|
1577
|
-
* that a concurrent `dropSegment` tombstoned or a retention sweep purged is left out of the ledger
|
|
1578
|
-
*
|
|
1652
|
+
* that a concurrent `dropSegment` tombstoned or a retention sweep purged is left out of the ledger. A tombstoned
|
|
1653
|
+
* segment is still searched: a cleartext object left under it that holds the id is deleted, with everything under
|
|
1654
|
+
* the tombstone, and the entry reads `erased: true`.
|
|
1579
1655
|
*
|
|
1580
1656
|
* **Read `note` on any `erased: false` entry — the two reasons mean different things.** `'superseded'` means
|
|
1581
1657
|
* another writer (a load, another erasure, or a rollback) moved the pointer mid-call, so **this call** did not
|
|
@@ -1604,6 +1680,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1604
1680
|
*/
|
|
1605
1681
|
async eraseSubject(id, options = {}) {
|
|
1606
1682
|
this.erasureEpoch++;
|
|
1683
|
+
checkOptions(options, "eraseSubject", CALL_KEYS.eraseSubject, ["allNamespaces"]);
|
|
1684
|
+
options ??= {};
|
|
1607
1685
|
this.erasuresRunning++;
|
|
1608
1686
|
try {
|
|
1609
1687
|
return await this.eraseSubjectRun(id, options);
|
|
@@ -1632,7 +1710,6 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1632
1710
|
recs,
|
|
1633
1711
|
options.concurrency ?? DEFAULT_ADMIN_CONCURRENCY,
|
|
1634
1712
|
async (rec) => {
|
|
1635
|
-
if (rec.status === "destroyed") return null;
|
|
1636
1713
|
const ref = { segment: rec.segment, namespace: rec.namespace };
|
|
1637
1714
|
try {
|
|
1638
1715
|
const result = await eraseIdFromSegment(
|
|
@@ -1696,10 +1773,9 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1696
1773
|
* exists. It is `false` for a segment whose row was minted ahead of its first load (by `setRetention`) and
|
|
1697
1774
|
* for a `destroyed` tombstone, because a read answers empty in both cases.
|
|
1698
1775
|
*
|
|
1699
|
-
*
|
|
1776
|
+
* One state answers `true` where a read still gives you nothing: a torn restore (a live pointer whose object
|
|
1700
1777
|
* was deleted) makes reads of the object *throw* rather than answer empty, while a cold `count()` answers the number
|
|
1701
|
-
* its row records — `checkConsistency` is the call for that
|
|
1702
|
-
* a handle carrying an expired `expiresAt` reads empty by a rule that lives on the handle, not the row.
|
|
1778
|
+
* its row records — `checkConsistency` is the call for that.
|
|
1703
1779
|
*
|
|
1704
1780
|
* Not a lock: the answer can change the moment it returns. If it has to hold, use the fence built for that —
|
|
1705
1781
|
* `load`'s `guard`, or `expectFrom`/`expectToken` on a publish. Needs a storage backend.
|
|
@@ -1742,6 +1818,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1742
1818
|
* ```
|
|
1743
1819
|
*/
|
|
1744
1820
|
segments(options = {}) {
|
|
1821
|
+
checkOptions(options, "segments", CALL_KEYS.segments);
|
|
1822
|
+
options ??= {};
|
|
1745
1823
|
if (options.namespace !== void 0) {
|
|
1746
1824
|
validateSegmentRef({ segment: "x", namespace: options.namespace });
|
|
1747
1825
|
refuseReservedNamespace(options.namespace);
|
|
@@ -1787,6 +1865,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1787
1865
|
*/
|
|
1788
1866
|
async rollback(ref, toGeneration, options = {}) {
|
|
1789
1867
|
validateSegmentRef(ref);
|
|
1868
|
+
checkOptions(options, "rollback", CALL_KEYS.rollback, ["allowForward"]);
|
|
1869
|
+
options ??= {};
|
|
1790
1870
|
const deps = this.lifecycleDeps("rollback");
|
|
1791
1871
|
try {
|
|
1792
1872
|
return await rollbackSegment(ref, toGeneration, deps, options);
|
|
@@ -1879,6 +1959,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1879
1959
|
*/
|
|
1880
1960
|
async load(ref, input, options = {}) {
|
|
1881
1961
|
validateSegmentRef(ref);
|
|
1962
|
+
checkOptions(options, "load", CALL_KEYS.load, ["allowEmpty"]);
|
|
1963
|
+
options ??= {};
|
|
1882
1964
|
const deps = this.lifecycleDeps("load");
|
|
1883
1965
|
try {
|
|
1884
1966
|
return await loadSegment(ref, bitmapAsLoadInput(input), deps, options);
|
|
@@ -1933,6 +2015,10 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1933
2015
|
*/
|
|
1934
2016
|
async dropSegment(ref, options) {
|
|
1935
2017
|
validateSegmentRef(ref);
|
|
2018
|
+
if (options === void 0 || options === null) {
|
|
2019
|
+
throw new ValidationError2("dropSegment: options { confirmSegment } are required");
|
|
2020
|
+
}
|
|
2021
|
+
checkOptions(options, "dropSegment", CALL_KEYS.dropSegment, ["dryRun"]);
|
|
1936
2022
|
const deps = this.lifecycleDeps("dropSegment");
|
|
1937
2023
|
try {
|
|
1938
2024
|
return await dropSegment(
|
|
@@ -2060,69 +2146,20 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
2060
2146
|
* because retiring a segment deletes its storage objects. `now` defaults to the store's clock.
|
|
2061
2147
|
*/
|
|
2062
2148
|
async retireExpired(options = {}) {
|
|
2149
|
+
checkOptions(options, "retireExpired", CALL_KEYS.retireExpired, ["dryRun", "purgeTombstones"]);
|
|
2150
|
+
const opts = picked(options, CALL_KEYS.retireExpired);
|
|
2063
2151
|
const deps = this.lifecycleDeps("retireExpired");
|
|
2064
2152
|
const result = await retireExpired(
|
|
2065
2153
|
{ registry: deps.registry, storage: deps.storage, clock: this.clock, rng: this.rng },
|
|
2066
|
-
{ ...
|
|
2154
|
+
{ ...opts, now: opts.now ?? this.clock.now() }
|
|
2067
2155
|
);
|
|
2068
|
-
if (
|
|
2156
|
+
if (opts.dryRun !== true) {
|
|
2069
2157
|
for (const entry of result.entries) {
|
|
2070
2158
|
this.engine.invalidate({ segment: entry.segment, namespace: entry.namespace });
|
|
2071
2159
|
}
|
|
2072
2160
|
}
|
|
2073
2161
|
return result;
|
|
2074
2162
|
}
|
|
2075
|
-
/**
|
|
2076
|
-
* **Remove the registry rows that releases before 0.12.0 left as `deleted: true` envelopes.** Those releases never
|
|
2077
|
-
* removed a row they deleted, and no later call removes it either: it is invisible to every read, and every full
|
|
2078
|
-
* listing of the registry still reads it (one GET each). This is the one call that removes them. It is an admin call,
|
|
2079
|
-
* not a sweep, and nothing schedules it: run it once after the last process on a release before 0.12.0 is gone.
|
|
2080
|
-
*
|
|
2081
|
-
* ```ts
|
|
2082
|
-
* const preview = await store.reapRegistryTombstones({ dryRun: true });
|
|
2083
|
-
* console.log(`would remove ${preview.wouldReap} of ${preview.examined} rows read`);
|
|
2084
|
-
*
|
|
2085
|
-
* const done = await store.reapRegistryTombstones({ confirmNoLegacyWriters: true });
|
|
2086
|
-
* if (done.limited) runAgain(); // `limit` (default 1,000) stopped the run with rows unread
|
|
2087
|
-
* ```
|
|
2088
|
-
*
|
|
2089
|
-
* **What it removes.** Only a row that is `deleted: true` and whose token carries no incarnation id, which is what a
|
|
2090
|
-
* 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
|
|
2091
|
-
* remove), whatever its `status`. It never touches a live row, a `destroyed` row that is not `deleted` (a crypto-shred's
|
|
2092
|
-
* or a `dropSegment`'s tombstone: the attestation of an erasure, which it cannot tell from another), or a deleted row
|
|
2093
|
-
* with an incarnation id, and it never touches a generation. **So it does not clean a bucket completely**: the
|
|
2094
|
-
* `destroyed` tombstones `dropSegment` leaves stay, so do live rows written before 0.12.0, and so does a tombstone a
|
|
2095
|
-
* 0.12.0 or later release wrote while `conditionalDelete` was off (GCS by default, S3 on a custom endpoint): every full
|
|
2096
|
-
* listing still reads it, and this call neither removes it nor runs on such a store.
|
|
2097
|
-
* It also covers the library's own bookkeeping rows when `namespace` is not set, by the same rule. The default
|
|
2098
|
-
* namespace cannot be scoped to: leave `namespace` out to cover it.
|
|
2099
|
-
*
|
|
2100
|
-
* **The two things it asks.** `confirmNoLegacyWriters: true` is required for a real run (without it, a
|
|
2101
|
-
* `ValidationError`, and nothing is requested): your statement that no process on a release before 0.12.0 writes
|
|
2102
|
-
* this registry, since one that did would, re-creating a removed name, issue the removed row's tokens again. And the
|
|
2103
|
-
* registry must apply a delete under a precondition: where its `conditionalDelete` capability is off (GCS by default,
|
|
2104
|
-
* S3 on a custom endpoint) it throws `CapabilityError` before any request, dry run included. A registry that keeps
|
|
2105
|
-
* no such rows (the in-memory and local-filesystem ones) throws `UnsupportedError`.
|
|
2106
|
-
*
|
|
2107
|
-
* **Each removal is fenced** on the version it read, so a `create` that lands over the envelope first wins, the
|
|
2108
|
-
* delete is refused, and the row is counted as `skipped.raced` (which also counts a row already gone). That holds where
|
|
2109
|
-
* the backend applies the precondition, which auto-detection checks (S3 only on an AWS host). On an endpoint that
|
|
2110
|
-
* ignores `If-Match` (MinIO, fake-gcs-server), a `conditionalDelete: true` you set makes this an unfenced delete: run
|
|
2111
|
-
* it with every writer stopped. On a versioned bucket a delete leaves a delete marker and the earlier version, record
|
|
2112
|
-
* included, so the name is not erased from the bucket. A `create` that had already read the envelope and
|
|
2113
|
-
* whose write meets the removal throws `WriteConflictError`, which this library does not retry: retry the create, and
|
|
2114
|
-
* it finds no row and writes one.
|
|
2115
|
-
*
|
|
2116
|
-
* **Cost** over R rows read and E removed: `ceil(R / 1000)` LIST requests, R GETs and E DELETEs (a dry run, no
|
|
2117
|
-
* DELETEs). `limit` bounds the removals, and the run stops listing once it is spent. **A run is not resumable**: each
|
|
2118
|
-
* lists and reads from the start (R GETs whatever the `limit`), a dry run with a `limit` reports the same first rows
|
|
2119
|
-
* every time, so `wouldReap` is a total only when `limited` is `false`, and `limited` is `true` whenever the limit was
|
|
2120
|
-
* spent with keys left, removable or not. An unreadable object stops the run with an error naming its key, and
|
|
2121
|
-
* removes nothing more: it may be a newer release's row. Scope the run with `namespace` to get past one.
|
|
2122
|
-
*/
|
|
2123
|
-
async reapRegistryTombstones(options = {}) {
|
|
2124
|
-
return reapRegistryTombstones(this.requireRegistry("reapRegistryTombstones"), options);
|
|
2125
|
-
}
|
|
2126
2163
|
/**
|
|
2127
2164
|
* **Forget everything this store has cached about a segment.** Use it after destroying or retiring a segment
|
|
2128
2165
|
* through a path this store did not perform itself.
|
|
@@ -2169,7 +2206,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
2169
2206
|
* pinned view reports the version captured at pin time, marked as a pin's, so its decoded chunks are never
|
|
2170
2207
|
* those of a live read that fetched across a publish (see {@link PinnedStorageChunkSource.currentVersion}).
|
|
2171
2208
|
*/
|
|
2172
|
-
async pinSegment(ref,
|
|
2209
|
+
async pinSegment(ref, leaseUntil, named) {
|
|
2173
2210
|
const crbm = this.crbmSource;
|
|
2174
2211
|
if (crbm === void 0) {
|
|
2175
2212
|
throw new UnsupportedError(
|
|
@@ -2229,9 +2266,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
2229
2266
|
clock: this.clock,
|
|
2230
2267
|
metrics: this.metrics,
|
|
2231
2268
|
materialize: (dest, ids, op, options) => this.materialize(dest, ids, op, options),
|
|
2232
|
-
pinned: (r,
|
|
2269
|
+
pinned: (r, l, at2) => this.pinSegment(r, l, at2),
|
|
2233
2270
|
combineEngine: (handles) => this.engineForCombine(handles),
|
|
2234
|
-
expiresAt,
|
|
2235
2271
|
pinnedAt,
|
|
2236
2272
|
lease: hold
|
|
2237
2273
|
});
|
|
@@ -2337,6 +2373,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
2337
2373
|
* `storage` and `registry` with a higher `maxScanSegments`.
|
|
2338
2374
|
*/
|
|
2339
2375
|
async checkConsistency(options = {}) {
|
|
2376
|
+
checkOptions(options, "checkConsistency", CALL_KEYS.checkConsistency, ["summaries"]);
|
|
2377
|
+
options ??= {};
|
|
2340
2378
|
const deps = this.lifecycleDeps("checkConsistency");
|
|
2341
2379
|
return runConsistencyCheck(
|
|
2342
2380
|
{ storage: deps.storage, registry: deps.registry, keystore: deps.keystore },
|
|
@@ -2356,15 +2394,16 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
2356
2394
|
* CLI wraps this with a filesystem sink.
|
|
2357
2395
|
*/
|
|
2358
2396
|
async exportSegments(sink, options = {}) {
|
|
2397
|
+
checkOptions(options, "exportSegments", CALL_KEYS.exportSegments);
|
|
2398
|
+
const opts = picked(options, CALL_KEYS.exportSegments);
|
|
2359
2399
|
const registry = this.requireRegistry("exportSegments");
|
|
2360
|
-
return runExport(this, registry, sink, {
|
|
2361
|
-
...options,
|
|
2362
|
-
codec: options.codec ?? roaringCodec
|
|
2363
|
-
});
|
|
2400
|
+
return runExport(this, registry, sink, { ...opts, codec: opts.codec ?? roaringCodec });
|
|
2364
2401
|
}
|
|
2365
2402
|
/**
|
|
2366
2403
|
* Planning cost estimate — pure, no instance/data needed: sizing, sales, what-if. For a real, grounded report
|
|
2367
2404
|
* from live segment sizes, use `store.segment(name).costReport()`. See {@link CostReport}.
|
|
2405
|
+
* @deprecated Moves to `@cloudbitmaps/tools`, a package of its own, with a `size` on `stat()` so a grounded report
|
|
2406
|
+
* needs nothing internal. It is a planning tool, and its price list is as old as the release that ships it.
|
|
2368
2407
|
*/
|
|
2369
2408
|
static estimateCost(input) {
|
|
2370
2409
|
return estimateCost(input);
|
|
@@ -2394,24 +2433,17 @@ var readOptions = (options) => ({
|
|
|
2394
2433
|
allowAbsentOperands: options.allowAbsentOperands
|
|
2395
2434
|
});
|
|
2396
2435
|
var NO_SEGMENTS = [];
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
}
|
|
2402
|
-
};
|
|
2436
|
+
function segmentList(list, what) {
|
|
2437
|
+
if (!Array.isArray(list)) throw new ValidationError2(`${what} must be an array of segments`);
|
|
2438
|
+
return list;
|
|
2439
|
+
}
|
|
2403
2440
|
var AS_IDS = {
|
|
2404
|
-
none: EMPTY_IDS,
|
|
2405
2441
|
failing,
|
|
2406
2442
|
guard: guardIds,
|
|
2407
2443
|
intersect: (engine, refs, opts) => withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts)),
|
|
2408
2444
|
union: (engine, refs, opts) => withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts)),
|
|
2409
2445
|
andNot: (engine, base, refs, opts) => withBatches(engine.andNot(base, refs, opts), () => engine.andNotBatches(base, refs, opts))
|
|
2410
2446
|
};
|
|
2411
|
-
var NO_CHUNKS = {
|
|
2412
|
-
async *[Symbol.asyncIterator]() {
|
|
2413
|
-
}
|
|
2414
|
-
};
|
|
2415
2447
|
var CombineChunks = class {
|
|
2416
2448
|
constructor(chunks) {
|
|
2417
2449
|
this.chunks = chunks;
|
|
@@ -2419,7 +2451,6 @@ var CombineChunks = class {
|
|
|
2419
2451
|
chunks;
|
|
2420
2452
|
};
|
|
2421
2453
|
var AS_CHUNKS = {
|
|
2422
|
-
none: NO_CHUNKS,
|
|
2423
2454
|
guard: guardChunks,
|
|
2424
2455
|
failing: (err) => ({
|
|
2425
2456
|
[Symbol.asyncIterator]: () => ({ next: () => Promise.reject(err) })
|
|
@@ -2429,39 +2460,14 @@ var AS_CHUNKS = {
|
|
|
2429
2460
|
andNot: (engine, base, refs, opts) => engine.andNotChunks(base, refs, opts)
|
|
2430
2461
|
};
|
|
2431
2462
|
var KEEP_EVERY_GENERATION = Number.MAX_SAFE_INTEGER;
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
heldOf = (operand) => operand.view;
|
|
2439
|
-
makeMemoryOperand = (view) => {
|
|
2440
|
-
mintingMemory = true;
|
|
2441
|
-
try {
|
|
2442
|
-
return new _MemoryOperand(view);
|
|
2443
|
-
} finally {
|
|
2444
|
-
mintingMemory = false;
|
|
2445
|
-
}
|
|
2446
|
-
};
|
|
2463
|
+
async function timeOp(metrics, clock, name, fn) {
|
|
2464
|
+
const startedAt = clock.now();
|
|
2465
|
+
try {
|
|
2466
|
+
return await fn();
|
|
2467
|
+
} finally {
|
|
2468
|
+
metrics.onEvent({ kind: "op", name, ms: Math.max(0, clock.now() - startedAt) });
|
|
2447
2469
|
}
|
|
2448
|
-
|
|
2449
|
-
constructor(view) {
|
|
2450
|
-
if (!mintingMemory) {
|
|
2451
|
-
throw new ValidationError2(
|
|
2452
|
-
"a MemoryOperand is not constructed directly; call `await store.memory(ids)`"
|
|
2453
|
-
);
|
|
2454
|
-
}
|
|
2455
|
-
this.view = view;
|
|
2456
|
-
}
|
|
2457
|
-
/**
|
|
2458
|
-
* Zero and drop the bytes it holds. Any later use throws {@link ValidationError}; releasing twice does nothing. A call
|
|
2459
|
-
* still reading it fails the outputs that read it.
|
|
2460
|
-
*/
|
|
2461
|
-
release() {
|
|
2462
|
-
this.view.chunks.release();
|
|
2463
|
-
}
|
|
2464
|
-
};
|
|
2470
|
+
}
|
|
2465
2471
|
var makeSegment;
|
|
2466
2472
|
var minting = false;
|
|
2467
2473
|
var Segment = class _Segment {
|
|
@@ -2473,8 +2479,6 @@ var Segment = class _Segment {
|
|
|
2473
2479
|
materialize;
|
|
2474
2480
|
pinned;
|
|
2475
2481
|
combineEngine;
|
|
2476
|
-
/** Absolute epoch-ms deadline from {@link SegmentOptions.expiresAt}; `undefined` ⇒ this handle never expires. */
|
|
2477
|
-
expiresAt;
|
|
2478
2482
|
/**
|
|
2479
2483
|
* The generation this handle is held at, when it came from {@link Segment.pin}. Read by the store so a
|
|
2480
2484
|
* pinned handle passed as an **operand** is still read at its pin rather than live.
|
|
@@ -2499,9 +2503,7 @@ var Segment = class _Segment {
|
|
|
2499
2503
|
viewOf = (segment) => ({
|
|
2500
2504
|
ref: segment.ref,
|
|
2501
2505
|
pinnedAt: segment.pinnedAt,
|
|
2502
|
-
|
|
2503
|
-
leaseError: () => segment.leaseError(),
|
|
2504
|
-
expired: () => segment.expired()
|
|
2506
|
+
leaseError: () => segment.leaseError()
|
|
2505
2507
|
});
|
|
2506
2508
|
}
|
|
2507
2509
|
/**
|
|
@@ -2521,7 +2523,6 @@ var Segment = class _Segment {
|
|
|
2521
2523
|
this.materialize = parts.materialize;
|
|
2522
2524
|
this.pinned = parts.pinned;
|
|
2523
2525
|
this.combineEngine = parts.combineEngine;
|
|
2524
|
-
this.expiresAt = parts.expiresAt;
|
|
2525
2526
|
this.pinnedAt = parts.pinnedAt;
|
|
2526
2527
|
this.leaseHold = parts.lease;
|
|
2527
2528
|
if (parts.lease !== void 0) {
|
|
@@ -2611,12 +2612,12 @@ var Segment = class _Segment {
|
|
|
2611
2612
|
async pin(options) {
|
|
2612
2613
|
this.assertLeases([this]);
|
|
2613
2614
|
const leaseUntil = this.leaseUntilOf("pin", options);
|
|
2614
|
-
return this.pinned(this.ref,
|
|
2615
|
+
return this.pinned(this.ref, leaseUntil);
|
|
2615
2616
|
}
|
|
2616
2617
|
/**
|
|
2617
2618
|
* Hold what this pin names until `leaseUntil`, or `undefined` for a pin with no lease. Refuses an option it does not
|
|
2618
|
-
* know, a value that is not epoch-milliseconds in the future, one past the longest lease,
|
|
2619
|
-
*
|
|
2619
|
+
* know, a value that is not epoch-milliseconds in the future, and one past the longest lease, before any request is
|
|
2620
|
+
* made.
|
|
2620
2621
|
*/
|
|
2621
2622
|
leaseUntilOf(op, options) {
|
|
2622
2623
|
if (options === void 0 || options === null) return void 0;
|
|
@@ -2647,11 +2648,6 @@ var Segment = class _Segment {
|
|
|
2647
2648
|
`${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`
|
|
2648
2649
|
);
|
|
2649
2650
|
}
|
|
2650
|
-
if (this.expiresAt !== void 0 && until > this.expiresAt) {
|
|
2651
|
-
throw new ValidationError2(
|
|
2652
|
-
`${op}: leaseUntil outlasts this handle's own expiresAt, which would have it read empty while it still holds the generation`
|
|
2653
|
-
);
|
|
2654
|
-
}
|
|
2655
2651
|
return until;
|
|
2656
2652
|
}
|
|
2657
2653
|
/**
|
|
@@ -2676,7 +2672,7 @@ var Segment = class _Segment {
|
|
|
2676
2672
|
}
|
|
2677
2673
|
/**
|
|
2678
2674
|
* The error a read of this handle must throw, or `undefined` when it holds no lease or the lease is live. One property
|
|
2679
|
-
* read on a handle with no lease.
|
|
2675
|
+
* read on a handle with no lease. A read past a lease throws; it never reads empty.
|
|
2680
2676
|
*/
|
|
2681
2677
|
leaseError() {
|
|
2682
2678
|
const hold = this.leaseHold;
|
|
@@ -2698,9 +2694,21 @@ var Segment = class _Segment {
|
|
|
2698
2694
|
}
|
|
2699
2695
|
return void 0;
|
|
2700
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Refuse with a {@link ValidationError} anything among `handles` that is not a segment, a value a JavaScript caller
|
|
2699
|
+
* passed. It runs first in every combine, before the lease check reads the handles.
|
|
2700
|
+
*/
|
|
2701
|
+
assertSegments(handles) {
|
|
2702
|
+
for (const h of handles) {
|
|
2703
|
+
if (h instanceof _Segment || typeof h === "object" && h !== null && typeof h.leaseError === "function") {
|
|
2704
|
+
continue;
|
|
2705
|
+
}
|
|
2706
|
+
throw new ValidationError2("an operand must be a segment from store.segment()");
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2701
2709
|
/**
|
|
2702
2710
|
* Throw the first lease error among `handles`: this handle and every operand and exclude of a call. It runs before
|
|
2703
|
-
*
|
|
2711
|
+
* the engine, so no operand, exclude or cached reader can answer past a lease.
|
|
2704
2712
|
*/
|
|
2705
2713
|
assertLeases(handles) {
|
|
2706
2714
|
for (const h of handles) {
|
|
@@ -2745,7 +2753,7 @@ var Segment = class _Segment {
|
|
|
2745
2753
|
);
|
|
2746
2754
|
}
|
|
2747
2755
|
const leaseUntil = this.leaseUntilOf("pinAt", options);
|
|
2748
|
-
return this.pinned(this.ref,
|
|
2756
|
+
return this.pinned(this.ref, leaseUntil, { generation, fingerprint });
|
|
2749
2757
|
}
|
|
2750
2758
|
/**
|
|
2751
2759
|
* An opaque string that names this handle's segment, namespace included: two handles of one segment have the
|
|
@@ -2757,49 +2765,17 @@ var Segment = class _Segment {
|
|
|
2757
2765
|
return segmentKey(this.ref);
|
|
2758
2766
|
}
|
|
2759
2767
|
/**
|
|
2760
|
-
*
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
* Deliberately evaluated per call rather than cached: a long-lived handle created before its deadline must
|
|
2764
|
-
* start reading empty the moment the deadline passes, without the caller re-creating it.
|
|
2765
|
-
*/
|
|
2766
|
-
expired() {
|
|
2767
|
-
return this.expiresAt !== void 0 && this.clock.now() >= this.expiresAt;
|
|
2768
|
-
}
|
|
2769
|
-
/**
|
|
2770
|
-
* Refuse a materialisation that involves an **expired** handle. Called by the three `*Into` verbs only.
|
|
2771
|
-
*
|
|
2772
|
-
* On the read verbs an expired handle answers empty, which is the point of lazy expiry. On a *write* the same
|
|
2773
|
-
* rule would be destructive in a way nobody asks for: `a.intersectInto(dest, [b])` where `b`'s deadline has
|
|
2774
|
-
* quietly passed would, unrefused, replace `dest` with an **empty generation** — a wipe, reported as a successful
|
|
2775
|
-
* write, with the cause (a deadline on a handle somewhere) nowhere in the result. Reads degrade to empty; writes must not.
|
|
2776
|
-
*
|
|
2777
|
-
* So every handle in the call is checked, `dest` included: an expired `dest` does not change the bytes written,
|
|
2778
|
-
* but a caller who put a deadline on the thing they are writing into has said something contradictory and is
|
|
2779
|
-
* better told than guessed at. Open a handle without `expiresAt` to write, or drop the deadline.
|
|
2780
|
-
*
|
|
2781
|
-
* (The broader guard — refusing to publish an empty or implausible generation over a non-empty one, with an
|
|
2782
|
-
* `allowEmpty` override — covers these verbs too: they route through the same guarded write path as
|
|
2783
|
-
* {@link CloudRoaring.load}. The two stay separate because they differ in kind. That one is a REPORTED
|
|
2784
|
-
* refusal a caller may legitimately override; an expired handle is a wiring mistake, so it THROWS, before
|
|
2785
|
-
* any object is written — and `allowEmpty: true` does not reach it.)
|
|
2768
|
+
* Refuse an `*Into` whose handles cannot be read, before any object is written: anything that is not a segment
|
|
2769
|
+
* ({@link ValidationError}), and a handle (this one, `dest` or an operand) whose lease has ended or was released
|
|
2770
|
+
* ({@link LeaseExpiredError}). Called by the three `*Into` verbs only.
|
|
2786
2771
|
*
|
|
2787
2772
|
* The verbs are `async` so this surfaces as a **rejected promise**, like every other validation in the facade —
|
|
2788
2773
|
* a synchronous throw out of a promise-returning method escapes a caller who attached `.catch()` instead of
|
|
2789
2774
|
* awaiting.
|
|
2790
2775
|
*/
|
|
2791
|
-
|
|
2776
|
+
assertIntoHandles(dest, operands) {
|
|
2777
|
+
this.assertSegments([dest, ...operands]);
|
|
2792
2778
|
this.assertLeases([this, dest, ...operands]);
|
|
2793
|
-
const stale = [];
|
|
2794
|
-
for (const seg of [this, dest, ...operands]) {
|
|
2795
|
-
if (seg.expired())
|
|
2796
|
-
stale.push(seg.ref.namespace ? `${seg.ref.namespace}/${seg.ref.segment}` : seg.ref.segment);
|
|
2797
|
-
}
|
|
2798
|
-
if (stale.length > 0) {
|
|
2799
|
-
throw new ValidationError2(
|
|
2800
|
-
`${op}: refusing to publish a generation while these handles have expired \u2014 ${[...new Set(stale)].join(", ")}. An expired handle reads as empty, so this would write an empty (or short) generation over "${dest.ref.segment}". Open the handles without \`expiresAt\` if you meant to materialise, or drop the deadline.`
|
|
2801
|
-
);
|
|
2802
|
-
}
|
|
2803
2779
|
}
|
|
2804
2780
|
/** `result` as is, or, when a handle of the call holds a lease, checking that lease each time it reads a chunk. */
|
|
2805
2781
|
guarded(out, handles, result) {
|
|
@@ -2822,7 +2798,6 @@ var Segment = class _Segment {
|
|
|
2822
2798
|
has(id) {
|
|
2823
2799
|
const lease = this.leaseError();
|
|
2824
2800
|
if (lease !== void 0) return Promise.reject(lease);
|
|
2825
|
-
if (this.expired()) return Promise.resolve(false);
|
|
2826
2801
|
return this.timed("has", () => this.engine.has(this.ref, id));
|
|
2827
2802
|
}
|
|
2828
2803
|
/**
|
|
@@ -2846,15 +2821,14 @@ var Segment = class _Segment {
|
|
|
2846
2821
|
count() {
|
|
2847
2822
|
const lease = this.leaseError();
|
|
2848
2823
|
if (lease !== void 0) return Promise.reject(lease);
|
|
2849
|
-
if (this.expired()) return Promise.resolve(0);
|
|
2850
2824
|
return this.timed("count", () => this.engine.count(this.ref));
|
|
2851
2825
|
}
|
|
2852
2826
|
/**
|
|
2853
2827
|
* What the generation this handle reads is, from one resolution: its number, its id count and the metadata it
|
|
2854
2828
|
* was loaded with (absent when it has none). It is what answers {@link Segment.count}, so the three describe one
|
|
2855
2829
|
* generation and cannot straddle a publish. One registry read when cold, none while warm, and none on a pinned
|
|
2856
|
-
* handle, which answers for the generation it pinned. A segment with no generation
|
|
2857
|
-
*
|
|
2830
|
+
* handle, which answers for the generation it pinned. A segment with no generation answers
|
|
2831
|
+
* `{ generation: null, cardinality: 0 }`.
|
|
2858
2832
|
*
|
|
2859
2833
|
* Trust is as for `count()`: the registry row's word, not confirmed against the object until the object is
|
|
2860
2834
|
* opened, when a disagreement makes this process stop using that row's summary.
|
|
@@ -2865,7 +2839,6 @@ var Segment = class _Segment {
|
|
|
2865
2839
|
*/
|
|
2866
2840
|
async stat() {
|
|
2867
2841
|
this.assertLeases([this]);
|
|
2868
|
-
if (this.expired()) return { generation: null, cardinality: 0 };
|
|
2869
2842
|
return this.engine.stat(this.ref);
|
|
2870
2843
|
}
|
|
2871
2844
|
/**
|
|
@@ -2880,13 +2853,13 @@ var Segment = class _Segment {
|
|
|
2880
2853
|
* ```
|
|
2881
2854
|
*
|
|
2882
2855
|
* Each bound is optional and an integer in `0..4294967295`; a bad one throws {@link ValidationError} when the
|
|
2883
|
-
* stream is first read. `after >= through` is an empty range, which reads nothing.
|
|
2884
|
-
* without checking its options, as every read of one does.
|
|
2856
|
+
* stream is first read. `after >= through` is an empty range, which reads nothing.
|
|
2885
2857
|
*/
|
|
2886
2858
|
iterate(options) {
|
|
2887
2859
|
const lease = this.leaseError();
|
|
2888
2860
|
if (lease !== void 0) return failing(lease);
|
|
2889
|
-
|
|
2861
|
+
const bad = optionsError(options, "iterate", CALL_KEYS.range);
|
|
2862
|
+
if (bad !== void 0) return failing(bad);
|
|
2890
2863
|
const range = options == null ? void 0 : rangeOf(options);
|
|
2891
2864
|
const none = range === void 0 || range.after === void 0 && range.through === void 0;
|
|
2892
2865
|
const { engine, ref } = this;
|
|
@@ -2919,7 +2892,7 @@ var Segment = class _Segment {
|
|
|
2919
2892
|
*
|
|
2920
2893
|
* A live handle is refused with {@link UnsupportedError} at the first read, because its counts and its chunks could
|
|
2921
2894
|
* come from two generations and name the wrong id. `n` that is not a positive integer throws
|
|
2922
|
-
* {@link ValidationError} at the first read, as does a bad bound.
|
|
2895
|
+
* {@link ValidationError} at the first read, as does a bad bound.
|
|
2923
2896
|
*
|
|
2924
2897
|
* ```ts
|
|
2925
2898
|
* const audience = await store.segment('audience').pin();
|
|
@@ -2930,7 +2903,8 @@ var Segment = class _Segment {
|
|
|
2930
2903
|
everyNth(n, options) {
|
|
2931
2904
|
const lease = this.leaseError();
|
|
2932
2905
|
if (lease !== void 0) return failing(lease);
|
|
2933
|
-
|
|
2906
|
+
const bad = optionsError(options, "everyNth", CALL_KEYS.range);
|
|
2907
|
+
if (bad !== void 0) return failing(bad);
|
|
2934
2908
|
if (this.pinnedAt === void 0) {
|
|
2935
2909
|
return failing(
|
|
2936
2910
|
new UnsupportedError(
|
|
@@ -2968,30 +2942,8 @@ var Segment = class _Segment {
|
|
|
2968
2942
|
*/
|
|
2969
2943
|
excludesOf(options) {
|
|
2970
2944
|
const exclude = options?.exclude;
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
/**
|
|
2974
|
-
* The refusal for a combine whose **exclusion** has expired, or `undefined` when none has. Unlike an expired
|
|
2975
|
-
* operand, which reads empty, an exclusion is a suppression or opt-out list: left out silently, the result
|
|
2976
|
-
* would include the very ids it was passed to remove. So it is refused with a {@link ValidationError} naming each
|
|
2977
|
-
* expired exclusion, as the `*Into` verbs refuse an expired handle.
|
|
2978
|
-
*
|
|
2979
|
-
* It is judged against the injected clock when the combine is called, before the engine is asked for anything:
|
|
2980
|
-
* no request is made, and an expired exclusion that names a segment that does not exist is refused as expired,
|
|
2981
|
-
* not as an absent operand. It runs ahead of the expired-operand shortcuts, so a combine that would read empty
|
|
2982
|
-
* anyway still refuses a lapsed exclusion. A stream already being read is not re-checked when an exclusion
|
|
2983
|
-
* expires part-way, as an operand expiring part-way does not stop it either.
|
|
2984
|
-
*/
|
|
2985
|
-
expiredExcludes(op, excludes) {
|
|
2986
|
-
let stale;
|
|
2987
|
-
for (const e of excludes) {
|
|
2988
|
-
if (!e.expired()) continue;
|
|
2989
|
-
(stale ??= []).push(e.ref.namespace ? `${e.ref.namespace}/${e.ref.segment}` : e.ref.segment);
|
|
2990
|
-
}
|
|
2991
|
-
if (stale === void 0) return void 0;
|
|
2992
|
-
return new ValidationError2(
|
|
2993
|
-
`${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.`
|
|
2994
|
-
);
|
|
2945
|
+
if (exclude == null) return NO_SEGMENTS;
|
|
2946
|
+
return segmentList(exclude, "`exclude`").length === 0 ? NO_SEGMENTS : exclude;
|
|
2995
2947
|
}
|
|
2996
2948
|
/**
|
|
2997
2949
|
* Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
|
|
@@ -3003,19 +2955,21 @@ var Segment = class _Segment {
|
|
|
3003
2955
|
* override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
|
|
3004
2956
|
*/
|
|
3005
2957
|
intersect(others, options) {
|
|
3006
|
-
|
|
2958
|
+
const bad = optionsError(options, "intersect", CALL_KEYS.combine, ["allowAbsentOperands"]);
|
|
2959
|
+
if (bad !== void 0) return failing(bad);
|
|
2960
|
+
return this.intersectAs(AS_IDS, others, options ?? void 0);
|
|
3007
2961
|
}
|
|
3008
2962
|
intersectAs(out, others, options) {
|
|
3009
|
-
|
|
3010
|
-
|
|
2963
|
+
let exclude;
|
|
2964
|
+
let handles;
|
|
3011
2965
|
try {
|
|
2966
|
+
exclude = this.excludesOf(options);
|
|
2967
|
+
handles = [this, ...segmentList(others, "intersect: `others`"), ...exclude];
|
|
2968
|
+
this.assertSegments(handles);
|
|
3012
2969
|
this.assertLeases(handles);
|
|
3013
2970
|
} catch (err) {
|
|
3014
2971
|
return out.failing(err);
|
|
3015
2972
|
}
|
|
3016
|
-
const refused = this.expiredExcludes("intersect", exclude);
|
|
3017
|
-
if (refused) return out.failing(refused);
|
|
3018
|
-
if (this.expired() || others.some((o) => o.expired())) return out.none;
|
|
3019
2973
|
let engine;
|
|
3020
2974
|
try {
|
|
3021
2975
|
engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
|
|
@@ -3039,16 +2993,21 @@ var Segment = class _Segment {
|
|
|
3039
2993
|
* write is **refused and reported**: `published: false`, `reason: 'empty'`, and `dest` keeps what it had.
|
|
3040
2994
|
* Pass `allowEmpty: true` when emptying the destination is the point.
|
|
3041
2995
|
*
|
|
3042
|
-
* `guard` adds the same plausibility bounds `load()` takes — `minCardinality` and `
|
|
2996
|
+
* `guard` adds the same plausibility bounds `load()` takes — `minCardinality`, `minRetained` and `maxGrowth` — judged
|
|
3043
2997
|
* against what `dest` held before. A refusal is **reported, not thrown**, exactly as on `load()`; branch on
|
|
3044
2998
|
* `published`. A lost race is the one outcome that still throws ({@link WriteConflictError}), because a
|
|
3045
2999
|
* materialisation that silently did not take effect is the one thing a caller cannot detect on its own.
|
|
3046
3000
|
*
|
|
3047
|
-
* A call involving
|
|
3001
|
+
* A call involving a handle whose lease has ended, or anything that is not a segment, is refused before any of this.
|
|
3048
3002
|
*/
|
|
3049
3003
|
async intersectInto(dest, others, options) {
|
|
3050
|
-
|
|
3051
|
-
|
|
3004
|
+
checkOptions(options, "intersectInto", CALL_KEYS.into, ["allowEmpty", "allowAbsentOperands"]);
|
|
3005
|
+
const operands = [
|
|
3006
|
+
...segmentList(others, "intersectInto: `others`"),
|
|
3007
|
+
...this.excludesOf(options)
|
|
3008
|
+
];
|
|
3009
|
+
this.assertIntoHandles(dest, operands);
|
|
3010
|
+
this.combineEngine([this, ...operands]);
|
|
3052
3011
|
return this.timed(
|
|
3053
3012
|
"intersectInto",
|
|
3054
3013
|
() => this.materialize(
|
|
@@ -3070,27 +3029,21 @@ var Segment = class _Segment {
|
|
|
3070
3029
|
* segment once (`unionInto`, or a load) is the cheaper shape.
|
|
3071
3030
|
*/
|
|
3072
3031
|
union(others, options) {
|
|
3073
|
-
|
|
3032
|
+
const bad = optionsError(options, "union", CALL_KEYS.combine, ["allowAbsentOperands"]);
|
|
3033
|
+
if (bad !== void 0) return failing(bad);
|
|
3034
|
+
return this.unionAs(AS_IDS, others, options ?? void 0);
|
|
3074
3035
|
}
|
|
3075
3036
|
unionAs(out, others, options) {
|
|
3076
|
-
|
|
3077
|
-
|
|
3037
|
+
let exclude;
|
|
3038
|
+
let handles;
|
|
3078
3039
|
try {
|
|
3040
|
+
exclude = this.excludesOf(options);
|
|
3041
|
+
handles = [this, ...segmentList(others, "union: `others`"), ...exclude];
|
|
3042
|
+
this.assertSegments(handles);
|
|
3079
3043
|
this.assertLeases(handles);
|
|
3080
3044
|
} catch (err) {
|
|
3081
3045
|
return out.failing(err);
|
|
3082
3046
|
}
|
|
3083
|
-
const refused = this.expiredExcludes("union", exclude);
|
|
3084
|
-
if (refused) return out.failing(refused);
|
|
3085
|
-
const live = others.filter((o) => !o.expired());
|
|
3086
|
-
if (this.expired()) {
|
|
3087
|
-
if (live.length === 0) return out.none;
|
|
3088
|
-
return live[0].unionAs(out, live.slice(1), options);
|
|
3089
|
-
}
|
|
3090
|
-
if (live.length === 0 && others.length > 0) {
|
|
3091
|
-
return exclude.length > 0 ? this.andNotAs(out, [...exclude], options) : this.unionAs(out, [], options);
|
|
3092
|
-
}
|
|
3093
|
-
if (live.length !== others.length) return this.unionAs(out, live, options);
|
|
3094
3047
|
let engine;
|
|
3095
3048
|
try {
|
|
3096
3049
|
engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
|
|
@@ -3103,8 +3056,10 @@ var Segment = class _Segment {
|
|
|
3103
3056
|
}
|
|
3104
3057
|
/** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
3105
3058
|
async unionInto(dest, others, options) {
|
|
3106
|
-
|
|
3107
|
-
|
|
3059
|
+
checkOptions(options, "unionInto", CALL_KEYS.into, ["allowEmpty", "allowAbsentOperands"]);
|
|
3060
|
+
const operands = [...segmentList(others, "unionInto: `others`"), ...this.excludesOf(options)];
|
|
3061
|
+
this.assertIntoHandles(dest, operands);
|
|
3062
|
+
this.combineEngine([this, ...operands]);
|
|
3108
3063
|
return this.timed(
|
|
3109
3064
|
"unionInto",
|
|
3110
3065
|
() => this.materialize(
|
|
@@ -3123,26 +3078,23 @@ var Segment = class _Segment {
|
|
|
3123
3078
|
* suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
|
|
3124
3079
|
* opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
|
|
3125
3080
|
*
|
|
3126
|
-
* An expired handle in `excludes` is refused, not skipped: the stream throws {@link ValidationError} naming it,
|
|
3127
|
-
* before any request is made, because a lapsed suppression list would otherwise let through the ids it was
|
|
3128
|
-
* passed to remove. An expired `this` is empty. `exclude` on `intersect` and `union` follows the same rule.
|
|
3129
|
-
*
|
|
3130
3081
|
* To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
|
|
3131
3082
|
* the suppression folds into the same pass rather than materializing an intermediate segment first.
|
|
3132
3083
|
*/
|
|
3133
3084
|
andNot(excludes, options) {
|
|
3134
|
-
|
|
3085
|
+
const bad = optionsError(options, "andNot", CALL_KEYS.andNot, ["allowAbsentOperands"]);
|
|
3086
|
+
if (bad !== void 0) return failing(bad);
|
|
3087
|
+
return this.andNotAs(AS_IDS, excludes, options ?? void 0);
|
|
3135
3088
|
}
|
|
3136
3089
|
andNotAs(out, excludes, options) {
|
|
3137
|
-
|
|
3090
|
+
let handles;
|
|
3138
3091
|
try {
|
|
3092
|
+
handles = [this, ...segmentList(excludes, "andNot: `excludes`")];
|
|
3093
|
+
this.assertSegments(handles);
|
|
3139
3094
|
this.assertLeases(handles);
|
|
3140
3095
|
} catch (err) {
|
|
3141
3096
|
return out.failing(err);
|
|
3142
3097
|
}
|
|
3143
|
-
const refused = this.expiredExcludes("andNot", excludes);
|
|
3144
|
-
if (refused) return out.failing(refused);
|
|
3145
|
-
if (this.expired()) return out.none;
|
|
3146
3098
|
let engine;
|
|
3147
3099
|
try {
|
|
3148
3100
|
engine = this.combineEngine([this, ...excludes]) ?? this.engine;
|
|
@@ -3156,7 +3108,11 @@ var Segment = class _Segment {
|
|
|
3156
3108
|
}
|
|
3157
3109
|
/** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
3158
3110
|
async andNotInto(dest, excludes, options) {
|
|
3159
|
-
|
|
3111
|
+
checkOptions(options, "andNotInto", CALL_KEYS.andNotInto, [
|
|
3112
|
+
"allowEmpty",
|
|
3113
|
+
"allowAbsentOperands"
|
|
3114
|
+
]);
|
|
3115
|
+
this.assertIntoHandles(dest, segmentList(excludes, "andNotInto: `excludes`"));
|
|
3160
3116
|
this.combineEngine([this, ...excludes]);
|
|
3161
3117
|
return this.timed(
|
|
3162
3118
|
"andNotInto",
|
|
@@ -3174,13 +3130,21 @@ var Segment = class _Segment {
|
|
|
3174
3130
|
* storage. The pointer refresh is priced at the store's own `cache.genTtlMs`, or at none when the store never
|
|
3175
3131
|
* refreshes, unless the workload sets `genTtlMs`. See {@link CostReport} — it always includes a verdict (incl.
|
|
3176
3132
|
* the lose-zone).
|
|
3133
|
+
* @deprecated Moves to `@cloudbitmaps/tools`, a package of its own, with a `size` on `stat()` so a grounded report
|
|
3134
|
+
* needs nothing internal. It is a planning tool, and its price list is as old as the release that ships it.
|
|
3177
3135
|
*/
|
|
3178
3136
|
async costReport(options) {
|
|
3137
|
+
checkOptions(options, "costReport", CALL_KEYS.costReport);
|
|
3138
|
+
const shape = options?.workload;
|
|
3139
|
+
if (shape !== void 0 && shape !== null && (typeof shape !== "object" || Array.isArray(shape))) {
|
|
3140
|
+
throw new ValidationError2("costReport: workload must be an object");
|
|
3141
|
+
}
|
|
3179
3142
|
this.assertLeases([this]);
|
|
3180
3143
|
const canMeasure = this.engine.supportsStorageSize;
|
|
3181
3144
|
const size = canMeasure ? await this.engine.segmentSize(this.ref) : null;
|
|
3182
3145
|
const refreshMs = this.engine.pointerRefreshMs;
|
|
3183
|
-
const
|
|
3146
|
+
const given = options?.workload === void 0 || options.workload === null ? void 0 : picked(options.workload, CALL_KEYS.workload);
|
|
3147
|
+
const workload = refreshMs === void 0 || given?.genTtlMs !== void 0 ? given : { ...given, genTtlMs: refreshMs };
|
|
3184
3148
|
return groundedReport({
|
|
3185
3149
|
storageBytes: size?.sizeBytes ?? 0,
|
|
3186
3150
|
grounded: canMeasure,
|
|
@@ -3211,8 +3175,7 @@ export {
|
|
|
3211
3175
|
LocalFsStorage,
|
|
3212
3176
|
MAX_LEASES_PER_SEGMENT,
|
|
3213
3177
|
MAX_LEASE_MS2 as MAX_LEASE_MS,
|
|
3214
|
-
|
|
3215
|
-
MemoryOperand,
|
|
3178
|
+
MIN_EXPIRES_AT_MS,
|
|
3216
3179
|
MemoryStorage,
|
|
3217
3180
|
NodeAead,
|
|
3218
3181
|
NotFoundError2 as NotFoundError,
|
|
@@ -3229,13 +3192,17 @@ export {
|
|
|
3229
3192
|
destroySegment,
|
|
3230
3193
|
eraseNamespace,
|
|
3231
3194
|
excludingReservedRows2 as excludingReservedRows,
|
|
3195
|
+
isBudgetExceededError,
|
|
3196
|
+
isCapabilityError,
|
|
3232
3197
|
isCloudRoaringError,
|
|
3233
3198
|
isIntegrityError,
|
|
3199
|
+
isKeyUnavailableError,
|
|
3234
3200
|
isLeaseExpiredError,
|
|
3235
3201
|
isLeaseLimitError,
|
|
3236
3202
|
isNotFoundError,
|
|
3237
3203
|
isStaleOperandError,
|
|
3238
3204
|
isTransientError,
|
|
3205
|
+
isUnsupportedError,
|
|
3239
3206
|
isValidationError,
|
|
3240
3207
|
isWriteConflictError,
|
|
3241
3208
|
readRetentionPolicy
|