@cloudbitmaps/roaring 0.18.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 as MIN_EXPIRES_AT_MS2,
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,
@@ -665,6 +667,14 @@ function validateConcurrency(concurrency) {
665
667
  }
666
668
  }
667
669
  function requireScope(options, op) {
670
+ if (options.namespace !== void 0) {
671
+ if (typeof options.namespace !== "string") {
672
+ throw new ValidationError2(
673
+ `${op}: namespace must be a string; got ${kindOf(options.namespace)}`
674
+ );
675
+ }
676
+ validateSegmentRef({ segment: "x", namespace: options.namespace });
677
+ }
668
678
  refuseReservedNamespace(options.namespace);
669
679
  if (options.namespace === void 0 && options.allNamespaces !== true) {
670
680
  throw new ValidationError2(
@@ -672,9 +682,154 @@ function requireScope(options, op) {
672
682
  );
673
683
  }
674
684
  }
685
+ var keysOf = () => (keys) => keys;
686
+ var CALL_KEYS = {
687
+ range: keysOf()(["after", "through"]),
688
+ andNot: keysOf()([
689
+ "after",
690
+ "through",
691
+ "concurrency",
692
+ "budget",
693
+ "allowAbsentOperands"
694
+ ]),
695
+ combine: keysOf()([
696
+ "after",
697
+ "through",
698
+ "concurrency",
699
+ "budget",
700
+ "allowAbsentOperands",
701
+ "exclude"
702
+ ]),
703
+ into: keysOf()([
704
+ "after",
705
+ "through",
706
+ "concurrency",
707
+ "budget",
708
+ "allowAbsentOperands",
709
+ "exclude",
710
+ "audit",
711
+ "allowEmpty",
712
+ "guard",
713
+ "metadata",
714
+ "keep"
715
+ ]),
716
+ andNotInto: keysOf()([
717
+ "after",
718
+ "through",
719
+ "concurrency",
720
+ "budget",
721
+ "allowAbsentOperands",
722
+ "audit",
723
+ "allowEmpty",
724
+ "guard",
725
+ "metadata",
726
+ "keep"
727
+ ]),
728
+ load: keysOf()(["allowEmpty", "guard", "keep", "metadata", "audit"]),
729
+ segments: keysOf()(["namespace"]),
730
+ subjectReport: keysOf()([
731
+ "namespace",
732
+ "allNamespaces",
733
+ "concurrency",
734
+ "budget"
735
+ ]),
736
+ eraseSubject: keysOf()([
737
+ "namespace",
738
+ "allNamespaces",
739
+ "audit",
740
+ "concurrency",
741
+ "budget"
742
+ ]),
743
+ rollback: keysOf()([
744
+ "audit",
745
+ "allowForward"
746
+ ]),
747
+ dropSegment: keysOf()([
748
+ "confirmSegment",
749
+ "dryRun",
750
+ "audit"
751
+ ]),
752
+ retireExpired: keysOf()([
753
+ "namespace",
754
+ "now",
755
+ "limit",
756
+ "dryRun",
757
+ "audit",
758
+ "maxScanSegments",
759
+ "scan",
760
+ "shards",
761
+ "totalShards",
762
+ "lookbackBuckets",
763
+ "purgeTombstones",
764
+ "tombstoneGraceMs"
765
+ ]),
766
+ checkConsistency: keysOf()([
767
+ "namespace",
768
+ "concurrency",
769
+ "summaries"
770
+ ]),
771
+ exportSegments: keysOf()(["format", "codec", "namespace", "ndjsonBatchBytes"]),
772
+ costReport: keysOf()(["pricing", "workload"]),
773
+ workload: keysOf()([
774
+ "readsPerSec",
775
+ "intersectsPerSec",
776
+ "cacheHitRate",
777
+ "chunksPerIntersect",
778
+ "operandsPerIntersect",
779
+ "loadsPerMonth",
780
+ "requestsPerLoad",
781
+ "hotSegments",
782
+ "readerProcesses",
783
+ "genTtlMs",
784
+ "retirementsPerMonth",
785
+ "purgesPerMonth",
786
+ "conditionalDelete"
787
+ ])
788
+ };
789
+ var kindOf = (v) => v === null ? "null" : Array.isArray(v) ? "an array" : `a ${typeof v}`;
790
+ function optionsError(options, op, known, booleans = []) {
791
+ if (options === void 0 || options === null) return void 0;
792
+ if (typeof options !== "object" || Array.isArray(options)) {
793
+ return new ValidationError2(`${op}: options must be an object; got ${kindOf(options)}`);
794
+ }
795
+ const bag = options;
796
+ const unknown = Object.keys(bag).filter((k) => !known.includes(k) && bag[k] !== void 0);
797
+ if (unknown.length > 0) {
798
+ return new ValidationError2(
799
+ `${op}: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; it takes { ${known.join(", ")} }`
800
+ );
801
+ }
802
+ const audit = bag.audit;
803
+ if (audit !== void 0 && audit !== null && typeof audit.onEvent !== "function") {
804
+ return new ValidationError2(
805
+ `${op}: audit must be a sink with an onEvent(event) method, such as a RecordingAuditSink`
806
+ );
807
+ }
808
+ for (const name of booleans) {
809
+ const v = bag[name];
810
+ if (v !== void 0 && typeof v !== "boolean") {
811
+ return new ValidationError2(`${op}: \`${name}\` must be a boolean; got ${kindOf(v)}`);
812
+ }
813
+ }
814
+ return void 0;
815
+ }
816
+ function picked(options, keys) {
817
+ const out = {};
818
+ if (options === void 0 || options === null) return out;
819
+ for (const key of keys) {
820
+ const value = options[key];
821
+ if (value !== void 0) out[key] = value;
822
+ }
823
+ return out;
824
+ }
825
+ function checkOptions(options, op, known, booleans) {
826
+ const err = optionsError(options, op, known, booleans);
827
+ if (err !== void 0) throw err;
828
+ }
675
829
  var viewOf;
676
830
  var MATERIALIZE_MANY_KEYS = [
677
831
  "operands",
832
+ "dryRun",
678
833
  "outputs",
679
834
  "keep",
680
835
  "pin",
@@ -863,8 +1018,29 @@ var CloudRoaring = class _CloudRoaring {
863
1018
  ).join(" ") + " https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/api-reference.md#build-a-store--new-cloudroaringoptions"
864
1019
  );
865
1020
  }
