@cloudbitmaps/roaring 0.7.0 → 0.8.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.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, 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';
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, DropResult, ConsistencyReport, ExportSink, ExportOptions, ExportManifest, EstimateInput } from '@cloudbitmaps/core';
2
2
  export * from '@cloudbitmaps/core';
3
3
 
4
4
  /**
@@ -114,8 +114,13 @@ declare const roaringCodec: CodecInterface;
114
114
  * In-process lifecycle helpers — `compact`, `eraseSubject`, `subjectReport` — reuse the store's own drivers, so
115
115
  * you never re-pass them (they need the store built with a raw cold driver + registry). Out-of-process cold
116
116
  * writers (the compaction daemon, the seed/bulk-load CLI, `destroySegment`/`eraseNamespace`) wire their own deps
117
- * against the same drivers — they run in separate processes.md + the
118
- * getting-started guide.
117
+ * against the same drivers — they run in separate processes.
118
+ *
119
+ * **You do not need any of them to start.** Construct a store and call `add`/`addMany`/`has`/`remove` — a segment
120
+ * with no Cold generation at all is fully functional, because a read merges `(cold ∪ warm.adds) \ warm.removes`
121
+ * and an absent Cold tier just makes that merge trivial. `bulkLoadCrbmGeneration` is an **import** path for data
122
+ * you already have elsewhere, not an initialization step, and compaction is a **cost** optimization, never a
123
+ * correctness requirement. See the README and the getting-started guide.
119
124
  */
120
125
 
121
126
  /**
@@ -396,6 +401,48 @@ declare class CloudRoaring {
396
401
  * a small daemon-less deployment, or right after a targeted `remove`.
397
402
  */
398
403
  compact(ref: SegmentRef, options: CompactionOptions): Promise<CompactionResult>;
404
+ /**
405
+ * **Dispose of a segment — tombstone it, delete its Warm rows, delete its Cold objects.** Irreversible.
406
+ *
407
+ * The operation a rolling window needs: `destroySegment` crypto-shreds (bytes unreadable everywhere including
408
+ * backups, but still sitting in your bucket and still billed, and it requires encryption), while this one
409
+ * removes the storage and works on a cleartext segment. On an encrypted segment it does both.
410
+ *
411
+ * Pass `{ dryRun: true }` first — it reports the generations it *would* delete and changes nothing. That
412
+ * matters more than the `confirmSegment` guard for anything automated, because in a loop the guard is the same
413
+ * variable twice.
414
+ *
415
+ * ```ts
416
+ * for (const day of expiredDays) {
417
+ * // A colon is NOT legal in a name — the family goes in the namespace, the date in the segment.
418
+ * const ref = { namespace: 'active-daily', segment: day };
419
+ * await store.dropSegment(ref, { confirmSegment: ref.segment });
420
+ * }
421
+ * ```
422
+ *
423
+ * **Omitting the namespace addresses a different segment and is a silent no-op** — you get
424
+ * `{ dropped: false, reason: 'absent' }`, not a throw, so a retention loop with that mistake deletes nothing
425
+ * forever and quietly. Check `reason` in an automated sweep.
426
+ *
427
+ * **Inspect `generationsRemaining`.** Empty is the normal outcome; non-empty means the storage was NOT fully
428
+ * reclaimed and the drop should be re-run. A compaction that was already in flight when the tombstone landed
429
+ * still finishes staging one more object, so a single sweep can miss it — this call re-sweeps and then reports
430
+ * whatever it still could not remove rather than returning a result that looks like a clean drop.
431
+ *
432
+ * Reads become empty within `coldGenTtlMs` (default 2 s), not instantly: a store that had already read this
433
+ * segment may answer from its cached generation + hot chunks until that window lapses. A reader that never
434
+ * touched it sees empty at once. **That bound needs a clock and `coldGenTtlMs > 0`** — a store built without a
435
+ * clock, or with `coldGenTtlMs: 0` ("pin forever"), holds its resolved snapshot for its own lifetime and can
436
+ * keep answering `true` for a dropped segment indefinitely; restart it.
437
+ *
438
+ * Needs the store built with a **raw cold driver + a registry** (throws {@link UnsupportedError} otherwise),
439
+ * because it has to enumerate and delete generations — a pre-built `ColdChunkSource` only reads.
440
+ */
441
+ dropSegment(ref: SegmentRef, options: {
442
+ confirmSegment: string;
443
+ dryRun?: boolean;
444
+ audit?: IAuditSink;
445
+ }): Promise<DropResult>;
399
446
  /**
400
447
  * **Cross-tier DR consistency check (audit gap #11).** After a restore/failover, verify every registered
401
448
  * segment's `currentGen` actually has its `.crbm` present in Cold — catching a **torn restore** where the
@@ -494,6 +541,32 @@ declare class Segment {
494
541
  remove(id: number): Promise<void>;
495
542
  /** Remove many ids, from a **sync or async** iterable. Not atomic across chunks — see {@link addMany}. */
