@cloudbitmaps/core 0.23.0 → 0.24.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.
@@ -19,7 +19,7 @@ import {
19
19
  requirePositiveInteger,
20
20
  shown,
21
21
  tailBytesFit
22
- } from "./chunk-MVUD4R72.js";
22
+ } from "./chunk-2B2QF4FU.js";
23
23
 
24
24
  // src/drivers/_shared/options.ts
25
25
  function checkNow(name, now) {
@@ -638,4 +638,4 @@ export {
638
638
  namespaceKeyPart,
639
639
  namespacePathPart
640
640
  };
641
- //# sourceMappingURL=chunk-7GT632UF.js.map
641
+ //# sourceMappingURL=chunk-45ML6YU4.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  requirePositiveInteger
3
- } from "./chunk-MVUD4R72.js";
3
+ } from "./chunk-2B2QF4FU.js";
4
4
 
5
5
  // src/core/concurrency.ts
6
6
  async function mapWithConcurrency(items, limit, fn) {
@@ -70,4 +70,4 @@ export {
70
70
  mapWithConcurrency,
71
71
  boundedAll
72
72
  };
73
- //# sourceMappingURL=chunk-CGILDXGP.js.map
73
+ //# sourceMappingURL=chunk-BUDAVGBI.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ValidationError,
3
3
  encodeNameForKey
4
- } from "./chunk-MVUD4R72.js";
4
+ } from "./chunk-2B2QF4FU.js";
5
5
 
6
6
  // src/core/keys.ts
7
7
  var FIELD = " ";
@@ -118,4 +118,4 @@ export {
118
118
  BufferSink,
119
119
  BufferReader
120
120
  };
