@cloudbitmaps/core 0.1.3 → 0.3.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
@@ -171,6 +171,18 @@ function checkBudget(budget, projectedRequests, op) {
171
171
  );
172
172
  }
173
173
  }
174
+ async function collectWithinBudget(source, budget, op) {
175
+ const out = [];
176
+ for await (const item of source) {
177
+ out.push(item);
178
+ if (budget !== null && out.length > budget.maxRequests) {
179
+ throw new BudgetExceededError(
180
+ `${op} would fan out to more than ${budget.maxRequests} units, over the per-op budget \u2014 the scan was abandoned at that point rather than completed, so no exact total is available. Raise \`budget.maxRequests\`, override it per-op, or set \`budget: false\``
181
+ );
182
+ }
183
+ }
184
+ return out;
185
+ }
174
186
 
175
187
  // src/core/concurrency.ts
176
188
  async function mapWithConcurrency(items, limit, fn) {
@@ -397,6 +409,7 @@ var ZeroRng = class {
397
409
  return 0;
398
410
  }
399
411
  };
412
+ var DEFAULT_MAX_WARM_SCAN_BYTES = 64 * 1024 * 1024;
400
413
  var SegmentEngine = class {
401
414
  warm;
402
415
  cold;
@@ -412,6 +425,7 @@ var SegmentEngine = class {
412
425
  /** Consistency for READ-path Warm fetches (precomputed; OCC RMW passes strong explicitly, see `warmGet`). */
413
426
  readOpts;
414
427
  writeConcurrency;
428
+ maxWarmScanBytes;
415
429
  /** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. See {@link checkBudget}. */
416
430
  budget;
417
431
  constructor(deps) {
@@ -426,6 +440,12 @@ var SegmentEngine = class {
426
440
  this.occBackoff = deps.occBackoff ?? DEFAULT_OCC_BACKOFF;
427
441
  this.readOpts = { consistent: (deps.warmReadConsistency ?? "strong") !== "eventual" };
428
442
  this.writeConcurrency = deps.writeConcurrency ?? 1;
443
+ this.maxWarmScanBytes = deps.maxWarmScanBytes ?? DEFAULT_MAX_WARM_SCAN_BYTES;
444
+ if (!Number.isFinite(this.maxWarmScanBytes) || this.maxWarmScanBytes < 1) {
445
+ throw new ValidationError(
446
+ `maxWarmScanBytes must be a finite number >= 1 (got ${String(deps.maxWarmScanBytes)})`
447
+ );
448
+ }
429
449
  this.budget = deps.budget === void 0 ? DEFAULT_BUDGET : deps.budget;
430
450
  this.metrics = safeMetrics(deps.metrics ?? NOOP_METRICS);
431
451
  this.metricsOn = this.metrics !== NOOP_METRICS;
@@ -457,7 +477,7 @@ var SegmentEngine = class {
457
477
  return cold ? cold.has(remainder) : false;
458
478
  }
459
479
  async count(seg) {
460
- const warmRows = await this.collectWarm(seg);
480
+ const warmRows = await this.collectWarm(seg, "count", this.budget?.maxRequests ?? null);
461
481
  const cardinalities = this.cold.cardinalities ? await this.cold.cardinalities(seg) : null;
462
482
  if (!cardinalities) {
463
483
  const chunkKeys = await this.chunkKeys(seg, warmRows);
@@ -489,7 +509,7 @@ var SegmentEngine = class {
489
509
  return this.cold.sizeOf ? this.cold.sizeOf(seg) : Promise.resolve(null);
490
510
  }
491
511
  async *iterate(seg) {
492
- const warmRows = await this.collectWarm(seg);
512
+ const warmRows = await this.collectWarm(seg, "iterate", this.budget?.maxRequests ?? null);
493
513
  const chunkKeys = await this.chunkKeys(seg, warmRows);
494
514
  checkBudget(this.budget, chunkKeys.length, "iterate");
495
515
  const gen = await this.currentGen(seg);
@@ -536,7 +556,7 @@ var SegmentEngine = class {
536
556
  const budget = resolvePerOpBudget(options?.budget, this.budget);
537
557
  const operands = await Promise.all(
538
558
  segs.map(async (seg) => {
539
- const warmRows = await this.collectWarm(seg);
559
+ const warmRows = await this.collectWarm(seg, "intersect", null);
540
560
  const keys = await this.chunkKeys(seg, warmRows);
541
561
  const gen = await this.currentGen(seg);
542
562
  return { seg, warmRows, keys: new Set(keys), gen };
@@ -677,12 +697,22 @@ var SegmentEngine = class {
677
697
  }
678
698
  return row;
679
699
  }
680
- async collectWarm(seg) {
700
+ async collectWarm(seg, op, rowCeiling) {
681
701
  const rows = /* @__PURE__ */ new Map();
682
702
  let bytes = 0;
683
703
  for await (const row of this.warm.listChunks(seg, this.readOpts)) {
684
704
  rows.set(row.chunkKey, row.bytes);
685
705
  bytes += row.bytes.length;
706
+ if (bytes > this.maxWarmScanBytes) {
707
+ throw new BudgetExceededError(
708
+ `${op} would hold more than ${this.maxWarmScanBytes} bytes of warm deltas resident for one segment \u2014 the scan was abandoned there rather than completed. Raise \`maxWarmScanBytes\`, narrow the segment, or compact it so fewer chunks carry warm deltas. (This ceiling is independent of \`budget\` and stays in force when \`budget: false\`.)`
709
+ );
710
+ }
711
+ if (rowCeiling !== null && rows.size > rowCeiling) {
712
+ throw new BudgetExceededError(
713
+ `${op} would read more than ${rowCeiling} warm chunk rows, over the per-op budget \u2014 the scan was abandoned at that point rather than completed, so no exact total is available. Raise \`budget.maxRequests\`, override it per-op, or set \`budget: false\``
714
+ );
715
+ }
686
716
  }
687
717
  if (this.metricsOn) {
688
718
  this.metrics.onEvent({
@@ -1193,22 +1223,44 @@ function validateChunkRef(ref) {
1193
1223
  }
1194
1224
 
1195
1225
  // src/core/crbm/crc32c.ts
1196
- var TABLE = (() => {
1226
+ var TABLES = (() => {
1197
1227
  const POLY = 2197175160;
1198
- const table = new Uint32Array(256);
1228
+ const t0 = new Uint32Array(256);
1199
1229
  for (let n = 0; n < 256; n++) {
1200
1230
  let c = n;
1201
- for (let k = 0; k < 8; k++) {
1202
- c = c & 1 ? POLY ^ c >>> 1 : c >>> 1;
1231
+ for (let k = 0; k < 8; k++) c = c & 1 ? POLY ^ c >>> 1 : c >>> 1;
1232
+ t0[n] = c >>> 0;
1233
+ }
1234
+ const tables = [t0];
1235
+ for (let k = 1; k < 8; k++) {
1236
+ const prev = tables[k - 1];
1237
+ const next = new Uint32Array(256);
1238
+ for (let n = 0; n < 256; n++) {
1239
+ const v = prev[n];
1240
+ next[n] = (v >>> 8 ^ t0[v & 255]) >>> 0;
1203
1241
  }
1204
- table[n] = c >>> 0;
1242
+ tables.push(next);
1205
1243
  }
1206
- return table;
1244
+ return tables;
1207
1245
  })();
1246
+ var T0 = TABLES[0];
1247
+ var T1 = TABLES[1];
1248
+ var T2 = TABLES[2];
1249
+ var T3 = TABLES[3];
1250
+ var T4 = TABLES[4];
1251
+ var T5 = TABLES[5];
1252
+ var T6 = TABLES[6];
1253
+ var T7 = TABLES[7];
1208
1254
  function crc32c(bytes, seed = 0) {
1209
1255
  let crc = (seed ^ 4294967295) >>> 0;
1210
- for (let i = 0; i < bytes.length; i++) {
1211
- crc = (TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8) >>> 0;
1256
+ let i = 0;
1257
+ const n8 = bytes.length - bytes.length % 8;
1258
+ for (; i < n8; i += 8) {
1259
+ crc ^= bytes[i] | bytes[i + 1] << 8 | bytes[i + 2] << 16 | bytes[i + 3] << 24;
1260
+ crc = (T7[crc & 255] ^ T6[crc >>> 8 & 255] ^ T5[crc >>> 16 & 255] ^ T4[crc >>> 24 & 255] ^ T3[bytes[i + 4]] ^ T2[bytes[i + 5]] ^ T1[bytes[i + 6]] ^ T0[bytes[i + 7]]) >>> 0;
1261
+ }
1262
+ for (; i < bytes.length; i++) {
1263
+ crc = (T0[(crc ^ bytes[i]) & 255] ^ crc >>> 8) >>> 0;
1212
1264
  }
1213
1265
  return (crc ^ 4294967295) >>> 0;
1214
1266
  }
@@ -1719,6 +1771,7 @@ function coldBlobReader(driver, key) {
1719
1771
  };
1720
1772
  }
1721
1773
  var DEFAULT_CURRENT_GEN_TTL_MS = 2e3;
1774
+ var BULK_FLUSH_IDS = 1 << 20;
1722
1775
  var DEFAULT_MAX_OPEN_SEGMENTS = 1024;
1723
1776
  var DEFAULT_MAX_OPEN_INDEX_BYTES = 64 * 1024 * 1024;
1724
1777
  var CrbmColdChunkSource = class {
@@ -1996,15 +2049,29 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
1996
2049
  }
1997
2050
  const codec = requireCodec(options.codec, "bulkLoadCrbmGeneration");
1998
2051
  const byChunk = /* @__PURE__ */ new Map();
2052
+ const pendingByChunk = /* @__PURE__ */ new Map();
2053
+ let pending = 0;
2054
+ const flushPending = () => {
2055
+ for (const [chunkKey, rems] of pendingByChunk) {
2056
+ if (rems.length === 0) continue;
2057
+ const existingBitmap = byChunk.get(chunkKey);
2058
+ if (existingBitmap === void 0) byChunk.set(chunkKey, codec.fromValues(rems));
2059
+ else existingBitmap.addMany(rems);
2060
+ rems.length = 0;
2061
+ }
2062
+ pending = 0;
2063
+ };
1999
2064
  for await (const id of toAsyncIterable(ids)) {
2000
2065
  const { chunkKey, remainder } = splitId(id);
2001
- let bitmap = byChunk.get(chunkKey);
2002
- if (bitmap === void 0) {
2003
- bitmap = codec.empty();
2004
- byChunk.set(chunkKey, bitmap);
2066
+ let bucket = pendingByChunk.get(chunkKey);
2067
+ if (bucket === void 0) {
2068
+ bucket = [];
2069
+ pendingByChunk.set(chunkKey, bucket);
2005
2070
  }
2006
- bitmap.add(remainder);
2071
+ bucket.push(remainder);
2072
+ if (++pending >= BULK_FLUSH_IDS) flushPending();
2007
2073
  }
2074
+ flushPending();
2008
2075
  let cardinality = 0;
2009
2076
  const chunks = [];
2010
2077
  for (const [chunkKey, bitmap] of byChunk) {
@@ -3788,6 +3855,7 @@ async function eraseWarm(warm, ref) {
3788
3855
  }
3789
3856
 
3790
3857
  // src/core/consistency.ts
3858
+ var DEFAULT_MAX_SCAN_SEGMENTS = 25e4;
3791
3859
  var DEFAULT_CHECK_CONCURRENCY = 8;
3792
3860
  async function listGenerations(cold, ref) {
3793
3861
  const present = /* @__PURE__ */ new Set();
@@ -3799,8 +3867,21 @@ async function runConsistencyCheck(deps, options = {}) {
3799
3867
  if (!Number.isInteger(concurrency) || concurrency < 1) {
3800
3868
  throw new ValidationError(`concurrency must be a positive integer; got ${concurrency}`);
3801
3869
  }
3870
+ const maxScanSegments = options.maxScanSegments ?? DEFAULT_MAX_SCAN_SEGMENTS;
3871
+ if (!Number.isFinite(maxScanSegments) || maxScanSegments < 1) {
3872
+ throw new ValidationError(
3873
+ `maxScanSegments must be a finite number >= 1; got ${String(options.maxScanSegments)}`
3874
+ );
3875
+ }
3802
3876
  const recs = [];
3803
- for await (const rec of deps.registry.list(options.namespace)) recs.push(rec);
3877
+ for await (const rec of deps.registry.list(options.namespace)) {
3878
+ recs.push(rec);
3879
+ if (recs.length > maxScanSegments) {
3880
+ throw new BudgetExceededError(
3881
+ `checkConsistency would enumerate more than ${maxScanSegments} segments \u2014 the scan was abandoned there rather than completed. Narrow it with \`namespace\`, or raise \`maxScanSegments\` if the fleet really is that large and the memory is available.`
3882
+ );
3883
+ }
3884
+ }
3804
3885
  const results = await mapWithConcurrency(recs, concurrency, async (rec) => {
3805
3886
  if (rec.status === "destroyed") return { kind: "ok" };
3806
3887
  const ref = { segment: rec.segment, namespace: rec.namespace };
@@ -4004,6 +4085,8 @@ exports.CrbmColdChunkSource = CrbmColdChunkSource;
4004
4085
  exports.CrbmReader = CrbmReader;
4005
4086
  exports.CrbmWriter = CrbmWriter;
4006
4087
  exports.DEFAULT_BUDGET = DEFAULT_BUDGET;
4088
+ exports.DEFAULT_MAX_SCAN_SEGMENTS = DEFAULT_MAX_SCAN_SEGMENTS;
4089
+ exports.DEFAULT_MAX_WARM_SCAN_BYTES = DEFAULT_MAX_WARM_SCAN_BYTES;
4007
4090
  exports.DEFAULT_OCC_BACKOFF = DEFAULT_OCC_BACKOFF;
4008
4091
  exports.DEFAULT_PRICING = DEFAULT_PRICING;
4009
4092
  exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
@@ -4037,6 +4120,7 @@ exports.aadFor = aadFor;
4037
4120
  exports.bulkLoadCrbmGeneration = bulkLoadCrbmGeneration;
4038
4121
  exports.checkBudget = checkBudget;
4039
4122
  exports.chunkRefKey = chunkRefKey;
4123
+ exports.collectWithinBudget = collectWithinBudget;
4040
4124
  exports.compactSegment = compactSegment;
4041
4125
  exports.destroySegment = destroySegment;
4042
4126
  exports.eraseNamespace = eraseNamespace;