496
543
  removeMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
544
+ /**
545
+ * **Claim ids atomically — add them, and get back only the ones that were not already there.**
546
+ *
547
+ * The durable analogue of Redis `SETBIT` returning the prior bit, which is what an exactly-once *"have I
548
+ * already sent to / already processed this id?"* check needs. `has()` then `add()` cannot give you this: two
549
+ * workers can both read absent and both proceed.
550
+ *
551
+ * ```ts
552
+ * const sent = store.segment(day, { namespace: 'sent-daily' });
553
+ * const toSend = await sent.claimMany(candidateIds); // only the ids this worker won
554
+ * for (const id of toSend) enqueue(id); // ...so nobody else will send them
555
+ * ```
556
+ *
557
+ * **Pass a batch, not one id.** A Warm write rewrites a whole 64K-id chunk bitmap, so claiming ids one at a
558
+ * time is the single most expensive way to use this library — measured at ~5,000 writes and ~23 MB for 5,000
559
+ * ids, against 1 write and ~8 KB for the same ids in one call. This method does one OCC read-modify-write per
560
+ * distinct chunk, giving you Redis's *semantics* without Redis's per-id *cost shape*.
561
+ *
562
+ * **Exactly-once holds per id**, which is the guarantee that matters: each id lives in one chunk, a chunk is
563
+ * one OCC row, so exactly one concurrent claimer sees any given id as new. Like `addMany` it is **not** atomic
564
+ * across chunks — a mid-flight failure can leave some chunks claimed — but re-running is safe, because
565
+ * already-claimed ids simply come back as not-new.
566
+ *
567
+ * Order of the returned ids is unspecified.
568
+ */
569
+ claimMany(ids: Iterable<number>): Promise<number[]>;
497
570
  has(id: number): Promise<boolean>;
498
571
  count(): Promise<number>;
499
572
  iterate(): AsyncIterable<number>;
@@ -559,6 +632,6 @@ declare class Segment {
559
632
  }
560
633
 
561
634
  /** Package version marker. Kept in sync with package.json at release. */
562
- declare const VERSION = "0.7.0";
635
+ declare const VERSION = "0.8.1";
563
636
 
564
637
  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, collectWithinBudget, 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, gcOrphanGenerations, dropSegment, runConsistencyCheck, estimateCost, groundedReport, CrbmColdChunkSource } from '@cloudbitmaps/core';
2
2
  export * from '@cloudbitmaps/core';
3
3
  import roaring from 'roaring';
4
4
 
