@cloudbitmaps/core 0.15.0 → 0.16.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
@@ -52,7 +52,7 @@ import {
52
52
  validateUserNamespace,
53
53
  validateUserRef,
54
54
  webCryptoEntropy
55
- } from "./chunk-B7IGS3W6.js";
55
+ } from "./chunk-LFUNPMXZ.js";
56
56
 
57
57
  // src/core/bit-route.ts
58
58
  var U32_MAX = 4294967295;
@@ -494,6 +494,8 @@ var SegmentEngine = class {
494
494
  * they deliver, as a read whose entry an invalidation dropped does not (streams hold no `openReads` entry).
495
495
  */
496
496
  openStreams = /* @__PURE__ */ new Set();
497
+ /** How many times {@link invalidate} has been called: a read compares it with the count it began under. */
498
+ invalidations = 0;
497
499
  constructor(deps) {
498
500
  this.storage = deps.storage;
499
501
  this.cache = deps.cache;
@@ -578,8 +580,15 @@ var SegmentEngine = class {
578
580
  * coalesced ranges when the source has `getChunks`, opening 1, 2, 4 … ranges wide up to {@link DEFAULT_INTERSECT_CONCURRENCY},
579
581
  * else the chunk-by-chunk window of that many reads. `close` stops what is left of it.
580
582
  */
581
- chunkSequence(seg, chunkKeys, gen) {
582
- const streamed = this.openStreamed(seg, chunkKeys, gen, DEFAULT_INTERSECT_CONCURRENCY, 1);
583
+ chunkSequence(seg, chunkKeys, gen, epoch) {
584
+ const streamed = this.openStreamed(
585
+ seg,
586
+ chunkKeys,
587
+ gen,
588
+ epoch,
589
+ DEFAULT_INTERSECT_CONCURRENCY,
590
+ 1
591
+ );
583
592
  if (streamed !== void 0) {
584
593
  return {
585
594
  take: (chunkKey) => this.streamedChunk(streamed, chunkKey),
@@ -604,10 +613,11 @@ var SegmentEngine = class {
604
613
  return range === void 0 ? this.iterateAll(seg) : this.iterateRange(seg, range);
605
614
  }
606
615
  async *iterateAll(seg) {
616
+ const epoch = this.invalidations;
607
617
  const chunkKeys = await this.chunkKeys(seg);
608
618
  checkBudget(this.budget, chunkKeys.length, "iterate");
609
619
  const gen = await this.cacheVersion(seg);
610
- const chunks = this.chunkSequence(seg, chunkKeys, gen);
620
+ const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
611
621
  try {
612
622
  for (const chunkKey of chunkKeys) {
613
623
  const chunk = await chunks.take(chunkKey);
@@ -619,12 +629,13 @@ var SegmentEngine = class {
619
629
  }
620
630
  }
621
631
  async *iterateRange(seg, range) {
632
+ const epoch = this.invalidations;
622
633
  const w = windowOf(range) ?? WHOLE_ID_SPACE;
623
634
  if (w === "empty") return;
624
635
  const chunkKeys = keysWithin(await this.chunkKeys(seg), w);
625
636
  checkBudget(this.budget, chunkKeys.length, "iterate");
626
637
  const gen = await this.cacheVersion(seg);
627
- const chunks = this.chunkSequence(seg, chunkKeys, gen);
638
+ const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
628
639
  try {
629
640
  for (const chunkKey of chunkKeys) {
630
641
  const chunk = await chunks.take(chunkKey);
@@ -646,12 +657,13 @@ var SegmentEngine = class {
646
657
  * the caller's to keep.
647
658
  */
648
659
  async *iterateBatches(seg, range) {
660
+ const epoch = this.invalidations;
649
661
  const w = windowOf(range);
650
662
  if (w === "empty") return;
651
663
  const chunkKeys = w === null ? await this.chunkKeys(seg) : keysWithin(await this.chunkKeys(seg), w);
652
664
  checkBudget(this.budget, chunkKeys.length, "iterate");
653
665
  const gen = await this.cacheVersion(seg);
654
- const chunks = this.chunkSequence(seg, chunkKeys, gen);
666
+ const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
655
667
  try {
656
668
  for (const chunkKey of chunkKeys) {
657
669
  const chunk = await chunks.take(chunkKey);
@@ -731,6 +743,26 @@ var SegmentEngine = class {
731
743
  andNotBatches(seg, excludes, options) {
732
744
  return this.combineBatches([seg], excludes, "all", "andNot", options);
733
745
  }
746
+ /**
747
+ * {@link intersect}, as the chunks the result is made of: `{ chunkKey, bitmap }` in ascending key order, each a
748
+ * non-empty bitmap of the chunk's 16-bit remainders, cut to the read's range. What a materialisation writes
749
+ * straight into its new generation, with no id built on the way.
750
+ *
751
+ * **Each bitmap belongs to the caller, who may change it:** it is the combine's own accumulator or a bitmap built
752
+ * for the range's edge, never a chunk the cache holds or another operand's. The reads, the budget, the metric and the
753
+ * errors are the id reads' own.
754
+ */
755
+ intersectChunks(segs, options) {
756
+ return this.combineChunks(segs, options?.exclude ?? [], "all", "intersect", options);
757
+ }
758
+ /** {@link union}, as chunks: see {@link intersectChunks}. */
759
+ unionChunks(segs, options) {
760
+ return this.combineChunks(segs, options?.exclude ?? [], "any", "union", options);
761
+ }
762
+ /** {@link andNot}, as chunks: see {@link intersectChunks}. */
763
+ andNotChunks(seg, excludes, options) {
764
+ return this.combineChunks([seg], excludes, "all", "andNot", options);
765
+ }
734
766
  /**
735
767
  * Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
736
768
  * absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
@@ -753,10 +785,11 @@ var SegmentEngine = class {
753
785
  const w = windowOf(options);
754
786
  if (w === "empty") return null;
755
787
  const extract = async (seg) => {
788
+ const epoch = this.invalidations;
756
789
  const all = await this.chunkKeys(seg);
757
790
  const gen = await this.cacheVersion(seg);
758
791
  const keys = w === null ? all : keysWithin(all, w);
759
- return { seg, keys: new Set(keys), chunkless: all.length === 0, gen };
792
+ return { seg, keys: new Set(keys), chunkless: all.length === 0, gen, epoch };
760
793
  };
761
794
  const [operands, excludes] = await Promise.all([
762
795
  Promise.all(segs.map(extract)),
@@ -801,14 +834,15 @@ var SegmentEngine = class {
801
834
  const open4 = (o, wanted) => {
802
835
  o.streamed = this.openStreamed(
803
836
  o.seg,
804
- common.filter(wanted),
837
+ wanted === void 0 ? common : common.filter(wanted),
805
838
  o.gen,
839
+ o.epoch,
806
840
  limit,
807
841
  COMBINE_RANGE_START
808
842
  );
809
- if (o.streamed?.stream) streams.push(o.streamed);
843
+ if (o.streamed) streams.push(o.streamed);
810
844
  };
811
- for (const o of operands) open4(o, mode === "all" ? () => true : (k) => o.keys.has(k));
845
+ for (const o of operands) open4(o, mode === "all" ? void 0 : (k) => o.keys.has(k));
812
846
  if (streamExcludes) for (const e of excludes) open4(e, (k) => e.keys.has(k));
813
847
  return {
814
848
  w,
@@ -853,6 +887,28 @@ var SegmentEngine = class {
853
887
  for (const stream of plan.streams) this.closeStreamed(stream);
854
888
  }
855
889
  }
890
+ /** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
891
+ async *combineChunks(segs, excludeSegs, mode, op, options) {
892
+ const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
893
+ if (plan === null) return;
894
+ const { w, window } = plan;
895
+ try {
896
+ for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
897
+ if (!slot.result || slot.result.isEmpty) continue;
898
+ const bitmap = w !== null && isEdge(slot.key, w) ? this.cutToWindow(slot.result, slot.key, w) : slot.result;
899
+ if (!bitmap.isEmpty) yield { chunkKey: slot.key, bitmap };
900
+ }
901
+ } finally {
902
+ for (const stream of plan.streams) this.closeStreamed(stream);
903
+ }
904
+ }
905
+ /** An edge chunk's bitmap cut to the window, as a new bitmap: the remainders `edgeIds` would yield. */
906
+ cutToWindow(chunk, chunkKey, w) {
907
+ const from = chunkKey === w.loKey ? w.loRem : 0;
908
+ const to = chunkKey === w.hiKey ? w.hiRem : MAX_REMAINDER;
909
+ const rem = chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
910
+ return this.codec.fromValues(rem.subarray(firstAbove(rem, from - 1), firstAbove(rem, to)));
911
+ }
856
912
  /**
857
913
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
858
914
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
@@ -902,25 +958,43 @@ var SegmentEngine = class {
902
958
  return op.streamed === void 0 ? this.storageChunk({ ...op.seg, chunkKey }, op.gen) : this.streamedChunk(op.streamed, chunkKey);
903
959
  }
904
960
  /**
905
- * Open `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
906
- * `getChunks`, a segment with no generation, or nothing to read. The chunks already decoded in the cache are taken out
907
- * of the key list first and served from there, so a repeat read asks the source for nothing it holds. Nothing is
908
- * requested until the first chunk is asked for.
961
+ * `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
962
+ * `getChunks`, a segment with no generation, or nothing to read. Nothing is requested, and no cache lookup made, until
963
+ * the first chunk is asked for (see {@link StreamedChunks}).
964
+ */
965
+ openStreamed(seg, keys, gen, epoch, concurrency, rampStart) {
966
+ if (this.storage.getChunks === void 0 || gen === null || keys.length === 0) return void 0;
967
+ return {
968
+ seg,
969
+ gen,
970
+ epoch,
971
+ keys,
972
+ concurrency,
973
+ rampStart,
974
+ opened: false,
975
+ inStream: void 0,
976
+ stream: void 0,
977
+ invalidated: false
978
+ };
979
+ }
980
+ /**
981
+ * Open the stream of `streamed`, at its first uncached chunk `from`: over that chunk and the later keys the cache does
982
+ * not hold. Each later key is counted as a cache hit or miss here, so a key is counted once whichever way it is served.
909
983
  */
910
- openStreamed(seg, keys, gen, concurrency, rampStart) {
984
+ startStream(streamed, from) {
985
+ const { seg, gen } = streamed;
911
986
  const getChunks = this.storage.getChunks;
912
- if (getChunks === void 0 || gen === null || keys.length === 0) return void 0;
913
- let wanted = keys;
914
- let inStream;
987
+ let wanted = streamed.keys.slice(streamed.keys.indexOf(from));
915
988
  if (this.cache) {
916
- const misses = [];
917
- for (const chunkKey of keys) {
989
+ const misses = [from];
990
+ for (let i = 1; i < wanted.length; i++) {
991
+ const chunkKey = wanted[i];
918
992
  const hit = this.cache.get(this.chunkCacheKey({ ...seg, chunkKey }, gen));
919
993
  if (!hit) misses.push(chunkKey);
920
994
  if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
921
995
  }
996
+ if (misses.length < wanted.length) streamed.inStream = new Set(misses);
922
997
  wanted = misses;
923
- if (misses.length < keys.length) inStream = new Set(misses);
924
998
  }
925
999
  const onRequest = this.metricsOn ? (request) => {
926
1000
  this.metrics.onEvent({
@@ -931,27 +1005,36 @@ var SegmentEngine = class {
931
1005
  ms: request.ms
932
1006
  });
933
1007
  } : void 0;
934
- const stream = wanted.length === 0 ? void 0 : new ChunkStream(
1008
+ streamed.stream = new ChunkStream(
935
1009
  getChunks.call(this.storage, seg, wanted, {
936
- concurrency,
937
- ramp: rampStart,
1010
+ concurrency: streamed.concurrency,
1011
+ ramp: streamed.rampStart,
938
1012
  ...onRequest === void 0 ? {} : { onRequest }
939
1013
  })
940
1014
  );
941
- const streamed = { seg, gen, inStream, stream, invalidated: false };
942
- if (stream) this.openStreams.add(streamed);
943
- return streamed;
1015
+ streamed.opened = true;
1016
+ streamed.invalidated = this.storage.currentVersion === void 0 && this.invalidations !== streamed.epoch;
1017
+ this.openStreams.add(streamed);
944
1018
  }
945
1019
  closeStreamed(streamed) {
946
1020
  streamed.stream?.close();
947
1021
  this.openStreams.delete(streamed);
948
1022
  }
949
1023
  /**
950
- * The decoded chunk at `chunkKey` of a streamed read: the next chunk of its stream, or, for a chunk the cache held
951
- * when the read opened, the cached instance while it is still there, and a read of that chunk alone once it is not.
1024
+ * The decoded chunk at `chunkKey` of a streamed read: from the cache while the stream is unopened and the cache holds
1025
+ * it; else the next chunk of its stream, or, for a chunk the cache held when the stream opened, the cached instance
1026
+ * while it is still there, and a read of that chunk alone once it is not.
952
1027
  */
953
1028
  streamedChunk(streamed, chunkKey) {
954
- if (streamed.inStream !== void 0 && !streamed.inStream.has(chunkKey)) {
1029
+ if (!streamed.opened) {
1030
+ const cache = this.cache;
1031
+ if (cache) {
1032
+ const hit = cache.get(this.chunkCacheKey({ ...streamed.seg, chunkKey }, streamed.gen));
1033
+ if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
1034
+ if (hit) return Promise.resolve(hit);
1035
+ }
1036
+ this.startStream(streamed, chunkKey);
1037
+ } else if (streamed.inStream !== void 0 && !streamed.inStream.has(chunkKey)) {
955
1038
  const ref = { ...streamed.seg, chunkKey };
956
1039
  const hit = this.cache?.get(this.chunkCacheKey(ref, streamed.gen));
957
1040
  return hit ? Promise.resolve(hit) : this.storageChunk(ref, streamed.gen);
@@ -1074,6 +1157,7 @@ var SegmentEngine = class {
1074
1157
  */
1075
1158
  invalidate(ref) {
1076
1159
  const prefix = segmentPrefix(ref);
1160
+ this.invalidations += 1;
1077
1161
  for (const streamed of this.openStreams) {
1078
1162
  if (segmentPrefix(streamed.seg) === prefix) streamed.invalidated = true;
1079
1163
  }
@@ -1491,7 +1575,7 @@ var HOURS_PER_MONTH = 730;
1491
1575
  var SECONDS_PER_MONTH = HOURS_PER_MONTH * 3600;
1492
1576
  var GIB = 1024 ** 3;
1493
1577
  var STORE_LOAD_PUT_CLASS = 1 + 1 / LIST_COLLECTION_CADENCE;
1494
- var STORE_LOAD_POINTER_READS = 3 + 2 / LIST_COLLECTION_CADENCE;
1578
+ var STORE_LOAD_POINTER_READS = 2 + 2 / LIST_COLLECTION_CADENCE;
1495
1579
  var STORE_LOAD_EXISTENCE_CHECKS = 2 - 1 / LIST_COLLECTION_CADENCE;
1496
1580
  var RETENTION_SWEEP_REQUESTS = deepFreeze({
1497
1581
  conditionalDelete: {
@@ -2108,6 +2192,10 @@ var MemoryRegistryDriver = class {
2108
2192
  this.rows.set(key, structuredClone(recordFromNew(ref, checked, this.now(), token)));
2109
2193
  return { token };
2110
2194
  }
2195
+ /**
2196
+ * A caller's `held` row (see `RegistryWriteOptions`) has no read to spare here: the token is compared with the stored row in the same step
2197
+ * as the write, in memory, so a stale one fails exactly as a lost race does.
2198
+ */
2111
2199
  async compareAndSwap(ref, expected, patch) {
2112
2200
  validateSegmentRef(ref);
2113
2201
  const checked = validateRegistryPatch(patch);
@@ -2463,6 +2551,10 @@ var LocalFsRegistryDriver = class {
2463
2551
  return { token };
2464
2552
  });
2465
2553
  }
2554
+ /**
2555
+ * A caller's `held` row (see `RegistryWriteOptions`) has no read to spare here: the row's file is read under its lock and its token compared
2556
+ * there, a local read and no request, so a stale one fails exactly as a lost race does.
2557
+ */
2466
2558
  async compareAndSwap(ref, expected, patch) {
2467
2559
  const checked = validateRegistryPatch(patch);
2468
2560
  const path = registryRowPath(this.root, ref);
@@ -3717,6 +3809,12 @@ var DecodedLoadInput = class {
3717
3809
  }
3718
3810
  bitmap;
3719
3811
  };
3812
+ var ChunkLoadInput = class {
3813
+ constructor(chunks) {
3814
+ this.chunks = chunks;
3815
+ }
3816
+ chunks;
3817
+ };
3720
3818
  function tagOf(value) {
3721
3819
  return Object.prototype.toString.call(value).slice(8, -1);
3722
3820
  }
@@ -5057,7 +5155,9 @@ async function publishGeneration(registry, key, options = {}) {
5057
5155
  let lastRecord;
5058
5156
  for (let attempt = 0; attempt < 5; attempt++) {
5059
5157
  const reused = attempt === 0 && options.row !== void 0;
5158
+ const holdsRow = fresh === void 0;
5060
5159
  const record = fresh !== void 0 ? fresh : reused ? options.row ?? null : await registry.get(key);
5160
+ const write = holdsRow ? { held: record } : void 0;
5061
5161
  fresh = void 0;
5062
5162
  unanswered = void 0;
5063
5163
  lastRecord = record;
@@ -5089,22 +5189,31 @@ async function publishGeneration(registry, key, options = {}) {
5089
5189
  return false;
5090
5190
  }
5091
5191
  if (record === null) {
5092
- await registry.create(key, {
5093
- currentGen: key.generation,
5094
- wrappedDeks: options.wrappedDeks,
5095
- ...options.summary === void 0 ? {} : { summary: options.summary }
5096
- });
5192
+ await registry.create(
5193
+ key,
5194
+ {
5195
+ currentGen: key.generation,
5196
+ wrappedDeks: options.wrappedDeks,
5197
+ ...options.summary === void 0 ? {} : { summary: options.summary }
5198
+ },
5199
+ write
5200
+ );
5097
5201
  } else if (record.status === "destroyed") {
5098
5202
  throw new ValidationError(
5099
5203
  `segment "${key.segment}" was destroyed (crypto-shredded) while generation ${key.generation} was being written \u2014 refusing to publish it; the written object is unreadable. Use a new segment.`
5100
5204
  );
5101
5205
  } else if (record.currentGen === null) {
5102
5206
  refuseCleartextOntoKey(key, record, options.cleartext);
5103
- await registry.compareAndSwap(key, record.token, {
5104
- currentGen: key.generation,
5105
- ...options.wrappedDeks === void 0 ? {} : { wrappedDeks: options.wrappedDeks },
5106
- ...options.summary === void 0 ? {} : { summary: options.summary }
5107
- });
5207
+ await registry.compareAndSwap(
5208
+ key,
5209
+ record.token,
5210
+ {
5211
+ currentGen: key.generation,
5212
+ ...options.wrappedDeks === void 0 ? {} : { wrappedDeks: options.wrappedDeks },
5213
+ ...options.summary === void 0 ? {} : { summary: options.summary }
5214
+ },
5215
+ write
5216
+ );
5108
5217
  } else if (record.currentGen > key.generation) {
5109
5218
  return false;
5110
5219
  } else if (record.currentGen === key.generation) {
@@ -5117,10 +5226,15 @@ async function publishGeneration(registry, key, options = {}) {
5117
5226
  );
5118
5227
  }
5119
5228
  refuseCleartextOntoKey(key, record, options.cleartext);
5120
- await registry.compareAndSwap(key, record.token, {
5121
- currentGen: key.generation,
5122
- ...options.summary === void 0 ? {} : { summary: options.summary }
5123
- });
5229
+ await registry.compareAndSwap(
5230
+ key,
5231
+ record.token,
5232
+ {
5233
+ currentGen: key.generation,
5234
+ ...options.summary === void 0 ? {} : { summary: options.summary }
5235
+ },
5236
+ write
5237
+ );
5124
5238
  }
5125
5239
  return true;
5126
5240
  } catch (err) {
@@ -5206,6 +5320,8 @@ async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
5206
5320
  await pause();
5207
5321
  chunks = ids.bitmap.encodeChunks();
5208
5322
  await pause();
5323
+ } else if (ids instanceof ChunkLoadInput) {
5324
+ chunks = encodeEach(rangeCheckedAtWrite(await collectChunks(ids, codec, options.clock)));
5209
5325
  } else {
5210
5326
  chunks = encodeEach(
5211
5327
  await bucketIds(ids instanceof DecodedLoadInput ? ids.bitmap : ids, codec, options.clock)
@@ -5298,6 +5414,52 @@ async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
5298
5414
  fingerprint
5299
5415
  };
5300
5416
  }
5417
+ function* rangeCheckedAtWrite(chunks) {
5418
+ for (const chunk of chunks) {
5419
+ const max = chunk.bitmap.maximum?.();
5420
+ if (max !== void 0 && max > MAX_REMAINDER) {
5421
+ throw new ValidationError(
5422
+ `chunk ${chunk.chunkKey} holds ${max}, outside the 16-bit range [0, ${MAX_REMAINDER}]`
5423
+ );
5424
+ }
5425
+ yield chunk;
5426
+ }
5427
+ }
5428
+ async function collectChunks(input, codec, clock) {
5429
+ const chunks = [];
5430
+ const tick = yieldEvery(clock);
5431
+ let last = -1;
5432
+ for await (const chunk of input.chunks) {
5433
+ if (typeof chunk !== "object" || chunk === null) {
5434
+ throw new ValidationError(
5435
+ `a chunk must be an object with a chunkKey and a bitmap; got ${String(chunk)}`
5436
+ );
5437
+ }
5438
+ const { chunkKey, bitmap } = chunk;
5439
+ if (codec.owns?.(bitmap) !== true) {
5440
+ throw new ValidationError(
5441
+ `chunk ${String(chunkKey)}: its bitmap is not one this load's codec made`
5442
+ );
5443
+ }
5444
+ if (!Number.isInteger(chunkKey) || chunkKey < 0 || chunkKey > MAX_REMAINDER) {
5445
+ throw new ValidationError(`a chunk's key must be an integer in [0, 65535]; got ${chunkKey}`);
5446
+ }
5447
+ if (chunkKey <= last) {
5448
+ throw new ValidationError(`chunk ${chunkKey} does not ascend: the last was ${last}`);
5449
+ }
5450
+ last = chunkKey;
5451
+ const max = bitmap.maximum?.();
5452
+ if (max !== void 0 && max > MAX_REMAINDER) {
5453
+ throw new ValidationError(
5454
+ `chunk ${chunkKey} holds ${max}, outside the 16-bit range [0, ${MAX_REMAINDER}]`
5455
+ );
5456
+ }
5457
+ chunks.push({ chunkKey, bitmap });
5458
+ const pause = tick();
5459
+ if (pause !== null) await pause;
5460
+ }
5461
+ return chunks;
5462
+ }
5301
5463
  async function bucketIds(ids, codec, clock) {
5302
5464
  const byChunk = /* @__PURE__ */ new Map();
5303
5465
  const pendingByChunk = /* @__PURE__ */ new Map();
@@ -5575,7 +5737,25 @@ async function currentCardinality(ref, deps, record, unwrapped) {
5575
5737
  for (const n of reader.cardinalities().values()) total += n;
5576
5738
  return { cardinality: total, fromSummary: false };
5577
5739
  }
5578
- async function loadSegment(ref, input, deps, options = {}) {
5740
+ function loadSegment(ref, input, deps, options = {}) {
5741
+ return runLoad(ref, (codec) => prepareLoadInput(input, codec), deps, options);
5742
+ }
5743
+ function loadSegmentChunks(ref, chunks, deps, options = {}) {
5744
+ return runLoad(
5745
+ ref,
5746
+ () => {
5747
+ if (typeof chunks !== "object" || chunks === null || !(Symbol.asyncIterator in chunks)) {
5748
+ throw new ValidationError(
5749
+ "loadSegmentChunks takes an async iterable of { chunkKey, bitmap }"
5750
+ );
5751
+ }
5752
+ return new ChunkLoadInput(chunks);
5753
+ },
5754
+ deps,
5755
+ options
5756
+ );
5757
+ }
5758
+ async function runLoad(ref, prepare, deps, options) {
5579
5759
  validateUserRef(ref);
5580
5760
  const codec = requireCodec(deps.codec, "loadSegment");
5581
5761
  const keep = options.keep ?? 1;
@@ -5601,7 +5781,7 @@ async function loadSegment(ref, input, deps, options = {}) {
5601
5781
  throw new ValidationError(message);
5602
5782
  });
5603
5783
  const audit = safeAudit(options.audit ?? NOOP_AUDIT);
5604
- const ids = prepareLoadInput(input, codec);
5784
+ const ids = prepare(codec);
5605
5785
  assertRegistryCanWrite(deps.registry, "load");
5606
5786
  const row = await deps.registry.get(ref);
5607
5787
  const fromToken = row?.token;
@@ -7195,6 +7375,7 @@ export {
7195
7375
  listGenerations,
7196
7376
  listSegments,
7197
7377
  loadSegment,
7378
+ loadSegmentChunks,
7198
7379
  mapWithConcurrency,
7199
7380
  readRetentionPolicy,
7200
7381
  resolveBudget,