@cloudbitmaps/roaring 0.3.0 → 0.4.1

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,16 @@ 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 end-to-end at 450 ms of unbroken blocking for a 1M-id
29
+ * load, now ~19 ms worst case). `core/` cannot
30
+ * default it — it is timer-free by lint, which is precisely why waiting goes through the `Clock` seam — so the
31
+ * flavor package supplies it, exactly as it supplies the codec. A caller who passes their own clock (a virtual
32
+ * one in a simulation, say) keeps it.
33
+ */
25
34
  declare const bulkLoadCrbmGeneration: BulkLoad;
26
35
  /** {@link coreCompactSegment} with the roaring codec pre-bound. */
27
36
  declare const compactSegment: CompactSegment;
@@ -191,9 +200,18 @@ interface CloudRoaringOptions {
191
200
  */
192
201
  readonly warmReadConsistency?: 'strong' | 'eventual';
193
202
  /**
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.
203
+ * Max Warm chunk writes in flight per `addMany`/`removeMany` (the bounded flusher). Default **4**
204
+ * (`DEFAULT_WRITE_CONCURRENCY`); set it to `1` for strictly serial writes.
205
+ *
206
+ * Distinct chunks are independent OCC rows, so fanning them out cannot make them conflict with each other.
207
+ * The bound exists for the *backend*: a provisioned-capacity store answers a burst by throttling, and 4 stays
208
+ * comfortably inside what the transient-retry path absorbs (measured 8x below the first observed failure).
209
+ * Raise it if your backend is provisioned for the fan-out — on-demand DynamoDB, or a connection pool sized to
210
+ * match. A pool *smaller* than this needs no action: `pg` and `mysql2` both queue rather than reject, so the
211
+ * flusher simply degrades toward serial.
212
+ *
213
+ * As before, a mid-flush failure can leave a **partial result**; `addMany`/`removeMany` are not atomic. What
214
+ * concurrency changes is only how much of the batch may already have landed when the first error surfaces.
197
215
  */
198
216
  readonly writeConcurrency?: number;
199
217
  /**
@@ -393,15 +411,38 @@ declare class CloudRoaring {
393
411
  static estimateCost(input: EstimateInput): CostReport;
394
412
  }
395
413
  /**
396
- * A handle bound to one segment — the seven ops.
414
+ * A handle bound to one segment — the read/write ops.
397
415
  *
398
416
  * **IDs must be integers in `[0, 2^32)`** (dense 32-bit). A non-integer / negative / out-of-range id
399
417
  * throws {@link ValidationError}.
400
418
  *
401
- * **`addMany`/`removeMany`/`intersectInto` are not atomic across chunks**: ids are grouped by chunk and
419
+ * **`addMany`/`removeMany`/`intersectInto`/`unionInto`/`andNotInto` are not atomic across chunks**: ids are grouped by chunk and
402
420
  * applied one chunk at a time, so if a later chunk fails (e.g. {@link WriteConflictError} after retries)
403
421
  * earlier chunks are already applied. Within a single chunk the update is atomic.
404
422
  */
423
+ /** Options common to every chunk-aligned combine (`intersect` / `union` / `andNot`). */
424
+ interface BaseCombineOptions {
425
+ /** Max chunk keys resolved concurrently — bounds the Cold footprint. A positive integer. */
426
+ readonly concurrency?: number;
427
+ /** Override the store's per-op denial-of-wallet budget for this call (`false` lifts it). */
428
+ readonly budget?: BudgetOption;
429
+ }
430
+ /**
431
+ * A combine that can also subtract.
432
+ *
433
+ * `exclude` is why suppression does not need an intermediate segment: applied here it folds into the same
434
+ * chunk-aligned pass, and each exclude is read **only at the keys that survived** — so a large global opt-out
435
+ * list costs reads proportional to the audience, not to itself.
436
+ */
437
+ interface CombineOptions extends BaseCombineOptions {
438
+ /** Segments whose ids are subtracted from the result. */
439
+ readonly exclude?: Segment[];
440
+ }
441
+ /** {@link CombineOptions} plus the write batching used by the `*Into` variants. */
442
+ interface CombineIntoOptions extends CombineOptions {
443
+ /** Ids buffered per `addMany` while materializing into `dest`. */
444
+ readonly batchSize?: number;
445
+ }
405
446
  declare class Segment {
406
447
  private readonly engine;
407
448
  private readonly ref;
@@ -416,12 +457,24 @@ declare class Segment {
416
457
  private timed;
417
458
  /** Add an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
418
459
  add(id: number): Promise<void>;
419
- /** Add many ids. Not atomic across chunks — see the class note. */
420
- addMany(ids: Iterable<number>): Promise<void>;
460
+ /**
461
+ * Add many ids, from a **sync or async** iterable. Not atomic across chunks — see the class note.
462
+ *
463
+ * An async source means a database cursor streams straight in — `addMany(athenaCursor())` rather than
464
+ * hand-batching `page → addMany(page)`. Ids are grouped by chunk and each chunk is written **exactly once**,
465
+ * however long the stream: pending ids are held compressed, not buffered-and-flushed, so a long stream costs
466
+ * no more backend writes than a short one. Peak memory is the roaring representation of the ids you pass.
467
+ *
468
+ * **For a very large set, reach for `bulkLoadCrbmGeneration` instead.** `addMany` expresses a *delta* and
469
+ * writes one warm row per touched chunk — around 61,000 of them for a set spread across the whole id space.
470
+ * Bulk-load *replaces* the segment with one immutable Cold object. See the guide's cost comparison; picking
471
+ * the wrong one of these is the most expensive mistake available in this library.
472
+ */
473
+ addMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
421
474
  /** Remove an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
422
475
  remove(id: number): Promise<void>;
423
- /** Remove many ids. Not atomic across chunks — see the class note. */
424
- removeMany(ids: Iterable<number>): Promise<void>;
476
+ /** Remove many ids, from a **sync or async** iterable. Not atomic across chunks — see {@link addMany}. */
477
+ removeMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
425
478
  has(id: number): Promise<boolean>;
426
479
  count(): Promise<number>;
427
480
  iterate(): AsyncIterable<number>;
@@ -434,16 +487,44 @@ declare class Segment {
434
487
  * up front, so total memory also carries each operand's Warm size — negligible under Topology-A.) Pass
435
488
  * `budget` to override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
436
489
  */
437
- intersect(others: Segment[], options?: {
438
- concurrency?: number;
439
- budget?: BudgetOption;
440
- }): AsyncIterable<number>;
441
490
  /**
442
- * Materialize `this ∩ others…` **into** `dest` (added, not replaced). Streaming + bounded-memory, but
443
- * **not atomic** across chunks — see {@link addMany}.
491
+ * Map the facade's `Segment` handles in `exclude` down to the plain refs `core` takes. A method rather than
492
+ * a module function because `ref` is class-private — the encapsulation is worth more than the free function.
444
493
  */
445
- intersectInto(dest: Segment, others: Segment[], options?: {
446
- concurrency?: number;
494
+ private refsIn;
495
+ intersect(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
496
+ /**
497
+ * Materialize `this ∩ others…` (minus `exclude`) **into** `dest` (added, not replaced). Streaming +
498
+ * bounded-memory, but **not atomic** across chunks — see {@link addMany}.
499
+ */
500
+ intersectInto(dest: Segment, others: Segment[], options?: CombineIntoOptions): Promise<void>;
501
+ /**
502
+ * `this ∪ others…`, minus `options.exclude` — streamed ascending.
503
+ *
504
+ * **The one composite read with no chunk-skipping,** and that is inherent to union rather than a limitation
505
+ * here: an id in *any* operand belongs to the result, so every chunk of every operand must be read.
506
+ * `intersect` prunes any key missing from any operand; union has nothing to prune. It is charged against the
507
+ * same per-op budget, so a wide union is refused rather than quietly billed — pass `budget` to raise it
508
+ * deliberately. If you find yourself unioning the same segments on every read, materializing the combined
509
+ * segment once (`unionInto`, or a bulk-load rebuild) is the cheaper shape.
510
+ */
511
+ union(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
512
+ /** Materialize `this ∪ others…` (minus `exclude`) **into** `dest`. **Not atomic** — see {@link addMany}. */
513
+ unionInto(dest: Segment, others: Segment[], options?: CombineIntoOptions): Promise<void>;
514
+ /**
515
+ * `this \ (excludes…)` — streamed ascending. Suppression on its own.
516
+ *
517
+ * Reads every chunk of `this` (any of them may survive the subtraction) but each exclude **only where it
518
+ * overlaps this segment**, so the cost tracks the segment being filtered rather than the size of the
519
+ * suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
520
+ * opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
521
+ *
522
+ * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
523
+ * the suppression folds into the same pass rather than materializing an intermediate segment first.
524
+ */
525
+ andNot(excludes: Segment[], options?: BaseCombineOptions): AsyncIterable<number>;
526
+ /** Materialize `this \ (excludes…)` **into** `dest`. **Not atomic** — see {@link addMany}. */
527
+ andNotInto(dest: Segment, excludes: Segment[], options?: BaseCombineOptions & {
447
528
  batchSize?: number;
448
529
  }): Promise<void>;
449
530
  /**
@@ -459,6 +540,6 @@ declare class Segment {
459
540
  }
460
541
 
461
542
  /** Package version marker. Kept in sync with package.json at release. */
462
- declare const VERSION = "0.3.0";
543
+ declare const VERSION = "0.4.1";
463
544
 
464
- export { CloudRoaring, type CloudRoaringOptions, type EraseSubjectResult, SafeBitmap, Segment, type SegmentOptions, type SubjectErasureEntry, type SubjectReport, type SubjectSegmentRef, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
545
+ 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,16 @@ 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 end-to-end at 450 ms of unbroken blocking for a 1M-id
29
+ * load, now ~19 ms worst case). `core/` cannot
30
+ * default it — it is timer-free by lint, which is precisely why waiting goes through the `Clock` seam — so the
31
+ * flavor package supplies it, exactly as it supplies the codec. A caller who passes their own clock (a virtual
32
+ * one in a simulation, say) keeps it.
33
+ */
25
34
  declare const bulkLoadCrbmGeneration: BulkLoad;
26
35
  /** {@link coreCompactSegment} with the roaring codec pre-bound. */
27
36
  declare const compactSegment: CompactSegment;
@@ -191,9 +200,18 @@ interface CloudRoaringOptions {
191
200
  */
192
201
  readonly warmReadConsistency?: 'strong' | 'eventual';
193
202
  /**
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.
203
+ * Max Warm chunk writes in flight per `addMany`/`removeMany` (the bounded flusher). Default **4**
204
+ * (`DEFAULT_WRITE_CONCURRENCY`); set it to `1` for strictly serial writes.
205
+ *
206
+ * Distinct chunks are independent OCC rows, so fanning them out cannot make them conflict with each other.
207
+ * The bound exists for the *backend*: a provisioned-capacity store answers a burst by throttling, and 4 stays
208
+ * comfortably inside what the transient-retry path absorbs (measured 8x below the first observed failure).
209
+ * Raise it if your backend is provisioned for the fan-out — on-demand DynamoDB, or a connection pool sized to
210
+ * match. A pool *smaller* than this needs no action: `pg` and `mysql2` both queue rather than reject, so the
211
+ * flusher simply degrades toward serial.
212
+ *
213
+ * As before, a mid-flush failure can leave a **partial result**; `addMany`/`removeMany` are not atomic. What
214
+ * concurrency changes is only how much of the batch may already have landed when the first error surfaces.
197
215
  */
198
216
  readonly writeConcurrency?: number;
199
217
  /**
@@ -393,15 +411,38 @@ declare class CloudRoaring {
393
411
  static estimateCost(input: EstimateInput): CostReport;
394
412
  }
395
413
  /**
396
- * A handle bound to one segment — the seven ops.
414
+ * A handle bound to one segment — the read/write ops.
397
415
  *
398
416
  * **IDs must be integers in `[0, 2^32)`** (dense 32-bit). A non-integer / negative / out-of-range id
399
417
  * throws {@link ValidationError}.
400
418
  *
401
- * **`addMany`/`removeMany`/`intersectInto` are not atomic across chunks**: ids are grouped by chunk and
419
+ * **`addMany`/`removeMany`/`intersectInto`/`unionInto`/`andNotInto` are not atomic across chunks**: ids are grouped by chunk and
402
420
  * applied one chunk at a time, so if a later chunk fails (e.g. {@link WriteConflictError} after retries)
403
421
  * earlier chunks are already applied. Within a single chunk the update is atomic.
404
422
  */
423
+ /** Options common to every chunk-aligned combine (`intersect` / `union` / `andNot`). */
424
+ interface BaseCombineOptions {
425
+ /** Max chunk keys resolved concurrently — bounds the Cold footprint. A positive integer. */
426
+ readonly concurrency?: number;
427
+ /** Override the store's per-op denial-of-wallet budget for this call (`false` lifts it). */
428
+ readonly budget?: BudgetOption;
429
+ }
430
+ /**
431
+ * A combine that can also subtract.
432
+ *
433
+ * `exclude` is why suppression does not need an intermediate segment: applied here it folds into the same
434
+ * chunk-aligned pass, and each exclude is read **only at the keys that survived** — so a large global opt-out
435
+ * list costs reads proportional to the audience, not to itself.
436
+ */
437
+ interface CombineOptions extends BaseCombineOptions {
438
+ /** Segments whose ids are subtracted from the result. */
439
+ readonly exclude?: Segment[];
440
+ }
441
+ /** {@link CombineOptions} plus the write batching used by the `*Into` variants. */
442
+ interface CombineIntoOptions extends CombineOptions {
443
+ /** Ids buffered per `addMany` while materializing into `dest`. */
444
+ readonly batchSize?: number;
445
+ }
405
446
  declare class Segment {
406
447
  private readonly engine;
407
448
  private readonly ref;
@@ -416,12 +457,24 @@ declare class Segment {
416
457
  private timed;
417
458
  /** Add an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
418
459
  add(id: number): Promise<void>;
419
- /** Add many ids. Not atomic across chunks — see the class note. */
420
- addMany(ids: Iterable<number>): Promise<void>;
460
+ /**
461
+ * Add many ids, from a **sync or async** iterable. Not atomic across chunks — see the class note.
462
+ *
463
+ * An async source means a database cursor streams straight in — `addMany(athenaCursor())` rather than
464
+ * hand-batching `page → addMany(page)`. Ids are grouped by chunk and each chunk is written **exactly once**,
465
+ * however long the stream: pending ids are held compressed, not buffered-and-flushed, so a long stream costs
466
+ * no more backend writes than a short one. Peak memory is the roaring representation of the ids you pass.
467
+ *
468
+ * **For a very large set, reach for `bulkLoadCrbmGeneration` instead.** `addMany` expresses a *delta* and
469
+ * writes one warm row per touched chunk — around 61,000 of them for a set spread across the whole id space.
470
+ * Bulk-load *replaces* the segment with one immutable Cold object. See the guide's cost comparison; picking
471
+ * the wrong one of these is the most expensive mistake available in this library.
472
+ */
473
+ addMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
421
474
  /** Remove an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
422
475
  remove(id: number): Promise<void>;
423
- /** Remove many ids. Not atomic across chunks — see the class note. */
424
- removeMany(ids: Iterable<number>): Promise<void>;
476
+ /** Remove many ids, from a **sync or async** iterable. Not atomic across chunks — see {@link addMany}. */
477
+ removeMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
425
478
  has(id: number): Promise<boolean>;
426
479
  count(): Promise<number>;
427
480
  iterate(): AsyncIterable<number>;
@@ -434,16 +487,44 @@ declare class Segment {
434
487
  * up front, so total memory also carries each operand's Warm size — negligible under Topology-A.) Pass
435
488
  * `budget` to override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
436
489
  */
437
- intersect(others: Segment[], options?: {
438
- concurrency?: number;
439
- budget?: BudgetOption;
440
- }): AsyncIterable<number>;
441
490
  /**
442
- * Materialize `this ∩ others…` **into** `dest` (added, not replaced). Streaming + bounded-memory, but
443
- * **not atomic** across chunks — see {@link addMany}.
491
+ * Map the facade's `Segment` handles in `exclude` down to the plain refs `core` takes. A method rather than
492
+ * a module function because `ref` is class-private — the encapsulation is worth more than the free function.
444
493
  */
445
- intersectInto(dest: Segment, others: Segment[], options?: {
446
- concurrency?: number;
494
+ private refsIn;
495
+ intersect(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
496
+ /**
497
+ * Materialize `this ∩ others…` (minus `exclude`) **into** `dest` (added, not replaced). Streaming +
498
+ * bounded-memory, but **not atomic** across chunks — see {@link addMany}.
499
+ */
500
+ intersectInto(dest: Segment, others: Segment[], options?: CombineIntoOptions): Promise<void>;
501
+ /**
502
+ * `this ∪ others…`, minus `options.exclude` — streamed ascending.
503
+ *
504
+ * **The one composite read with no chunk-skipping,** and that is inherent to union rather than a limitation
505
+ * here: an id in *any* operand belongs to the result, so every chunk of every operand must be read.
506
+ * `intersect` prunes any key missing from any operand; union has nothing to prune. It is charged against the
507
+ * same per-op budget, so a wide union is refused rather than quietly billed — pass `budget` to raise it
508
+ * deliberately. If you find yourself unioning the same segments on every read, materializing the combined
509
+ * segment once (`unionInto`, or a bulk-load rebuild) is the cheaper shape.
510
+ */
511
+ union(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
512
+ /** Materialize `this ∪ others…` (minus `exclude`) **into** `dest`. **Not atomic** — see {@link addMany}. */
513
+ unionInto(dest: Segment, others: Segment[], options?: CombineIntoOptions): Promise<void>;
514
+ /**
515
+ * `this \ (excludes…)` — streamed ascending. Suppression on its own.
516
+ *
517
+ * Reads every chunk of `this` (any of them may survive the subtraction) but each exclude **only where it
518
+ * overlaps this segment**, so the cost tracks the segment being filtered rather than the size of the
519
+ * suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
520
+ * opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
521
+ *
522
+ * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
523
+ * the suppression folds into the same pass rather than materializing an intermediate segment first.
524
+ */
525
+ andNot(excludes: Segment[], options?: BaseCombineOptions): AsyncIterable<number>;
526
+ /** Materialize `this \ (excludes…)` **into** `dest`. **Not atomic** — see {@link addMany}. */
527
+ andNotInto(dest: Segment, excludes: Segment[], options?: BaseCombineOptions & {
447
528
  batchSize?: number;
448
529
  }): Promise<void>;
449
530
  /**
@@ -459,6 +540,6 @@ declare class Segment {
459
540
  }
460
541
 
461
542
  /** Package version marker. Kept in sync with package.json at release. */
462
- declare const VERSION = "0.3.0";
543
+ declare const VERSION = "0.4.1";
463
544
 
464
- export { CloudRoaring, type CloudRoaringOptions, type EraseSubjectResult, SafeBitmap, Segment, type SegmentOptions, type SubjectErasureEntry, type SubjectReport, type SubjectSegmentRef, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
545
+ 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
@@ -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,45 @@ 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 on the **synthetic yield-primitive benchmark** — a
112
+ * 61,035-iteration loop yielding every 1,024, isolating the primitive rather than timing a real load (the
113
+ * end-to-end figures are 450 ms → 19 ms; see `cooperative.ts`, which lists all three experiments):
114
+ *
115
+ * ```text
116
+ * no yield 568 ms wall 568.0 ms worst event-loop gap
117
+ * sleep(0) 555 ms wall 555.2 ms ← a microtask. Yields NOTHING.
118
+ * setImmediate 569 ms wall 13.8 ms ← free, and 41x less starvation
119
+ * setTimeout(1) 625 ms wall 15.0 ms ← +10% wall for no extra relief
120
+ * ```
121
+ *
122
+ * `sleep(0)` resolving on a microtask is correct for its own contract and useless for this: microtasks drain
123
+ * before the loop ever advances a phase, so `await sleep(0)` in a tight loop never lets a single pending I/O
124
+ * callback run. `setImmediate` fires in the check phase *after* pending I/O, which is exactly the ordering a
125
+ * server wants — inbound requests are serviced before the loop resumes chewing.
126
+ *
127
+ * A timer is not ref'd here (unlike {@link sleep}): a yield expresses "someone else may go first", never
128
+ * "there is outstanding work" — and `setImmediate` cannot outlive the turn that scheduled it anyway.
129
+ */
130
+ yieldNow() {
131
+ return new Promise((resolve) => {
132
+ setImmediate(resolve);
133
+ });
134
+ }
111
135
  };
136
+ var bulkLoadCrbmGeneration = (driver, key, ids, options = {}) => bulkLoadCrbmGeneration$1(driver, key, ids, {
137
+ ...options,
138
+ codec: options.codec ?? roaringCodec,
139
+ clock: options.clock ?? new SystemClock()
140
+ });
141
+ var compactSegment = (ref, deps, options) => compactSegment$1(ref, { ...deps, codec: deps.codec ?? roaringCodec }, options);
142
+ var runCompactionCycle = (deps, options) => runCompactionCycle$1({ ...deps, codec: deps.codec ?? roaringCodec }, options);
143
+ var runExport = (reader, registry, sink, options = {}) => runExport$1(reader, registry, sink, { ...options, codec: options.codec ?? roaringCodec });
144
+
145
+ // src/index.ts
112
146
  var SystemRng = class {
113
147
  next() {
114
148
  return Math.random();
@@ -486,7 +520,19 @@ var Segment = class {
486
520
  add(id) {
487
521
  return this.timed("add", () => this.engine.add(this.ref, id));
488
522
  }
489
- /** Add many ids. Not atomic across chunks — see the class note. */
523
+ /**
524
+ * Add many ids, from a **sync or async** iterable. Not atomic across chunks — see the class note.
525
+ *
526
+ * An async source means a database cursor streams straight in — `addMany(athenaCursor())` rather than
527
+ * hand-batching `page → addMany(page)`. Ids are grouped by chunk and each chunk is written **exactly once**,
528
+ * however long the stream: pending ids are held compressed, not buffered-and-flushed, so a long stream costs
529
+ * no more backend writes than a short one. Peak memory is the roaring representation of the ids you pass.
530
+ *
531
+ * **For a very large set, reach for `bulkLoadCrbmGeneration` instead.** `addMany` expresses a *delta* and
532
+ * writes one warm row per touched chunk — around 61,000 of them for a set spread across the whole id space.
533
+ * Bulk-load *replaces* the segment with one immutable Cold object. See the guide's cost comparison; picking
534
+ * the wrong one of these is the most expensive mistake available in this library.
535
+ */
490
536
  addMany(ids) {
491
537
  return this.timed("addMany", () => this.engine.addMany(this.ref, ids));
492
538
  }
@@ -494,7 +540,7 @@ var Segment = class {
494
540
  remove(id) {
495
541
  return this.timed("remove", () => this.engine.remove(this.ref, id));
496
542
  }
497
- /** Remove many ids. Not atomic across chunks — see the class note. */
543
+ /** Remove many ids, from a **sync or async** iterable. Not atomic across chunks — see {@link addMany}. */
498
544
  removeMany(ids) {
499
545
  return this.timed("removeMany", () => this.engine.removeMany(this.ref, ids));
500
546
  }
@@ -516,17 +562,84 @@ var Segment = class {
516
562
  * up front, so total memory also carries each operand's Warm size — negligible under Topology-A.) Pass
517
563
  * `budget` to override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
518
564
  */
565
+ /**
566
+ * Map the facade's `Segment` handles in `exclude` down to the plain refs `core` takes. A method rather than
567
+ * a module function because `ref` is class-private — the encapsulation is worth more than the free function.
568
+ */
569
+ refsIn(options) {
570
+ if (options === void 0) return void 0;
571
+ const { exclude, ...rest } = options;
572
+ return exclude === void 0 ? rest : { ...rest, exclude: exclude.map((o) => o.ref) };
573
+ }
519
574
  intersect(others, options) {
520
- return this.engine.intersect([this.ref, ...others.map((o) => o.ref)], options);
575
+ return this.engine.intersect([this.ref, ...others.map((o) => o.ref)], this.refsIn(options));
521
576
  }
522
577
  /**
523
- * Materialize `this ∩ others…` **into** `dest` (added, not replaced). Streaming + bounded-memory, but
524
- * **not atomic** across chunks — see {@link addMany}.
578
+ * Materialize `this ∩ others…` (minus `exclude`) **into** `dest` (added, not replaced). Streaming +
579
+ * bounded-memory, but **not atomic** across chunks — see {@link addMany}.
525
580
  */
526
581
  intersectInto(dest, others, options) {
527
582
  return this.timed(
528
583
  "intersectInto",
529
- () => this.engine.intersectInto(dest.ref, [this.ref, ...others.map((o) => o.ref)], options)
584
+ () => this.engine.intersectInto(
585
+ dest.ref,
586
+ [this.ref, ...others.map((o) => o.ref)],
587
+ this.refsIn(options)
588
+ )
589
+ );
590
+ }
591
+ /**
592
+ * `this ∪ others…`, minus `options.exclude` — streamed ascending.
593
+ *
594
+ * **The one composite read with no chunk-skipping,** and that is inherent to union rather than a limitation
595
+ * here: an id in *any* operand belongs to the result, so every chunk of every operand must be read.
596
+ * `intersect` prunes any key missing from any operand; union has nothing to prune. It is charged against the
597
+ * same per-op budget, so a wide union is refused rather than quietly billed — pass `budget` to raise it
598
+ * deliberately. If you find yourself unioning the same segments on every read, materializing the combined
599
+ * segment once (`unionInto`, or a bulk-load rebuild) is the cheaper shape.
600
+ */
601
+ union(others, options) {
602
+ return this.engine.union([this.ref, ...others.map((o) => o.ref)], this.refsIn(options));
603
+ }
604
+ /** Materialize `this ∪ others…` (minus `exclude`) **into** `dest`. **Not atomic** — see {@link addMany}. */
605
+ unionInto(dest, others, options) {
606
+ return this.timed(
607
+ "unionInto",
608
+ () => this.engine.unionInto(
609
+ dest.ref,
610
+ [this.ref, ...others.map((o) => o.ref)],
611
+ this.refsIn(options)
612
+ )
613
+ );
614
+ }
615
+ /**
616
+ * `this \ (excludes…)` — streamed ascending. Suppression on its own.
617
+ *
618
+ * Reads every chunk of `this` (any of them may survive the subtraction) but each exclude **only where it
619
+ * overlaps this segment**, so the cost tracks the segment being filtered rather than the size of the
620
+ * suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
621
+ * opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
622
+ *
623
+ * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
624
+ * the suppression folds into the same pass rather than materializing an intermediate segment first.
625
+ */
626
+ andNot(excludes, options) {
627
+ return this.engine.andNot(
628
+ this.ref,
629
+ excludes.map((o) => o.ref),
630
+ options
631
+ );
632
+ }
633
+ /** Materialize `this \ (excludes…)` **into** `dest`. **Not atomic** — see {@link addMany}. */
634
+ andNotInto(dest, excludes, options) {
635
+ return this.timed(
636
+ "andNotInto",
637
+ () => this.engine.andNotInto(
638
+ dest.ref,
639
+ this.ref,
640
+ excludes.map((o) => o.ref),
641
+ options
642
+ )
530
643
  );
531
644
  }
532
645
  /**
@@ -547,7 +660,7 @@ var Segment = class {
547
660
  });
548
661
  }
549
662
  };
550
- var VERSION = "0.3.0";
663
+ var VERSION = "0.4.1";
551
664
 
552
665
  export { CloudRoaring, SafeBitmap, Segment, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
553
666
  //# sourceMappingURL=index.js.map