@cloudbitmaps/roaring 0.2.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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { bulkLoadCrbmGeneration as bulkLoadCrbmGeneration$1, compactSegment as compactSegment$1, runCompactionCycle as runCompactionCycle$1, runExport as runExport$1, CodecBitmap, CodecInterface, IColdDriver, ColdChunkSource, IWarmDriver, IRegistryDriver, IKeystore, Clock, Rng, RetryPolicy, IMetricsSink, BudgetOption, SegmentEngine, SegmentRef, PricingProfile, Workload, Topology, CostReport, IAuditSink, CompactionOptions, CompactionResult, ConsistencyReport, ExportSink, ExportOptions, ExportManifest, EstimateInput } from '@cloudbitmaps/core';
1
+ import { bulkLoadCrbmGeneration as bulkLoadCrbmGeneration$1, compactSegment as compactSegment$1, runCompactionCycle as runCompactionCycle$1, runExport as runExport$1, CodecBitmap, CodecInterface, BudgetOption, IColdDriver, ColdChunkSource, IWarmDriver, IRegistryDriver, IKeystore, Clock, Rng, RetryPolicy, IMetricsSink, SegmentEngine, SegmentRef, PricingProfile, Workload, Topology, CostReport, IAuditSink, CompactionOptions, CompactionResult, ConsistencyReport, ExportSink, ExportOptions, ExportManifest, EstimateInput } from '@cloudbitmaps/core';
2
2
  export * from '@cloudbitmaps/core';
3
3
 
4
4
  /**
@@ -21,7 +21,15 @@ type BulkLoad = typeof bulkLoadCrbmGeneration$1;
21
21
  type CompactSegment = typeof compactSegment$1;
22
22
  type RunCompactionCycle = typeof runCompactionCycle$1;
23
23
  type RunExport = typeof runExport$1;
24
- /** {@link coreBulkLoad} with the roaring codec pre-bound. */
24
+ /**
25
+ * {@link coreBulkLoad} with the roaring codec **and a real clock** pre-bound.
26
+ *
27
+ * The clock is what makes a large load **cooperative**: bulk-load yields the event loop periodically instead of
28
+ * stalling the process for its whole duration (measured at 819 ms for 1M ids before this). `core/` cannot
29
+ * default it — it is timer-free by lint, which is precisely why waiting goes through the `Clock` seam — so the
30
+ * flavor package supplies it, exactly as it supplies the codec. A caller who passes their own clock (a virtual
31
+ * one in a simulation, say) keeps it.
32
+ */
25
33
  declare const bulkLoadCrbmGeneration: BulkLoad;
26
34
  /** {@link coreCompactSegment} with the roaring codec pre-bound. */
27
35
  declare const compactSegment: CompactSegment;