1021
+ /**
1022
+ * A seam the store calls through must have the methods it calls. Without this a `clock` with no `now`, an `rng` with
1023
+ * no `next` or a keystore with no `createDek` builds a store that throws a raw `TypeError` at its first load or read.
1024
+ */
1025
+ static rejectHollowSeams(options) {
1026
+ const seams = [
1027
+ [options.seams?.clock, "seams.clock", ["now", "sleep"]],
1028
+ [options.seams?.rng, "seams.rng", ["next"]],
1029
+ [options.encryption?.keystore, "encryption.keystore", ["createDek", "openDek"]]
1030
+ ];
1031
+ for (const [seam, name, needs] of seams) {
1032
+ if (seam === void 0 || seam === null) continue;
1033
+ const has = (m) => typeof seam[m] === "function";
1034
+ if (typeof seam !== "object" || !needs.every(has)) {
1035
+ throw new ValidationError2(
1036
+ `CloudRoaring's \`${name}\` must be an object with ${needs.map((m) => `\`${m}()\``).join(" and ")}`
1037
+ );
1038
+ }
1039
+ }
1040
+ }
866
1041
  constructor(options) {
867
1042
  _CloudRoaring.rejectUnknownOptions(options);
1043
+ _CloudRoaring.rejectHollowSeams(options);
868
1044
  const clock = options.seams?.clock ?? new SystemClock();
869
1045
  const rng = options.seams?.rng ?? new SystemRng();
870
1046
  const metrics = safeMetrics(options.metrics ?? NOOP_METRICS);
@@ -880,6 +1056,9 @@ var CloudRoaring = class _CloudRoaring {
880
1056
  let storage = resolved.source;
881
1057
  if (options.retry !== false) {
882
1058
  const { onRetry: userOnRetry, ...ov } = options.retry ?? {};
1059
+ if (userOnRetry !== void 0 && typeof userOnRetry !== "function") {
1060
+ throw new ValidationError2("retry.onRetry must be a function");
1061
+ }
883
1062
  const policy = {
884
1063
  maxAttempts: ov.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts,
885
1064
  baseDelayMs: ov.baseDelayMs ?? DEFAULT_RETRY_POLICY.baseDelayMs,
@@ -955,28 +1134,43 @@ var CloudRoaring = class _CloudRoaring {
955
1134
  readRetry: this.retryOptions
956
1135
  };
957
1136
  }
958
- /** Get a handle to a segment. Validates the name (non-empty, well-formed, within the encoded-length cap). */
1137
+ /**
1138
+ * Get a handle to a segment. Validates the name (non-empty, well-formed, within the encoded-length cap). The options
1139
+ * are `{ namespace }` and nothing else: anything else is refused with {@link ValidationError}, by name, since a
1140
+ * misspelt `namespace` would otherwise address the default namespace. An `expiresAt` among them is refused with its own
1141
+ * message: a handle carries no deadline, and one left unread would serve the data past it. A key whose value is
1142
+ * `undefined` is read as absent, so a spread of options keeps working.
1143
+ */
959
1144
  segment(name, options) {
960
- const ref = { segment: name, namespace: options?.namespace };
961
- validateSegmentRef(ref);
962
- refuseReservedNamespace(ref.namespace);
963
- const expiresAt = options?.expiresAt;
964
- if (expiresAt !== void 0) {
965
- if (!Number.isFinite(expiresAt) || !Number.isInteger(expiresAt) || expiresAt < MIN_EXPIRES_AT_MS) {
1145
+ if (options !== void 0 && options !== null) {
1146
+ if (typeof options !== "object" || Array.isArray(options)) {
1147
+ throw new ValidationError2("segment: options must be an object such as { namespace }");
1148
+ }
1149
+ if (options.expiresAt !== void 0) {
1150
+ throw new ValidationError2(
1151
+ "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()`"
1152
+ );
1153
+ }
1154
+ const unknown = Object.keys(options).filter(
1155
+ (k) => k !== "namespace" && options[k] !== void 0
1156
+ );
1157
+ if (unknown.length > 0) {
966
1158
  throw new ValidationError2(
967
- `segment: expiresAt must be an integer epoch-MILLISECONDS >= ${MIN_EXPIRES_AT_MS}; got ${expiresAt} (a value in seconds lands in 1970 and would make the segment read as permanently empty)`
1159
+ `segment: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; a handle takes { namespace } only`
968
1160
  );
969
1161
  }
970
1162
  }
1163
+ const ref = { segment: name, namespace: options?.namespace };
1164
+ validateSegmentRef(ref);
1165
+ refuseReservedNamespace(ref.namespace);
971
1166
  return makeSegment({
972
1167
  engine: this.engine,
973
1168
  ref,
974
1169
  clock: this.clock,
975
1170
  metrics: this.metrics,
976
1171
  materialize: (dest, ids, op, options2) => this.materialize(dest, ids, op, options2),
977
- pinned: (r, e, l, at) => this.pinSegment(r, e, l, at),
978
- combineEngine: (handles) => this.engineForCombine(handles),
979
- expiresAt
1172
+ pinned: (r, l, at) => this.pinSegment(r, l, at),
1173
+ combineEngine: (handles) => this.engineForCombine(handles)
980
1174
  });
981
1175
  }
