@cloudbitmaps/roaring 0.8.2 → 0.9.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.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, DropResult, 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, RetentionPolicy, SetRetentionResult, RetireExpiredOptions, RetireExpiredResult, ConsistencyReport, ExportSink, ExportOptions, ExportManifest, EstimateInput } from '@cloudbitmaps/core';
2
2
  export * from '@cloudbitmaps/core';
3
3
 
4
4
  /**
@@ -448,6 +448,98 @@ declare class CloudRoaring {
448
448
  dryRun?: boolean;
449
449
  audit?: IAuditSink;
450
450
  }): Promise<DropResult>;
451
+ /**
452
+ * **Record when this segment becomes eligible for retirement.** One registry write; nothing is deleted here,
453
+ * and nothing starts running. `retireExpired` is what acts on the policy, and **you** decide when that runs —
454
+ * an EventBridge rule, a CronJob, a queue consumer, whatever your deployment already has. This library starts
455
+ * no background thread (it has to work identically in a Lambda, an edge isolate and a long-lived server).
456
+ *
457
+ * `expiresAt` is an **absolute epoch-ms you compute**, not a duration the library derives. A relative TTL would
458
+ * have to be anchored to something the library knows — `updatedAt`, or the current generation — and compaction
459
+ * rewrites both, so "expire 30 days after the last write" would keep a busy daily bucket alive forever
460
+ * precisely because the daemon is working.
461
+ *
462
+ * ```ts
463
+ * const DAY = 86_400_000;
464
+ * const ref = { namespace: 'active-daily', segment: '2026-08-05' };
465
+ * await store.setRetention(ref, { expiresAt: Date.now() + 30 * DAY });
466
+ * ```
467
+ *
468
+ * **Works on an accumulator**, which is the whole point: a segment you created by writing to it has no
469
+ * registry row, so it is invisible to `registry.list()` and therefore to every fleet-wide operation, including
470
+ * the sweep. This call mints the row (`createdRow: true` in the result) with **no Cold generation**, so it
471
+ * becomes enumerable while every read still resolves exactly as before.
472
+ *
473
+ * A value in the past is legal and means "eligible on the next sweep" — backfilling a policy onto existing
474
+ * buckets is normal. A value below `MIN_EXPIRES_AT_MS` (2001-09-09) is rejected: it is almost certainly epoch
475
+ * **seconds**, which would read as long-expired and retire the segment on the next pass. Needs a `registry`
476
+ * in the store config (throws {@link UnsupportedError} otherwise), and refuses a crypto-shredded segment.
477
+ */
478
+ setRetention(ref: SegmentRef, policy: RetentionPolicy): Promise<SetRetentionResult>;
479
+ /**
480
+ * **The stored retention policy**, or `null` if the segment has none (or has no registry row, or is a
481
+ * tombstone). Returns the string `'invalid'` for a row whose `expiresAt` is present but unusable — a
482
+ * hand-edited row, or one from a restore — so a malformed policy is visible rather than silently reading as
483
+ * "never expires" on a segment someone believes is expiring.
484
+ */
485
+ getRetention(ref: SegmentRef): Promise<RetentionPolicy | null | 'invalid'>;
486
+ /**
487
+ * **Cancel a segment's expiry** so no sweep retires it. Returns whether a policy was actually removed (`false`
488
+ * when there was none). A separate verb from `setRetention` on purpose: "never expire" as a magic value passed
489
+ * to the setter is how a typo becomes a deletion.
490
+ */
491
+ clearRetention(ref: SegmentRef): Promise<boolean>;
492
+ /**
493
+ * **Run the retention sweep: retire every segment whose `expiresAt` has passed.** This is the call that acts on
494
+ * the policies `setRetention` records — and it is a **call, not a daemon**. Nothing schedules it; you run it from
495
+ * whatever heartbeat your deployment already has (an EventBridge rule, a Kubernetes CronJob, a queue consumer,
496
+ * your compaction worker's loop). A library that started a timer would behave differently in a Lambda, an edge
497
+ * isolate and a long-lived server, which is worse than not having one.
498
+ *
499
+ * Each retirement goes through `dropSegment`, so the Warm → registry → Cold ordering, the re-sweep for a
500
+ * generation staged by an in-flight compaction, and the `generationsRemaining` report all come from one
501
+ * implementation rather than two.
502
+ *
503
+ * ```ts
504
+ * // In your scheduled handler. Start with a preview in a new deployment.
505
+ * const preview = await store.retireExpired({ namespace: 'active-daily', dryRun: true });
506
+ * console.log(`would retire ${preview.retired} of ${preview.scanned} (limited: ${preview.limited})`);
507
+ *
508
+ * const swept = await store.retireExpired({ namespace: 'active-daily' });
509
+ * for (const e of swept.entries) {
510
+ * if (e.action === 'skipped') console.warn(`${e.segment}: ${e.reason}`); // invalid-policy / limit / failed: …
511
+ * if (e.action === 'retired' && e.result.generationsRemaining.length > 0) {
512
+ * console.warn(`${e.segment}: storage not fully reclaimed — re-run`);
513
+ * }
514
+ * }
515
+ * if (swept.limited) scheduleAnotherPassSoon(); // more are still eligible
516
+ * ```
517
+ *
518
+ * **Read the ledger.** A per-segment *fault* is an entry, not an exception — a throw from the middle of a fleet
519
+ * sweep would leave the caller unable to say which segments were retired, having already retired some. (A bad
520
+ * argument does throw, and so does a fleet larger than `maxScanSegments`.) `limited: true` means the per-cycle
521
+ * `limit` (default 100) cut the pass short and more are eligible. That cap is charged on **attempts**, not
522
+ * successes, which is what makes it a real bound: `dropSegment` deletes Warm and writes the tombstone before
523
+ * sweeping Cold, so a fault in the Cold phase is a segment that is already retired. Retirements are sequential
524
+ * (~8 round trips each), so `limit` is a wall-clock knob too, and `retired` counts deletions only — a dry run
525
+ * reports `wouldRetire` instead, so a dashboard summing `retired` can never show a phantom deletion.
526
+ *
527
+ * It also **deletes the tombstone rows its own past retirements left**, after `tombstoneGraceMs` (default 24 h)
528
+ * and only once that segment's Warm rows and Cold generations are provably gone — collecting a straggler
529
+ * generation itself first, since nothing else ever would for a tombstoned segment. Attribution is a **positive
530
+ * marker the sweep stamps on its own retirements**, not an inference from "destroyed + an expired policy": a
531
+ * crypto-shred leaves `retention` untouched, so setting a policy and then honouring a right-to-erasure request
532
+ * mid-window produces exactly that row, and deleting it would destroy the Art. 17 attestation and un-fence the
533
+ * name. Pass `purgeTombstones: false` to keep every tombstone.
534
+ *
535
+ * Needs the store built with a **raw cold driver + a registry** (throws {@link UnsupportedError} otherwise),
536
+ * because retiring a segment deletes its Cold objects. `now` defaults to the store's clock.
537
+ */
538
+ retireExpired(options?: Omit<RetireExpiredOptions, 'now'> & {
539
+ now?: number;
540
+ }): Promise<RetireExpiredResult>;
541
+ /** The store's registry, or a typed error naming the operation that needs one. */
542
+ private requireRegistry;
451
543
  /**
452
544
  * **Cross-tier DR consistency check (audit gap #11).** After a restore/failover, verify every registered
453
545
  * segment's `currentGen` actually has its `.crbm` present in Cold — catching a **torn restore** where the
@@ -637,6 +729,6 @@ declare class Segment {
637
729
  }
638
730
 
639
731
  /** Package version marker. Kept in sync with package.json at release. */