121
- //# sourceMappingURL=chunk-GKHERVVQ.js.map
121
+ //# sourceMappingURL=chunk-WPER7R2R.js.map
@@ -33,7 +33,8 @@ export declare function requireFiniteNumber(value: unknown, bounds: {
33
33
  /**
34
34
  * Refuse an options bag that is not an object, or that holds a key not in `keys`, naming each such key and the keys it
35
35
  * takes. `hint`, when given, ends the message, to point at where a likely mistake belongs (`an S3 client goes in
36
- * \`client\``). Only the bag's own enumerable string keys are read.
36
+ * \`client\``). Only the bag's own enumerable string keys are read, so a key inherited through a prototype is not
37
+ * checked.
37
38
  */
38
39
  export declare function refuseUnknownOptions(name: string, options: unknown, keys: readonly string[], hint?: string): void;
39
40
  /**
@@ -54,7 +54,7 @@ export interface CodecBitmap {
54
54
  toArray(): number[];
55
55
  /**
56
56
  * The set's values as one ascending `Uint32Array` the caller owns: what a batch read converts a chunk with, in one
57
- * native step instead of one iterator step per value.
57
+ * step instead of one iterator step per value.
58
58
  *
59
59
  * **Optional.** A codec without it is read through its iterator, which is correct and slower.
60
60
  *
@@ -125,7 +125,7 @@ export interface EncodedChunk {
125
125
  }
126
126
  /**
127
127
  * A pluggable bitmap codec — the factory the codec-agnostic engine constructs {@link CodecBitmap}s through.
128
- * Implementations **must** size-cap before handing untrusted bytes to any native decoder and use a safe
128
+ * Implementations **must** size-cap before handing untrusted bytes to a decoder and use a safe
129
129
  * (never a trusting/frozen) deserializer — hard invariant 5.
130
130
  */
131
131
  export interface CodecInterface {
@@ -135,7 +135,7 @@ export interface CodecInterface {
135
135
  fromValues(values: Iterable<number>): CodecBitmap;
136
136
  /**
137
137
  * Size-cap (`bytes.length <= maxBytes`) then portable-deserialize. Throws `IntegrityError` when the input
138
- * exceeds the cap or fails to decode — the native decoder is never handed unbounded or unsafe-format input.
138
+ * exceeds the cap or fails to decode — the decoder is never handed unbounded or unsafe-format input.
139
139
  *
140
140
  * "Fails to decode" includes bytes that are the right length and the wrong shape. A bitmap this returns must
141
141
  * satisfy every invariant its own operations assume (for roaring: containers and values in order, runs
@@ -161,6 +161,16 @@ export interface CodecInterface {
161
161
  * The answer is the whole check: an `owns` that answers `true` for anything lets any object through as a bitmap.
162
162
  */
163
163
  owns?(bitmap: unknown): boolean;
164
+ /**
165
+ * Whether an intersect of three or more operands should meet each chunk's operands smallest-first (by `size`).
166
+ *
167
+ * **Optional, and off by default.** A codec whose combine cost depends on the order, because an early operand that is
168
+ * large and of one container kind makes the running result expensive, sets it; the engine then pays one `size` read per
169
+ * operand per chunk to put the sparsest first. A codec whose combines cost about the same in any order, or whose
170
+ * `size` is expensive to read, leaves it out and keeps the operand order it was given. The answer is the
171
+ * same either way.
172
+ */
173
+ readonly intersectSmallestFirst?: boolean;
164
174
  }
165
175
  /**
166
176
  * Resolve a codec that a **public core entry point** was given, failing fast when it is missing.
@@ -3,13 +3,16 @@
3
3
  *
4
4
  * Everything this library does on the hot read path is small. Bulk-load is the exception: it is the one place
5
5
  * where a single call legitimately occupies the CPU for hundreds of milliseconds, and Node has exactly one thread
6
- * to occupy. Measured on a 1M-id load of 65,536 chunks onto in-memory storage, on a loaded Apple M3 Pro: **518 ms
7
- * wall, and 519 ms during which the event loop did not turn at all** (medians of 20 fresh-process runs). Wire that to a request handler and every other in-flight request on the instance — health
6
+ * to occupy. Measured on the native codec of 0.23.0 (every figure in this header is the native codec's, to be
7
+ * re-measured on the TypeScript codec), on a 1M-id load of 65,536 chunks onto in-memory storage, on a loaded Apple M3
8
+ * Pro: **518 ms wall, and 519 ms during which the event loop did not turn at all** (medians of 20 fresh-process
9
+ * runs). Wire that to a request handler and every other in-flight request on the instance — health
8
10
  * checks included — waits out the whole load.
9
11
  *
10
- * The fix is not to make the work asynchronous. That was measured too, and it is a 7x *regression*: handing each
11
- * chunk's insert to the threadpool costs ~9 µs of dispatch against ~1.5 µs of actual work once ids are spread
12
- * across ~62,000 chunks (636 ms versus 92 ms). The work is genuinely CPU-bound and genuinely small per chunk;
12
+ * The fix is not to make the work asynchronous. That was measured too, on the native codec's threadpool insert, and
13
+ * it was a 7x *regression*: handing each chunk's insert to the threadpool cost ~9 µs of dispatch against ~1.5 µs of
14
+ * actual work once ids are spread across ~62,000 chunks (636 ms versus 92 ms). The TypeScript codec has no
15
+ * threadpool path. The work is genuinely CPU-bound and genuinely small per chunk;
13
16
  * the only thing wrong with it is that it never stops to let anyone else go. So the fix is to keep the work
14
17
  * synchronous and periodically hand the loop back.
15
18
  *
@@ -179,7 +179,7 @@ export declare class CrbmReader {
179
179
  has(chunkKey: number): boolean;
180
180
  /**
181
181
  * Range-GET one chunk's payload, verifying its CRC32C before returning. Returns `null` if the chunk is
182
- * absent. Throws `IntegrityError` on a CRC mismatch (the bytes must never reach the native
182
+ * absent. Throws `IntegrityError` on a CRC mismatch (the bytes must never reach the codec's
183
183
  * deserializer). The returned bytes may be shared (a small generation's chunks are kept by the reader and handed out
184
184
  * again), so callers must not change them.
185
185
  */
@@ -401,7 +401,7 @@ export declare class SegmentEngine {
401
401
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
402
402
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
403
403
  *
404
- * The accumulator is a **clone** of the first operand's chunk, and that clone is load-bearing: every other
404
+ * The accumulator is a **clone** of one operand's chunk (the first, or for a codec that asks the one with the fewest values), and that clone is load-bearing: every other
405
405
  * bitmap here is the cached, shared Storage instance and must never be mutated, or the cache is poisoned for
406
406
  * every later reader. The remaining operands and the excludes are only *read* by the in-place ops, so they are
407
407
  * used as-is — one clone per key, not one per operand.
@@ -66,7 +66,7 @@
66
66
  * its number after the winner's object was already in the bucket. Up there nothing collects it, and it was derived
67
67
  * from a generation the winner has replaced, so when the winner was another erasure it still holds the id that
68
68
  * erasure has just reported gone. That object is deleted before this call returns, once its footer matches the
69
- * rewrite's and a renewal of the row has refused every publish fenced on a row read before it, under the version the
69
+ * rewrite's (a read of it that faults is thrown instead, and the object stays) and a renewal of the row has refused every publish fenced on a row read before it, under the version the
70
70
  * footer read reported, unless {@link DISCARD_ATTEMPTS} renewals lose to other writes (a load, a rollback, a
71
71
  * retention write): then it is kept, and can hold the winner's id, until a later call takes it: any erasure that
72
72
  * rewrites the segment (this one's re-run among them, while its id is still in the current generation) numbers above it
@@ -119,6 +119,6 @@ export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface
119
119
  * are copied first. An empty or detached buffer is the empty bitmap.
120
120
  *
121
121
  * @param what names the input in a refusal's message.
122
- * @throws {ValidationError} for anything that is not such bytes; nothing reaches the codec's native decoder then.
122
+ * @throws {ValidationError} for anything that is not such bytes; nothing reaches the codec's decoder then.
123
123
  */
124
124
  export declare function decodeSerialized(bytes: unknown, codec: CodecInterface, what?: string): CodecBitmap;
@@ -148,8 +148,8 @@ export interface LoadOptions {
148
148
  * `ValidationError`, naming its position and never the id. The object is the same, byte for byte, as an unsorted load
149
149
  * of the same ids writes under the same write identity.
150
150
  *
151
- * For ids only (an iterable or an async iterable): with `{ serialized }`, `{ bitmap }` or a `RoaringBitmap32` of the
152
- * flavor's own `roaring` (one from another copy loads as its ids, and takes `sorted`), and on core's
151
+ * For ids only (an iterable or an async iterable): with `{ serialized }`, `{ bitmap }` or a bitmap itself (an
152
+ * iterable with a `serialize` method, which a flavor loads as `{ bitmap }`), and on core's
153
153
  * `loadSegmentChunks`, it throws `ValidationError`, as does a value that is not a boolean, before any request.
154
154
  *
155
155
  * The ids are read after the segment's row is read and judged, and after its number and key are settled, so a load
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  mapWithConcurrency
3
- } from "./chunk-CGILDXGP.js";
3
+ } from "./chunk-BUDAVGBI.js";
4
4
  import {
5
5
  DEFAULT_NAMESPACE,
6
6
  applyRegistryPatch,
@@ -18,7 +18,7 @@ import {
18
18
  validateNewRegistryRecord,
19
19
  validateRegistryPatch,
20
20
  webCryptoEntropy
21
- } from "./chunk-7GT632UF.js";
21
+ } from "./chunk-45ML6YU4.js";
22
22
  import {
23
23
  CloudRoaringError,
24
24
  IntegrityError,
@@ -45,7 +45,7 @@ import {
45
45
  requireNonEmptyString,
46
46
  unsettledConflict,
47
47
  validateSegmentRef
48
- } from "./chunk-MVUD4R72.js";
48
+ } from "./chunk-2B2QF4FU.js";
49
49
 
50
50
  // src/drivers/_shared/object-registry-keys.ts
51
51
  var REGISTRY_SUFFIX = ".reg";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  boundedAll,
3
3
  mapWithConcurrency
4
- } from "./chunk-CGILDXGP.js";
4
+ } from "./chunk-BUDAVGBI.js";
5
5
  import {
6
6
  BufferReader,
7
7
  KeptSegmentKeys,
@@ -9,7 +9,7 @@ import {
9
9
  segmentKey,
10
10
  segmentPrefix,
11
11
  shardOf
12
- } from "./chunk-GKHERVVQ.js";
12
+ } from "./chunk-WPER7R2R.js";
13
13
  import {
14
14
  BudgetExceededError,
15
15
  CHUNK_COUNT,
@@ -99,6 +99,7 @@ import {
99
99
  rollbackKnown,
100
100
  sameIncarnation,
101
101
  sameKeys,
102
+ sameKnownGens,
102
103
  sizeOfFingerprint,
103
104
  sizePart,
104
105
  splitId,
@@ -115,7 +116,7 @@ import {
115
116
  validateUserRef,
116
117
  windowOf,
117
118
  withRetry
118
- } from "./chunk-MVUD4R72.js";
119
+ } from "./chunk-2B2QF4FU.js";
119
120
 
120
121
  // src/core/budget.ts
121
122
  var DEFAULT_BUDGET = { maxRequests: 1e6 };
@@ -758,6 +759,23 @@ function chunkIds(chunk, chunkKey, w) {
758
759
  for (let i = 0; i < rem.length; i++) rem[i] = base + (rem[i] & MAX_REMAINDER);
759
760
  return rem;
760
761
  }
762
+ function smallestFirst(chunks) {
763
+ const out = chunks.slice();
764
+ const sizes = out.map((c) => c.size);
765
+ for (let i = 1; i < out.length; i++) {
766
+ const c = out[i];
767
+ const size = sizes[i];
768
+ let j = i - 1;
769
+ while (j >= 0 && sizes[j] > size) {
770
+ out[j + 1] = out[j];
771
+ sizes[j + 1] = sizes[j];
772
+ j--;
773
+ }
774
+ out[j + 1] = c;
775
+ sizes[j + 1] = size;
776
+ }
777
+ return out;
778
+ }
761
779
  function alignKeys(operands, mode) {
762
780
  const candidates = [];
763
781
  if (mode === "all") {
@@ -1504,7 +1522,7 @@ var SegmentEngine = class _SegmentEngine {
1504
1522
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
1505
1523
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
1506
1524
  *
1507
- * The accumulator is a **clone** of the first operand's chunk, and that clone is load-bearing: every other
1525
+ * The accumulator is a **clone** of one operand's chunk (the first, or for a codec that asks the one with the fewest values), and that clone is load-bearing: every other
1508
1526
  * bitmap here is the cached, shared Storage instance and must never be mutated, or the cache is poisoned for
1509
1527
  * every later reader. The remaining operands and the excludes are only *read* by the in-place ops, so they are
1510
1528
  * used as-is — one clone per key, not one per operand.
@@ -1518,10 +1536,11 @@ var SegmentEngine = class _SegmentEngine {
1518
1536
  const fetched = await Promise.all(reads.map((op) => this.operandChunk(op, chunkKey, moves)));
1519
1537
  const chunks = together ? fetched.slice(0, present.length) : fetched;
1520
1538
  if (mode === "all" && chunks.some((c) => c === null)) return null;
1521
- const first = chunks[0];
1539
+ const ordered = mode === "all" && chunks.length > 2 && this.codec.intersectSmallestFirst === true ? smallestFirst(chunks) : chunks;
1540
+ const first = ordered[0];
1522
1541
  const acc = first ? first.clone() : this.codec.empty();
1523
- for (let i = 1; i < chunks.length; i++) {
1524
- const other = chunks[i];
1542
+ for (let i = 1; i < ordered.length; i++) {
1543
+ const other = ordered[i];
1525
1544
  if (other === null || other === void 0) continue;
1526
1545
  if (mode === "all") {
1527
1546
  acc.andInPlace(other);
@@ -2301,6 +2320,22 @@ function requireCodec(codec, api) {
2301
2320
  // src/export/index.ts
2302
2321
  var UTF8 = new TextEncoder();
2303
2322
  var DEFAULT_NDJSON_BATCH_BYTES = 64 * 1024;
2323
+ var EXPORT_BATCH = 1 << 16;
2324
+ async function addAll(bitmap, ids) {
2325
+ const batch = new Uint32Array(EXPORT_BATCH);
2326
+ let held = 0;
2327
+ let count = 0;
2328
+ for await (const id of ids) {
2329
+ batch[held++] = id;
2330
+ count += 1;
2331
+ if (held === EXPORT_BATCH) {
2332
+ bitmap.addMany(batch);
2333
+ held = 0;
2334
+ }
2335
+ }
2336
+ if (held > 0) bitmap.addMany(batch.subarray(0, held));
2337
+ return count;
2338
+ }
2304
2339
  var errMessage = (err) => err instanceof Error ? err.message : String(err);
2305
2340
  async function runExport(reader, registry, sink, options = {}) {
2306
2341
  if (sink === null || typeof sink !== "object" || typeof sink.open !== "function") {
@@ -2360,10 +2395,7 @@ async function runExport(reader, registry, sink, options = {}) {
2360
2395
  writer = await sink.open(ref, ext);
2361
2396
  if (format === "roaring") {
2362
2397
  const bm = requireCodec(options.codec, "runExport({ format: 'roaring' })").empty();
2363
- for await (const id of ids) {
2364
- bm.add(id);
2365
- count += 1;
2366
- }
2398
+ count = await addAll(bm, ids);
2367
2399
  if (count === 0 && await rowIsGone(ref)) return await skipGone(ref, writer);
2368
2400
  const out = bm.serialize();
2369
2401
  await writer.write(out);
@@ -2974,7 +3006,7 @@ var CrbmReader = class _CrbmReader {
2974
3006
  }
2975
3007
  /**
2976
3008
  * Range-GET one chunk's payload, verifying its CRC32C before returning. Returns `null` if the chunk is
2977
- * absent. Throws `IntegrityError` on a CRC mismatch (the bytes must never reach the native
3009
+ * absent. Throws `IntegrityError` on a CRC mismatch (the bytes must never reach the codec's
2978
3010
  * deserializer). The returned bytes may be shared (a small generation's chunks are kept by the reader and handed out
2979
3011
  * again), so callers must not change them.
2980
3012
  */
@@ -3357,8 +3389,9 @@ async function provenObject(storage, key, fingerprint) {
3357
3389
  version ??= tail.version;
3358
3390
  });
3359
3391
  return await CrbmReader.sameObject(blob, fingerprint) ? { version } : null;
3360
- } catch {
3361
- return null;
3392
+ } catch (err) {
3393
+ if (isNotFoundError(err)) return null;
3394
+ throw err;
3362
3395
  }
3363
3396
  }
3364
3397
  function sameObjectUnder(storage, key, fingerprint) {
@@ -3409,12 +3442,7 @@ async function footerCrcUnder(storage, key) {
3409
3442
  return (await readFooter(storageBlobReader(storage, key))).footerCrc;
3410
3443
  }
3411
3444
  async function provesOwnObject(storage, key, fingerprint) {
3412
- try {
3413
- return await CrbmReader.sameObject(storageBlobReader(storage, key), fingerprint);
3414
- } catch (err) {
3415
- if (isNotFoundError(err)) return false;
3416
- throw err;
3417
- }
3445
+ return await provenObject(storage, key, fingerprint) !== null;
3418
3446
  }
3419
3447
  var openedVersions = /* @__PURE__ */ new WeakMap();
3420
3448
  function openGenerationReader(storage, key, crypto, options = {}, named) {
@@ -7271,7 +7299,7 @@ var KeyBits = class {
7271
7299
  };
7272
7300
 
7273
7301
  // src/core/combine-ledger.ts
7274
- var RESIDENT_BITMAP_OVERHEAD = 512;
7302
+ var RESIDENT_BITMAP_OVERHEAD = 832;
7275
7303
  var MAX_CHUNK_SERIALIZED = 8208;
7276
7304
  function serializedBound(cardinality) {
7277
7305
  return Math.min(2 * cardinality + 16, MAX_CHUNK_SERIALIZED);
@@ -7279,7 +7307,7 @@ function serializedBound(cardinality) {
7279
7307
  var MAX_RESIDENT_DATA = 8800;
7280
7308
  function residentBytes(serialized) {
7281
7309
  return Math.ceil(
7282
- RESIDENT_BITMAP_OVERHEAD + Math.min(1.6 * serialized, serialized + 2e3, MAX_RESIDENT_DATA)
7310
+ RESIDENT_BITMAP_OVERHEAD + Math.min(1.6 * serialized, serialized + 2200, MAX_RESIDENT_DATA)
7283
7311
  );
7284
7312
  }
7285
7313
  function residentBound(cardinality) {
@@ -9681,7 +9709,8 @@ async function fence(c, plan) {
9681
9709
  const ledger = usableKnownGens(row);
9682
9710
  const doomed = doomedGenerations(c, plan, byName, ledger, pointer);
9683
9711
  const named = ledger !== void 0 && ledger.some((e) => doomed.has(e.generation));
9684
- const renew = byName.length > 0;
9712
+ const sweepOwes = plan.sweeps && !named && row.pointerId === record.pointerId && sameKnownGens(row.knownGens, record.knownGens);
9713
+ const renew = byName.length > 0 || sweepOwes;
9685
9714
  const renewedAfter = held && row.pointerId !== first.pointerId;
9686
9715
  if (!named && (!renew || renewedAfter)) {
9687
9716
  c.premise = row;
@@ -10039,6 +10068,34 @@ async function stage(c) {
10039
10068
  throw err;
10040
10069
  }
10041
10070
  }
10071
+ async function provenOwn(c, key, fingerprint) {
10072
+ const { deps } = c;
10073
+ const retry = deps.readRetry;
10074
+ const base = retry?.isRetryable ?? isTransientError;
10075
+ let fault;
10076
+ try {
10077
+ return await retryRead(
10078
+ async () => {
10079
+ try {
10080
+ return await provenObject(deps.storage, key, fingerprint);
10081
+ } catch (err) {
10082
+ if (!isIntegrityError(err)) fault = err;
10083
+ throw err;
10084
+ }
10085
+ },
10086
+ retry === void 0 ? void 0 : {
10087
+ ...retry,
10088
+ isRetryable: (err) => isIntegrityError(err) || base(err),
10089
+ onRetry: (info) => {
10090
+ if (!isIntegrityError(info.err)) retry.onRetry?.(info);
10091
+ }
10092
+ }
10093
+ );
10094
+ } catch (err) {
10095
+ if (isIntegrityError(err) && fault === void 0) return null;
10096
+ throw fault ?? err;
10097
+ }
10098
+ }
10042
10099
  async function discardRefused(c, own) {
10043
10100
  const { ref, deps, base, from } = c;
10044
10101
  const key = { ...base, generation: own.generation };
@@ -10055,7 +10112,7 @@ async function discardRefused(c, own) {
10055
10112
  }
10056
10113
  if (row === null || row.status !== "active" || row.currentGen === null) return row;
10057
10114
  if (row.currentGen <= from || row.currentGen >= own.generation) return row;
10058
- const proven = await provenObject(deps.storage, key, own.fingerprint);
10115
+ const proven = await provenOwn(c, key, own.fingerprint);
10059
10116
  if (proven === null) return row;
10060
10117
  const renewed = await renewal.renew(row);
10061
10118
  if ("row" in renewed) {