982
1176
  /**
@@ -984,7 +1178,7 @@ var CloudRoaring = class _CloudRoaring {
984
1178
  * materialisation is a load, and publishes the same way (hard invariant 1): every load that finds a row fences its
985
1179
  * publish on the row's token; a guarded load (the default, since the empty refusal needs the size of the current generation)
986
1180
  * also fences on the pointer it judged (`expectFrom`), and one that found no row fences on that absence instead.
987
- * Only an unguarded load (`allowEmpty: true` and no `guard.minRetained`) onto a segment with no row publishes bare
1181
+ * Only an unguarded load (`allowEmpty: true`, with neither `guard.minRetained` nor `guard.maxGrowth`) onto a segment with no row publishes bare
988
1182
  * forward-only. The destination's previous generation stays readable until the publish lands (readers re-resolve
989
1183
  * within `cache.genTtlMs`).
990
1184
  *
@@ -1060,131 +1254,7 @@ var CloudRoaring = class _CloudRoaring {
1060
1254
  collected: result.collected
1061
1255
  };
1062
1256
  }
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
- */
1257
+ /** The one body of the forms above. */
1188
1258
  async materializeMany(options) {
1189
1259
  if (typeof options !== "object" || options === null) {
1190
1260
  throw new ValidationError2(
@@ -1200,12 +1270,17 @@ var CloudRoaring = class _CloudRoaring {
1200
1270
  `materializeMany: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}`
1201
1271
  );
1202
1272
  }
1273
+ const dryRunIn = options.dryRun;
1274
+ if (dryRunIn !== void 0 && typeof dryRunIn !== "boolean") {
1275
+ throw new ValidationError2("materializeMany: dryRun must be a boolean");
1276
+ }
1277
+ const dryRun = dryRunIn === true;
1203
1278
  const operandsIn = options.operands;
1204
1279
  const outputsIn = options.outputs;
1205
1280
  const keep = options.keep;
1206
1281
  const after = options.after;
1207
1282
  const through = options.through;
1208
- const pinOption = options.pin ?? true;
1283
+ const pinOption = options.pin === void 0 ? true : options.pin;
1209
1284
  const feedIn = options.feed;
1210
1285
  const mayBeEmptyIn = options.mayBeEmpty;
1211
1286
  if (feedIn !== void 0 && options.maxBufferedBytes === void 0) {
@@ -1231,50 +1306,26 @@ var CloudRoaring = class _CloudRoaring {
1231
1306
  }
1232
1307
  const names = Object.keys(operandsIn);
1233
1308
  const handles = /* @__PURE__ */ new Map();
1234
- const heldHandles = /* @__PURE__ */ new Map();
1235
1309
  for (const name of names) {
1236
1310
  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
1311
  if (!(handle instanceof Segment)) {
1258
1312
  throw new ValidationError2(
1259
- `materializeMany: operands["${name}"] must be a segment from store.segment() or a memory operand from store.memory()`
1313
+ `materializeMany: operands["${name}"] must be a segment from store.segment()`
1260
1314
  );
1261
1315
  }
1262
1316
  handles.set(name, handle);
1263
1317
  }
1264
- const heldMayBeEmpty = /* @__PURE__ */ new Set();
1265
- let fedMayBeEmpty = mayBeEmptyIn;
1266
- if (mayBeEmptyIn !== void 0) {
1267
- if (Array.isArray(mayBeEmptyIn)) {
1268
- const rest = [];
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;
1318
+ if (mayBeEmptyIn !== void 0 && feedIn === void 0) {
1319
+ if (!Array.isArray(mayBeEmptyIn)) {
1320
+ throw new ValidationError2(
1321
+ "materializeMany: mayBeEmpty must be an array of fed operand names"
1322
+ );
1274
1323
  }
1275
- if (feedIn === void 0 && (!Array.isArray(fedMayBeEmpty) || fedMayBeEmpty.length > 0)) {
1324
+ if (mayBeEmptyIn.length > 0) {
1325
+ const first = mayBeEmptyIn[0];
1326
+ 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
1327
  throw new ValidationError2(
1277
- "materializeMany: mayBeEmpty names fed operands, and the call has no feed"
1328
+ `materializeMany: mayBeEmpty names ${what}; it names fed operands, and this call has no feed`
1278
1329
  );
1279
1330
  }
1280
1331
  }
@@ -1314,26 +1365,13 @@ var CloudRoaring = class _CloudRoaring {
1314
1365
  const storeCeiling = this.budget === null || this.budget.maxRequests !== DEFAULT_BUDGET.maxRequests ? this.budget : void 0;
1315
1366
  const budget = budgetOption === void 0 ? storeCeiling : resolvePerOpBudget(budgetOption, this.budget);
1316
1367
  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
1368
  const handle = handles.get(name);
1330
1369
  const seg = viewOf(handle);
1331
1370
  const pinnedAt = pins2.get(handle.key());
1332
- const guarded = seg.expiresAt !== void 0 || handle.lease !== void 0;
1333
1371
  return {
1334
1372
  name,
1335
1373
  ref: seg.ref,
1336
- ...guarded ? { check: () => this.assertReadable("materializeMany", handle) } : {},
1374
+ ...handle.lease === void 0 ? {} : { check: () => this.assertLease(handle) },
1337
1375
  ...pinnedAt === void 0 ? {} : {
1338
1376
  pinnedGeneration: pinnedAt.generation,
1339
1377
  pinnedVersion: pinnedAt.version,
@@ -1344,18 +1382,18 @@ var CloudRoaring = class _CloudRoaring {
1344
1382
  });
1345
1383
  const request = (pins2) => ({
1346
1384
  operands: operandSpecs(pins2),
1347
- outputs: outputs.map(({ spec, dest }, i) => ({
1385
+ outputs: outputs.map(({ spec, dest }) => ({
1348
1386
  expr: spec.expr,
1349
1387
  ...spec.exclude === void 0 ? {} : { exclude: spec.exclude },
1350
1388
  ...spec.allowEmpty === void 0 ? {} : { allowEmpty: spec.allowEmpty },
1351
1389
  ...spec.guard === void 0 ? {} : { guard: spec.guard },
1352
1390
  ...spec.metadata === void 0 ? {} : { metadata: spec.metadata },
1353
1391
  ...spec.keep === void 0 ? {} : { keep: spec.keep },
1354
- beforePublish: () => this.assertWritable(`outputs[${i}]`, dest),
1392
+ beforePublish: () => this.assertLease(dest),
1355
1393
  // 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) => ({ get: 0, put: result.published ? 2 : 1 }),
1358
- publish: (chunks, write) => this.materialize(viewOf(dest).ref, new CombineChunks(chunks), "materializeMany", {
1394
+ // wrote its object and deleted it. A dry run's judgement wrote nothing, and read the row and maybe the object.
1395
+ requestsOf: (result) => "dryRun" in result ? { get: result.reads, put: 0 } : { get: 0, put: result.published ? 2 : 1 },
1396
+ publish: (chunks, write) => dryRun ? this.judgeOutput(viewOf(dest).ref, chunks, write) : this.materialize(viewOf(dest).ref, new CombineChunks(chunks), "materializeMany", {
1359
1397
  ...write,
1360
1398
  ...spec.audit === void 0 ? {} : { audit: spec.audit }
1361
1399
  })
@@ -1368,11 +1406,12 @@ var CloudRoaring = class _CloudRoaring {
1368
1406
  concurrency,
1369
1407
  ...budget === void 0 ? {} : { budget },
1370
1408
  ...allowAbsentOperands === void 0 ? {} : { allowAbsentOperands },
1371
- ...feedIn === void 0 ? {} : { feed: this.feedOf(feedIn, fedMayBeEmpty, epochAt) }
1409
+ ...feedIn === void 0 ? {} : { feed: this.feedOf(feedIn, mayBeEmptyIn, epochAt) },
1410
+ ...dryRun ? { dryRun: true } : {}
1372
1411
  });
1373
1412
  const compiled = compileCombineMany(request(/* @__PURE__ */ new Map()));
1374
- for (const { dest } of outputs) this.assertWritable("materializeMany", dest);
1375
- for (const handle of handles.values()) this.assertReadable("materializeMany", handle);
1413
+ for (const { dest } of outputs) this.assertLease(dest);
1414
+ for (const handle of handles.values()) this.assertLease(handle);
1376
1415
  this.lifecycleDeps("materializeMany");
1377
1416
  const crbm = this.crbmSource;
1378
1417
  if (crbm === void 0) {
@@ -1409,25 +1448,64 @@ var CloudRoaring = class _CloudRoaring {
1409
1448
  else if (pinOption) toPin.push([key, handle]);
1410
1449
  else pins.set(key, void 0);
1411
1450
  }
1412
- const taken = await mapWithConcurrency(
1413
- toPin,
1414
- PIN_PARALLELISM,
1415
- async ([key, handle]) => [key, (await handle.pin()).pinnedAt]
1416
- );
1417
- for (const [key, at] of taken) pins.set(key, at);
1418
- const live = new Map([...pins].filter((e) => e[1] !== void 0));
1419
- const pinnedSource = new PinnedStorageChunkSource(crbm, live);
1420
- const source = this.retryOptions === void 0 ? pinnedSource : new RetryingStorageChunkSource(pinnedSource, this.retryOptions);
1421
- const final = rebindCombineMany(compiled, operandSpecs(pins));
1422
- const run = await runCombineMany(final, { source, codec: roaringCodec, clock: this.clock });
1423
- return {
1424
- outputs: run.outputs.map(
1425
- (o) => o.ok ? o.value : {
1451
+ const metricsOn = this.metrics !== NOOP_METRICS;
1452
+ const work = async () => {
1453
+ const taken = await mapWithConcurrency(
1454
+ toPin,
1455
+ PIN_PARALLELISM,
1456
+ async ([key, handle]) => [key, (await handle.pin()).pinnedAt]
1457
+ );
1458
+ for (const [key, at] of taken) pins.set(key, at);
1459
+ const live = new Map([...pins].filter((e) => e[1] !== void 0));
1460
+ const pinnedSource = new PinnedStorageChunkSource(crbm, live);
1461
+ const source = this.retryOptions === void 0 ? pinnedSource : new RetryingStorageChunkSource(pinnedSource, this.retryOptions);
1462
+ const final = rebindCombineMany(compiled, operandSpecs(pins));
1463
+ return runCombineMany(final, {
1464
+ source,
1465
+ codec: roaringCodec,
1466
+ clock: this.clock,
1467
+ ...metricsOn ? { metrics: this.metrics } : {}
1468
+ });
1469
+ };
1470
+ const run = metricsOn ? await timeOp(this.metrics, this.clock, "materializeMany", work) : await work();
1471
+ const results = run.outputs.map((o) => {
1472
+ if (!o.ok) {
1473
+ return {
1426
1474
  published: false,
1427
1475
  error: o.error instanceof Error ? o.error : new Error(String(o.error))
1428
- }
1429
- ),
1430
- stats: run.stats
1476
+ };
1477
+ }
1478
+ if (!("dryRun" in o.value)) return o.value;
1479
+ const v = o.value;
1480
+ const judged = {
1481
+ dryRun: true,
1482
+ published: false,
1483
+ cardinality: v.cardinality,
1484
+ cardinalityBefore: v.cardinalityBefore,
1485
+ ...v.wouldRefuse === void 0 ? {} : { wouldRefuse: v.wouldRefuse }
1486
+ };
1487
+ return judged;
1488
+ });
1489
+ return dryRun ? { outputs: results, stats: run.stats } : { outputs: results, stats: run.stats };
1490
+ }
1491
+ /**
1492
+ * A dry run's publish of one output: count what the finished chunks hold, and judge that size against `dest` as a
1493
+ * load would, writing nothing.
1494
+ */
1495
+ async judgeOutput(dest, chunks, write) {
1496
+ let cardinality = 0;
1497
+ for await (const chunk of chunks) cardinality += chunk.bitmap.size;
1498
+ const judged = await judgeLoad(dest, cardinality, this.lifecycleDeps("materializeMany"), {
1499
+ ...write.allowEmpty === void 0 ? {} : { allowEmpty: write.allowEmpty },
1500
+ ...write.guard === void 0 ? {} : { guard: write.guard }
1501
+ });
1502
+ return {
1503
+ dryRun: true,
1504
+ published: false,
1505
+ cardinality,
1506
+ cardinalityBefore: judged.cardinalityBefore,
1507
+ ...judged.wouldRefuse === void 0 ? {} : { wouldRefuse: judged.wouldRefuse },
1508
+ reads: judged.reads
1431
1509
  };
1432
1510
  }
1433
1511
  epochNow() {
@@ -1480,20 +1558,10 @@ var CloudRoaring = class _CloudRoaring {
1480
1558
  const row = await this.withRetries(() => registry.get(ref));
1481
1559
  return row === null ? null : { generation: row.currentGen, token: row.token };
1482
1560
  }
1483
- /** Throw what a read of `handle` must throw now: its lease error, or a refusal for a deadline that has passed. */
1484
- assertReadable(op, handle) {
1485
- const view = viewOf(handle);
1486
- const lease = view.leaseError();
1561
+ /** Throw what a read of, or a publish to, `handle` must throw now: its lease error, if it has one. */
1562
+ assertLease(handle) {
1563
+ const lease = viewOf(handle).leaseError();
1487
1564
  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
1565
  }
1498
1566
  /**
1499
1567
  * **Subject access (GDPR Art. 15 / CCPA right-to-know): which segments is this id a member of?**
@@ -1511,6 +1579,8 @@ var CloudRoaring = class _CloudRoaring {
1511
1579
  * generation 0 while this store held its old generation 0 is told apart too.
1512
1580
  */
1513
1581
  async subjectReport(id, options = {}) {
1582
+ checkOptions(options, "subjectReport", CALL_KEYS.subjectReport, ["allNamespaces"]);
1583
+ options ??= {};
1514
1584
  const registry = this.requireRegistry("subjectReport");
1515
1585
  requireScope(options, "subjectReport");
1516
1586
  validateConcurrency(options.concurrency);
@@ -1574,8 +1644,9 @@ var CloudRoaring = class _CloudRoaring {
1574
1644
  * every generation below its new pointer — the one this rewrite is streaming, and the object this rewrite
1575
1645
  * just wrote — so the loser can find its own inputs deleted mid-flight. That surfaces as an outcome rather than
1576
1646
  * 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, as a fresh
1578
- * call would leave it out.
1647
+ * that a concurrent `dropSegment` tombstoned or a retention sweep purged is left out of the ledger. A tombstoned
1648
+ * segment is still searched: a cleartext object left under it that holds the id is deleted, with everything under
1649
+ * the tombstone, and the entry reads `erased: true`.
1579
1650
  *
1580
1651
  * **Read `note` on any `erased: false` entry — the two reasons mean different things.** `'superseded'` means
1581
1652
  * another writer (a load, another erasure, or a rollback) moved the pointer mid-call, so **this call** did not
@@ -1604,6 +1675,8 @@ var CloudRoaring = class _CloudRoaring {
1604
1675
  */
1605
1676
  async eraseSubject(id, options = {}) {
1606
1677
  this.erasureEpoch++;
1678
+ checkOptions(options, "eraseSubject", CALL_KEYS.eraseSubject, ["allNamespaces"]);
1679
+ options ??= {};
1607
1680
  this.erasuresRunning++;
1608
1681
  try {
1609
1682
  return await this.eraseSubjectRun(id, options);
@@ -1632,7 +1705,6 @@ var CloudRoaring = class _CloudRoaring {
1632
1705
  recs,
1633
1706
  options.concurrency ?? DEFAULT_ADMIN_CONCURRENCY,
1634
1707
  async (rec) => {
1635
- if (rec.status === "destroyed") return null;
1636
1708
  const ref = { segment: rec.segment, namespace: rec.namespace };
1637
1709
  try {
1638
1710
  const result = await eraseIdFromSegment(
@@ -1696,10 +1768,9 @@ var CloudRoaring = class _CloudRoaring {
1696
1768
  * exists. It is `false` for a segment whose row was minted ahead of its first load (by `setRetention`) and
1697
1769
  * for a `destroyed` tombstone, because a read answers empty in both cases.
1698
1770
  *
1699
- * Two states answer `true` where a read still gives you nothing: a torn restore (a live pointer whose object
1771
+ * One state answers `true` where a read still gives you nothing: a torn restore (a live pointer whose object
1700
1772
  * 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 — and
1702
- * a handle carrying an expired `expiresAt` reads empty by a rule that lives on the handle, not the row.
1773
+ * its row records — `checkConsistency` is the call for that.
1703
1774
  *
1704
1775
  * Not a lock: the answer can change the moment it returns. If it has to hold, use the fence built for that —
1705
1776
  * `load`'s `guard`, or `expectFrom`/`expectToken` on a publish. Needs a storage backend.
@@ -1742,6 +1813,8 @@ var CloudRoaring = class _CloudRoaring {
1742
1813
  * ```
1743
1814
  */
1744
1815
  segments(options = {}) {
1816
+ checkOptions(options, "segments", CALL_KEYS.segments);
1817
+ options ??= {};
1745
1818
  if (options.namespace !== void 0) {
1746
1819
  validateSegmentRef({ segment: "x", namespace: options.namespace });
1747
1820
  refuseReservedNamespace(options.namespace);
@@ -1787,6 +1860,8 @@ var CloudRoaring = class _CloudRoaring {
1787
1860
  */
1788
1861
  async rollback(ref, toGeneration, options = {}) {
1789
1862
  validateSegmentRef(ref);
1863
+ checkOptions(options, "rollback", CALL_KEYS.rollback, ["allowForward"]);
1864
+ options ??= {};
1790
1865
  const deps = this.lifecycleDeps("rollback");
1791
1866
  try {
1792
1867
  return await rollbackSegment(ref, toGeneration, deps, options);
@@ -1879,6 +1954,8 @@ var CloudRoaring = class _CloudRoaring {
1879
1954
  */
1880
1955
  async load(ref, input, options = {}) {
1881
1956
  validateSegmentRef(ref);
1957
+ checkOptions(options, "load", CALL_KEYS.load, ["allowEmpty"]);
1958
+ options ??= {};
1882
1959
  const deps = this.lifecycleDeps("load");
1883
1960
  try {
1884
1961
  return await loadSegment(ref, bitmapAsLoadInput(input), deps, options);
@@ -1933,6 +2010,10 @@ var CloudRoaring = class _CloudRoaring {
1933
2010
  */
1934
2011
  async dropSegment(ref, options) {
1935
2012
  validateSegmentRef(ref);
2013
+ if (options === void 0 || options === null) {
2014
+ throw new ValidationError2("dropSegment: options { confirmSegment } are required");
2015
+ }
2016
+ checkOptions(options, "dropSegment", CALL_KEYS.dropSegment, ["dryRun"]);
1936
2017
  const deps = this.lifecycleDeps("dropSegment");
1937
2018
  try {
1938
2019
  return await dropSegment(
@@ -2060,69 +2141,20 @@ var CloudRoaring = class _CloudRoaring {
2060
2141
  * because retiring a segment deletes its storage objects. `now` defaults to the store's clock.
2061
2142
  */
2062
2143
  async retireExpired(options = {}) {
2144
+ checkOptions(options, "retireExpired", CALL_KEYS.retireExpired, ["dryRun", "purgeTombstones"]);
2145
+ const opts = picked(options, CALL_KEYS.retireExpired);
2063
2146
  const deps = this.lifecycleDeps("retireExpired");
2064
2147
  const result = await retireExpired(
2065
2148
  { registry: deps.registry, storage: deps.storage, clock: this.clock, rng: this.rng },
2066
- { ...options, now: options.now ?? this.clock.now() }
2149
+ { ...opts, now: opts.now ?? this.clock.now() }
2067
2150
  );
2068
- if (options.dryRun !== true) {
2151
+ if (opts.dryRun !== true) {
2069
2152
  for (const entry of result.entries) {
2070
2153
  this.engine.invalidate({ segment: entry.segment, namespace: entry.namespace });
2071
2154
  }
2072
2155
  }
2073
2156
  return result;
2074
2157
  }
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
2158
  /**
2127
2159
  * **Forget everything this store has cached about a segment.** Use it after destroying or retiring a segment
2128
2160
  * through a path this store did not perform itself.
@@ -2169,7 +2201,7 @@ var CloudRoaring = class _CloudRoaring {
2169
2201
  * pinned view reports the version captured at pin time, marked as a pin's, so its decoded chunks are never
2170
2202
  * those of a live read that fetched across a publish (see {@link PinnedStorageChunkSource.currentVersion}).
2171
2203
  */
2172
- async pinSegment(ref, expiresAt, leaseUntil, named) {
2204
+ async pinSegment(ref, leaseUntil, named) {
2173
2205
  const crbm = this.crbmSource;
2174
2206
  if (crbm === void 0) {
2175
2207
  throw new UnsupportedError(
@@ -2229,9 +2261,8 @@ var CloudRoaring = class _CloudRoaring {
2229
2261
  clock: this.clock,
2230
2262
  metrics: this.metrics,
2231
2263
  materialize: (dest, ids, op, options) => this.materialize(dest, ids, op, options),
2232
- pinned: (r, e, l, at2) => this.pinSegment(r, e, l, at2),
2264
+ pinned: (r, l, at2) => this.pinSegment(r, l, at2),
2233
2265
  combineEngine: (handles) => this.engineForCombine(handles),
2234
- expiresAt,
2235
2266
  pinnedAt,
2236
2267
  lease: hold
2237
2268
  });
@@ -2337,6 +2368,8 @@ var CloudRoaring = class _CloudRoaring {
2337
2368
  * `storage` and `registry` with a higher `maxScanSegments`.
2338
2369
  */
2339
2370
  async checkConsistency(options = {}) {
2371
+ checkOptions(options, "checkConsistency", CALL_KEYS.checkConsistency, ["summaries"]);
2372
+ options ??= {};
2340
2373
  const deps = this.lifecycleDeps("checkConsistency");
2341
2374
  return runConsistencyCheck(
2342
2375
  { storage: deps.storage, registry: deps.registry, keystore: deps.keystore },
@@ -2356,15 +2389,16 @@ var CloudRoaring = class _CloudRoaring {
2356
2389
  * CLI wraps this with a filesystem sink.
2357
2390
  */
2358
2391
  async exportSegments(sink, options = {}) {
2392
+ checkOptions(options, "exportSegments", CALL_KEYS.exportSegments);
2393
+ const opts = picked(options, CALL_KEYS.exportSegments);
2359
2394
  const registry = this.requireRegistry("exportSegments");
2360
- return runExport(this, registry, sink, {
2361
- ...options,
2362
- codec: options.codec ?? roaringCodec
2363
- });
2395
+ return runExport(this, registry, sink, { ...opts, codec: opts.codec ?? roaringCodec });
2364
2396
  }
2365
2397
  /**
2366
2398
  * Planning cost estimate — pure, no instance/data needed: sizing, sales, what-if. For a real, grounded report
2367
2399
  * from live segment sizes, use `store.segment(name).costReport()`. See {@link CostReport}.
2400
+ * @deprecated Moves to `@cloudbitmaps/tools`, a package of its own, with a `size` on `stat()` so a grounded report
2401
+ * needs nothing internal. It is a planning tool, and its price list is as old as the release that ships it.
2368
2402
  */
2369
2403
  static estimateCost(input) {
2370
2404
  return estimateCost(input);
@@ -2394,24 +2428,17 @@ var readOptions = (options) => ({
2394
2428
  allowAbsentOperands: options.allowAbsentOperands
2395
2429
  });
2396
2430
  var NO_SEGMENTS = [];
2397
- var EMPTY_IDS = {
2398
- async *[Symbol.asyncIterator]() {
2399
- },
2400
- async *batches() {
2401
- }
2402
- };
2431
+ function segmentList(list, what) {
2432
+ if (!Array.isArray(list)) throw new ValidationError2(`${what} must be an array of segments`);
2433
+ return list;
2434
+ }
2403
2435
  var AS_IDS = {
2404
- none: EMPTY_IDS,
2405
2436
  failing,
2406
2437
  guard: guardIds,
2407
2438
  intersect: (engine, refs, opts) => withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts)),
2408
2439
  union: (engine, refs, opts) => withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts)),
2409
2440
  andNot: (engine, base, refs, opts) => withBatches(engine.andNot(base, refs, opts), () => engine.andNotBatches(base, refs, opts))
2410
2441
  };
2411
- var NO_CHUNKS = {
2412
- async *[Symbol.asyncIterator]() {
2413
- }
2414
- };
2415
2442
  var CombineChunks = class {
2416
2443
  constructor(chunks) {
2417
2444
  this.chunks = chunks;
@@ -2419,7 +2446,6 @@ var CombineChunks = class {
2419
2446
  chunks;
2420
2447
  };
2421
2448
  var AS_CHUNKS = {
2422
- none: NO_CHUNKS,
2423
2449
  guard: guardChunks,
2424
2450
  failing: (err) => ({
2425
2451
  [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(err) })
@@ -2429,39 +2455,14 @@ var AS_CHUNKS = {
2429
2455
  andNot: (engine, base, refs, opts) => engine.andNotChunks(base, refs, opts)
2430
2456
  };
2431
2457
  var KEEP_EVERY_GENERATION = Number.MAX_SAFE_INTEGER;
2432
- var heldOf;
2433
- var makeMemoryOperand;
2434
- var mintingMemory = false;
2435
- var MemoryOperand = class _MemoryOperand {
2436
- view;
2437
- static {
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
- };
2458
+ async function timeOp(metrics, clock, name, fn) {
2459
+ const startedAt = clock.now();
2460
+ try {
2461
+ return await fn();
2462
+ } finally {
2463
+ metrics.onEvent({ kind: "op", name, ms: Math.max(0, clock.now() - startedAt) });
2447
2464
  }
2448
- /** Not constructible: a handle comes from {@link CloudRoaring.memory}. */
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
- };
2465
+ }
2465
2466
  var makeSegment;
2466
2467
  var minting = false;
2467
2468
  var Segment = class _Segment {
@@ -2473,8 +2474,6 @@ var Segment = class _Segment {
2473
2474
  materialize;
2474
2475
  pinned;
2475
2476
  combineEngine;
2476
- /** Absolute epoch-ms deadline from {@link SegmentOptions.expiresAt}; `undefined` ⇒ this handle never expires. */
2477
- expiresAt;
2478
2477
  /**
2479
2478
  * The generation this handle is held at, when it came from {@link Segment.pin}. Read by the store so a
2480
2479
  * pinned handle passed as an **operand** is still read at its pin rather than live.
@@ -2499,9 +2498,7 @@ var Segment = class _Segment {
2499
2498
  viewOf = (segment) => ({
2500
2499
  ref: segment.ref,
2501
2500
  pinnedAt: segment.pinnedAt,
2502
- expiresAt: segment.expiresAt,
2503
- leaseError: () => segment.leaseError(),
2504
- expired: () => segment.expired()
2501
+ leaseError: () => segment.leaseError()
2505
2502
  });
2506
2503
  }
2507
2504
  /**
@@ -2521,7 +2518,6 @@ var Segment = class _Segment {
2521
2518
  this.materialize = parts.materialize;
2522
2519
  this.pinned = parts.pinned;
2523
2520
  this.combineEngine = parts.combineEngine;
2524
- this.expiresAt = parts.expiresAt;
2525
2521
  this.pinnedAt = parts.pinnedAt;
2526
2522
  this.leaseHold = parts.lease;
2527
2523
  if (parts.lease !== void 0) {
@@ -2611,12 +2607,12 @@ var Segment = class _Segment {
2611
2607
  async pin(options) {
2612
2608
  this.assertLeases([this]);
2613
2609
  const leaseUntil = this.leaseUntilOf("pin", options);
2614
- return this.pinned(this.ref, this.expiresAt, leaseUntil);
2610
+ return this.pinned(this.ref, leaseUntil);
2615
2611
  }
2616
2612
  /**
2617
2613
  * 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, and one past the handle's own
2619
- * `expiresAt`, before any request is made.
2614
+ * know, a value that is not epoch-milliseconds in the future, and one past the longest lease, before any request is
2615
+ * made.
2620
2616
  */
2621
2617
  leaseUntilOf(op, options) {
2622
2618
  if (options === void 0 || options === null) return void 0;
@@ -2647,11 +2643,6 @@ var Segment = class _Segment {
2647
2643
  `${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
2644
  );
2649
2645
  }
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
2646
  return until;
2656
2647
  }
2657
2648
  /**
@@ -2676,7 +2667,7 @@ var Segment = class _Segment {
2676
2667
  }
2677
2668
  /**
2678
2669
  * 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. It is not {@link Segment.expired}: that rule reads empty, and a lease must never.
2670
+ * read on a handle with no lease. A read past a lease throws; it never reads empty.
2680
2671
  */
2681
2672
  leaseError() {
2682
2673
  const hold = this.leaseHold;
@@ -2698,9 +2689,21 @@ var Segment = class _Segment {
2698
2689
  }
2699
2690
  return void 0;
2700
2691
  }
2692
+ /**
2693
+ * Refuse with a {@link ValidationError} anything among `handles` that is not a segment, a value a JavaScript caller
2694
+ * passed. It runs first in every combine, before the lease check reads the handles.
2695
+ */
2696
+ assertSegments(handles) {
2697
+ for (const h of handles) {
2698
+ if (h instanceof _Segment || typeof h === "object" && h !== null && typeof h.leaseError === "function") {
2699
+ continue;
2700
+ }
2701
+ throw new ValidationError2("an operand must be a segment from store.segment()");
2702
+ }
2703
+ }
2701
2704
  /**
2702
2705
  * Throw the first lease error among `handles`: this handle and every operand and exclude of a call. It runs before
2703
- * any `expiresAt` rule and before the engine, so no operand, exclude or cached reader can answer past a lease.
2706
+ * the engine, so no operand, exclude or cached reader can answer past a lease.
2704
2707
  */
2705
2708
  assertLeases(handles) {
2706
2709
  for (const h of handles) {
@@ -2745,7 +2748,7 @@ var Segment = class _Segment {
2745
2748
  );
2746
2749
  }
2747
2750
  const leaseUntil = this.leaseUntilOf("pinAt", options);
2748
- return this.pinned(this.ref, this.expiresAt, leaseUntil, { generation, fingerprint });
2751
+ return this.pinned(this.ref, leaseUntil, { generation, fingerprint });
2749
2752
  }
2750
2753
  /**
2751
2754
  * An opaque string that names this handle's segment, namespace included: two handles of one segment have the
@@ -2757,49 +2760,17 @@ var Segment = class _Segment {
2757
2760
  return segmentKey(this.ref);
2758
2761
  }
2759
2762
  /**
2760
- * Has this handle's deadline passed? **The lazy half of expiry** — the whole check is one comparison against
2761
- * the injected clock, so it costs nothing on a handle with no deadline and no I/O on one that has expired.
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.)
2763
+ * Refuse an `*Into` whose handles cannot be read, before any object is written: anything that is not a segment
2764
+ * ({@link ValidationError}), and a handle (this one, `dest` or an operand) whose lease has ended or was released
2765
+ * ({@link LeaseExpiredError}). Called by the three `*Into` verbs only.
2786
2766
  *
2787
2767
  * The verbs are `async` so this surfaces as a **rejected promise**, like every other validation in the facade —
2788
2768
  * a synchronous throw out of a promise-returning method escapes a caller who attached `.catch()` instead of
2789
2769
  * awaiting.
2790
2770
  */
2791
- refuseIfExpired(op, dest, operands) {
2771
+ assertIntoHandles(dest, operands) {
2772
+ this.assertSegments([dest, ...operands]);
2792
2773
  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
2774
  }
2804
2775
  /** `result` as is, or, when a handle of the call holds a lease, checking that lease each time it reads a chunk. */
2805
2776
  guarded(out, handles, result) {
@@ -2822,7 +2793,6 @@ var Segment = class _Segment {
2822
2793
  has(id) {
2823
2794
  const lease = this.leaseError();
2824
2795
  if (lease !== void 0) return Promise.reject(lease);
2825
- if (this.expired()) return Promise.resolve(false);
2826
2796
  return this.timed("has", () => this.engine.has(this.ref, id));
2827
2797
  }
2828
2798
  /**
@@ -2846,15 +2816,14 @@ var Segment = class _Segment {
2846
2816
  count() {
2847
2817
  const lease = this.leaseError();
2848
2818
  if (lease !== void 0) return Promise.reject(lease);
2849
- if (this.expired()) return Promise.resolve(0);
2850
2819
  return this.timed("count", () => this.engine.count(this.ref));
2851
2820
  }
2852
2821
  /**
2853
2822
  * What the generation this handle reads is, from one resolution: its number, its id count and the metadata it
2854
2823
  * was loaded with (absent when it has none). It is what answers {@link Segment.count}, so the three describe one
2855
2824
  * 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, and an expired handle,
2857
- * answer `{ generation: null, cardinality: 0 }`.
2825
+ * handle, which answers for the generation it pinned. A segment with no generation answers
2826
+ * `{ generation: null, cardinality: 0 }`.
2858
2827
  *
2859
2828
  * Trust is as for `count()`: the registry row's word, not confirmed against the object until the object is
2860
2829
  * opened, when a disagreement makes this process stop using that row's summary.
@@ -2865,7 +2834,6 @@ var Segment = class _Segment {
2865
2834
  */
2866
2835
  async stat() {
2867
2836
  this.assertLeases([this]);
2868
- if (this.expired()) return { generation: null, cardinality: 0 };
2869
2837
  return this.engine.stat(this.ref);
2870
2838
  }
2871
2839
  /**
@@ -2880,13 +2848,13 @@ var Segment = class _Segment {
2880
2848
  * ```
2881
2849
  *
2882
2850
  * 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. An expired handle reads empty
2884
- * without checking its options, as every read of one does.
2851
+ * stream is first read. `after >= through` is an empty range, which reads nothing.
2885
2852
  */
2886
2853
  iterate(options) {
2887
2854
  const lease = this.leaseError();
2888
2855
  if (lease !== void 0) return failing(lease);
2889
- if (this.expired()) return EMPTY_IDS;
2856
+ const bad = optionsError(options, "iterate", CALL_KEYS.range);
2857
+ if (bad !== void 0) return failing(bad);
2890
2858
  const range = options == null ? void 0 : rangeOf(options);
2891
2859
  const none = range === void 0 || range.after === void 0 && range.through === void 0;
2892
2860
  const { engine, ref } = this;
@@ -2919,7 +2887,7 @@ var Segment = class _Segment {
2919
2887
  *
2920
2888
  * A live handle is refused with {@link UnsupportedError} at the first read, because its counts and its chunks could
2921
2889
  * 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. An expired handle reads empty.
2890
+ * {@link ValidationError} at the first read, as does a bad bound.
2923
2891
  *
2924
2892
  * ```ts
2925
2893
  * const audience = await store.segment('audience').pin();
@@ -2930,7 +2898,8 @@ var Segment = class _Segment {
2930
2898
  everyNth(n, options) {
2931
2899
  const lease = this.leaseError();
2932
2900
  if (lease !== void 0) return failing(lease);
2933
- if (this.expired()) return EMPTY_IDS;
2901
+ const bad = optionsError(options, "everyNth", CALL_KEYS.range);
2902
+ if (bad !== void 0) return failing(bad);
2934
2903
  if (this.pinnedAt === void 0) {
2935
2904
  return failing(
2936
2905
  new UnsupportedError(
@@ -2968,30 +2937,8 @@ var Segment = class _Segment {
2968
2937
  */
2969
2938
  excludesOf(options) {
2970
2939
  const exclude = options?.exclude;
2971
- return exclude == null || exclude.length === 0 ? NO_SEGMENTS : exclude;
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
- );
2940
+ if (exclude == null) return NO_SEGMENTS;
2941
+ return segmentList(exclude, "`exclude`").length === 0 ? NO_SEGMENTS : exclude;
2995
2942
  }
2996
2943
  /**
2997
2944
  * Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
@@ -3003,19 +2950,21 @@ var Segment = class _Segment {
3003
2950
  * override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
3004
2951
  */
3005
2952
  intersect(others, options) {
3006
- return this.intersectAs(AS_IDS, others, options);
2953
+ const bad = optionsError(options, "intersect", CALL_KEYS.combine, ["allowAbsentOperands"]);
2954
+ if (bad !== void 0) return failing(bad);
2955
+ return this.intersectAs(AS_IDS, others, options ?? void 0);
3007
2956
  }
3008
2957
  intersectAs(out, others, options) {
3009
- const exclude = this.excludesOf(options);
3010
- const handles = [this, ...others, ...exclude];
2958
+ let exclude;
2959
+ let handles;
3011
2960
  try {
2961
+ exclude = this.excludesOf(options);
2962
+ handles = [this, ...segmentList(others, "intersect: `others`"), ...exclude];
2963
+ this.assertSegments(handles);
3012
2964
  this.assertLeases(handles);
3013
2965
  } catch (err) {
3014
2966
  return out.failing(err);
3015
2967
  }
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
2968
  let engine;
3020
2969
  try {
3021
2970
  engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
@@ -3039,16 +2988,21 @@ var Segment = class _Segment {
3039
2988
  * write is **refused and reported**: `published: false`, `reason: 'empty'`, and `dest` keeps what it had.
3040
2989
  * Pass `allowEmpty: true` when emptying the destination is the point.
3041
2990
  *
3042
- * `guard` adds the same plausibility bounds `load()` takes — `minCardinality` and `minRetained` — judged
2991
+ * `guard` adds the same plausibility bounds `load()` takes — `minCardinality`, `minRetained` and `maxGrowth` — judged
3043
2992
  * against what `dest` held before. A refusal is **reported, not thrown**, exactly as on `load()`; branch on
3044
2993
  * `published`. A lost race is the one outcome that still throws ({@link WriteConflictError}), because a
3045
2994
  * materialisation that silently did not take effect is the one thing a caller cannot detect on its own.
3046
2995
  *
3047
- * A call involving an **expired handle** is refused before any of this. See {@link refuseIfExpired}.
2996
+ * A call involving a handle whose lease has ended, or anything that is not a segment, is refused before any of this.
3048
2997
  */
3049
2998
  async intersectInto(dest, others, options) {
3050
- this.refuseIfExpired("intersectInto", dest, [...others, ...options?.exclude ?? []]);
3051
- this.combineEngine([this, ...others, ...options?.exclude ?? []]);
2999
+ checkOptions(options, "intersectInto", CALL_KEYS.into, ["allowEmpty", "allowAbsentOperands"]);
3000
+ const operands = [
3001
+ ...segmentList(others, "intersectInto: `others`"),
3002
+ ...this.excludesOf(options)
3003
+ ];
3004
+ this.assertIntoHandles(dest, operands);
3005
+ this.combineEngine([this, ...operands]);
3052
3006
  return this.timed(
3053
3007
  "intersectInto",
3054
3008
  () => this.materialize(
@@ -3070,27 +3024,21 @@ var Segment = class _Segment {
3070
3024
  * segment once (`unionInto`, or a load) is the cheaper shape.
3071
3025
  */
3072
3026
  union(others, options) {
3073
- return this.unionAs(AS_IDS, others, options);
3027
+ const bad = optionsError(options, "union", CALL_KEYS.combine, ["allowAbsentOperands"]);
3028
+ if (bad !== void 0) return failing(bad);
3029
+ return this.unionAs(AS_IDS, others, options ?? void 0);
3074
3030
  }
3075
3031
  unionAs(out, others, options) {
3076
- const exclude = this.excludesOf(options);
3077
- const handles = [this, ...others, ...exclude];
3032
+ let exclude;
3033
+ let handles;
3078
3034
  try {
3035
+ exclude = this.excludesOf(options);
3036
+ handles = [this, ...segmentList(others, "union: `others`"), ...exclude];
3037
+ this.assertSegments(handles);
3079
3038
  this.assertLeases(handles);
3080
3039
  } catch (err) {
3081
3040
  return out.failing(err);
3082
3041
  }
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
3042
  let engine;
3095
3043
  try {
3096
3044
  engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
@@ -3103,8 +3051,10 @@ var Segment = class _Segment {
3103
3051
  }
3104
3052
  /** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
3105
3053
  async unionInto(dest, others, options) {
3106
- this.refuseIfExpired("unionInto", dest, [...others, ...options?.exclude ?? []]);
3107
- this.combineEngine([this, ...others, ...options?.exclude ?? []]);
3054
+ checkOptions(options, "unionInto", CALL_KEYS.into, ["allowEmpty", "allowAbsentOperands"]);
3055
+ const operands = [...segmentList(others, "unionInto: `others`"), ...this.excludesOf(options)];
3056
+ this.assertIntoHandles(dest, operands);
3057
+ this.combineEngine([this, ...operands]);
3108
3058
  return this.timed(
3109
3059
  "unionInto",
3110
3060
  () => this.materialize(
@@ -3123,26 +3073,23 @@ var Segment = class _Segment {
3123
3073
  * suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
3124
3074
  * opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
3125
3075
  *
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
3076
  * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
3131
3077
  * the suppression folds into the same pass rather than materializing an intermediate segment first.
3132
3078
  */
3133
3079
  andNot(excludes, options) {
3134
- return this.andNotAs(AS_IDS, excludes, options);
3080
+ const bad = optionsError(options, "andNot", CALL_KEYS.andNot, ["allowAbsentOperands"]);
3081
+ if (bad !== void 0) return failing(bad);
3082
+ return this.andNotAs(AS_IDS, excludes, options ?? void 0);
3135
3083
  }
3136
3084
  andNotAs(out, excludes, options) {
3137
- const handles = [this, ...excludes];
3085
+ let handles;
3138
3086
  try {
3087
+ handles = [this, ...segmentList(excludes, "andNot: `excludes`")];
3088
+ this.assertSegments(handles);
3139
3089
  this.assertLeases(handles);
3140
3090
  } catch (err) {
3141
3091
  return out.failing(err);
3142
3092
  }
3143
- const refused = this.expiredExcludes("andNot", excludes);
3144
- if (refused) return out.failing(refused);
3145
- if (this.expired()) return out.none;
3146
3093
  let engine;
3147
3094
  try {
3148
3095
  engine = this.combineEngine([this, ...excludes]) ?? this.engine;
@@ -3156,7 +3103,11 @@ var Segment = class _Segment {
3156
3103
  }
3157
3104
  /** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */
3158
3105
  async andNotInto(dest, excludes, options) {
3159
- this.refuseIfExpired("andNotInto", dest, excludes);
3106
+ checkOptions(options, "andNotInto", CALL_KEYS.andNotInto, [
3107
+ "allowEmpty",
3108
+ "allowAbsentOperands"
3109
+ ]);
3110
+ this.assertIntoHandles(dest, segmentList(excludes, "andNotInto: `excludes`"));
3160
3111
  this.combineEngine([this, ...excludes]);
3161
3112
  return this.timed(
3162
3113
  "andNotInto",
@@ -3174,13 +3125,21 @@ var Segment = class _Segment {
3174
3125
  * storage. The pointer refresh is priced at the store's own `cache.genTtlMs`, or at none when the store never
3175
3126
  * refreshes, unless the workload sets `genTtlMs`. See {@link CostReport} — it always includes a verdict (incl.
3176
3127
  * the lose-zone).
3128
+ * @deprecated Moves to `@cloudbitmaps/tools`, a package of its own, with a `size` on `stat()` so a grounded report
3129
+ * needs nothing internal. It is a planning tool, and its price list is as old as the release that ships it.
3177
3130
  */
3178
3131
  async costReport(options) {
3132
+ checkOptions(options, "costReport", CALL_KEYS.costReport);
3133
+ const shape = options?.workload;
3134
+ if (shape !== void 0 && shape !== null && (typeof shape !== "object" || Array.isArray(shape))) {
3135
+ throw new ValidationError2("costReport: workload must be an object");
3136
+ }
3179
3137
  this.assertLeases([this]);
3180
3138
  const canMeasure = this.engine.supportsStorageSize;
3181
3139
  const size = canMeasure ? await this.engine.segmentSize(this.ref) : null;
3182
3140
  const refreshMs = this.engine.pointerRefreshMs;
3183
- const workload = refreshMs === void 0 || options?.workload?.genTtlMs !== void 0 ? options?.workload : { ...options?.workload, genTtlMs: refreshMs };
3141
+ const given = options?.workload === void 0 || options.workload === null ? void 0 : picked(options.workload, CALL_KEYS.workload);
3142
+ const workload = refreshMs === void 0 || given?.genTtlMs !== void 0 ? given : { ...given, genTtlMs: refreshMs };
3184
3143
  return groundedReport({
3185
3144
  storageBytes: size?.sizeBytes ?? 0,
3186
3145
  grounded: canMeasure,
@@ -3211,8 +3170,7 @@ export {
3211
3170
  LocalFsStorage,
3212
3171
  MAX_LEASES_PER_SEGMENT,
3213
3172
  MAX_LEASE_MS2 as MAX_LEASE_MS,
3214
- MIN_EXPIRES_AT_MS2 as MIN_EXPIRES_AT_MS,
3215
- MemoryOperand,
3173
+ MIN_EXPIRES_AT_MS,
3216
3174
  MemoryStorage,
3217
3175
  NodeAead,
3218
3176
  NotFoundError2 as NotFoundError,
@@ -3229,13 +3187,17 @@ export {
3229
3187
  destroySegment,
3230
3188
  eraseNamespace,
3231
3189
  excludingReservedRows2 as excludingReservedRows,
3190
+ isBudgetExceededError,
3191
+ isCapabilityError,
3232
3192
  isCloudRoaringError,
3233
3193
  isIntegrityError,
3194
+ isKeyUnavailableError,
3234
3195
  isLeaseExpiredError,
3235
3196
  isLeaseLimitError,
3236
3197
  isNotFoundError,
3237
3198
  isStaleOperandError,
3238
3199
  isTransientError,
3200
+ isUnsupportedError,
3239
3201
  isValidationError,
3240
3202
  isWriteConflictError,
3241
3203
  readRetentionPolicy