640
- declare const VERSION = "0.8.2";
732
+ declare const VERSION = "0.9.0";
641
733
 
642
734
  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, gcOrphanGenerations, dropSegment, 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, setSegmentRetention, getSegmentRetention, clearSegmentRetention, retireExpired, runConsistencyCheck, estimateCost, groundedReport, CrbmColdChunkSource } from '@cloudbitmaps/core';
2
2
  export * from '@cloudbitmaps/core';
3
3
  import roaring from 'roaring';
4
4
 
@@ -305,12 +305,12 @@ var CloudRoaring = class {
305
305
  compactionDeps() {
306
306
  if (this.coldDriver === void 0) {
307
307
  throw new UnsupportedError(
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"
308
+ "compact/dropSegment/retireExpired/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) {
312
312
  throw new UnsupportedError(
313
- "compact/eraseSubject/checkConsistency need a `registry` in the store config"
313
+ "compact/dropSegment/retireExpired/eraseSubject/checkConsistency need a `registry` in the store config"
314
314
  );
315
315
  }
316
316
  return {
@@ -343,10 +343,7 @@ var CloudRoaring = class {
343
343
  * (throws {@link UnsupportedError} otherwise).
344
344
  */
345
345
  async subjectReport(id, options = {}) {
346
- if (this.registry === void 0) {
347
- throw new UnsupportedError("subjectReport needs a `registry` in the store config");
348
- }
349
- const registry = this.registry;
346
+ const registry = this.requireRegistry("subjectReport");
350
347
  requireScope(options, "subjectReport");
351
348
  validateConcurrency(options.concurrency);
352
349
  const budget = resolvePerOpBudget(options.budget, this.budget);
@@ -522,6 +519,116 @@ var CloudRoaring = class {
522
519
  const deps = this.compactionDeps();
523
520
  return dropSegment(ref, { registry: deps.registry, warm: deps.warm, cold: deps.cold }, options);
524
521
  }
522
+ /**
523
+ * **Record when this segment becomes eligible for retirement.** One registry write; nothing is deleted here,
524
+ * and nothing starts running. `retireExpired` is what acts on the policy, and **you** decide when that runs —
525
+ * an EventBridge rule, a CronJob, a queue consumer, whatever your deployment already has. This library starts
526
+ * no background thread (it has to work identically in a Lambda, an edge isolate and a long-lived server).
527
+ *
528
+ * `expiresAt` is an **absolute epoch-ms you compute**, not a duration the library derives. A relative TTL would
529
+ * have to be anchored to something the library knows — `updatedAt`, or the current generation — and compaction
530
+ * rewrites both, so "expire 30 days after the last write" would keep a busy daily bucket alive forever
531
+ * precisely because the daemon is working.
532
+ *
533
+ * ```ts
534
+ * const DAY = 86_400_000;
535
+ * const ref = { namespace: 'active-daily', segment: '2026-08-05' };
536
+ * await store.setRetention(ref, { expiresAt: Date.now() + 30 * DAY });
537
+ * ```
538
+ *
539
+ * **Works on an accumulator**, which is the whole point: a segment you created by writing to it has no
540
+ * registry row, so it is invisible to `registry.list()` and therefore to every fleet-wide operation, including
541
+ * the sweep. This call mints the row (`createdRow: true` in the result) with **no Cold generation**, so it
542
+ * becomes enumerable while every read still resolves exactly as before.
543
+ *
544
+ * A value in the past is legal and means "eligible on the next sweep" — backfilling a policy onto existing
545
+ * buckets is normal. A value below `MIN_EXPIRES_AT_MS` (2001-09-09) is rejected: it is almost certainly epoch
546
+ * **seconds**, which would read as long-expired and retire the segment on the next pass. Needs a `registry`
547
+ * in the store config (throws {@link UnsupportedError} otherwise), and refuses a crypto-shredded segment.
548
+ */
549
+ async setRetention(ref, policy) {
550
+ validateSegmentRef(ref);
551
+ return setSegmentRetention(ref, { registry: this.requireRegistry("setRetention") }, policy);
552
+ }
553
+ /**
554
+ * **The stored retention policy**, or `null` if the segment has none (or has no registry row, or is a
555
+ * tombstone). Returns the string `'invalid'` for a row whose `expiresAt` is present but unusable — a
556
+ * hand-edited row, or one from a restore — so a malformed policy is visible rather than silently reading as
557
+ * "never expires" on a segment someone believes is expiring.
558
+ */
559
+ async getRetention(ref) {
560
+ validateSegmentRef(ref);
561
+ return getSegmentRetention(ref, { registry: this.requireRegistry("getRetention") });
562
+ }
563
+ /**
564
+ * **Cancel a segment's expiry** so no sweep retires it. Returns whether a policy was actually removed (`false`
565
+ * when there was none). A separate verb from `setRetention` on purpose: "never expire" as a magic value passed
566
+ * to the setter is how a typo becomes a deletion.
567
+ */
568
+ async clearRetention(ref) {
569
+ validateSegmentRef(ref);
570
+ return clearSegmentRetention(ref, { registry: this.requireRegistry("clearRetention") });
571
+ }
572
+ /**
573
+ * **Run the retention sweep: retire every segment whose `expiresAt` has passed.** This is the call that acts on
574
+ * the policies `setRetention` records — and it is a **call, not a daemon**. Nothing schedules it; you run it from
575
+ * whatever heartbeat your deployment already has (an EventBridge rule, a Kubernetes CronJob, a queue consumer,
576
+ * your compaction worker's loop). A library that started a timer would behave differently in a Lambda, an edge
577
+ * isolate and a long-lived server, which is worse than not having one.
578
+ *
579
+ * Each retirement goes through `dropSegment`, so the Warm → registry → Cold ordering, the re-sweep for a
580
+ * generation staged by an in-flight compaction, and the `generationsRemaining` report all come from one
581
+ * implementation rather than two.
582
+ *
583
+ * ```ts
584
+ * // In your scheduled handler. Start with a preview in a new deployment.
585
+ * const preview = await store.retireExpired({ namespace: 'active-daily', dryRun: true });
586
+ * console.log(`would retire ${preview.retired} of ${preview.scanned} (limited: ${preview.limited})`);
587
+ *
588
+ * const swept = await store.retireExpired({ namespace: 'active-daily' });
589
+ * for (const e of swept.entries) {
590
+ * if (e.action === 'skipped') console.warn(`${e.segment}: ${e.reason}`); // invalid-policy / limit / failed: …
591
+ * if (e.action === 'retired' && e.result.generationsRemaining.length > 0) {
592
+ * console.warn(`${e.segment}: storage not fully reclaimed — re-run`);
593
+ * }
594
+ * }
595
+ * if (swept.limited) scheduleAnotherPassSoon(); // more are still eligible
596
+ * ```
597
+ *
598
+ * **Read the ledger.** A per-segment *fault* is an entry, not an exception — a throw from the middle of a fleet
599
+ * sweep would leave the caller unable to say which segments were retired, having already retired some. (A bad
600
+ * argument does throw, and so does a fleet larger than `maxScanSegments`.) `limited: true` means the per-cycle
601
+ * `limit` (default 100) cut the pass short and more are eligible. That cap is charged on **attempts**, not
602
+ * successes, which is what makes it a real bound: `dropSegment` deletes Warm and writes the tombstone before
603
+ * sweeping Cold, so a fault in the Cold phase is a segment that is already retired. Retirements are sequential
604
+ * (~8 round trips each), so `limit` is a wall-clock knob too, and `retired` counts deletions only — a dry run
605
+ * reports `wouldRetire` instead, so a dashboard summing `retired` can never show a phantom deletion.
606
+ *
607
+ * It also **deletes the tombstone rows its own past retirements left**, after `tombstoneGraceMs` (default 24 h)
608
+ * and only once that segment's Warm rows and Cold generations are provably gone — collecting a straggler
609
+ * generation itself first, since nothing else ever would for a tombstoned segment. Attribution is a **positive
610
+ * marker the sweep stamps on its own retirements**, not an inference from "destroyed + an expired policy": a
611
+ * crypto-shred leaves `retention` untouched, so setting a policy and then honouring a right-to-erasure request
612
+ * mid-window produces exactly that row, and deleting it would destroy the Art. 17 attestation and un-fence the
613
+ * name. Pass `purgeTombstones: false` to keep every tombstone.
614
+ *
615
+ * Needs the store built with a **raw cold driver + a registry** (throws {@link UnsupportedError} otherwise),
616
+ * because retiring a segment deletes its Cold objects. `now` defaults to the store's clock.
617
+ */
618
+ async retireExpired(options = {}) {
619
+ const deps = this.compactionDeps();
620
+ return retireExpired(
621
+ { registry: deps.registry, warm: deps.warm, cold: deps.cold },
622
+ { ...options, now: options.now ?? this.clock.now() }
623
+ );
624
+ }
625
+ /** The store's registry, or a typed error naming the operation that needs one. */
626
+ requireRegistry(op) {
627
+ if (this.registry === void 0) {
628
+ throw new UnsupportedError(`${op} needs a \`registry\` in the store config`);
629
+ }
630
+ return this.registry;
631
+ }
525
632
  /**
526
633
  * **Cross-tier DR consistency check (audit gap #11).** After a restore/failover, verify every registered
527
634
  * segment's `currentGen` actually has its `.crbm` present in Cold — catching a **torn restore** where the
@@ -549,10 +656,8 @@ var CloudRoaring = class {
549
656
  * exists" means the run finished — check `failed`. The `export-segments` CLI wraps this with a filesystem sink.
550
657
  */
551
658
  async exportSegments(sink, options = {}) {
552
- if (this.registry === void 0) {
553
- throw new UnsupportedError("exportSegments needs a `registry` in the store config");
554
- }
555
- return runExport$1(this, this.registry, sink, {
659
+ const registry = this.requireRegistry("exportSegments");
660
+ return runExport$1(this, registry, sink, {
556
661
  ...options,
557
662
  codec: options.codec ?? roaringCodec
558
663
  });
@@ -763,7 +868,7 @@ var Segment = class {
763
868
  });
764
869
  }
765
870
  };
766
- var VERSION = "0.8.2";
871
+ var VERSION = "0.9.0";
767
872
 
768
873
  export { CloudRoaring, SafeBitmap, Segment, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
769
874
  //# sourceMappingURL=index.js.map