@@ -191,11 +199,33 @@ interface CloudRoaringOptions {
191
199
  */
192
200
  readonly warmReadConsistency?: 'strong' | 'eventual';
193
201
  /**
194
- * Max Warm chunk writes in flight per `addMany`/`removeMany` (the bounded flusher). Default **1** (serial —
195
- * unchanged). Raise it to fan distinct-chunk writes out for throughput on wide multi-chunk batches; each
196
- * chunk is its own OCC row so this is safe, and (as before) a mid-flush failure can leave a partial result.
202
+ * Max Warm chunk writes in flight per `addMany`/`removeMany` (the bounded flusher). Default **4**
203
+ * (`DEFAULT_WRITE_CONCURRENCY`); set it to `1` for strictly serial writes.
204
+ *
205
+ * Distinct chunks are independent OCC rows, so fanning them out cannot make them conflict with each other.
206
+ * The bound exists for the *backend*: a provisioned-capacity store answers a burst by throttling, and 4 stays
207
+ * comfortably inside what the transient-retry path absorbs (measured 8x below the first observed failure).
208
+ * Raise it if your backend is provisioned for the fan-out — on-demand DynamoDB, or a connection pool sized to
209
+ * match. A pool *smaller* than this needs no action: `pg` and `mysql2` both queue rather than reject, so the
210
+ * flusher simply degrades toward serial.
211
+ *
212
+ * As before, a mid-flush failure can leave a **partial result**; `addMany`/`removeMany` are not atomic. What
213
+ * concurrency changes is only how much of the batch may already have landed when the first error surfaces.
197
214
  */
198
215
  readonly writeConcurrency?: number;
216
+ /**
217
+ * Hard ceiling on the warm-delta bytes one segment scan may hold resident. Default **64 MiB**.
218
+ *
219
+ * **A memory bound, deliberately separate from `budget`, and still enforced when `budget: false`.** The
220
+ * budget limits *cost* (billable requests); this limits *memory*. They are different axes, and treating the
221
+ * budget as a memory control is what allowed a segment with thousands of warm chunks to materialise ~12 MB
222
+ * before a `maxRequests: 2` budget could refuse it. It is also the only bound available to `intersect`,
223
+ * whose budget is `common keys × operands` — a product a single wide operand can legitimately exceed in row
224
+ * count while remaining entirely within contract.
225
+ *
226
+ * Raise it for genuinely large segments; it exists to keep a modest container alive, not to second-guess you.
227
+ */
228
+ readonly maxWarmScanBytes?: number;
199
229
  /**
200
230
  * Per-op **denial-of-wallet** budget (Decision #3 / invariant T3): the max backend requests a
201
231
  * single `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` may fan out into before it's refused
@@ -389,6 +419,29 @@ declare class CloudRoaring {
389
419
  * applied one chunk at a time, so if a later chunk fails (e.g. {@link WriteConflictError} after retries)
390
420
  * earlier chunks are already applied. Within a single chunk the update is atomic.
391
421
  */
422
+ /** Options common to every chunk-aligned combine (`intersect` / `union` / `andNot`). */
423
+ interface BaseCombineOptions {
424
+ /** Max chunk keys resolved concurrently — bounds the Cold footprint. A positive integer. */
425
+ readonly concurrency?: number;
426
+ /** Override the store's per-op denial-of-wallet budget for this call (`false` lifts it). */
427
+ readonly budget?: BudgetOption;
428
+ }
429
+ /**
430
+ * A combine that can also subtract.
431
+ *
432
+ * `exclude` is why suppression does not need an intermediate segment: applied here it folds into the same
433
+ * chunk-aligned pass, and each exclude is read **only at the keys that survived** — so a large global opt-out
434
+ * list costs reads proportional to the audience, not to itself.
435
+ */
436
+ interface CombineOptions extends BaseCombineOptions {
437
+ /** Segments whose ids are subtracted from the result. */
438
+ readonly exclude?: Segment[];
439
+ }
440
+ /** {@link CombineOptions} plus the write batching used by the `*Into` variants. */
441
+ interface CombineIntoOptions extends CombineOptions {
442
+ /** Ids buffered per `addMany` while materializing into `dest`. */
443
+ readonly batchSize?: number;
444
+ }
392
445
  declare class Segment {
393
446
  private readonly engine;
394
447
  private readonly ref;
@@ -403,12 +456,24 @@ declare class Segment {
403
456
  private timed;
404
457
  /** Add an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
405
458
  add(id: number): Promise<void>;
406
- /** Add many ids. Not atomic across chunks — see the class note. */
407
- addMany(ids: Iterable<number>): Promise<void>;
459
+ /**
460
+ * Add many ids, from a **sync or async** iterable. Not atomic across chunks — see the class note.
461
+ *
462
+ * An async source means a database cursor streams straight in — `addMany(athenaCursor())` rather than
463
+ * hand-batching `page → addMany(page)`. Ids are grouped by chunk and each chunk is written **exactly once**,
464
+ * however long the stream: pending ids are held compressed, not buffered-and-flushed, so a long stream costs
465
+ * no more backend writes than a short one. Peak memory is the roaring representation of the ids you pass.
466
+ *
467
+ * **For a very large set, reach for `bulkLoadCrbmGeneration` instead.** `addMany` expresses a *delta* and
468
+ * writes one warm row per touched chunk — around 61,000 of them for a set spread across the whole id space.
469
+ * Bulk-load *replaces* the segment with one immutable Cold object. See the guide's cost comparison; picking
470
+ * the wrong one of these is the most expensive mistake available in this library.
471
+ */
472
+ addMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
408
473
  /** Remove an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
409
474
  remove(id: number): Promise<void>;
410
- /** Remove many ids. Not atomic across chunks — see the class note. */
411
- removeMany(ids: Iterable<number>): Promise<void>;
475
+ /** Remove many ids, from a **sync or async** iterable. Not atomic across chunks — see {@link addMany}. */
476
+ removeMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
412
477
  has(id: number): Promise<boolean>;
413
478
  count(): Promise<number>;
414
479
  iterate(): AsyncIterable<number>;
@@ -421,16 +486,44 @@ declare class Segment {
421
486
  * up front, so total memory also carries each operand's Warm size — negligible under Topology-A.) Pass
422
487
  * `budget` to override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
423
488
  */
424
- intersect(others: Segment[], options?: {
425
- concurrency?: number;
426
- budget?: BudgetOption;
427
- }): AsyncIterable<number>;
428
489
  /**
429
- * Materialize `this ∩ others…` **into** `dest` (added, not replaced). Streaming + bounded-memory, but
430
- * **not atomic** across chunks — see {@link addMany}.
490
+ * Map the facade's `Segment` handles in `exclude` down to the plain refs `core` takes. A method rather than
491
+ * a module function because `ref` is class-private — the encapsulation is worth more than the free function.
431
492
  */
432
- intersectInto(dest: Segment, others: Segment[], options?: {
433
- concurrency?: number;
493
+ private refsIn;
494
+ intersect(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
495
+ /**
496
+ * Materialize `this ∩ others…` (minus `exclude`) **into** `dest` (added, not replaced). Streaming +
497
+ * bounded-memory, but **not atomic** across chunks — see {@link addMany}.
498
+ */
499
+ intersectInto(dest: Segment, others: Segment[], options?: CombineIntoOptions): Promise<void>;
500
+ /**
501
+ * `this ∪ others…`, minus `options.exclude` — streamed ascending.
502
+ *
503
+ * **The one composite read with no chunk-skipping,** and that is inherent to union rather than a limitation
504
+ * here: an id in *any* operand belongs to the result, so every chunk of every operand must be read.
505
+ * `intersect` prunes any key missing from any operand; union has nothing to prune. It is charged against the
506
+ * same per-op budget, so a wide union is refused rather than quietly billed — pass `budget` to raise it
507
+ * deliberately. If you find yourself unioning the same segments on every read, materializing the combined
508
+ * segment once (`unionInto`, or a bulk-load rebuild) is the cheaper shape.
509
+ */
510
+ union(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
511
+ /** Materialize `this ∪ others…` (minus `exclude`) **into** `dest`. **Not atomic** — see {@link addMany}. */
512
+ unionInto(dest: Segment, others: Segment[], options?: CombineIntoOptions): Promise<void>;
513
+ /**
514
+ * `this \ (excludes…)` — streamed ascending. Suppression on its own.
515
+ *
516
+ * Reads every chunk of `this` (any of them may survive the subtraction) but each exclude **only where it
517
+ * overlaps this segment**, so the cost tracks the segment being filtered rather than the size of the
518
+ * suppression list. Subtracting a 61,000-chunk global opt-out list from a narrow audience costs a handful of
519
+ * reads, not 61,000.
520
+ *
521
+ * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
522
+ * the suppression folds into the same pass rather than materializing an intermediate segment first.
523
+ */
524
+ andNot(excludes: Segment[], options?: BaseCombineOptions): AsyncIterable<number>;
525
+ /** Materialize `this \ (excludes…)` **into** `dest`. **Not atomic** — see {@link addMany}. */
526
+ andNotInto(dest: Segment, excludes: Segment[], options?: BaseCombineOptions & {
434
527
  batchSize?: number;
435
528
  }): Promise<void>;
436
529
  /**
@@ -446,6 +539,6 @@ declare class Segment {
446
539
  }
447
540
 
448
541
  /** Package version marker. Kept in sync with package.json at release. */
449
- declare const VERSION = "0.2.0";
542
+ declare const VERSION = "0.4.0";
450
543
 
451
- export { CloudRoaring, type CloudRoaringOptions, type EraseSubjectResult, SafeBitmap, Segment, type SegmentOptions, type SubjectErasureEntry, type SubjectReport, type SubjectSegmentRef, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
544
+ export { type BaseCombineOptions, CloudRoaring, type CloudRoaringOptions, type CombineIntoOptions, type CombineOptions, type EraseSubjectResult, SafeBitmap, Segment, type SegmentOptions, type SubjectErasureEntry, type SubjectReport, type SubjectSegmentRef, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { bulkLoadCrbmGeneration as bulkLoadCrbmGeneration$1, compactSegment as compactSegment$1, runCompactionCycle as runCompactionCycle$1, runExport as runExport$1, CodecBitmap, CodecInterface, IColdDriver, ColdChunkSource, IWarmDriver, IRegistryDriver, IKeystore, Clock, Rng, RetryPolicy, IMetricsSink, BudgetOption, SegmentEngine, SegmentRef, PricingProfile, Workload, Topology, CostReport, IAuditSink, CompactionOptions, CompactionResult, ConsistencyReport, ExportSink, ExportOptions, ExportManifest, EstimateInput } from '@cloudbitmaps/core';
1
+ import { bulkLoadCrbmGeneration as bulkLoadCrbmGeneration$1, compactSegment as compactSegment$1, runCompactionCycle as runCompactionCycle$1, runExport as runExport$1, CodecBitmap, CodecInterface, BudgetOption, IColdDriver, ColdChunkSource, IWarmDriver, IRegistryDriver, IKeystore, Clock, Rng, RetryPolicy, IMetricsSink, SegmentEngine, SegmentRef, PricingProfile, Workload, Topology, CostReport, IAuditSink, CompactionOptions, CompactionResult, ConsistencyReport, ExportSink, ExportOptions, ExportManifest, EstimateInput } from '@cloudbitmaps/core';
2
2
  export * from '@cloudbitmaps/core';
3
3
 
4
4
  /**
@@ -21,7 +21,15 @@ type BulkLoad = typeof bulkLoadCrbmGeneration$1;
21
21
  type CompactSegment = typeof compactSegment$1;
22
22
  type RunCompactionCycle = typeof runCompactionCycle$1;
23
23
  type RunExport = typeof runExport$1;
24
- /** {@link coreBulkLoad} with the roaring codec pre-bound. */
24
+ /**
25
+ * {@link coreBulkLoad} with the roaring codec **and a real clock** pre-bound.
26
+ *
27
+ * The clock is what makes a large load **cooperative**: bulk-load yields the event loop periodically instead of
28
+ * stalling the process for its whole duration (measured at 819 ms for 1M ids before this). `core/` cannot
29
+ * default it — it is timer-free by lint, which is precisely why waiting goes through the `Clock` seam — so the
30
+ * flavor package supplies it, exactly as it supplies the codec. A caller who passes their own clock (a virtual
31
+ * one in a simulation, say) keeps it.
32
+ */
25
33
  declare const bulkLoadCrbmGeneration: BulkLoad;
26
34
  /** {@link coreCompactSegment} with the roaring codec pre-bound. */
27
35
  declare const compactSegment: CompactSegment;
@@ -191,11 +199,33 @@ interface CloudRoaringOptions {
191
199
  */
192
200
  readonly warmReadConsistency?: 'strong' | 'eventual';
193
201
  /**
194
- * Max Warm chunk writes in flight per `addMany`/`removeMany` (the bounded flusher). Default **1** (serial —
195
- * unchanged). Raise it to fan distinct-chunk writes out for throughput on wide multi-chunk batches; each
196
- * chunk is its own OCC row so this is safe, and (as before) a mid-flush failure can leave a partial result.
202
+ * Max Warm chunk writes in flight per `addMany`/`removeMany` (the bounded flusher). Default **4**
203
+ * (`DEFAULT_WRITE_CONCURRENCY`); set it to `1` for strictly serial writes.
204
+ *
205
+ * Distinct chunks are independent OCC rows, so fanning them out cannot make them conflict with each other.
206
+ * The bound exists for the *backend*: a provisioned-capacity store answers a burst by throttling, and 4 stays
207
+ * comfortably inside what the transient-retry path absorbs (measured 8x below the first observed failure).
208
+ * Raise it if your backend is provisioned for the fan-out — on-demand DynamoDB, or a connection pool sized to
209
+ * match. A pool *smaller* than this needs no action: `pg` and `mysql2` both queue rather than reject, so the
210
+ * flusher simply degrades toward serial.
211
+ *
212
+ * As before, a mid-flush failure can leave a **partial result**; `addMany`/`removeMany` are not atomic. What
213
+ * concurrency changes is only how much of the batch may already have landed when the first error surfaces.
197
214
  */
198
215
  readonly writeConcurrency?: number;
216
+ /**
217
+ * Hard ceiling on the warm-delta bytes one segment scan may hold resident. Default **64 MiB**.
218
+ *
219
+ * **A memory bound, deliberately separate from `budget`, and still enforced when `budget: false`.** The
220
+ * budget limits *cost* (billable requests); this limits *memory*. They are different axes, and treating the
221
+ * budget as a memory control is what allowed a segment with thousands of warm chunks to materialise ~12 MB
222
+ * before a `maxRequests: 2` budget could refuse it. It is also the only bound available to `intersect`,
223
+ * whose budget is `common keys × operands` — a product a single wide operand can legitimately exceed in row
224
+ * count while remaining entirely within contract.
225
+ *
226
+ * Raise it for genuinely large segments; it exists to keep a modest container alive, not to second-guess you.
227
+ */
228
+ readonly maxWarmScanBytes?: number;
199
229
  /**
200
230
  * Per-op **denial-of-wallet** budget (Decision #3 / invariant T3): the max backend requests a
201
231
  * single `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` may fan out into before it's refused
@@ -389,6 +419,29 @@ declare class CloudRoaring {
389
419
  * applied one chunk at a time, so if a later chunk fails (e.g. {@link WriteConflictError} after retries)
390
420
  * earlier chunks are already applied. Within a single chunk the update is atomic.
391
421
  */
422
+ /** Options common to every chunk-aligned combine (`intersect` / `union` / `andNot`). */
423
+ interface BaseCombineOptions {
424
+ /** Max chunk keys resolved concurrently — bounds the Cold footprint. A positive integer. */
425
+ readonly concurrency?: number;
426
+ /** Override the store's per-op denial-of-wallet budget for this call (`false` lifts it). */
427
+ readonly budget?: BudgetOption;
428
+ }
429
+ /**
430
+ * A combine that can also subtract.
431
+ *
432
+ * `exclude` is why suppression does not need an intermediate segment: applied here it folds into the same
433
+ * chunk-aligned pass, and each exclude is read **only at the keys that survived** — so a large global opt-out
434
+ * list costs reads proportional to the audience, not to itself.
435
+ */
436
+ interface CombineOptions extends BaseCombineOptions {
437
+ /** Segments whose ids are subtracted from the result. */
438
+ readonly exclude?: Segment[];
439
+ }
440
+ /** {@link CombineOptions} plus the write batching used by the `*Into` variants. */
441
+ interface CombineIntoOptions extends CombineOptions {
442
+ /** Ids buffered per `addMany` while materializing into `dest`. */
443
+ readonly batchSize?: number;
444
+ }
392
445
  declare class Segment {
393
446
  private readonly engine;
394
447
  private readonly ref;
@@ -403,12 +456,24 @@ declare class Segment {
403
456
  private timed;
404
457
  /** Add an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
405
458
  add(id: number): Promise<void>;
406
- /** Add many ids. Not atomic across chunks — see the class note. */
407
- addMany(ids: Iterable<number>): Promise<void>;
459
+ /**
460
+ * Add many ids, from a **sync or async** iterable. Not atomic across chunks — see the class note.
461
+ *
462
+ * An async source means a database cursor streams straight in — `addMany(athenaCursor())` rather than
463
+ * hand-batching `page → addMany(page)`. Ids are grouped by chunk and each chunk is written **exactly once**,
464
+ * however long the stream: pending ids are held compressed, not buffered-and-flushed, so a long stream costs
465
+ * no more backend writes than a short one. Peak memory is the roaring representation of the ids you pass.
466
+ *
467
+ * **For a very large set, reach for `bulkLoadCrbmGeneration` instead.** `addMany` expresses a *delta* and
468
+ * writes one warm row per touched chunk — around 61,000 of them for a set spread across the whole id space.
469
+ * Bulk-load *replaces* the segment with one immutable Cold object. See the guide's cost comparison; picking
470
+ * the wrong one of these is the most expensive mistake available in this library.
471
+ */
472
+ addMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
408
473
  /** Remove an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
409
474
  remove(id: number): Promise<void>;
410
- /** Remove many ids. Not atomic across chunks — see the class note. */
411
- removeMany(ids: Iterable<number>): Promise<void>;
475
+ /** Remove many ids, from a **sync or async** iterable. Not atomic across chunks — see {@link addMany}. */
476
+ removeMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
412
477
  has(id: number): Promise<boolean>;
413
478
  count(): Promise<number>;
414
479
  iterate(): AsyncIterable<number>;
@@ -421,16 +486,44 @@ declare class Segment {
421
486
  * up front, so total memory also carries each operand's Warm size — negligible under Topology-A.) Pass
422
487
  * `budget` to override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
423
488
  */
424
- intersect(others: Segment[], options?: {
425
- concurrency?: number;
426
- budget?: BudgetOption;
427
- }): AsyncIterable<number>;
428
489
  /**
429
- * Materialize `this ∩ others…` **into** `dest` (added, not replaced). Streaming + bounded-memory, but
430
- * **not atomic** across chunks — see {@link addMany}.
490
+ * Map the facade's `Segment` handles in `exclude` down to the plain refs `core` takes. A method rather than
491
+ * a module function because `ref` is class-private — the encapsulation is worth more than the free function.
431
492
  */
432
- intersectInto(dest: Segment, others: Segment[], options?: {
433
- concurrency?: number;
493
+ private refsIn;
494
+ intersect(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
495
+ /**
496
+ * Materialize `this ∩ others…` (minus `exclude`) **into** `dest` (added, not replaced). Streaming +
497
+ * bounded-memory, but **not atomic** across chunks — see {@link addMany}.
498
+ */
499
+ intersectInto(dest: Segment, others: Segment[], options?: CombineIntoOptions): Promise<void>;
500
+ /**
501
+ * `this ∪ others…`, minus `options.exclude` — streamed ascending.
502
+ *
503
+ * **The one composite read with no chunk-skipping,** and that is inherent to union rather than a limitation
504
+ * here: an id in *any* operand belongs to the result, so every chunk of every operand must be read.
505
+ * `intersect` prunes any key missing from any operand; union has nothing to prune. It is charged against the
506
+ * same per-op budget, so a wide union is refused rather than quietly billed — pass `budget` to raise it
507
+ * deliberately. If you find yourself unioning the same segments on every read, materializing the combined
508
+ * segment once (`unionInto`, or a bulk-load rebuild) is the cheaper shape.
509
+ */
510
+ union(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
511
+ /** Materialize `this ∪ others…` (minus `exclude`) **into** `dest`. **Not atomic** — see {@link addMany}. */
512
+ unionInto(dest: Segment, others: Segment[], options?: CombineIntoOptions): Promise<void>;
513
+ /**
514
+ * `this \ (excludes…)` — streamed ascending. Suppression on its own.
515
+ *
516
+ * Reads every chunk of `this` (any of them may survive the subtraction) but each exclude **only where it
517
+ * overlaps this segment**, so the cost tracks the segment being filtered rather than the size of the
518
+ * suppression list. Subtracting a 61,000-chunk global opt-out list from a narrow audience costs a handful of
519
+ * reads, not 61,000.
520
+ *
521
+ * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
522
+ * the suppression folds into the same pass rather than materializing an intermediate segment first.
523
+ */
524
+ andNot(excludes: Segment[], options?: BaseCombineOptions): AsyncIterable<number>;
525
+ /** Materialize `this \ (excludes…)` **into** `dest`. **Not atomic** — see {@link addMany}. */
526
+ andNotInto(dest: Segment, excludes: Segment[], options?: BaseCombineOptions & {
434
527
  batchSize?: number;
435
528
  }): Promise<void>;
436
529
  /**
@@ -446,6 +539,6 @@ declare class Segment {
446
539
  }
447
540
 
448
541
  /** Package version marker. Kept in sync with package.json at release. */
449
- declare const VERSION = "0.2.0";
542
+ declare const VERSION = "0.4.0";
450
543
 
451
- export { CloudRoaring, type CloudRoaringOptions, type EraseSubjectResult, SafeBitmap, Segment, type SegmentOptions, type SubjectErasureEntry, type SubjectReport, type SubjectSegmentRef, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
544
+ export { type BaseCombineOptions, CloudRoaring, type CloudRoaringOptions, type CombineIntoOptions, type CombineOptions, type EraseSubjectResult, SafeBitmap, Segment, type SegmentOptions, type SubjectErasureEntry, type SubjectReport, type SubjectSegmentRef, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { IntegrityError, bulkLoadCrbmGeneration as bulkLoadCrbmGeneration$1, compactSegment as compactSegment$1, runCompactionCycle as runCompactionCycle$1, runExport as runExport$1, safeMetrics, NOOP_METRICS, BoundedLru, RetryingWarmDriver, RetryingColdChunkSource, ValidationError, resolveBudget, DEFAULT_BUDGET, SegmentEngine, UnsupportedError, validateSegmentRef, resolvePerOpBudget, splitId, checkBudget, mapWithConcurrency, validateCompactionOptions, runConsistencyCheck, estimateCost, groundedReport, CrbmColdChunkSource } from '@cloudbitmaps/core';
1
+ import { IntegrityError, bulkLoadCrbmGeneration as bulkLoadCrbmGeneration$1, compactSegment as compactSegment$1, runCompactionCycle as runCompactionCycle$1, runExport as runExport$1, safeMetrics, NOOP_METRICS, BoundedLru, RetryingWarmDriver, RetryingColdChunkSource, ValidationError, resolveBudget, DEFAULT_BUDGET, SegmentEngine, UnsupportedError, validateSegmentRef, resolvePerOpBudget, splitId, collectWithinBudget, mapWithConcurrency, validateCompactionOptions, runConsistencyCheck, estimateCost, groundedReport, CrbmColdChunkSource } from '@cloudbitmaps/core';
2
2
  export * from '@cloudbitmaps/core';
3
3
  import roaring from 'roaring';
4
4
 
@@ -92,12 +92,8 @@ var roaringCodec = {
92
92
  fromValues: (values) => SafeBitmap.fromValues(values),
93
93
  safeDeserialize: (bytes, maxBytes) => SafeBitmap.safeDeserialize(bytes, maxBytes)
94
94
  };
95
- var bulkLoadCrbmGeneration = (driver, key, ids, options = {}) => bulkLoadCrbmGeneration$1(driver, key, ids, { ...options, codec: options.codec ?? roaringCodec });
96
- var compactSegment = (ref, deps, options) => compactSegment$1(ref, { ...deps, codec: deps.codec ?? roaringCodec }, options);
97
- var runCompactionCycle = (deps, options) => runCompactionCycle$1({ ...deps, codec: deps.codec ?? roaringCodec }, options);
98
- var runExport = (reader, registry, sink, options = {}) => runExport$1(reader, registry, sink, { ...options, codec: options.codec ?? roaringCodec });
99
95
 
100
- // src/index.ts
96
+ // src/system-clock.ts
101
97
  var SystemClock = class {
102
98
  now() {
103
99
  return Date.now();
@@ -108,7 +104,43 @@ var SystemClock = class {
108
104
  setTimeout(resolve, ms);
109
105
  });
110
106
  }
107
+ /**
108
+ * `setImmediate`, and it has to be — this is the one place where the choice of primitive is the whole feature.
109
+ *
110
+ * A long CPU-bound loop in `core/` (bulk-load is the one that matters) periodically hands the loop back so a
111
+ * co-resident HTTP server keeps answering. Measured over a 61,035-chunk load, yielding every 1,024 chunks:
112
+ *
113
+ * ```text
114
+ * no yield 568 ms wall 568.0 ms worst event-loop gap
115
+ * sleep(0) 555 ms wall 555.2 ms ← a microtask. Yields NOTHING.
116
+ * setImmediate 569 ms wall 13.8 ms ← free, and 41x less starvation
117
+ * setTimeout(1) 625 ms wall 15.0 ms ← +10% wall for no extra relief
118
+ * ```
119
+ *
120
+ * `sleep(0)` resolving on a microtask is correct for its own contract and useless for this: microtasks drain
121
+ * before the loop ever advances a phase, so `await sleep(0)` in a tight loop never lets a single pending I/O
122
+ * callback run. `setImmediate` fires in the check phase *after* pending I/O, which is exactly the ordering a
123
+ * server wants — inbound requests are serviced before the loop resumes chewing.
124
+ *
125
+ * A timer is not ref'd here (unlike {@link sleep}): a yield expresses "someone else may go first", never
126
+ * "there is outstanding work" — and `setImmediate` cannot outlive the turn that scheduled it anyway.
127
+ */
128
+ yieldNow() {
129
+ return new Promise((resolve) => {
130
+ setImmediate(resolve);
131
+ });
132
+ }
111
133
  };
134
+ var bulkLoadCrbmGeneration = (driver, key, ids, options = {}) => bulkLoadCrbmGeneration$1(driver, key, ids, {
135
+ ...options,
136
+ codec: options.codec ?? roaringCodec,
137
+ clock: options.clock ?? new SystemClock()
138
+ });
139
+ var compactSegment = (ref, deps, options) => compactSegment$1(ref, { ...deps, codec: deps.codec ?? roaringCodec }, options);
140
+ var runCompactionCycle = (deps, options) => runCompactionCycle$1({ ...deps, codec: deps.codec ?? roaringCodec }, options);
141
+ var runExport = (reader, registry, sink, options = {}) => runExport$1(reader, registry, sink, { ...options, codec: options.codec ?? roaringCodec });
142
+
143
+ // src/index.ts
112
144
  var SystemRng = class {
113
145
  next() {
114
146
  return Math.random();
@@ -228,6 +260,7 @@ var CloudRoaring = class {
228
260
  metrics,
229
261
  warmReadConsistency: options.warmReadConsistency,
230
262
  writeConcurrency: options.writeConcurrency,
263
+ maxWarmScanBytes: options.maxWarmScanBytes,
231
264
  budget: this.budget
232
265
  };
233
266
  this.engine = new SegmentEngine(deps);
@@ -295,9 +328,11 @@ var CloudRoaring = class {
295
328
  validateConcurrency(options.concurrency);
296
329
  const budget = resolvePerOpBudget(options.budget, this.budget);
297
330
  splitId(id);
298
- const recs = [];
299
- for await (const rec of registry.list(options.namespace)) recs.push(rec);
300
- checkBudget(budget, recs.length, "subjectReport");
331
+ const recs = await collectWithinBudget(
332
+ registry.list(options.namespace),
333
+ budget,
334
+ "subjectReport"
335
+ );
301
336
  const membership = await mapWithConcurrency(
302
337
  recs,
303
338
  options.concurrency ?? DEFAULT_ADMIN_CONCURRENCY,
@@ -352,9 +387,11 @@ var CloudRoaring = class {
352
387
  validateConcurrency(options.concurrency);
353
388
  const budget = resolvePerOpBudget(options.budget, this.budget);
354
389
  splitId(id);
355
- const recs = [];
356
- for await (const rec of compaction.registry.list(options.namespace)) recs.push(rec);
357
- checkBudget(budget, recs.length, "eraseSubject");
390
+ const recs = await collectWithinBudget(
391
+ compaction.registry.list(options.namespace),
392
+ budget,
393
+ "eraseSubject"
394
+ );
358
395
  const entries = await mapWithConcurrency(
359
396
  recs,
360
397
  options.concurrency ?? DEFAULT_ADMIN_CONCURRENCY,
@@ -481,7 +518,19 @@ var Segment = class {
481
518
  add(id) {
482
519
  return this.timed("add", () => this.engine.add(this.ref, id));
483
520
  }
484
- /** Add many ids. Not atomic across chunks — see the class note. */
521
+ /**
522
+ * Add many ids, from a **sync or async** iterable. Not atomic across chunks — see the class note.
523
+ *
524
+ * An async source means a database cursor streams straight in — `addMany(athenaCursor())` rather than
525
+ * hand-batching `page → addMany(page)`. Ids are grouped by chunk and each chunk is written **exactly once**,
526
+ * however long the stream: pending ids are held compressed, not buffered-and-flushed, so a long stream costs
527
+ * no more backend writes than a short one. Peak memory is the roaring representation of the ids you pass.
528
+ *
529
+ * **For a very large set, reach for `bulkLoadCrbmGeneration` instead.** `addMany` expresses a *delta* and
530
+ * writes one warm row per touched chunk — around 61,000 of them for a set spread across the whole id space.
531
+ * Bulk-load *replaces* the segment with one immutable Cold object. See the guide's cost comparison; picking
532
+ * the wrong one of these is the most expensive mistake available in this library.
533
+ */
485
534
  addMany(ids) {
486
535
  return this.timed("addMany", () => this.engine.addMany(this.ref, ids));
487
536
  }
@@ -489,7 +538,7 @@ var Segment = class {
489
538
  remove(id) {
490
539
  return this.timed("remove", () => this.engine.remove(this.ref, id));
491
540
  }
492
- /** Remove many ids. Not atomic across chunks — see the class note. */
541
+ /** Remove many ids, from a **sync or async** iterable. Not atomic across chunks — see {@link addMany}. */
493
542
  removeMany(ids) {
494
543
  return this.timed("removeMany", () => this.engine.removeMany(this.ref, ids));
495
544
  }
@@ -511,17 +560,84 @@ var Segment = class {
511
560
  * up front, so total memory also carries each operand's Warm size — negligible under Topology-A.) Pass
512
561
  * `budget` to override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
513
562
  */
563
+ /**
564
+ * Map the facade's `Segment` handles in `exclude` down to the plain refs `core` takes. A method rather than
565
+ * a module function because `ref` is class-private — the encapsulation is worth more than the free function.
566
+ */
567
+ refsIn(options) {
568
+ if (options === void 0) return void 0;
569
+ const { exclude, ...rest } = options;
570
+ return exclude === void 0 ? rest : { ...rest, exclude: exclude.map((o) => o.ref) };
571
+ }
514
572
  intersect(others, options) {
515
- return this.engine.intersect([this.ref, ...others.map((o) => o.ref)], options);
573
+ return this.engine.intersect([this.ref, ...others.map((o) => o.ref)], this.refsIn(options));
516
574
  }
517
575
  /**
518
- * Materialize `this ∩ others…` **into** `dest` (added, not replaced). Streaming + bounded-memory, but
519
- * **not atomic** across chunks — see {@link addMany}.
576
+ * Materialize `this ∩ others…` (minus `exclude`) **into** `dest` (added, not replaced). Streaming +
577
+ * bounded-memory, but **not atomic** across chunks — see {@link addMany}.
520
578
  */
521
579
  intersectInto(dest, others, options) {
522
580
  return this.timed(
523
581
  "intersectInto",
524
- () => this.engine.intersectInto(dest.ref, [this.ref, ...others.map((o) => o.ref)], options)
582
+ () => this.engine.intersectInto(
583
+ dest.ref,
584
+ [this.ref, ...others.map((o) => o.ref)],
585
+ this.refsIn(options)
586
+ )
587
+ );
588
+ }
589
+ /**
590
+ * `this ∪ others…`, minus `options.exclude` — streamed ascending.
591
+ *
592
+ * **The one composite read with no chunk-skipping,** and that is inherent to union rather than a limitation
593
+ * here: an id in *any* operand belongs to the result, so every chunk of every operand must be read.
594
+ * `intersect` prunes any key missing from any operand; union has nothing to prune. It is charged against the
595
+ * same per-op budget, so a wide union is refused rather than quietly billed — pass `budget` to raise it
596
+ * deliberately. If you find yourself unioning the same segments on every read, materializing the combined
597
+ * segment once (`unionInto`, or a bulk-load rebuild) is the cheaper shape.
598
+ */
599
+ union(others, options) {
600
+ return this.engine.union([this.ref, ...others.map((o) => o.ref)], this.refsIn(options));
601
+ }
602
+ /** Materialize `this ∪ others…` (minus `exclude`) **into** `dest`. **Not atomic** — see {@link addMany}. */
603
+ unionInto(dest, others, options) {
604
+ return this.timed(
605
+ "unionInto",
606
+ () => this.engine.unionInto(
607
+ dest.ref,
608
+ [this.ref, ...others.map((o) => o.ref)],
609
+ this.refsIn(options)
610
+ )
611
+ );
612
+ }
613
+ /**
614
+ * `this \ (excludes…)` — streamed ascending. Suppression on its own.
615
+ *
616
+ * Reads every chunk of `this` (any of them may survive the subtraction) but each exclude **only where it
617
+ * overlaps this segment**, so the cost tracks the segment being filtered rather than the size of the
618
+ * suppression list. Subtracting a 61,000-chunk global opt-out list from a narrow audience costs a handful of
619
+ * reads, not 61,000.
620
+ *
621
+ * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
622
+ * the suppression folds into the same pass rather than materializing an intermediate segment first.
623
+ */
624
+ andNot(excludes, options) {
625
+ return this.engine.andNot(
626
+ this.ref,
627
+ excludes.map((o) => o.ref),
628
+ options
629
+ );
630
+ }
631
+ /** Materialize `this \ (excludes…)` **into** `dest`. **Not atomic** — see {@link addMany}. */
632
+ andNotInto(dest, excludes, options) {
633
+ return this.timed(
634
+ "andNotInto",
635
+ () => this.engine.andNotInto(
636
+ dest.ref,
637
+ this.ref,
638
+ excludes.map((o) => o.ref),
639
+ options
640
+ )
525
641
  );
526
642
  }
527
643
  /**
@@ -542,7 +658,7 @@ var Segment = class {
542
658
  });
543
659
  }
544
660
  };
545
- var VERSION = "0.2.0";
661
+ var VERSION = "0.4.0";
546
662
 
547
663
  export { CloudRoaring, SafeBitmap, Segment, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
548
664
  //# sourceMappingURL=index.js.map