@@ -305,7 +305,7 @@ var CloudRoaring = class {
305
305
  compactionDeps() {
306
306
  if (this.coldDriver === void 0) {
307
307
  throw new UnsupportedError(
308
- "compact/eraseSubject/checkConsistency need the store built with a raw cold driver (IColdDriver), not a pre-built ColdChunkSource \u2014 or call the compactSegment/runConsistencyCheck free functions with explicit deps"
308
+ "compact/dropSegment/eraseSubject/checkConsistency need the store built with a raw cold driver (IColdDriver), not a pre-built ColdChunkSource \u2014 or call the compactSegment/runConsistencyCheck free functions with explicit deps"
309
309
  );
310
310
  }
311
311
  if (this.registry === void 0) {
@@ -466,7 +466,56 @@ var CloudRoaring = class {
466
466
  */
467
467
  async compact(ref, options) {
468
468
  validateSegmentRef(ref);
469
- return compactSegment$1(ref, this.compactionDeps(), options);
469
+ const deps = this.compactionDeps();
470
+ const result = await compactSegment$1(ref, deps, options);
471
+ if (result.compacted) {
472
+ await gcOrphanGenerations(ref, { cold: deps.cold, registry: deps.registry }).catch(
473
+ () => void 0
474
+ );
475
+ }
476
+ return result;
477
+ }
478
+ /**
479
+ * **Dispose of a segment — tombstone it, delete its Warm rows, delete its Cold objects.** Irreversible.
480
+ *
481
+ * The operation a rolling window needs: `destroySegment` crypto-shreds (bytes unreadable everywhere including
482
+ * backups, but still sitting in your bucket and still billed, and it requires encryption), while this one
483
+ * removes the storage and works on a cleartext segment. On an encrypted segment it does both.
484
+ *
485
+ * Pass `{ dryRun: true }` first — it reports the generations it *would* delete and changes nothing. That
486
+ * matters more than the `confirmSegment` guard for anything automated, because in a loop the guard is the same
487
+ * variable twice.
488
+ *
489
+ * ```ts
490
+ * for (const day of expiredDays) {
491
+ * // A colon is NOT legal in a name — the family goes in the namespace, the date in the segment.
492
+ * const ref = { namespace: 'active-daily', segment: day };
493
+ * await store.dropSegment(ref, { confirmSegment: ref.segment });
494
+ * }
495
+ * ```
496
+ *
497
+ * **Omitting the namespace addresses a different segment and is a silent no-op** — you get
498
+ * `{ dropped: false, reason: 'absent' }`, not a throw, so a retention loop with that mistake deletes nothing
499
+ * forever and quietly. Check `reason` in an automated sweep.
500
+ *
501
+ * **Inspect `generationsRemaining`.** Empty is the normal outcome; non-empty means the storage was NOT fully
502
+ * reclaimed and the drop should be re-run. A compaction that was already in flight when the tombstone landed
503
+ * still finishes staging one more object, so a single sweep can miss it — this call re-sweeps and then reports
504
+ * whatever it still could not remove rather than returning a result that looks like a clean drop.
505
+ *
506
+ * Reads become empty within `coldGenTtlMs` (default 2 s), not instantly: a store that had already read this
507
+ * segment may answer from its cached generation + hot chunks until that window lapses. A reader that never
508
+ * touched it sees empty at once. **That bound needs a clock and `coldGenTtlMs > 0`** — a store built without a
509
+ * clock, or with `coldGenTtlMs: 0` ("pin forever"), holds its resolved snapshot for its own lifetime and can
510
+ * keep answering `true` for a dropped segment indefinitely; restart it.
511
+ *
512
+ * Needs the store built with a **raw cold driver + a registry** (throws {@link UnsupportedError} otherwise),
513
+ * because it has to enumerate and delete generations — a pre-built `ColdChunkSource` only reads.
514
+ */
515
+ async dropSegment(ref, options) {
516
+ validateSegmentRef(ref);
517
+ const deps = this.compactionDeps();
518
+ return dropSegment(ref, { registry: deps.registry, warm: deps.warm, cold: deps.cold }, options);
470
519
  }
471
520
  /**
472
521
  * **Cross-tier DR consistency check (audit gap #11).** After a restore/failover, verify every registered
@@ -565,6 +614,34 @@ var Segment = class {
565
614
  removeMany(ids) {
566
615
  return this.timed("removeMany", () => this.engine.removeMany(this.ref, ids));
567
616
  }
617
+ /**
618
+ * **Claim ids atomically — add them, and get back only the ones that were not already there.**
619
+ *
620
+ * The durable analogue of Redis `SETBIT` returning the prior bit, which is what an exactly-once *"have I
621
+ * already sent to / already processed this id?"* check needs. `has()` then `add()` cannot give you this: two
622
+ * workers can both read absent and both proceed.
623
+ *
624
+ * ```ts
625
+ * const sent = store.segment(day, { namespace: 'sent-daily' });
626
+ * const toSend = await sent.claimMany(candidateIds); // only the ids this worker won
627
+ * for (const id of toSend) enqueue(id); // ...so nobody else will send them
628
+ * ```
629
+ *
630
+ * **Pass a batch, not one id.** A Warm write rewrites a whole 64K-id chunk bitmap, so claiming ids one at a
631
+ * time is the single most expensive way to use this library — measured at ~5,000 writes and ~23 MB for 5,000
632
+ * ids, against 1 write and ~8 KB for the same ids in one call. This method does one OCC read-modify-write per
633
+ * distinct chunk, giving you Redis's *semantics* without Redis's per-id *cost shape*.
634
+ *
635
+ * **Exactly-once holds per id**, which is the guarantee that matters: each id lives in one chunk, a chunk is
636
+ * one OCC row, so exactly one concurrent claimer sees any given id as new. Like `addMany` it is **not** atomic
637
+ * across chunks — a mid-flight failure can leave some chunks claimed — but re-running is safe, because
638
+ * already-claimed ids simply come back as not-new.
639
+ *
640
+ * Order of the returned ids is unspecified.
641
+ */
642
+ claimMany(ids) {
643
+ return this.timed("claimMany", () => this.engine.claimMany(this.ref, ids));
644
+ }
568
645
  has(id) {
569
646
  return this.timed("has", () => this.engine.has(this.ref, id));
570
647
  }
@@ -681,7 +758,7 @@ var Segment = class {
681
758
  });
682
759
  }
683
760
  };
684
- var VERSION = "0.7.0";
761
+ var VERSION = "0.8.1";
685
762
 
686
763
  export { CloudRoaring, SafeBitmap, Segment, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
687
764
  //# sourceMappingURL=index.js.map