@cloudbitmaps/roaring 0.15.0 → 0.17.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.js CHANGED
@@ -11,6 +11,10 @@ import {
11
11
  NOOP_METRICS,
12
12
  RetryingStorageChunkSource,
13
13
  SegmentEngine,
14
+ LEASE_SKEW_MS,
15
+ LeaseExpiredError,
16
+ MAX_LEASE_MS,
17
+ NotFoundError,
14
18
  UnsupportedError,
15
19
  ValidationError as ValidationError2,
16
20
  WriteConflictError,
@@ -23,17 +27,21 @@ import {
23
27
  estimateCost,
24
28
  groundedReport,
25
29
  loadSegment,
30
+ loadSegmentChunks,
26
31
  mapWithConcurrency,
27
32
  resolveBudget,
28
33
  resolvePerOpBudget,
34
+ reapRegistryTombstones,
29
35
  retireExpired,
30
36
  runConsistencyCheck,
31
37
  runExport,
32
38
  setSegmentRetention,
33
39
  clearSegmentRetention,
34
40
  getSegmentRetention,
41
+ releaseLease,
35
42
  safeMetrics,
36
- splitId
43
+ splitId,
44
+ takeLease
37
45
  } from "@cloudbitmaps/core";
38
46
  import { validateSegmentRef } from "@cloudbitmaps/core/driver-kit";
39
47
 
@@ -93,7 +101,7 @@ function refuseReservedNamespace(namespace) {
93
101
 
94
102
  // src/roaring-codec.ts
95
103
  import roaring from "roaring";
96
- import { IntegrityError as IntegrityError2 } from "@cloudbitmaps/core";
104
+ import { IntegrityError as IntegrityError2, decodeSerialized } from "@cloudbitmaps/core";
97
105
 
98
106
  // src/portable/layout.ts
99
107
  import { IntegrityError } from "@cloudbitmaps/core";
@@ -341,6 +349,10 @@ var SafeBitmap = class _SafeBitmap {
341
349
  throw new IntegrityError2(`failed to deserialize bitmap: ${err.message}`);
342
350
  }
343
351
  }
352
+ /** The `roaring` bitmap this wraps, for a caller that was handed it once and keeps no wrapper. */
353
+ unwrap() {
354
+ return this.bitmap;
355
+ }
344
356
  serialize() {
345
357
  return this.bitmap.serialize(SerializationFormat.portable);
346
358
  }
@@ -450,8 +462,12 @@ function bitmapAsLoadInput(input) {
450
462
  var roaringCodec = {
451
463
  empty: () => SafeBitmap.empty(),
452
464
  fromValues: (values) => SafeBitmap.fromValues(values),
453
- safeDeserialize: (bytes, maxBytes, options) => SafeBitmap.safeDeserialize(bytes, maxBytes, options)
465
+ safeDeserialize: (bytes, maxBytes, options) => SafeBitmap.safeDeserialize(bytes, maxBytes, options),
466
+ owns: (bitmap) => bitmap instanceof SafeBitmap
454
467
  };
468
+ function deserializePortable(bytes) {
469
+ return decodeSerialized(bytes, roaringCodec).unwrap();
470
+ }
455
471
 
456
472
  // src/system-clock.ts
457
473
  var SystemClock = class {
@@ -504,6 +520,86 @@ var OPTION_KEYS = {
504
520
  seams: ["clock", "rng"]
505
521
  };
506
522
 
523
+ // src/lease-guards.ts
524
+ var guardIdIterable = (inner, check) => ({
525
+ [Symbol.asyncIterator]: () => {
526
+ const it = inner[Symbol.asyncIterator]();
527
+ let chunk = -1;
528
+ let first = true;
529
+ const stop = async (err) => {
530
+ await it.return?.();
531
+ throw err;
532
+ };
533
+ return {
534
+ async next() {
535
+ if (first) {
536
+ first = false;
537
+ try {
538
+ check();
539
+ } catch (err) {
540
+ return stop(err);
541
+ }
542
+ }
543
+ const r = await it.next();
544
+ if (r.done === true) return r;
545
+ const key = r.value >>> 16;
546
+ if (key !== chunk) {
547
+ try {
548
+ check();
549
+ } catch (err) {
550
+ return stop(err);
551
+ }
552
+ chunk = key;
553
+ }
554
+ return r;
555
+ },
556
+ async return() {
557
+ await it.return?.();
558
+ return { done: true, value: void 0 };
559
+ }
560
+ };
561
+ }
562
+ });
563
+ var guardIds = (inner, check) => ({
564
+ [Symbol.asyncIterator]: () => guardIdIterable(inner, check)[Symbol.asyncIterator](),
565
+ batches: () => guardItems(inner.batches(), check)
566
+ });
567
+ var guardItems = (inner, check) => ({
568
+ [Symbol.asyncIterator]: () => {
569
+ const it = inner[Symbol.asyncIterator]();
570
+ let first = true;
571
+ const stop = async (err) => {
572
+ await it.return?.();
573
+ throw err;
574
+ };
575
+ return {
576
+ async next() {
577
+ if (first) {
578
+ first = false;
579
+ try {
580
+ check();
581
+ } catch (err) {
582
+ return stop(err);
583
+ }
584
+ }
585
+ const r = await it.next();
586
+ if (r.done === true) return r;
587
+ try {
588
+ check();
589
+ } catch (err) {
590
+ return stop(err);
591
+ }
592
+ return r;
593
+ },
594
+ async return() {
595
+ await it.return?.();
596
+ return { done: true, value: void 0 };
597
+ }
598
+ };
599
+ }
600
+ });
601
+ var guardChunks = (inner, check) => guardItems(inner, check);
602
+
507
603
  // src/index.ts
508
604
  import {
509
605
  MemoryStorage,
@@ -514,6 +610,9 @@ import {
514
610
  excludingReservedRows as excludingReservedRows2,
515
611
  readRetentionPolicy,
516
612
  MIN_EXPIRES_AT_MS as MIN_EXPIRES_AT_MS2,
613
+ LEASE_SKEW_MS as LEASE_SKEW_MS2,
614
+ MAX_LEASE_MS as MAX_LEASE_MS2,
615
+ MAX_LEASES_PER_SEGMENT,
517
616
  InProcessKeystore,
518
617
  NodeAead,
519
618
  aadFor,
@@ -521,18 +620,22 @@ import {
521
620
  ValidationError as ValidationError3,
522
621
  WriteConflictError as WriteConflictError2,
523
622
  IntegrityError as IntegrityError3,
524
- NotFoundError,
623
+ NotFoundError as NotFoundError2,
525
624
  UnsupportedError as UnsupportedError2,
526
625
  CapabilityError,
527
626
  TransientError,
528
627
  KeyUnavailableError,
529
628
  BudgetExceededError,
629
+ LeaseExpiredError as LeaseExpiredError2,
630
+ LeaseLimitError,
530
631
  isCloudRoaringError,
531
632
  isWriteConflictError,
532
633
  isTransientError,
533
634
  isNotFoundError,
534
635
  isIntegrityError,
535
636
  isValidationError,
637
+ isLeaseExpiredError,
638
+ isLeaseLimitError,
536
639
  CrbmReader,
537
640
  BufferReader,
538
641
  CountingMetricsSink,
@@ -728,6 +831,9 @@ var CloudRoaring = class _CloudRoaring {
728
831
  clock
729
832
  });
730
833
  const resolved = resolveStorageSource(options, clock);
834
+ if (metrics !== NOOP_METRICS && isStorageBackend(options.storage)) {
835
+ options.storage.attachMetrics?.(metrics);
836
+ }
731
837
  let storage = resolved.source;
732
838
  if (options.retry !== false) {
733
839
  const { onRetry: userOnRetry, ...ov } = options.retry ?? {};
@@ -825,7 +931,7 @@ var CloudRoaring = class _CloudRoaring {
825
931
  clock: this.clock,
826
932
  metrics: this.metrics,
827
933
  materialize: (dest, ids, op, options2) => this.materialize(dest, ids, op, options2),
828
- pinned: (r, e) => this.pinSegment(r, e),
934
+ pinned: (r, e, l, at) => this.pinSegment(r, e, l, at),
829
935
  combineEngine: (handles) => this.engineForCombine(handles),
830
936
  expiresAt
831
937
  });
@@ -875,7 +981,7 @@ var CloudRoaring = class _CloudRoaring {
875
981
  const metadata = options?.metadata;
876
982
  let result;
877
983
  try {
878
- result = await loadSegment(dest, ids, deps, {
984
+ const loadOptions = {
879
985
  ...options?.allowEmpty === void 0 ? {} : { allowEmpty: options.allowEmpty },
880
986
  ...options?.guard === void 0 ? {} : { guard: options.guard },
881
987
  ...metadata === void 0 ? {} : { metadata },
@@ -888,7 +994,8 @@ var CloudRoaring = class _CloudRoaring {
888
994
  // as a side effect of adding a guard whose entire purpose is preventing data loss. Opt in with `keep`.
889
995
  keep: options?.keep ?? KEEP_EVERY_GENERATION,
890
996
  ...options?.audit === void 0 ? {} : { audit: options.audit }
891
- });
997
+ };
998
+ result = ids instanceof CombineChunks ? await loadSegmentChunks(dest, ids.chunks, deps, loadOptions) : await loadSegment(dest, ids, deps, loadOptions);
892
999
  } finally {
893
1000
  this.engine.invalidate(dest);
894
1001
  }
@@ -1249,24 +1356,26 @@ var CloudRoaring = class _CloudRoaring {
1249
1356
  * A refused load deletes the object it wrote before returning — it sits above `currentGen`, where generation
1250
1357
  * collection deliberately never looks — but only while the segment's registry row is unchanged or gone. Once
1251
1358
  * another write has changed the row, the generation number it holds may name another incarnation's live object,
1252
- * so it leaves the orphan rather than risk deleting live data. The orphan is an ordinary generation once a later
1253
- * one is current above it, and collection counts it within `keep`.
1359
+ * so it leaves the orphan rather than risk deleting live data. No row's list names it, so it takes no place in a
1360
+ * window, and a listing deletes it once a later generation is current above it.
1254
1361
  *
1255
- * **Collection is by name for the default `keep`.** With `keep` of 0 or 1, a load that found nothing above the
1256
- * pointer deletes the one generation its publish pushed out of the window and lists nothing; it lists the segment's
1257
- * objects on every sixteenth generation, and on any load that met an object above the pointer or whose check found
1258
- * the current generation's object gone, to take what the name-only passes leave, such as the generations an
1259
- * earlier, wider `keep` held. `keep` of 2 or more lists on every load. {@link LoadResult.collected} then names what
1260
- * the pass deleted by name, and that generation may have been gone already.
1362
+ * **Collection is by name at any `keep` up to 64.** The segment's row records the generations a load keeps, so a load
1363
+ * that found nothing above the pointer deletes the generations its publish pushed out of the window and lists
1364
+ * nothing. It lists the segment's objects on every sixteenth generation, and on any load that met an object above the
1365
+ * pointer or whose check found the current generation's object gone, to take what the name-only passes leave, such as
1366
+ * an object a crashed load left below the pointer. A `keep` above 64 records no list and lists on every load, and so
1367
+ * does the first load of a row that records none, which then records it. {@link LoadResult.collected} then names
1368
+ * what the pass deleted by name, and one name may have been gone already.
1261
1369
  *
1262
1370
  * What it **throws** for is a fault rather than an outcome: invalid options or ids, and a crypto-shredded
1263
1371
  * segment (`ValidationError`); a key the keystore cannot provide (`KeyUnavailableError`); a current generation
1264
1372
  * that will not open when a guard has to read its size, which it does only when the row has no usable summary of it
1265
- * (`IntegrityError`); a driver failure; and a collection pass
1266
- * by listing that could not prove the segment was still the same one (`WriteConflictError`). That one, and a
1267
- * failure in the collection's own reads or deletes, can be raised **after** the publish already landed, so a throw
1268
- * does not by itself mean the load did not take effect — re-read the pointer rather than assuming. A collection by
1269
- * name that finds the segment changed returns an empty `collected` instead.
1373
+ * (`IntegrityError`); a driver failure; and a fault in the collection's own reads, listing or deletes, or in the write
1374
+ * that records the window (anything but a lost race). A collection fault can be raised **after** the publish already
1375
+ * landed, so a throw does not by itself mean the load did not take effect — re-read the pointer rather than assuming.
1376
+ * A collection, by name or by listing, that finds another writer has moved the row since the publish (a load, a
1377
+ * rollback, a purge) stops and returns what it deleted in `collected`: a load that took effect does not throw for a
1378
+ * race it won.
1270
1379
  *
1271
1380
  * **A `TransientError` from the registry write can leave the publish unsettled, and deletes nothing.** The
1272
1381
  * generation's object is sent again after a throttle where the backend allows it (a write id tells a first send
@@ -1338,7 +1447,11 @@ var CloudRoaring = class _CloudRoaring {
1338
1447
  validateSegmentRef(ref);
1339
1448
  const deps = this.lifecycleDeps("dropSegment");
1340
1449
  try {
1341
- return await dropSegment(ref, { registry: deps.registry, storage: deps.storage }, options);
1450
+ return await dropSegment(
1451
+ ref,
1452
+ { registry: deps.registry, storage: deps.storage, clock: deps.clock, rng: deps.rng },
1453
+ options
1454
+ );
1342
1455
  } finally {
1343
1456
  if (options.dryRun !== true) this.engine.invalidate(ref);
1344
1457
  }
@@ -1371,7 +1484,11 @@ var CloudRoaring = class _CloudRoaring {
1371
1484
  */
1372
1485
  async setRetention(ref, policy) {
1373
1486
  validateSegmentRef(ref);
1374
- return setSegmentRetention(ref, { registry: this.requireRegistry("setRetention") }, policy);
1487
+ return setSegmentRetention(
1488
+ ref,
1489
+ { registry: this.requireRegistry("setRetention"), clock: this.clock, rng: this.rng },
1490
+ policy
1491
+ );
1375
1492
  }
1376
1493
  /**
1377
1494
  * **The stored retention policy**, or `null` if the segment has none (or has no registry row, or is a
@@ -1390,7 +1507,11 @@ var CloudRoaring = class _CloudRoaring {
1390
1507
  */
1391
1508
  async clearRetention(ref) {
1392
1509
  validateSegmentRef(ref);
1393
- return clearSegmentRetention(ref, { registry: this.requireRegistry("clearRetention") });
1510
+ return clearSegmentRetention(ref, {
1511
+ registry: this.requireRegistry("clearRetention"),
1512
+ clock: this.clock,
1513
+ rng: this.rng
1514
+ });
1394
1515
  }
1395
1516
  /**
1396
1517
  * **Run the retention sweep: retire every segment whose `expiresAt` has passed.** This is the call that acts on
@@ -1453,7 +1574,7 @@ var CloudRoaring = class _CloudRoaring {
1453
1574
  async retireExpired(options = {}) {
1454
1575
  const deps = this.lifecycleDeps("retireExpired");
1455
1576
  const result = await retireExpired(
1456
- { registry: deps.registry, storage: deps.storage },
1577
+ { registry: deps.registry, storage: deps.storage, clock: this.clock, rng: this.rng },
1457
1578
  { ...options, now: options.now ?? this.clock.now() }
1458
1579
  );
1459
1580
  if (options.dryRun !== true) {
@@ -1463,6 +1584,57 @@ var CloudRoaring = class _CloudRoaring {
1463
1584
  }
1464
1585
  return result;
1465
1586
  }
1587
+ /**
1588
+ * **Remove the registry rows that releases before 0.12.0 left as `deleted: true` envelopes.** Those releases never
1589
+ * removed a row they deleted, and no later call removes it either: it is invisible to every read, and every full
1590
+ * listing of the registry still reads it (one GET each). This is the one call that removes them. It is an admin call,
1591
+ * not a sweep, and nothing schedules it: run it once after the last process on a release before 0.12.0 is gone.
1592
+ *
1593
+ * ```ts
1594
+ * const preview = await store.reapRegistryTombstones({ dryRun: true });
1595
+ * console.log(`would remove ${preview.wouldReap} of ${preview.examined} rows read`);
1596
+ *
1597
+ * const done = await store.reapRegistryTombstones({ confirmNoLegacyWriters: true });
1598
+ * if (done.limited) runAgain(); // `limit` (default 1,000) stopped the run with rows unread
1599
+ * ```
1600
+ *
1601
+ * **What it removes.** Only a row that is `deleted: true` and whose token carries no incarnation id, which is what a
1602
+ * release before 0.12.0 left, or a 0.12.0 or later release left by deleting a row born before 0.12.0 (both are safe to
1603
+ * remove), whatever its `status`. It never touches a live row, a `destroyed` row that is not `deleted` (a crypto-shred's
1604
+ * or a `dropSegment`'s tombstone: the attestation of an erasure, which it cannot tell from another), or a deleted row
1605
+ * with an incarnation id, and it never touches a generation. **So it does not clean a bucket completely**: the
1606
+ * `destroyed` tombstones `dropSegment` leaves stay, so do live rows written before 0.12.0, and so does a tombstone a
1607
+ * 0.12.0 or later release wrote while `conditionalDelete` was off (GCS by default, S3 on a custom endpoint): every full
1608
+ * listing still reads it, and this call neither removes it nor runs on such a store.
1609
+ * It also covers the library's own bookkeeping rows when `namespace` is not set, by the same rule. The default
1610
+ * namespace cannot be scoped to: leave `namespace` out to cover it.
1611
+ *
1612
+ * **The two things it asks.** `confirmNoLegacyWriters: true` is required for a real run (without it, a
1613
+ * `ValidationError`, and nothing is requested): your statement that no process on a release before 0.12.0 writes
1614
+ * this registry, since one that did would, re-creating a removed name, issue the removed row's tokens again. And the
1615
+ * registry must apply a delete under a precondition: where its `conditionalDelete` capability is off (GCS by default,
1616
+ * S3 on a custom endpoint) it throws `CapabilityError` before any request, dry run included. A registry that keeps
1617
+ * no such rows (the in-memory and local-filesystem ones) throws `UnsupportedError`.
1618
+ *
1619
+ * **Each removal is fenced** on the version it read, so a `create` that lands over the envelope first wins, the
1620
+ * delete is refused, and the row is counted as `skipped.raced` (which also counts a row already gone). That holds where
1621
+ * the backend applies the precondition, which auto-detection checks (S3 only on an AWS host). On an endpoint that
1622
+ * ignores `If-Match` (MinIO, fake-gcs-server), a `conditionalDelete: true` you set makes this an unfenced delete: run
1623
+ * it with every writer stopped. On a versioned bucket a delete leaves a delete marker and the earlier version, record
1624
+ * included, so the name is not erased from the bucket. A `create` that had already read the envelope and
1625
+ * whose write meets the removal throws `WriteConflictError`, which this library does not retry: retry the create, and
1626
+ * it finds no row and writes one.
1627
+ *
1628
+ * **Cost** over R rows read and E removed: `ceil(R / 1000)` LIST requests, R GETs and E DELETEs (a dry run, no
1629
+ * DELETEs). `limit` bounds the removals, and the run stops listing once it is spent. **A run is not resumable**: each
1630
+ * lists and reads from the start (R GETs whatever the `limit`), a dry run with a `limit` reports the same first rows
1631
+ * every time, so `wouldReap` is a total only when `limited` is `false`, and `limited` is `true` whenever the limit was
1632
+ * spent with keys left, removable or not. An unreadable object stops the run with an error naming its key, and
1633
+ * removes nothing more: it may be a newer release's row. Scope the run with `namespace` to get past one.
1634
+ */
1635
+ async reapRegistryTombstones(options = {}) {
1636
+ return reapRegistryTombstones(this.requireRegistry("reapRegistryTombstones"), options);
1637
+ }
1466
1638
  /**
1467
1639
  * **Forget everything this store has cached about a segment.** Use it after destroying or retiring a segment
1468
1640
  * through a path this store did not perform itself.
@@ -1509,14 +1681,54 @@ var CloudRoaring = class _CloudRoaring {
1509
1681
  * pinned view reports the version captured at pin time, marked as a pin's, so its decoded chunks are never
1510
1682
  * those of a live read that fetched across a publish (see {@link PinnedStorageChunkSource.currentVersion}).
1511
1683
  */
1512
- async pinSegment(ref, expiresAt) {
1684
+ async pinSegment(ref, expiresAt, leaseUntil, named) {
1513
1685
  const crbm = this.crbmSource;
1514
1686
  if (crbm === void 0) {
1515
1687
  throw new UnsupportedError(
1516
1688
  "pin() needs the `.crbm` storage source \u2014 pass a backend or a raw IStorageDriver as `storage` (a pre-built StorageChunkSource that cannot resolve a generation has nothing to pin)"
1517
1689
  );
1518
1690
  }
1519
- const at = await this.withRetries(() => crbm.pinGeneration(ref));
1691
+ let hold;
1692
+ let take;
1693
+ if (leaseUntil !== void 0) {
1694
+ const registry = this.requireRegistry("pin({ leaseUntil })");
1695
+ if (registry.capabilities().canWrite === false) {
1696
+ throw new UnsupportedError(
1697
+ "pin({ leaseUntil }) needs a registry that can write a row, and this store's cannot in this runtime"
1698
+ );
1699
+ }
1700
+ const holder = this.mintHolder();
1701
+ const deps = { registry, clock: this.clock, rng: this.rng };
1702
+ const generationOf = (row) => named?.generation ?? row.currentGen;
1703
+ hold = {
1704
+ holder,
1705
+ until: leaseUntil,
1706
+ released: false,
1707
+ done: false,
1708
+ release: () => releaseLease(ref, deps, holder)
1709
+ };
1710
+ take = {
1711
+ take: (row) => takeLease(ref, deps, {
1712
+ holder,
1713
+ generation: generationOf(row),
1714
+ until: leaseUntil,
1715
+ row,
1716
+ current: named === void 0
1717
+ })
1718
+ };
1719
+ }
1720
+ let at;
1721
+ try {
1722
+ at = await this.withRetries(
1723
+ () => named === void 0 ? crbm.pinGeneration(ref, take) : crbm.pinGenerationAt(ref, named, take)
1724
+ );
1725
+ if (hold !== void 0 && at === null) {
1726
+ throw new NotFoundError(`segment "${ref.segment}" has no generation to lease`);
1727
+ }
1728
+ } catch (err) {
1729
+ if (hold !== void 0) await hold.release().catch(() => void 0);
1730
+ throw err;
1731
+ }
1520
1732
  const pinnedAt = {
1521
1733
  generation: at?.generation ?? null,
1522
1734
  version: at?.version ?? null,
@@ -1529,12 +1741,20 @@ var CloudRoaring = class _CloudRoaring {
1529
1741
  clock: this.clock,
1530
1742
  metrics: this.metrics,
1531
1743
  materialize: (dest, ids, op, options) => this.materialize(dest, ids, op, options),
1532
- pinned: (r, e) => this.pinSegment(r, e),
1744
+ pinned: (r, e, l, at2) => this.pinSegment(r, e, l, at2),
1533
1745
  combineEngine: (handles) => this.engineForCombine(handles),
1534
1746
  expiresAt,
1535
- pinnedAt
1747
+ pinnedAt,
1748
+ lease: hold
1536
1749
  });
1537
1750
  }
1751
+ /** 16 hex digits from the store's `Rng`, which names the holder of one lease: unique, not secret. */
1752
+ mintHolder() {
1753
+ let id = "";
1754
+ for (let i = 0; i < 4; i++)
1755
+ id += Math.floor(this.rng.next() * 65536).toString(16).padStart(4, "0");
1756
+ return id;
1757
+ }
1538
1758
  /**
1539
1759
  * An engine reading every segment in `pins` at its pinned generation and everything else live, with the store's
1540
1760
  * retries, so a transient fault on a pinned read is retried as it would be on a live one.
@@ -1692,6 +1912,34 @@ var EMPTY_IDS = {
1692
1912
  async *batches() {
1693
1913
  }
1694
1914
  };
1915
+ var AS_IDS = {
1916
+ none: EMPTY_IDS,
1917
+ failing,
1918
+ guard: guardIds,
1919
+ intersect: (engine, refs, opts) => withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts)),
1920
+ union: (engine, refs, opts) => withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts)),
1921
+ andNot: (engine, base, refs, opts) => withBatches(engine.andNot(base, refs, opts), () => engine.andNotBatches(base, refs, opts))
1922
+ };
1923
+ var NO_CHUNKS = {
1924
+ async *[Symbol.asyncIterator]() {
1925
+ }
1926
+ };
1927
+ var CombineChunks = class {
1928
+ constructor(chunks) {
1929
+ this.chunks = chunks;
1930
+ }
1931
+ chunks;
1932
+ };
1933
+ var AS_CHUNKS = {
1934
+ none: NO_CHUNKS,
1935
+ guard: guardChunks,
1936
+ failing: (err) => ({
1937
+ [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(err) })
1938
+ }),
1939
+ intersect: (engine, refs, opts) => engine.intersectChunks(refs, opts),
1940
+ union: (engine, refs, opts) => engine.unionChunks(refs, opts),
1941
+ andNot: (engine, base, refs, opts) => engine.andNotChunks(base, refs, opts)
1942
+ };
1695
1943
  var KEEP_EVERY_GENERATION = Number.MAX_SAFE_INTEGER;
1696
1944
  var makeSegment;
1697
1945
  var minting = false;
@@ -1711,6 +1959,13 @@ var Segment = class _Segment {
1711
1959
  * pinned handle passed as an **operand** is still read at its pin rather than live.
1712
1960
  */
1713
1961
  pinnedAt;
1962
+ /**
1963
+ * The lease this handle holds, when it came from `pin({ leaseUntil })`; `undefined` otherwise. It reports what was
1964
+ * taken and does not change when the lease ends: a read after `lease.until` (or after {@link Segment.release}) throws
1965
+ * {@link LeaseExpiredError}.
1966
+ */
1967
+ lease;
1968
+ leaseHold;
1714
1969
  static {
1715
1970
  makeSegment = (parts) => {
1716
1971
  minting = true;
@@ -1740,6 +1995,10 @@ var Segment = class _Segment {
1740
1995
  this.combineEngine = parts.combineEngine;
1741
1996
  this.expiresAt = parts.expiresAt;
1742
1997
  this.pinnedAt = parts.pinnedAt;
1998
+ this.leaseHold = parts.lease;
1999
+ if (parts.lease !== void 0) {
2000
+ this.lease = Object.freeze({ holder: parts.lease.holder, until: parts.lease.until });
2001
+ }
1743
2002
  this.metricsOn = parts.metrics !== NOOP_METRICS;
1744
2003
  }
1745
2004
  /**
@@ -1768,11 +2027,20 @@ var Segment = class _Segment {
1768
2027
  * 0, so a pin of the old segment never reads the new one: what it has already read still answers, as the
1769
2028
  * instant it pinned, and anything it would have to fetch fails with `NotFoundError`, as a swept pin's does.
1770
2029
  *
1771
- * **It is a hold, not a lease.** Nothing here stops a collection deleting the generation underneath
1772
- * you: a pinned read deliberately does **not** heal forward, because silently serving a different generation
1773
- * is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover your longest pinned
1774
- * job — see [Generations and `keep`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#generations-and-keep) — or take the pin on a segment
1775
- * you are not collecting.
2030
+ * **It is a hold, and `pin({ leaseUntil })` makes it a bounded one.** Without a lease nothing stops a collection
2031
+ * deleting the generation underneath you: a pinned read deliberately does **not** heal forward, because silently
2032
+ * serving a different generation is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover
2033
+ * your longest pinned job — see [Generations and `keep`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#generations-and-keep) — take the pin on a segment
2034
+ * you are not collecting, or lease it.
2035
+ *
2036
+ * **A lease** (`leaseUntil`: epoch **milliseconds**, after now and at most 14 days out) keeps the generation out of
2037
+ * a load's collection until then, from any process, and the pin is taken with it: the lease is written to the segment's
2038
+ * row before the object is opened. A read of the handle after the lease, or after {@link Segment.release}, throws
2039
+ * {@link LeaseExpiredError} at every read site, including when the handle is an operand or an `exclude` of a combine
2040
+ * or the target of an `*Into`, and never reads empty. A stream checks the lease each time it reads a chunk. A segment
2041
+ * holds at most 64 live leases ({@link LeaseLimitError}). A collector holds a lease 60 seconds past its end, which covers
2042
+ * clocks that differ by that much. Erasure, shred, drop and retention expiry ignore a lease. See
2043
+ * [Hold a generation for a job](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/reading.md#hold-a-generation-for-a-job-a-lease).
1776
2044
  *
1777
2045
  * A segment with no current generation pins nothing and reads empty, exactly as it would unpinned. A pinned
1778
2046
  * segment whose row is later dropped or destroyed fails once it must open its object again, rather than go empty
@@ -1812,8 +2080,144 @@ var Segment = class _Segment {
1812
2080
  * Needs a store built on the `.crbm` storage source (the default when you pass a backend or a raw driver). Throws
1813
2081
  * {@link UnsupportedError} on a store wired with a pre-built source that cannot pin.
1814
2082
  */
1815
- async pin() {
1816
- return this.pinned(this.ref, this.expiresAt);
2083
+ async pin(options) {
2084
+ this.assertLeases([this]);
2085
+ const leaseUntil = this.leaseUntilOf("pin", options);
2086
+ return this.pinned(this.ref, this.expiresAt, leaseUntil);
2087
+ }
2088
+ /**
2089
+ * Hold what this pin names until `leaseUntil`, or `undefined` for a pin with no lease. Refuses an option it does not
2090
+ * know, a value that is not epoch-milliseconds in the future, one past the longest lease, and one past the handle's own
2091
+ * `expiresAt`, before any request is made.
2092
+ */
2093
+ leaseUntilOf(op, options) {
2094
+ if (options === void 0 || options === null) return void 0;
2095
+ if (typeof options !== "object" || Array.isArray(options)) {
2096
+ throw new ValidationError2(`${op}: options must be an object such as { leaseUntil }`);
2097
+ }
2098
+ const unknown = Object.keys(options).filter((k) => k !== "leaseUntil");
2099
+ if (unknown.length > 0) {
2100
+ throw new ValidationError2(
2101
+ `${op}: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; this version takes { leaseUntil } only`
2102
+ );
2103
+ }
2104
+ const until = options.leaseUntil;
2105
+ if (until === void 0) return void 0;
2106
+ const now = this.clock.now();
2107
+ if (typeof until !== "number" || !Number.isSafeInteger(until)) {
2108
+ throw new ValidationError2(
2109
+ `${op}: leaseUntil must be an integer epoch-MILLISECONDS; got ${String(until)}`
2110
+ );
2111
+ }
2112
+ if (until <= now) {
2113
+ throw new ValidationError2(
2114
+ `${op}: leaseUntil (${until}) is not after now (${now}); it is epoch MILLISECONDS, so a value in seconds reads as long past. Use \`Date.now() + hours * 3_600_000\``
2115
+ );
2116
+ }
2117
+ if (until > now + MAX_LEASE_MS) {
2118
+ throw new ValidationError2(
2119
+ `${op}: leaseUntil is more than 14 days away; the longest lease is ${MAX_LEASE_MS} ms. Take a new lease when a job outlasts it`
2120
+ );
2121
+ }
2122
+ if (this.expiresAt !== void 0 && until > this.expiresAt) {
2123
+ throw new ValidationError2(
2124
+ `${op}: leaseUntil outlasts this handle's own expiresAt, which would have it read empty while it still holds the generation`
2125
+ );
2126
+ }
2127
+ return until;
2128
+ }
2129
+ /**
2130
+ * End this handle's lease now, so a load's collection may take its generation. Idempotent: a handle with no lease, or
2131
+ * one already released, does nothing and makes no request. Every read of the handle after it throws
2132
+ * {@link LeaseExpiredError} (reason `'released'`), including one already under way at its next chunk. A lease that has
2133
+ * ended by its own time needs no release: this then makes no request either, and the registry row's entry is pruned
2134
+ * by a later write. Otherwise it is one registry read and one write; a failure of either is thrown as the registry's own
2135
+ * typed error, and the entry then holds until its time ends, `release()` being safe to call again.
2136
+ */
2137
+ async release() {
2138
+ const hold = this.leaseHold;
2139
+ if (hold === void 0) return;
2140
+ hold.released = true;
2141
+ if (hold.done) return;
2142
+ if (this.clock.now() >= hold.until + LEASE_SKEW_MS) {
2143
+ hold.done = true;
2144
+ return;
2145
+ }
2146
+ await hold.release();
2147
+ hold.done = true;
2148
+ }
2149
+ /**
2150
+ * The error a read of this handle must throw, or `undefined` when it holds no lease or the lease is live. One property
2151
+ * read on a handle with no lease. It is not {@link Segment.expired}: that rule reads empty, and a lease must never.
2152
+ */
2153
+ leaseError() {
2154
+ const hold = this.leaseHold;
2155
+ if (hold === void 0) return void 0;
2156
+ const name = this.ref.namespace ? `${this.ref.namespace}/${this.ref.segment}` : this.ref.segment;
2157
+ if (hold.released) {
2158
+ return new LeaseExpiredError(
2159
+ `the lease on segment "${name}" was released`,
2160
+ hold.until,
2161
+ "released"
2162
+ );
2163
+ }
2164
+ if (this.clock.now() >= hold.until) {
2165
+ return new LeaseExpiredError(
2166
+ `the lease on segment "${name}" ended; a read of a handle past its lease throws, never reads empty`,
2167
+ hold.until,
2168
+ "expired"
2169
+ );
2170
+ }
2171
+ return void 0;
2172
+ }
2173
+ /**
2174
+ * Throw the first lease error among `handles`: this handle and every operand and exclude of a call. It runs before
2175
+ * any `expiresAt` rule and before the engine, so no operand, exclude or cached reader can answer past a lease.
2176
+ */
2177
+ assertLeases(handles) {
2178
+ for (const h of handles) {
2179
+ const err = h.leaseError();
2180
+ if (err !== void 0) throw err;
2181
+ }
2182
+ }
2183
+ /**
2184
+ * Reopen a generation a pin named earlier, as a pinned handle like the one {@link Segment.pin} returns, held at
2185
+ * `at.generation` instead of the current one. Pass what the earlier pin recorded, `{ generation, fingerprint }`
2186
+ * from its {@link Segment.pinnedAt}. A generation number alone is not an identity, since a purged and re-created
2187
+ * name starts again at 0, so the fingerprint is required: a bare number, a malformed fingerprint or a key other
2188
+ * than `generation` and `fingerprint` is a {@link ValidationError}. Identify a pin by its `generation` and
2189
+ * `fingerprint`: the handle's `pinnedAt.version` can differ from the earlier pin's.
2190
+ *
2191
+ * Throws {@link NotFoundError} when the generation is gone (collected, purged, or on a crypto-shredded segment),
2192
+ * is above the row's pointer, or is another object than the fingerprint names, and never reads empty. A rollback
2193
+ * deletes nothing, so once a later load moves the pointer past a generation it rolled back from, that one can be
2194
+ * reopened while its object is stored. Nothing keeps a generation alive unless you lease it: a later load's
2195
+ * collection can delete it, which is sized by `keep` on every writer, or held by `options.leaseUntil` (see
2196
+ * {@link PinOptions} and {@link Segment.pin}). A leased `pinAt` writes the lease before it verifies the object, and
2197
+ * releases it if the verify fails; for a generation below the pointer, a collector that read the row before the lease
2198
+ * landed can still delete the generation inside its own round trip, and the handle then fails with `NotFoundError`
2199
+ * like any swept pin. A leased handle is checked at every read site as a leased `pin()` is. With a registry it costs one row read and one tail read,
2200
+ * and a leased `pinAt` one conditional write to the row besides; the handle
2201
+ * then reads as one from `pin()` does, including its failure on a chunk it has not cached once the generation is
2202
+ * swept. Works for an encrypted segment. The first argument is an object so that fields can join it later; the lease
2203
+ * is in `options`, the second.
2204
+ */
2205
+ async pinAt(at, options) {
2206
+ this.assertLeases([this]);
2207
+ const { generation, fingerprint } = at ?? {};
2208
+ const unknown = typeof at === "object" && at !== null ? Object.keys(at).filter((k) => k !== "generation" && k !== "fingerprint") : [];
2209
+ if (unknown.length > 0) {
2210
+ throw new ValidationError2(
2211
+ `pinAt: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; this version takes { generation, fingerprint } only`
2212
+ );
2213
+ }
2214
+ if (typeof generation !== "number" || !Number.isSafeInteger(generation) || generation < 0 || typeof fingerprint !== "string" || !/^\d+:\d+$/.test(fingerprint)) {
2215
+ throw new ValidationError2(
2216
+ "pinAt: pass { generation, fingerprint } as recorded by an earlier pin (its pinnedAt); a generation number without the fingerprint of its object does not identify it"
2217
+ );
2218
+ }
2219
+ const leaseUntil = this.leaseUntilOf("pinAt", options);
2220
+ return this.pinned(this.ref, this.expiresAt, leaseUntil, { generation, fingerprint });
1817
2221
  }
1818
2222
  /**
1819
2223
  * An opaque string that names this handle's segment, namespace included: two handles of one segment have the
@@ -1857,6 +2261,7 @@ var Segment = class _Segment {
1857
2261
  * awaiting.
1858
2262
  */
1859
2263
  refuseIfExpired(op, dest, operands) {
2264
+ this.assertLeases([this, dest, ...operands]);
1860
2265
  const stale = [];
1861
2266
  for (const seg of [this, dest, ...operands]) {
1862
2267
  if (seg.expired())
@@ -1868,6 +2273,10 @@ var Segment = class _Segment {
1868
2273
  );
1869
2274
  }
1870
2275
  }
2276
+ /** `result` as is, or, when a handle of the call holds a lease, checking that lease each time it reads a chunk. */
2277
+ guarded(out, handles, result) {
2278
+ return handles.some((h) => h.leaseHold !== void 0) ? out.guard(result, () => this.assertLeases(handles)) : result;
2279
+ }
1871
2280
  /**
1872
2281
  * Time an op with the injected clock and emit an `op` metric on completion (success or throw). Skipped
1873
2282
  * entirely when no sink is wired, so the default path pays nothing.
@@ -1883,6 +2292,8 @@ var Segment = class _Segment {
1883
2292
  }
1884
2293
  /** Membership: one chunk — the cache, else one ranged GET. Throws {@link ValidationError} on a bad id. */
1885
2294
  has(id) {
2295
+ const lease = this.leaseError();
2296
+ if (lease !== void 0) return Promise.reject(lease);
1886
2297
  if (this.expired()) return Promise.resolve(false);
1887
2298
  return this.timed("has", () => this.engine.has(this.ref, id));
1888
2299
  }
@@ -1905,6 +2316,8 @@ var Segment = class _Segment {
1905
2316
  * structure is checked.
1906
2317
  */
1907
2318
  count() {
2319
+ const lease = this.leaseError();
2320
+ if (lease !== void 0) return Promise.reject(lease);
1908
2321
  if (this.expired()) return Promise.resolve(0);
1909
2322
  return this.timed("count", () => this.engine.count(this.ref));
1910
2323
  }
@@ -1923,6 +2336,7 @@ var Segment = class _Segment {
1923
2336
  * ```
1924
2337
  */
1925
2338
  async stat() {
2339
+ this.assertLeases([this]);
1926
2340
  if (this.expired()) return { generation: null, cardinality: 0 };
1927
2341
  return this.engine.stat(this.ref);
1928
2342
  }
@@ -1942,16 +2356,70 @@ var Segment = class _Segment {
1942
2356
  * without checking its options, as every read of one does.
1943
2357
  */
1944
2358
  iterate(options) {
2359
+ const lease = this.leaseError();
2360
+ if (lease !== void 0) return failing(lease);
1945
2361
  if (this.expired()) return EMPTY_IDS;
1946
2362
  const range = options == null ? void 0 : rangeOf(options);
1947
2363
  const none = range === void 0 || range.after === void 0 && range.through === void 0;
1948
2364
  const { engine, ref } = this;
1949
2365
  const bounds = none ? void 0 : range;
1950
- return withBatches(engine.iterate(ref, bounds), () => engine.iterateBatches(ref, bounds));
2366
+ const stream = withBatches(
2367
+ engine.iterate(ref, bounds),
2368
+ () => engine.iterateBatches(ref, bounds)
2369
+ );
2370
+ return this.leaseHold === void 0 ? stream : guardIds(stream, () => this.assertLeases([this]));
2371
+ }
2372
+ /**
2373
+ * **Pinned handles only.** The ids at 1-based ranks `n`, `2n`, `3n` …, counted over the ids in `(after, through]`
2374
+ * when a range is given, ascending: `everyNth(1000)` yields the 1,000th id, then the 2,000th, and so on. A last
2375
+ * partial window yields nothing, so a caller cutting a send into windows of `n` appends its own final end.
2376
+ *
2377
+ * It places each boundary from the per-chunk counts the pin already holds and reads only the chunks that hold one,
2378
+ * each once however many boundaries it holds, so it reads at most one chunk per boundary and never more chunks
2379
+ * than the object has. A range with `after` inside a chunk reads that chunk too, to count the ids at or below
2380
+ * `after`, even when fewer than `n` ids remain above it; when the chunks in range hold fewer than `n` ids in all,
2381
+ * nothing is fetched. `through` stops the read and no chunk past it is read, though a `through` inside a chunk may read that last chunk and
2382
+ * yield nothing from it. The reads and the read-ahead window are
2383
+ * those of {@link Segment.iterate}, and the per-op budget is charged before the first fetch, with an upper bound on
2384
+ * the chunks the read can take. A pinned read after the generation is swept throws {@link NotFoundError}.
2385
+ *
2386
+ * The ranks come from the index's counts, which are trusted as {@link Segment.count} trusts them. A chunk that is
2387
+ * read must hold the number of ids its index says, or the read throws {@link IntegrityError}; a chunk that is not
2388
+ * read is not checked, so an index that is wrong about one, and, on an unencrypted object, still adds up to the footer's
2389
+ * total, moves the ids after it to the wrong ranks. An encrypted object's index is authenticated and bound to the
2390
+ * object, so a wrong count there needs the key.
2391
+ *
2392
+ * A live handle is refused with {@link UnsupportedError} at the first read, because its counts and its chunks could
2393
+ * come from two generations and name the wrong id. `n` that is not a positive integer throws
2394
+ * {@link ValidationError} at the first read, as does a bad bound. An expired handle reads empty.
2395
+ *
2396
+ * ```ts
2397
+ * const audience = await store.segment('audience').pin();
2398
+ * const ends: number[] = [];
2399
+ * for await (const id of audience.everyNth(1_000)) ends.push(id); // the end of each window of 1,000
2400
+ * ```
2401
+ */
2402
+ everyNth(n, options) {
2403
+ const lease = this.leaseError();
2404
+ if (lease !== void 0) return failing(lease);
2405
+ if (this.expired()) return EMPTY_IDS;
2406
+ if (this.pinnedAt === void 0) {
2407
+ return failing(
2408
+ new UnsupportedError(
2409
+ "everyNth needs a pinned handle: a live handle can place a rank by one generation and read it from another. Call `pin()` first."
2410
+ )
2411
+ );
2412
+ }
2413
+ const stream = this.engine.everyNth(
2414
+ this.ref,
2415
+ n,
2416
+ options == null ? void 0 : rangeOf(options)
2417
+ );
2418
+ return this.leaseHold === void 0 ? stream : guardIdIterable(stream, () => this.assertLeases([this]));
1951
2419
  }
1952
2420
  /**
1953
2421
  * The options a combine hands the engine, read once, when it is called ({@link readOptions}), with the `exclude`
1954
- * it was given, already reduced to its live handles ({@link liveExcludes}) and mapped down to the plain refs
2422
+ * it was given ({@link excludesOf}) and mapped down to the plain refs
1955
2423
  * `core` takes. A method rather than a module function because `ref` is class-private — the encapsulation is
1956
2424
  * worth more than the free function.
1957
2425
  *
@@ -1967,18 +2435,35 @@ var Segment = class _Segment {
1967
2435
  };
1968
2436
  }
1969
2437
  /**
1970
- * The handles in `options.exclude` that have not expired. **An expired exclusion excludes nothing**, in every
1971
- * combine, so it is dropped here, before the engine is asked for anything: it is never fetched, never
1972
- * checked for absence, and never counted in the pin-consistency check. `exclude` is read once, so a getter sees
1973
- * one call.
1974
- *
1975
- * With no `exclude` this is one property read and a shared empty list, and with none expired it returns the
1976
- * caller's own array, so the common case allocates nothing.
2438
+ * The handles in `options.exclude`, read once so a getter sees one call. With none this is one property read and
2439
+ * a shared empty list; otherwise it is the caller's own array, so the common case allocates nothing.
1977
2440
  */
1978
- liveExcludes(options) {
2441
+ excludesOf(options) {
1979
2442
  const exclude = options?.exclude;
1980
- if (exclude == null || exclude.length === 0) return NO_SEGMENTS;
1981
- return exclude.some((e) => e.expired()) ? exclude.filter((e) => !e.expired()) : exclude;
2443
+ return exclude == null || exclude.length === 0 ? NO_SEGMENTS : exclude;
2444
+ }
2445
+ /**
2446
+ * The refusal for a combine whose **exclusion** has expired, or `undefined` when none has. Unlike an expired
2447
+ * operand, which reads empty, an exclusion is a suppression or opt-out list: left out silently, the result
2448
+ * would include the very ids it was passed to remove. So it is refused with a {@link ValidationError} naming each
2449
+ * expired exclusion, as the `*Into` verbs refuse an expired handle.
2450
+ *
2451
+ * It is judged against the injected clock when the combine is called, before the engine is asked for anything:
2452
+ * no request is made, and an expired exclusion that names a segment that does not exist is refused as expired,
2453
+ * not as an absent operand. It runs ahead of the expired-operand shortcuts, so a combine that would read empty
2454
+ * anyway still refuses a lapsed exclusion. A stream already being read is not re-checked when an exclusion
2455
+ * expires part-way, as an operand expiring part-way does not stop it either.
2456
+ */
2457
+ expiredExcludes(op, excludes) {
2458
+ let stale;
2459
+ for (const e of excludes) {
2460
+ if (!e.expired()) continue;
2461
+ (stale ??= []).push(e.ref.namespace ? `${e.ref.namespace}/${e.ref.segment}` : e.ref.segment);
2462
+ }
2463
+ if (stale === void 0) return void 0;
2464
+ return new ValidationError2(
2465
+ `${op}: refusing to read while these exclusions have expired \u2014 ${[...new Set(stale)].join(", ")}. An expired exclusion would subtract nothing, so the result would include the ids it was passed to remove. Renew the exclusion's \`expiresAt\`, open it without one, or leave it out of the call.`
2466
+ );
1982
2467
  }
1983
2468
  /**
1984
2469
  * Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
@@ -1990,17 +2475,28 @@ var Segment = class _Segment {
1990
2475
  * override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
1991
2476
  */
1992
2477
  intersect(others, options) {
1993
- if (this.expired() || others.some((o) => o.expired())) return EMPTY_IDS;
1994
- const exclude = this.liveExcludes(options);
2478
+ return this.intersectAs(AS_IDS, others, options);
2479
+ }
2480
+ intersectAs(out, others, options) {
2481
+ const exclude = this.excludesOf(options);
2482
+ const handles = [this, ...others, ...exclude];
2483
+ try {
2484
+ this.assertLeases(handles);
2485
+ } catch (err) {
2486
+ return out.failing(err);
2487
+ }
2488
+ const refused = this.expiredExcludes("intersect", exclude);
2489
+ if (refused) return out.failing(refused);
2490
+ if (this.expired() || others.some((o) => o.expired())) return out.none;
1995
2491
  let engine;
1996
2492
  try {
1997
2493
  engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
1998
2494
  } catch (err) {
1999
- return failing(err);
2495
+ return out.failing(err);
2000
2496
  }
2001
2497
  const refs = [this.ref, ...others.map((o) => o.ref)];
2002
2498
  const opts = this.refsIn(options, exclude);
2003
- return withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts));
2499
+ return this.guarded(out, handles, out.intersect(engine, refs, opts));
2004
2500
  }
2005
2501
  /**
2006
2502
  * Materialize `this ∩ others…` (minus `exclude`) as a **new generation of `dest`** — `dest`'s previous contents
@@ -2027,7 +2523,12 @@ var Segment = class _Segment {
2027
2523
  this.combineEngine([this, ...others, ...options?.exclude ?? []]);
2028
2524
  return this.timed(
2029
2525
  "intersectInto",
2030
- () => this.materialize(dest.ref, this.intersect(others, options), "intersectInto", options)
2526
+ () => this.materialize(
2527
+ dest.ref,
2528
+ new CombineChunks(this.intersectAs(AS_CHUNKS, others, options)),
2529
+ "intersectInto",
2530
+ options
2531
+ )
2031
2532
  );
2032
2533
  }
2033
2534
  /**
@@ -2041,26 +2542,36 @@ var Segment = class _Segment {
2041
2542
  * segment once (`unionInto`, or a load) is the cheaper shape.
2042
2543
  */
2043
2544
  union(others, options) {
2545
+ return this.unionAs(AS_IDS, others, options);
2546
+ }
2547
+ unionAs(out, others, options) {
2548
+ const exclude = this.excludesOf(options);
2549
+ const handles = [this, ...others, ...exclude];
2550
+ try {
2551
+ this.assertLeases(handles);
2552
+ } catch (err) {
2553
+ return out.failing(err);
2554
+ }
2555
+ const refused = this.expiredExcludes("union", exclude);
2556
+ if (refused) return out.failing(refused);
2044
2557
  const live = others.filter((o) => !o.expired());
2045
2558
  if (this.expired()) {
2046
- if (live.length === 0) return EMPTY_IDS;
2047
- return live[0].union(live.slice(1), options);
2559
+ if (live.length === 0) return out.none;
2560
+ return live[0].unionAs(out, live.slice(1), options);
2048
2561
  }
2049
2562
  if (live.length === 0 && others.length > 0) {
2050
- const exclude2 = this.liveExcludes(options);
2051
- return exclude2.length > 0 ? this.andNot([...exclude2], options) : this.union([], options);
2563
+ return exclude.length > 0 ? this.andNotAs(out, [...exclude], options) : this.unionAs(out, [], options);
2052
2564
  }
2053
- if (live.length !== others.length) return this.union(live, options);
2054
- const exclude = this.liveExcludes(options);
2565
+ if (live.length !== others.length) return this.unionAs(out, live, options);
2055
2566
  let engine;
2056
2567
  try {
2057
2568
  engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
2058
2569
  } catch (err) {
2059
- return failing(err);
2570
+ return out.failing(err);
2060
2571
  }
2061
2572
  const refs = [this.ref, ...others.map((o) => o.ref)];
2062
2573
  const opts = this.refsIn(options, exclude);
2063
- return withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts));
2574
+ return this.guarded(out, handles, out.union(engine, refs, opts));
2064
2575
  }
2065
2576
  /** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
2066
2577
  async unionInto(dest, others, options) {
@@ -2068,7 +2579,12 @@ var Segment = class _Segment {
2068
2579
  this.combineEngine([this, ...others, ...options?.exclude ?? []]);
2069
2580
  return this.timed(
2070
2581
  "unionInto",
2071
- () => this.materialize(dest.ref, this.union(others, options), "unionInto", options)
2582
+ () => this.materialize(
2583
+ dest.ref,
2584
+ new CombineChunks(this.unionAs(AS_CHUNKS, others, options)),
2585
+ "unionInto",
2586
+ options
2587
+ )
2072
2588
  );
2073
2589
  }
2074
2590
  /**
@@ -2079,33 +2595,36 @@ var Segment = class _Segment {
2079
2595
  * suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
2080
2596
  * opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
2081
2597
  *
2082
- * An expired handle in `excludes` excludes nothing, and is skipped without being read; if every one has
2083
- * expired the result is `this` whole. An expired `this` is empty. `exclude` on `intersect` and `union` follows
2084
- * the same rule.
2598
+ * An expired handle in `excludes` is refused, not skipped: the stream throws {@link ValidationError} naming it,
2599
+ * before any request is made, because a lapsed suppression list would otherwise let through the ids it was
2600
+ * passed to remove. An expired `this` is empty. `exclude` on `intersect` and `union` follows the same rule.
2085
2601
  *
2086
2602
  * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
2087
2603
  * the suppression folds into the same pass rather than materializing an intermediate segment first.
2088
2604
  */
2089
2605
  andNot(excludes, options) {
2090
- if (this.expired()) return EMPTY_IDS;
2091
- const liveExcludes = excludes.filter((e) => !e.expired());
2092
- if (liveExcludes.length === 0 && excludes.length > 0) {
2093
- return this.union([], options == null ? void 0 : readOptions(options));
2606
+ return this.andNotAs(AS_IDS, excludes, options);
2607
+ }
2608
+ andNotAs(out, excludes, options) {
2609
+ const handles = [this, ...excludes];
2610
+ try {
2611
+ this.assertLeases(handles);
2612
+ } catch (err) {
2613
+ return out.failing(err);
2094
2614
  }
2095
- if (liveExcludes.length !== excludes.length) return this.andNot(liveExcludes, options);
2615
+ const refused = this.expiredExcludes("andNot", excludes);
2616
+ if (refused) return out.failing(refused);
2617
+ if (this.expired()) return out.none;
2096
2618
  let engine;
2097
2619
  try {
2098
2620
  engine = this.combineEngine([this, ...excludes]) ?? this.engine;
2099
2621
  } catch (err) {
2100
- return failing(err);
2622
+ return out.failing(err);
2101
2623
  }
2102
2624
  const base = this.ref;
2103
2625
  const refs = excludes.map((o) => o.ref);
2104
2626
  const opts = options == null ? void 0 : readOptions(options);
2105
- return withBatches(
2106
- engine.andNot(base, refs, opts),
2107
- () => engine.andNotBatches(base, refs, opts)
2108
- );
2627
+ return this.guarded(out, handles, out.andNot(engine, base, refs, opts));
2109
2628
  }
2110
2629
  /** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */
2111
2630
  async andNotInto(dest, excludes, options) {
@@ -2113,7 +2632,12 @@ var Segment = class _Segment {
2113
2632
  this.combineEngine([this, ...excludes]);
2114
2633
  return this.timed(
2115
2634
  "andNotInto",
2116
- () => this.materialize(dest.ref, this.andNot(excludes, options), "andNotInto", options)
2635
+ () => this.materialize(
2636
+ dest.ref,
2637
+ new CombineChunks(this.andNotAs(AS_CHUNKS, excludes, options)),
2638
+ "andNotInto",
2639
+ options
2640
+ )
2117
2641
  );
2118
2642
  }
2119
2643
  /**
@@ -2124,6 +2648,7 @@ var Segment = class _Segment {
2124
2648
  * the lose-zone).
2125
2649
  */
2126
2650
  async costReport(options) {
2651
+ this.assertLeases([this]);
2127
2652
  const canMeasure = this.engine.supportsStorageSize;
2128
2653
  const size = canMeasure ? await this.engine.segmentSize(this.ref) : null;
2129
2654
  const refreshMs = this.engine.pointerRefreshMs;
@@ -2152,11 +2677,16 @@ export {
2152
2677
  InProcessKeystore,
2153
2678
  IntegrityError3 as IntegrityError,
2154
2679
  KeyUnavailableError,
2680
+ LEASE_SKEW_MS2 as LEASE_SKEW_MS,
2681
+ LeaseExpiredError2 as LeaseExpiredError,
2682
+ LeaseLimitError,
2155
2683
  LocalFsStorage,
2684
+ MAX_LEASES_PER_SEGMENT,
2685
+ MAX_LEASE_MS2 as MAX_LEASE_MS,
2156
2686
  MIN_EXPIRES_AT_MS2 as MIN_EXPIRES_AT_MS,
2157
2687
  MemoryStorage,
2158
2688
  NodeAead,
2159
- NotFoundError,
2689
+ NotFoundError2 as NotFoundError,
2160
2690
  ONE_REDIS_HA_CLUSTER,
2161
2691
  RecordingAuditSink,
2162
2692
  Segment,
@@ -2165,11 +2695,14 @@ export {
2165
2695
  ValidationError3 as ValidationError,
2166
2696
  WriteConflictError2 as WriteConflictError,
2167
2697
  aadFor,
2698
+ deserializePortable,
2168
2699
  destroySegment,
2169
2700
  eraseNamespace,
2170
2701
  excludingReservedRows2 as excludingReservedRows,
2171
2702
  isCloudRoaringError,
2172
2703
  isIntegrityError,
2704
+ isLeaseExpiredError,
2705
+ isLeaseLimitError,
2173
2706
  isNotFoundError,
2174
2707
  isTransientError,
2175
2708
  isValidationError,