@cloudbitmaps/core 0.3.0 → 0.4.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.cjs CHANGED
@@ -91,6 +91,14 @@ function applyRemove(delta, remainder) {
91
91
  delta.removes.add(remainder);
92
92
  delta.adds.remove(remainder);
93
93
  }
94
+ function applyAddAll(delta, ids) {
95
+ delta.adds.orInPlace(ids);
96
+ delta.removes.andNotInPlace(ids);
97
+ }
98
+ function applyRemoveAll(delta, ids) {
99
+ delta.removes.orInPlace(ids);
100
+ delta.adds.andNotInPlace(ids);
101
+ }
94
102
  function effective(cold, delta) {
95
103
  const out = cold.clone();
96
104
  out.orInPlace(delta.adds);
@@ -214,6 +222,19 @@ async function mapWithConcurrency(items, limit, fn) {
214
222
  return results;
215
223
  }
216
224
 
225
+ // src/core/cooperative.ts
226
+ var YIELD_EVERY = 1024;
227
+ function yieldEvery(clock, everyN = YIELD_EVERY) {
228
+ if (clock === void 0) return () => null;
229
+ const pause = clock.yieldNow !== void 0 ? () => clock.yieldNow() : () => clock.sleep(1);
230
+ let n = 0;
231
+ return () => {
232
+ if (++n < everyN) return null;
233
+ n = 0;
234
+ return pause();
235
+ };
236
+ }
237
+
217
238
  // src/core/keys.ts
218
239
  var FIELD = " ";
219
240
  var NO_NAMESPACE = "/";
@@ -396,6 +417,8 @@ async function withRetry(op, policy, deps) {
396
417
  var DEFAULT_MAX_BITMAP_BYTES = 1 << 20;
397
418
  var DEFAULT_MAX_RETRIES = 16;
398
419
  var DEFAULT_INTERSECT_CONCURRENCY = 8;
420
+ var MUTATE_STAGE_IDS = 1 << 20;
421
+ var YIELD_EVERY_IDS = 1 << 14;
399
422
  var InstantClock = class {
400
423
  now() {
401
424
  return 0;
@@ -410,6 +433,7 @@ var ZeroRng = class {
410
433
  }
411
434
  };
412
435
  var DEFAULT_MAX_WARM_SCAN_BYTES = 64 * 1024 * 1024;
436
+ var DEFAULT_WRITE_CONCURRENCY = 4;
413
437
  var SegmentEngine = class {
414
438
  warm;
415
439
  cold;
@@ -439,7 +463,7 @@ var SegmentEngine = class {
439
463
  this.rng = deps.rng ?? new ZeroRng();
440
464
  this.occBackoff = deps.occBackoff ?? DEFAULT_OCC_BACKOFF;
441
465
  this.readOpts = { consistent: (deps.warmReadConsistency ?? "strong") !== "eventual" };
442
- this.writeConcurrency = deps.writeConcurrency ?? 1;
466
+ this.writeConcurrency = deps.writeConcurrency ?? DEFAULT_WRITE_CONCURRENCY;
443
467
  this.maxWarmScanBytes = deps.maxWarmScanBytes ?? DEFAULT_MAX_WARM_SCAN_BYTES;
444
468
  if (!Number.isFinite(this.maxWarmScanBytes) || this.maxWarmScanBytes < 1) {
445
469
  throw new ValidationError(
@@ -459,10 +483,10 @@ var SegmentEngine = class {
459
483
  return this.readModifyWrite(seg, chunkKey, (d) => applyRemove(d, remainder));
460
484
  }
461
485
  addMany(seg, ids) {
462
- return this.mutateMany(seg, ids, applyAdd);
486
+ return this.mutateMany(seg, ids, applyAddAll);
463
487
  }
464
488
  removeMany(seg, ids) {
465
- return this.mutateMany(seg, ids, applyRemove);
489
+ return this.mutateMany(seg, ids, applyRemoveAll);
466
490
  }
467
491
  async has(seg, id, opts) {
468
492
  const { chunkKey, remainder } = splitId(id);
@@ -543,9 +567,44 @@ var SegmentEngine = class {
543
567
  * re-resolves fresh (bypassing the TTL), which can hop generations even sub-TTL — still whole/immutable per
544
568
  * read, never torn. Full intra-op snapshot isolation is deferred (gap #4).
545
569
  */
546
- async *intersect(segs, options) {
570
+ /**
571
+ * `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
572
+ *
573
+ * The `exclude` operands are what make suppression **compose without materialising**. Applying suppression
574
+ * afterwards (intersect into a temp segment, then remove) writes an intermediate segment that nobody wants;
575
+ * applying it here folds it into the same chunk-aligned pass. It is also cheaper than it looks: an exclude
576
+ * operand is read **only at the keys that survived the intersection**, so a 61,000-chunk global opt-out list
577
+ * intersected against a narrow audience costs a handful of reads, not 61,000.
578
+ */
579
+ intersect(segs, options) {
580
+ return this.combine(segs, options?.exclude ?? [], "all", "intersect", options);
581
+ }
582
+ /**
583
+ * `segs[0] ∪ segs[1] ∪ …`, minus every id in `options.exclude` — streamed ascending.
584
+ *
585
+ * **Union is the one composite read with no chunk-skipping**, and that is a property of the operation rather
586
+ * than of this implementation: an id in *any* operand belongs to the result, so every chunk of every operand
587
+ * has to be read. `intersect` can prune a key absent from any operand; union cannot prune anything. It is
588
+ * budgeted exactly like `intersect` so a wide union is refused rather than quietly billed.
589
+ */
590
+ union(segs, options) {
591
+ return this.combine(segs, options?.exclude ?? [], "any", "union", options);
592
+ }
593
+ /**
594
+ * `seg \ (excludes[0] ∪ excludes[1] ∪ …)` — streamed ascending. Suppression on its own.
595
+ *
596
+ * Reads every chunk of `seg` (any of them may survive) but each exclude **only where it overlaps `seg`**, so
597
+ * the cost scales with the segment being filtered rather than with the size of the suppression list.
598
+ */
599
+ andNot(seg, excludes, options) {
600
+ return this.combine([seg], excludes, "all", "andNot", options);
601
+ }
602
+ async *combine(segs, excludeSegs, mode, op, options) {
547
603
  if (segs.length === 0) {
548
- throw new ValidationError("intersect requires at least one segment");
604
+ throw new ValidationError(`${op} requires at least one segment`);
605
+ }
606
+ if (op === "andNot" && excludeSegs.length === 0) {
607
+ throw new ValidationError("andNot requires at least one segment to subtract");
549
608
  }
550
609
  const limit = options?.concurrency ?? DEFAULT_INTERSECT_CONCURRENCY;
551
610
  if (!Number.isInteger(limit) || limit < 1) {
@@ -554,34 +613,52 @@ var SegmentEngine = class {
554
613
  );
555
614
  }
556
615
  const budget = resolvePerOpBudget(options?.budget, this.budget);
557
- const operands = await Promise.all(
558
- segs.map(async (seg) => {
559
- const warmRows = await this.collectWarm(seg, "intersect", null);
560
- const keys = await this.chunkKeys(seg, warmRows);
561
- const gen = await this.currentGen(seg);
562
- return { seg, warmRows, keys: new Set(keys), gen };
563
- })
564
- );
565
- const pivot = operands.reduce((a, b) => a.keys.size <= b.keys.size ? a : b);
566
- const common = [];
567
- for (const k of pivot.keys) {
568
- if (operands.every((op) => op.keys.has(k))) common.push(k);
569
- }
570
- common.sort((a, b) => a - b);
571
- checkBudget(budget, common.length * operands.length, "intersect");
616
+ const extract = async (seg) => {
617
+ const warmRows = await this.collectWarm(seg, op, null);
618
+ const keys = await this.chunkKeys(seg, warmRows);
619
+ const gen = await this.currentGen(seg);
620
+ return { seg, warmRows, keys: new Set(keys), gen };
621
+ };
622
+ const [operands, excludes] = await Promise.all([
623
+ Promise.all(segs.map(extract)),
624
+ Promise.all(excludeSegs.map(extract))
625
+ ]);
626
+ const candidates = [];
627
+ if (mode === "all") {
628
+ const pivot = operands.reduce((a, b) => a.keys.size <= b.keys.size ? a : b);
629
+ for (const k of pivot.keys) {
630
+ if (operands.every((o) => o.keys.has(k))) candidates.push(k);
631
+ }
632
+ } else {
633
+ const seen = /* @__PURE__ */ new Set();
634
+ for (const o of operands) for (const k of o.keys) seen.add(k);
635
+ for (const k of seen) candidates.push(k);
636
+ }
637
+ candidates.sort((a, b) => a - b);
638
+ const common = candidates;
639
+ let units = 0;
640
+ for (const key of common) {
641
+ for (const o of operands) if (mode === "all" || o.keys.has(key)) units += 1;
642
+ for (const e of excludes) if (e.keys.has(key)) units += 1;
643
+ }
644
+ checkBudget(budget, units, op);
572
645
  if (this.metricsOn) {
573
646
  const distinctKeys = /* @__PURE__ */ new Set();
574
- for (const op of operands) {
575
- for (const k of op.keys) distinctKeys.add(k);
647
+ for (const op2 of operands) {
648
+ for (const k of op2.keys) distinctKeys.add(k);
576
649
  }
650
+ for (const e of excludes) for (const k of e.keys) distinctKeys.add(k);
577
651
  this.metrics.onEvent({
578
652
  kind: "intersect",
579
- operands: segs.length,
653
+ op,
654
+ operands: segs.length + excludes.length,
580
655
  fetchedChunks: common.length,
656
+ // Never negative: under 'any' every candidate key came from some operand, so `distinctKeys` is a
657
+ // superset of `common`; excludes only ever add keys here.
581
658
  skippedChunks: distinctKeys.size - common.length
582
659
  });
583
660
  }
584
- const startAt = (key) => this.intersectChunk(operands, key).then(
661
+ const startAt = (key) => this.combineChunk(operands, excludes, mode, key).then(
585
662
  (result) => ({ key, result }),
586
663
  (error) => ({ key, result: null, error })
587
664
  );
@@ -603,16 +680,33 @@ var SegmentEngine = class {
603
680
  * first operand's effective chunk and mutates it in place — safe because `effectiveChunk` always returns
604
681
  * a FRESH bitmap (never a cached/shared Cold instance), so this can't poison the HOT cache.
605
682
  */
606
- async intersectChunk(operands, chunkKey) {
683
+ async combineChunk(operands, excludes, mode, chunkKey) {
684
+ const present = mode === "all" ? operands : operands.filter((o) => o.keys.has(chunkKey));
685
+ if (present.length === 0) return null;
607
686
  const chunks = await Promise.all(
608
- operands.map((op) => this.effectiveChunk(op.seg, chunkKey, op.warmRows, op.gen))
687
+ present.map((op) => this.effectiveChunk(op.seg, chunkKey, op.warmRows, op.gen))
609
688
  );
610
689
  const acc = chunks[0];
611
690
  for (let i = 1; i < chunks.length; i++) {
612
- acc.andInPlace(chunks[i]);
613
- if (acc.isEmpty) return null;
691
+ if (mode === "all") {
692
+ acc.andInPlace(chunks[i]);
693
+ if (acc.isEmpty) return null;
694
+ } else {
695
+ acc.orInPlace(chunks[i]);
696
+ }
614
697
  }
615
- return acc.isEmpty ? null : acc;
698
+ if (acc.isEmpty) return null;
699
+ const relevant = excludes.filter((e) => e.keys.has(chunkKey));
700
+ if (relevant.length > 0) {
701
+ const cuts = await Promise.all(
702
+ relevant.map((e) => this.effectiveChunk(e.seg, chunkKey, e.warmRows, e.gen))
703
+ );
704
+ for (const cut of cuts) {
705
+ acc.andNotInPlace(cut);
706
+ if (acc.isEmpty) return null;
707
+ }
708
+ }
709
+ return acc;
616
710
  }
617
711
  /**
618
712
  * Materialize `seg[0] ∩ seg[1] ∩ …` into `dest` via batched `addMany` (streaming, bounded memory).
@@ -620,9 +714,30 @@ var SegmentEngine = class {
620
714
  * `dest` is added to, not replaced.
621
715
  */
622
716
  async intersectInto(dest, segs, options) {
623
- const batchSize = Math.max(1, options?.batchSize ?? 4096);
717
+ await this.drainInto(dest, this.intersect(segs, options), options?.batchSize);
718
+ }
719
+ /**
720
+ * Materialize `segs[0] ∪ segs[1] ∪ …` (minus `options.exclude`) into `dest`. **Not atomic**, like
721
+ * {@link intersectInto}. See {@link union} for why this one cannot skip any chunk.
722
+ */
723
+ async unionInto(dest, segs, options) {
724
+ await this.drainInto(dest, this.union(segs, options), options?.batchSize);
725
+ }
726
+ /** Materialize `seg \ (excludes…)` into `dest`. **Not atomic**, like {@link intersectInto}. */
727
+ async andNotInto(dest, seg, excludes, options) {
728
+ await this.drainInto(dest, this.andNot(seg, excludes, options), options?.batchSize);
729
+ }
730
+ /**
731
+ * Stream a combine's results into `dest` in bounded batches.
732
+ *
733
+ * `dest` is **added to, not replaced**, and this is **not atomic** — ids land chunk-by-chunk, so a failure
734
+ * mid-stream leaves a partial result, exactly as `addMany` does. Batching keeps memory bounded no matter how
735
+ * large the result is.
736
+ */
737
+ async drainInto(dest, source, batchSizeOption) {
738
+ const batchSize = Math.max(1, batchSizeOption ?? 4096);
624
739
  let batch = [];
625
- for await (const id of this.intersect(segs, options)) {
740
+ for await (const id of source) {
626
741
  batch.push(id);
627
742
  if (batch.length >= batchSize) {
628
743
  await this.addMany(dest, batch);
@@ -631,19 +746,77 @@ var SegmentEngine = class {
631
746
  }
632
747
  if (batch.length > 0) await this.addMany(dest, batch);
633
748
  }
749
+ /**
750
+ * Group ids by chunk, then apply each chunk's whole batch in a single OCC read-modify-write.
751
+ *
752
+ * Accepts a **sync or async** iterable, so an Athena/BigQuery cursor streams straight in without the caller
753
+ * hand-batching `page → addMany(page)`.
754
+ *
755
+ * **The pending ids are held as BITMAPS, not as JS numbers, and that is the load-bearing choice.** Streaming
756
+ * input is unbounded by definition, so something has to give. The obvious answer — buffer N ids, flush them to
757
+ * the backend, repeat — bounds memory and is quietly catastrophic on cost: with ids arriving in arbitrary
758
+ * order, every flush touches nearly every chunk again, so an 11M-id stream at a 1M-id buffer would issue
759
+ * **11x** the OCC round-trips of a single pass. In a library whose headline claim is honest cost, silently
760
+ * multiplying someone's write bill by the size of their input is not a bounded-memory strategy; it is a
761
+ * different bug.
762
+ *
763
+ * So the staging buffer is capped, but it folds into per-chunk **compressed bitmaps** rather than being
764
+ * written out. Peak memory is the roaring representation of the ids seen so far — which is what the segment
765
+ * itself costs, and the floor for any implementation that writes each chunk exactly once — while the backend
766
+ * still sees exactly one read-modify-write per distinct chunk regardless of stream length. This is strictly
767
+ * less memory than the previous sync-only version, which held every remainder as an uncompressed JS number.
768
+ *
769
+ * There is no way to do better without a sorted stream: a chunk can only be written once it is known that no
770
+ * further ids for it will arrive, and unsorted input never gives that guarantee. Callers streaming *very*
771
+ * large sets want `bulkLoadCrbmGeneration` instead — it writes one immutable Cold object rather than tens of
772
+ * thousands of warm rows. The crossover is documented in the guide.
773
+ */
634
774
  async mutateMany(seg, ids, apply) {
635
775
  const byChunk = /* @__PURE__ */ new Map();
636
- for (const id of ids) {
776
+ const staged = /* @__PURE__ */ new Map();
777
+ let pending = 0;
778
+ const tickId = yieldEvery(this.clock, YIELD_EVERY_IDS);
779
+ const compact = () => {
780
+ for (const [chunkKey, rems] of staged) {
781
+ if (rems.length === 0) continue;
782
+ const existing = byChunk.get(chunkKey);
783
+ if (existing === void 0) byChunk.set(chunkKey, this.codec.fromValues(rems));
784
+ else existing.addMany(rems);
785
+ rems.length = 0;
786
+ }
787
+ pending = 0;
788
+ };
789
+ const stage = (id) => {
637
790
  const { chunkKey, remainder } = splitId(id);
638
- const rems = byChunk.get(chunkKey) ?? [];
791
+ let rems = staged.get(chunkKey);
792
+ if (rems === void 0) {
793
+ rems = [];
794
+ staged.set(chunkKey, rems);
795
+ }
639
796
  rems.push(remainder);
640
- byChunk.set(chunkKey, rems);
797
+ pending += 1;
798
+ };
799
+ if (Symbol.asyncIterator in ids) {
800
+ for await (const id of ids) {
801
+ stage(id);
802
+ if (pending >= MUTATE_STAGE_IDS) compact();
803
+ const pause = tickId();
804
+ if (pause !== null) await pause;
805
+ }
806
+ } else {
807
+ for (const id of ids) {
808
+ stage(id);
809
+ if (pending >= MUTATE_STAGE_IDS) compact();
810
+ const pause = tickId();
811
+ if (pause !== null) await pause;
812
+ }
641
813
  }
814
+ compact();
642
815
  await mapWithConcurrency(
643
816
  [...byChunk],
644
817
  this.writeConcurrency,
645
- ([chunkKey, rems]) => this.readModifyWrite(seg, chunkKey, (d) => {
646
- for (const r of rems) apply(d, r);
818
+ ([chunkKey, bitmap]) => this.readModifyWrite(seg, chunkKey, (d) => {
819
+ apply(d, bitmap);
647
820
  })
648
821
  );
649
822
  }
@@ -1772,6 +1945,7 @@ function coldBlobReader(driver, key) {
1772
1945
  }
1773
1946
  var DEFAULT_CURRENT_GEN_TTL_MS = 2e3;
1774
1947
  var BULK_FLUSH_IDS = 1 << 20;
1948
+ var YIELD_EVERY_IDS2 = 1 << 14;
1775
1949
  var DEFAULT_MAX_OPEN_SEGMENTS = 1024;
1776
1950
  var DEFAULT_MAX_OPEN_INDEX_BYTES = 64 * 1024 * 1024;
1777
1951
  var CrbmColdChunkSource = class {
@@ -1990,11 +2164,14 @@ var CrbmColdChunkSource = class {
1990
2164
  };
1991
2165
  function writeCrbmGeneration(driver, key, chunks, options = {}) {
1992
2166
  const sorted = [...chunks].sort((a, b) => a.chunkKey - b.chunkKey);
2167
+ const tick = yieldEvery(options.clock);
1993
2168
  return driver.putImmutable(key, async (sink) => {
1994
2169
  const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
1995
2170
  for (const { chunkKey, bitmap } of sorted) {
1996
2171
  if (bitmap.isEmpty) continue;
1997
2172
  await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
2173
+ const pause = tick();
2174
+ if (pause !== null) await pause;
1998
2175
  }
1999
2176
  await writer.finish();
2000
2177
  });
@@ -2002,6 +2179,7 @@ function writeCrbmGeneration(driver, key, chunks, options = {}) {
2002
2179
  async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
2003
2180
  const chunkKeys = [];
2004
2181
  let cardinality = 0;
2182
+ const tick = yieldEvery(options.clock);
2005
2183
  const { size, sha256 } = await driver.putImmutable(key, async (sink) => {
2006
2184
  const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
2007
2185
  for await (const { chunkKey, bitmap } of chunks) {
@@ -2009,6 +2187,8 @@ async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
2009
2187
  await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
2010
2188
  chunkKeys.push(chunkKey);
2011
2189
  cardinality += bitmap.size;
2190
+ const pause = tick();
2191
+ if (pause !== null) await pause;
2012
2192
  }
2013
2193
  await writer.finish();
2014
2194
  });
@@ -2051,17 +2231,21 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
2051
2231
  const byChunk = /* @__PURE__ */ new Map();
2052
2232
  const pendingByChunk = /* @__PURE__ */ new Map();
2053
2233
  let pending = 0;
2054
- const flushPending = () => {
2234
+ const tickChunk = yieldEvery(options.clock);
2235
+ const tickId = yieldEvery(options.clock, YIELD_EVERY_IDS2);
2236
+ const flushPending = async () => {
2055
2237
  for (const [chunkKey, rems] of pendingByChunk) {
2056
2238
  if (rems.length === 0) continue;
2057
2239
  const existingBitmap = byChunk.get(chunkKey);
2058
2240
  if (existingBitmap === void 0) byChunk.set(chunkKey, codec.fromValues(rems));
2059
2241
  else existingBitmap.addMany(rems);
2060
2242
  rems.length = 0;
2243
+ const pause = tickChunk();
2244
+ if (pause !== null) await pause;
2061
2245
  }
2062
2246
  pending = 0;
2063
2247
  };
2064
- for await (const id of toAsyncIterable(ids)) {
2248
+ const ingest = (id) => {
2065
2249
  const { chunkKey, remainder } = splitId(id);
2066
2250
  let bucket = pendingByChunk.get(chunkKey);
2067
2251
  if (bucket === void 0) {
@@ -2069,14 +2253,29 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
2069
2253
  pendingByChunk.set(chunkKey, bucket);
2070
2254
  }
2071
2255
  bucket.push(remainder);
2072
- if (++pending >= BULK_FLUSH_IDS) flushPending();
2256
+ return ++pending >= BULK_FLUSH_IDS;
2257
+ };
2258
+ if (Symbol.asyncIterator in ids) {
2259
+ for await (const id of ids) {
2260
+ if (ingest(id)) await flushPending();
2261
+ const pause = tickId();
2262
+ if (pause !== null) await pause;
2263
+ }
2264
+ } else {
2265
+ for (const id of ids) {
2266
+ if (ingest(id)) await flushPending();
2267
+ const pause = tickId();
2268
+ if (pause !== null) await pause;
2269
+ }
2073
2270
  }
2074
- flushPending();
2271
+ await flushPending();
2075
2272
  let cardinality = 0;
2076
2273
  const chunks = [];
2077
2274
  for (const [chunkKey, bitmap] of byChunk) {
2078
2275
  cardinality += bitmap.size;
2079
2276
  chunks.push({ chunkKey, bitmap });
2277
+ const pause = tickChunk();
2278
+ if (pause !== null) await pause;
2080
2279
  }
2081
2280
  if (options.keystore !== void 0 && options.registry === void 0) {
2082
2281
  throw new ValidationError("bulk-load encryption requires a registry to store the wrapped DEK");
@@ -2099,7 +2298,10 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
2099
2298
  crypto = { aead: minted.aead, aadFor: (scope) => aadFor(key, key.generation, scope) };
2100
2299
  }
2101
2300
  }
2102
- const { size, sha256 } = await writeCrbmGeneration(driver, key, chunks, { crypto });
2301
+ const { size, sha256 } = await writeCrbmGeneration(driver, key, chunks, {
2302
+ crypto,
2303
+ clock: options.clock
2304
+ });
2103
2305
  if (options.registry !== void 0) {
2104
2306
  const becameCurrent = await publishGeneration(options.registry, key, {
2105
2307
  wrappedDeks: newWrapped
@@ -2115,13 +2317,6 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
2115
2317
  }
2116
2318
  return { size, sha256, chunkCount: chunks.length, cardinality };
2117
2319
  }
2118
- async function* toAsyncIterable(src) {
2119
- if (Symbol.asyncIterator in src) {
2120
- yield* src;
2121
- } else {
2122
- yield* src;
2123
- }
2124
- }
2125
2320
 
2126
2321
  // src/core/compaction.ts
2127
2322
  var DEFAULT_MAX_BITMAP_BYTES2 = 1 << 20;
@@ -4090,6 +4285,7 @@ exports.DEFAULT_MAX_WARM_SCAN_BYTES = DEFAULT_MAX_WARM_SCAN_BYTES;
4090
4285
  exports.DEFAULT_OCC_BACKOFF = DEFAULT_OCC_BACKOFF;
4091
4286
  exports.DEFAULT_PRICING = DEFAULT_PRICING;
4092
4287
  exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
4288
+ exports.DEFAULT_WRITE_CONCURRENCY = DEFAULT_WRITE_CONCURRENCY;
4093
4289
  exports.InProcessKeystore = InProcessKeystore;
4094
4290
  exports.IntegrityError = IntegrityError;
4095
4291
  exports.KeyUnavailableError = KeyUnavailableError;