@cloudbitmaps/roaring 0.16.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/PRIVACY.md +16 -0
- package/README.md +4 -4
- package/dist/bin/export-segments.js +531 -59
- package/dist/bin/export-segments.js.map +4 -4
- package/dist/index.d.ts +223 -36
- package/dist/index.js +543 -60
- package/dist/index.js.map +4 -4
- package/dist/lease-guards.d.ts +34 -0
- package/dist/roaring-codec.d.ts +23 -0
- package/package.json +2 -2
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
NOOP_METRICS,
|
|
22
22
|
RetryingStorageChunkSource,
|
|
23
23
|
SegmentEngine,
|
|
24
|
+
LEASE_SKEW_MS,
|
|
25
|
+
LeaseExpiredError,
|
|
26
|
+
MAX_LEASE_MS,
|
|
27
|
+
NotFoundError,
|
|
24
28
|
UnsupportedError,
|
|
25
29
|
ValidationError as ValidationError2,
|
|
26
30
|
WriteConflictError,
|
|
@@ -37,14 +41,17 @@ import {
|
|
|
37
41
|
mapWithConcurrency,
|
|
38
42
|
resolveBudget,
|
|
39
43
|
resolvePerOpBudget,
|
|
44
|
+
reapRegistryTombstones,
|
|
40
45
|
retireExpired,
|
|
41
46
|
runConsistencyCheck,
|
|
42
47
|
runExport,
|
|
43
48
|
setSegmentRetention,
|
|
44
49
|
clearSegmentRetention,
|
|
45
50
|
getSegmentRetention,
|
|
51
|
+
releaseLease,
|
|
46
52
|
safeMetrics,
|
|
47
|
-
splitId
|
|
53
|
+
splitId,
|
|
54
|
+
takeLease
|
|
48
55
|
} from "@cloudbitmaps/core";
|
|
49
56
|
import { validateSegmentRef } from "@cloudbitmaps/core/driver-kit";
|
|
50
57
|
|
|
@@ -104,7 +111,7 @@ function refuseReservedNamespace(namespace) {
|
|
|
104
111
|
|
|
105
112
|
// src/roaring-codec.ts
|
|
106
113
|
import roaring from "roaring";
|
|
107
|
-
import { IntegrityError as IntegrityError2 } from "@cloudbitmaps/core";
|
|
114
|
+
import { IntegrityError as IntegrityError2, decodeSerialized } from "@cloudbitmaps/core";
|
|
108
115
|
|
|
109
116
|
// src/portable/layout.ts
|
|
110
117
|
import { IntegrityError } from "@cloudbitmaps/core";
|
|
@@ -352,6 +359,10 @@ var SafeBitmap = class _SafeBitmap {
|
|
|
352
359
|
throw new IntegrityError2(`failed to deserialize bitmap: ${err.message}`);
|
|
353
360
|
}
|
|
354
361
|
}
|
|
362
|
+
/** The `roaring` bitmap this wraps, for a caller that was handed it once and keeps no wrapper. */
|
|
363
|
+
unwrap() {
|
|
364
|
+
return this.bitmap;
|
|
365
|
+
}
|
|
355
366
|
serialize() {
|
|
356
367
|
return this.bitmap.serialize(SerializationFormat.portable);
|
|
357
368
|
}
|
|
@@ -516,6 +527,86 @@ var OPTION_KEYS = {
|
|
|
516
527
|
seams: ["clock", "rng"]
|
|
517
528
|
};
|
|
518
529
|
|
|
530
|
+
// src/lease-guards.ts
|
|
531
|
+
var guardIdIterable = (inner, check) => ({
|
|
532
|
+
[Symbol.asyncIterator]: () => {
|
|
533
|
+
const it = inner[Symbol.asyncIterator]();
|
|
534
|
+
let chunk = -1;
|
|
535
|
+
let first = true;
|
|
536
|
+
const stop = async (err) => {
|
|
537
|
+
await it.return?.();
|
|
538
|
+
throw err;
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
async next() {
|
|
542
|
+
if (first) {
|
|
543
|
+
first = false;
|
|
544
|
+
try {
|
|
545
|
+
check();
|
|
546
|
+
} catch (err) {
|
|
547
|
+
return stop(err);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const r = await it.next();
|
|
551
|
+
if (r.done === true) return r;
|
|
552
|
+
const key = r.value >>> 16;
|
|
553
|
+
if (key !== chunk) {
|
|
554
|
+
try {
|
|
555
|
+
check();
|
|
556
|
+
} catch (err) {
|
|
557
|
+
return stop(err);
|
|
558
|
+
}
|
|
559
|
+
chunk = key;
|
|
560
|
+
}
|
|
561
|
+
return r;
|
|
562
|
+
},
|
|
563
|
+
async return() {
|
|
564
|
+
await it.return?.();
|
|
565
|
+
return { done: true, value: void 0 };
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
var guardIds = (inner, check) => ({
|
|
571
|
+
[Symbol.asyncIterator]: () => guardIdIterable(inner, check)[Symbol.asyncIterator](),
|
|
572
|
+
batches: () => guardItems(inner.batches(), check)
|
|
573
|
+
});
|
|
574
|
+
var guardItems = (inner, check) => ({
|
|
575
|
+
[Symbol.asyncIterator]: () => {
|
|
576
|
+
const it = inner[Symbol.asyncIterator]();
|
|
577
|
+
let first = true;
|
|
578
|
+
const stop = async (err) => {
|
|
579
|
+
await it.return?.();
|
|
580
|
+
throw err;
|
|
581
|
+
};
|
|
582
|
+
return {
|
|
583
|
+
async next() {
|
|
584
|
+
if (first) {
|
|
585
|
+
first = false;
|
|
586
|
+
try {
|
|
587
|
+
check();
|
|
588
|
+
} catch (err) {
|
|
589
|
+
return stop(err);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const r = await it.next();
|
|
593
|
+
if (r.done === true) return r;
|
|
594
|
+
try {
|
|
595
|
+
check();
|
|
596
|
+
} catch (err) {
|
|
597
|
+
return stop(err);
|
|
598
|
+
}
|
|
599
|
+
return r;
|
|
600
|
+
},
|
|
601
|
+
async return() {
|
|
602
|
+
await it.return?.();
|
|
603
|
+
return { done: true, value: void 0 };
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
var guardChunks = (inner, check) => guardItems(inner, check);
|
|
609
|
+
|
|
519
610
|
// src/index.ts
|
|
520
611
|
import {
|
|
521
612
|
MemoryStorage,
|
|
@@ -526,6 +617,9 @@ import {
|
|
|
526
617
|
excludingReservedRows as excludingReservedRows2,
|
|
527
618
|
readRetentionPolicy,
|
|
528
619
|
MIN_EXPIRES_AT_MS as MIN_EXPIRES_AT_MS2,
|
|
620
|
+
LEASE_SKEW_MS as LEASE_SKEW_MS2,
|
|
621
|
+
MAX_LEASE_MS as MAX_LEASE_MS2,
|
|
622
|
+
MAX_LEASES_PER_SEGMENT,
|
|
529
623
|
InProcessKeystore,
|
|
530
624
|
NodeAead,
|
|
531
625
|
aadFor,
|
|
@@ -533,18 +627,22 @@ import {
|
|
|
533
627
|
ValidationError as ValidationError3,
|
|
534
628
|
WriteConflictError as WriteConflictError2,
|
|
535
629
|
IntegrityError as IntegrityError3,
|
|
536
|
-
NotFoundError,
|
|
630
|
+
NotFoundError as NotFoundError2,
|
|
537
631
|
UnsupportedError as UnsupportedError2,
|
|
538
632
|
CapabilityError,
|
|
539
633
|
TransientError,
|
|
540
634
|
KeyUnavailableError,
|
|
541
635
|
BudgetExceededError,
|
|
636
|
+
LeaseExpiredError as LeaseExpiredError2,
|
|
637
|
+
LeaseLimitError,
|
|
542
638
|
isCloudRoaringError,
|
|
543
639
|
isWriteConflictError,
|
|
544
640
|
isTransientError,
|
|
545
641
|
isNotFoundError,
|
|
546
642
|
isIntegrityError,
|
|
547
643
|
isValidationError,
|
|
644
|
+
isLeaseExpiredError,
|
|
645
|
+
isLeaseLimitError,
|
|
548
646
|
CrbmReader,
|
|
549
647
|
BufferReader,
|
|
550
648
|
CountingMetricsSink,
|
|
@@ -740,6 +838,9 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
740
838
|
clock
|
|
741
839
|
});
|
|
742
840
|
const resolved = resolveStorageSource(options, clock);
|
|
841
|
+
if (metrics !== NOOP_METRICS && isStorageBackend(options.storage)) {
|
|
842
|
+
options.storage.attachMetrics?.(metrics);
|
|
843
|
+
}
|
|
743
844
|
let storage = resolved.source;
|
|
744
845
|
if (options.retry !== false) {
|
|
745
846
|
const { onRetry: userOnRetry, ...ov } = options.retry ?? {};
|
|
@@ -837,7 +938,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
837
938
|
clock: this.clock,
|
|
838
939
|
metrics: this.metrics,
|
|
839
940
|
materialize: (dest, ids, op, options2) => this.materialize(dest, ids, op, options2),
|
|
840
|
-
pinned: (r, e) => this.pinSegment(r, e),
|
|
941
|
+
pinned: (r, e, l, at) => this.pinSegment(r, e, l, at),
|
|
841
942
|
combineEngine: (handles) => this.engineForCombine(handles),
|
|
842
943
|
expiresAt
|
|
843
944
|
});
|
|
@@ -1262,24 +1363,26 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1262
1363
|
* A refused load deletes the object it wrote before returning — it sits above `currentGen`, where generation
|
|
1263
1364
|
* collection deliberately never looks — but only while the segment's registry row is unchanged or gone. Once
|
|
1264
1365
|
* another write has changed the row, the generation number it holds may name another incarnation's live object,
|
|
1265
|
-
* so it leaves the orphan rather than risk deleting live data.
|
|
1266
|
-
*
|
|
1366
|
+
* so it leaves the orphan rather than risk deleting live data. No row's list names it, so it takes no place in a
|
|
1367
|
+
* window, and a listing deletes it once a later generation is current above it.
|
|
1267
1368
|
*
|
|
1268
|
-
* **Collection is by name
|
|
1269
|
-
* pointer deletes the
|
|
1270
|
-
* objects on every sixteenth generation, and on any load that met an object above the
|
|
1271
|
-
* the current generation's object gone, to take what the name-only passes leave, such as
|
|
1272
|
-
*
|
|
1273
|
-
* the
|
|
1369
|
+
* **Collection is by name at any `keep` up to 64.** The segment's row records the generations a load keeps, so a load
|
|
1370
|
+
* that found nothing above the pointer deletes the generations its publish pushed out of the window and lists
|
|
1371
|
+
* nothing. It lists the segment's objects on every sixteenth generation, and on any load that met an object above the
|
|
1372
|
+
* pointer or whose check found the current generation's object gone, to take what the name-only passes leave, such as
|
|
1373
|
+
* an object a crashed load left below the pointer. A `keep` above 64 records no list and lists on every load, and so
|
|
1374
|
+
* does the first load of a row that records none, which then records it. {@link LoadResult.collected} then names
|
|
1375
|
+
* what the pass deleted by name, and one name may have been gone already.
|
|
1274
1376
|
*
|
|
1275
1377
|
* What it **throws** for is a fault rather than an outcome: invalid options or ids, and a crypto-shredded
|
|
1276
1378
|
* segment (`ValidationError`); a key the keystore cannot provide (`KeyUnavailableError`); a current generation
|
|
1277
1379
|
* 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
|
|
1278
|
-
* (`IntegrityError`); a driver failure; and a collection
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1380
|
+
* (`IntegrityError`); a driver failure; and a fault in the collection's own reads, listing or deletes, or in the write
|
|
1381
|
+
* that records the window (anything but a lost race). A collection fault can be raised **after** the publish already
|
|
1382
|
+
* landed, so a throw does not by itself mean the load did not take effect — re-read the pointer rather than assuming.
|
|
1383
|
+
* A collection, by name or by listing, that finds another writer has moved the row since the publish (a load, a
|
|
1384
|
+
* rollback, a purge) stops and returns what it deleted in `collected`: a load that took effect does not throw for a
|
|
1385
|
+
* race it won.
|
|
1283
1386
|
*
|
|
1284
1387
|
* **A `TransientError` from the registry write can leave the publish unsettled, and deletes nothing.** The
|
|
1285
1388
|
* generation's object is sent again after a throttle where the backend allows it (a write id tells a first send
|
|
@@ -1351,7 +1454,11 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1351
1454
|
validateSegmentRef(ref);
|
|
1352
1455
|
const deps = this.lifecycleDeps("dropSegment");
|
|
1353
1456
|
try {
|
|
1354
|
-
return await dropSegment(
|
|
1457
|
+
return await dropSegment(
|
|
1458
|
+
ref,
|
|
1459
|
+
{ registry: deps.registry, storage: deps.storage, clock: deps.clock, rng: deps.rng },
|
|
1460
|
+
options
|
|
1461
|
+
);
|
|
1355
1462
|
} finally {
|
|
1356
1463
|
if (options.dryRun !== true) this.engine.invalidate(ref);
|
|
1357
1464
|
}
|
|
@@ -1384,7 +1491,11 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1384
1491
|
*/
|
|
1385
1492
|
async setRetention(ref, policy) {
|
|
1386
1493
|
validateSegmentRef(ref);
|
|
1387
|
-
return setSegmentRetention(
|
|
1494
|
+
return setSegmentRetention(
|
|
1495
|
+
ref,
|
|
1496
|
+
{ registry: this.requireRegistry("setRetention"), clock: this.clock, rng: this.rng },
|
|
1497
|
+
policy
|
|
1498
|
+
);
|
|
1388
1499
|
}
|
|
1389
1500
|
/**
|
|
1390
1501
|
* **The stored retention policy**, or `null` if the segment has none (or has no registry row, or is a
|
|
@@ -1403,7 +1514,11 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1403
1514
|
*/
|
|
1404
1515
|
async clearRetention(ref) {
|
|
1405
1516
|
validateSegmentRef(ref);
|
|
1406
|
-
return clearSegmentRetention(ref, {
|
|
1517
|
+
return clearSegmentRetention(ref, {
|
|
1518
|
+
registry: this.requireRegistry("clearRetention"),
|
|
1519
|
+
clock: this.clock,
|
|
1520
|
+
rng: this.rng
|
|
1521
|
+
});
|
|
1407
1522
|
}
|
|
1408
1523
|
/**
|
|
1409
1524
|
* **Run the retention sweep: retire every segment whose `expiresAt` has passed.** This is the call that acts on
|
|
@@ -1466,7 +1581,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1466
1581
|
async retireExpired(options = {}) {
|
|
1467
1582
|
const deps = this.lifecycleDeps("retireExpired");
|
|
1468
1583
|
const result = await retireExpired(
|
|
1469
|
-
{ registry: deps.registry, storage: deps.storage },
|
|
1584
|
+
{ registry: deps.registry, storage: deps.storage, clock: this.clock, rng: this.rng },
|
|
1470
1585
|
{ ...options, now: options.now ?? this.clock.now() }
|
|
1471
1586
|
);
|
|
1472
1587
|
if (options.dryRun !== true) {
|
|
@@ -1476,6 +1591,57 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1476
1591
|
}
|
|
1477
1592
|
return result;
|
|
1478
1593
|
}
|
|
1594
|
+
/**
|
|
1595
|
+
* **Remove the registry rows that releases before 0.12.0 left as `deleted: true` envelopes.** Those releases never
|
|
1596
|
+
* removed a row they deleted, and no later call removes it either: it is invisible to every read, and every full
|
|
1597
|
+
* listing of the registry still reads it (one GET each). This is the one call that removes them. It is an admin call,
|
|
1598
|
+
* not a sweep, and nothing schedules it: run it once after the last process on a release before 0.12.0 is gone.
|
|
1599
|
+
*
|
|
1600
|
+
* ```ts
|
|
1601
|
+
* const preview = await store.reapRegistryTombstones({ dryRun: true });
|
|
1602
|
+
* console.log(`would remove ${preview.wouldReap} of ${preview.examined} rows read`);
|
|
1603
|
+
*
|
|
1604
|
+
* const done = await store.reapRegistryTombstones({ confirmNoLegacyWriters: true });
|
|
1605
|
+
* if (done.limited) runAgain(); // `limit` (default 1,000) stopped the run with rows unread
|
|
1606
|
+
* ```
|
|
1607
|
+
*
|
|
1608
|
+
* **What it removes.** Only a row that is `deleted: true` and whose token carries no incarnation id, which is what a
|
|
1609
|
+
* 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
|
|
1610
|
+
* remove), whatever its `status`. It never touches a live row, a `destroyed` row that is not `deleted` (a crypto-shred's
|
|
1611
|
+
* or a `dropSegment`'s tombstone: the attestation of an erasure, which it cannot tell from another), or a deleted row
|
|
1612
|
+
* with an incarnation id, and it never touches a generation. **So it does not clean a bucket completely**: the
|
|
1613
|
+
* `destroyed` tombstones `dropSegment` leaves stay, so do live rows written before 0.12.0, and so does a tombstone a
|
|
1614
|
+
* 0.12.0 or later release wrote while `conditionalDelete` was off (GCS by default, S3 on a custom endpoint): every full
|
|
1615
|
+
* listing still reads it, and this call neither removes it nor runs on such a store.
|
|
1616
|
+
* It also covers the library's own bookkeeping rows when `namespace` is not set, by the same rule. The default
|
|
1617
|
+
* namespace cannot be scoped to: leave `namespace` out to cover it.
|
|
1618
|
+
*
|
|
1619
|
+
* **The two things it asks.** `confirmNoLegacyWriters: true` is required for a real run (without it, a
|
|
1620
|
+
* `ValidationError`, and nothing is requested): your statement that no process on a release before 0.12.0 writes
|
|
1621
|
+
* this registry, since one that did would, re-creating a removed name, issue the removed row's tokens again. And the
|
|
1622
|
+
* registry must apply a delete under a precondition: where its `conditionalDelete` capability is off (GCS by default,
|
|
1623
|
+
* S3 on a custom endpoint) it throws `CapabilityError` before any request, dry run included. A registry that keeps
|
|
1624
|
+
* no such rows (the in-memory and local-filesystem ones) throws `UnsupportedError`.
|
|
1625
|
+
*
|
|
1626
|
+
* **Each removal is fenced** on the version it read, so a `create` that lands over the envelope first wins, the
|
|
1627
|
+
* delete is refused, and the row is counted as `skipped.raced` (which also counts a row already gone). That holds where
|
|
1628
|
+
* the backend applies the precondition, which auto-detection checks (S3 only on an AWS host). On an endpoint that
|
|
1629
|
+
* ignores `If-Match` (MinIO, fake-gcs-server), a `conditionalDelete: true` you set makes this an unfenced delete: run
|
|
1630
|
+
* it with every writer stopped. On a versioned bucket a delete leaves a delete marker and the earlier version, record
|
|
1631
|
+
* included, so the name is not erased from the bucket. A `create` that had already read the envelope and
|
|
1632
|
+
* whose write meets the removal throws `WriteConflictError`, which this library does not retry: retry the create, and
|
|
1633
|
+
* it finds no row and writes one.
|
|
1634
|
+
*
|
|
1635
|
+
* **Cost** over R rows read and E removed: `ceil(R / 1000)` LIST requests, R GETs and E DELETEs (a dry run, no
|
|
1636
|
+
* DELETEs). `limit` bounds the removals, and the run stops listing once it is spent. **A run is not resumable**: each
|
|
1637
|
+
* lists and reads from the start (R GETs whatever the `limit`), a dry run with a `limit` reports the same first rows
|
|
1638
|
+
* every time, so `wouldReap` is a total only when `limited` is `false`, and `limited` is `true` whenever the limit was
|
|
1639
|
+
* spent with keys left, removable or not. An unreadable object stops the run with an error naming its key, and
|
|
1640
|
+
* removes nothing more: it may be a newer release's row. Scope the run with `namespace` to get past one.
|
|
1641
|
+
*/
|
|
1642
|
+
async reapRegistryTombstones(options = {}) {
|
|
1643
|
+
return reapRegistryTombstones(this.requireRegistry("reapRegistryTombstones"), options);
|
|
1644
|
+
}
|
|
1479
1645
|
/**
|
|
1480
1646
|
* **Forget everything this store has cached about a segment.** Use it after destroying or retiring a segment
|
|
1481
1647
|
* through a path this store did not perform itself.
|
|
@@ -1522,14 +1688,54 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1522
1688
|
* pinned view reports the version captured at pin time, marked as a pin's, so its decoded chunks are never
|
|
1523
1689
|
* those of a live read that fetched across a publish (see {@link PinnedStorageChunkSource.currentVersion}).
|
|
1524
1690
|
*/
|
|
1525
|
-
async pinSegment(ref, expiresAt) {
|
|
1691
|
+
async pinSegment(ref, expiresAt, leaseUntil, named) {
|
|
1526
1692
|
const crbm = this.crbmSource;
|
|
1527
1693
|
if (crbm === void 0) {
|
|
1528
1694
|
throw new UnsupportedError(
|
|
1529
1695
|
"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)"
|
|
1530
1696
|
);
|
|
1531
1697
|
}
|
|
1532
|
-
|
|
1698
|
+
let hold;
|
|
1699
|
+
let take;
|
|
1700
|
+
if (leaseUntil !== void 0) {
|
|
1701
|
+
const registry = this.requireRegistry("pin({ leaseUntil })");
|
|
1702
|
+
if (registry.capabilities().canWrite === false) {
|
|
1703
|
+
throw new UnsupportedError(
|
|
1704
|
+
"pin({ leaseUntil }) needs a registry that can write a row, and this store's cannot in this runtime"
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
const holder = this.mintHolder();
|
|
1708
|
+
const deps = { registry, clock: this.clock, rng: this.rng };
|
|
1709
|
+
const generationOf = (row) => named?.generation ?? row.currentGen;
|
|
1710
|
+
hold = {
|
|
1711
|
+
holder,
|
|
1712
|
+
until: leaseUntil,
|
|
1713
|
+
released: false,
|
|
1714
|
+
done: false,
|
|
1715
|
+
release: () => releaseLease(ref, deps, holder)
|
|
1716
|
+
};
|
|
1717
|
+
take = {
|
|
1718
|
+
take: (row) => takeLease(ref, deps, {
|
|
1719
|
+
holder,
|
|
1720
|
+
generation: generationOf(row),
|
|
1721
|
+
until: leaseUntil,
|
|
1722
|
+
row,
|
|
1723
|
+
current: named === void 0
|
|
1724
|
+
})
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
let at;
|
|
1728
|
+
try {
|
|
1729
|
+
at = await this.withRetries(
|
|
1730
|
+
() => named === void 0 ? crbm.pinGeneration(ref, take) : crbm.pinGenerationAt(ref, named, take)
|
|
1731
|
+
);
|
|
1732
|
+
if (hold !== void 0 && at === null) {
|
|
1733
|
+
throw new NotFoundError(`segment "${ref.segment}" has no generation to lease`);
|
|
1734
|
+
}
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
if (hold !== void 0) await hold.release().catch(() => void 0);
|
|
1737
|
+
throw err;
|
|
1738
|
+
}
|
|
1533
1739
|
const pinnedAt = {
|
|
1534
1740
|
generation: at?.generation ?? null,
|
|
1535
1741
|
version: at?.version ?? null,
|
|
@@ -1542,12 +1748,20 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1542
1748
|
clock: this.clock,
|
|
1543
1749
|
metrics: this.metrics,
|
|
1544
1750
|
materialize: (dest, ids, op, options) => this.materialize(dest, ids, op, options),
|
|
1545
|
-
pinned: (r, e) => this.pinSegment(r, e),
|
|
1751
|
+
pinned: (r, e, l, at2) => this.pinSegment(r, e, l, at2),
|
|
1546
1752
|
combineEngine: (handles) => this.engineForCombine(handles),
|
|
1547
1753
|
expiresAt,
|
|
1548
|
-
pinnedAt
|
|
1754
|
+
pinnedAt,
|
|
1755
|
+
lease: hold
|
|
1549
1756
|
});
|
|
1550
1757
|
}
|
|
1758
|
+
/** 16 hex digits from the store's `Rng`, which names the holder of one lease: unique, not secret. */
|
|
1759
|
+
mintHolder() {
|
|
1760
|
+
let id = "";
|
|
1761
|
+
for (let i = 0; i < 4; i++)
|
|
1762
|
+
id += Math.floor(this.rng.next() * 65536).toString(16).padStart(4, "0");
|
|
1763
|
+
return id;
|
|
1764
|
+
}
|
|
1551
1765
|
/**
|
|
1552
1766
|
* An engine reading every segment in `pins` at its pinned generation and everything else live, with the store's
|
|
1553
1767
|
* retries, so a transient fault on a pinned read is retried as it would be on a live one.
|
|
@@ -1708,6 +1922,7 @@ var EMPTY_IDS = {
|
|
|
1708
1922
|
var AS_IDS = {
|
|
1709
1923
|
none: EMPTY_IDS,
|
|
1710
1924
|
failing,
|
|
1925
|
+
guard: guardIds,
|
|
1711
1926
|
intersect: (engine, refs, opts) => withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts)),
|
|
1712
1927
|
union: (engine, refs, opts) => withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts)),
|
|
1713
1928
|
andNot: (engine, base, refs, opts) => withBatches(engine.andNot(base, refs, opts), () => engine.andNotBatches(base, refs, opts))
|
|
@@ -1724,6 +1939,7 @@ var CombineChunks = class {
|
|
|
1724
1939
|
};
|
|
1725
1940
|
var AS_CHUNKS = {
|
|
1726
1941
|
none: NO_CHUNKS,
|
|
1942
|
+
guard: guardChunks,
|
|
1727
1943
|
failing: (err) => ({
|
|
1728
1944
|
[Symbol.asyncIterator]: () => ({ next: () => Promise.reject(err) })
|
|
1729
1945
|
}),
|
|
@@ -1750,6 +1966,13 @@ var Segment = class _Segment {
|
|
|
1750
1966
|
* pinned handle passed as an **operand** is still read at its pin rather than live.
|
|
1751
1967
|
*/
|
|
1752
1968
|
pinnedAt;
|
|
1969
|
+
/**
|
|
1970
|
+
* The lease this handle holds, when it came from `pin({ leaseUntil })`; `undefined` otherwise. It reports what was
|
|
1971
|
+
* taken and does not change when the lease ends: a read after `lease.until` (or after {@link Segment.release}) throws
|
|
1972
|
+
* {@link LeaseExpiredError}.
|
|
1973
|
+
*/
|
|
1974
|
+
lease;
|
|
1975
|
+
leaseHold;
|
|
1753
1976
|
static {
|
|
1754
1977
|
makeSegment = (parts) => {
|
|
1755
1978
|
minting = true;
|
|
@@ -1779,6 +2002,10 @@ var Segment = class _Segment {
|
|
|
1779
2002
|
this.combineEngine = parts.combineEngine;
|
|
1780
2003
|
this.expiresAt = parts.expiresAt;
|
|
1781
2004
|
this.pinnedAt = parts.pinnedAt;
|
|
2005
|
+
this.leaseHold = parts.lease;
|
|
2006
|
+
if (parts.lease !== void 0) {
|
|
2007
|
+
this.lease = Object.freeze({ holder: parts.lease.holder, until: parts.lease.until });
|
|
2008
|
+
}
|
|
1782
2009
|
this.metricsOn = parts.metrics !== NOOP_METRICS;
|
|
1783
2010
|
}
|
|
1784
2011
|
/**
|
|
@@ -1807,11 +2034,20 @@ var Segment = class _Segment {
|
|
|
1807
2034
|
* 0, so a pin of the old segment never reads the new one: what it has already read still answers, as the
|
|
1808
2035
|
* instant it pinned, and anything it would have to fetch fails with `NotFoundError`, as a swept pin's does.
|
|
1809
2036
|
*
|
|
1810
|
-
* **It is a hold,
|
|
1811
|
-
* you: a pinned read deliberately does **not** heal forward, because silently
|
|
1812
|
-
* is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover
|
|
1813
|
-
* job — see [Generations and `keep`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#generations-and-keep) —
|
|
1814
|
-
* you are not collecting.
|
|
2037
|
+
* **It is a hold, and `pin({ leaseUntil })` makes it a bounded one.** Without a lease nothing stops a collection
|
|
2038
|
+
* deleting the generation underneath you: a pinned read deliberately does **not** heal forward, because silently
|
|
2039
|
+
* serving a different generation is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover
|
|
2040
|
+
* 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
|
|
2041
|
+
* you are not collecting, or lease it.
|
|
2042
|
+
*
|
|
2043
|
+
* **A lease** (`leaseUntil`: epoch **milliseconds**, after now and at most 14 days out) keeps the generation out of
|
|
2044
|
+
* a load's collection until then, from any process, and the pin is taken with it: the lease is written to the segment's
|
|
2045
|
+
* row before the object is opened. A read of the handle after the lease, or after {@link Segment.release}, throws
|
|
2046
|
+
* {@link LeaseExpiredError} at every read site, including when the handle is an operand or an `exclude` of a combine
|
|
2047
|
+
* or the target of an `*Into`, and never reads empty. A stream checks the lease each time it reads a chunk. A segment
|
|
2048
|
+
* holds at most 64 live leases ({@link LeaseLimitError}). A collector holds a lease 60 seconds past its end, which covers
|
|
2049
|
+
* clocks that differ by that much. Erasure, shred, drop and retention expiry ignore a lease. See
|
|
2050
|
+
* [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).
|
|
1815
2051
|
*
|
|
1816
2052
|
* A segment with no current generation pins nothing and reads empty, exactly as it would unpinned. A pinned
|
|
1817
2053
|
* segment whose row is later dropped or destroyed fails once it must open its object again, rather than go empty
|
|
@@ -1851,8 +2087,144 @@ var Segment = class _Segment {
|
|
|
1851
2087
|
* Needs a store built on the `.crbm` storage source (the default when you pass a backend or a raw driver). Throws
|
|
1852
2088
|
* {@link UnsupportedError} on a store wired with a pre-built source that cannot pin.
|
|
1853
2089
|
*/
|
|
1854
|
-
async pin() {
|
|
1855
|
-
|
|
2090
|
+
async pin(options) {
|
|
2091
|
+
this.assertLeases([this]);
|
|
2092
|
+
const leaseUntil = this.leaseUntilOf("pin", options);
|
|
2093
|
+
return this.pinned(this.ref, this.expiresAt, leaseUntil);
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Hold what this pin names until `leaseUntil`, or `undefined` for a pin with no lease. Refuses an option it does not
|
|
2097
|
+
* know, a value that is not epoch-milliseconds in the future, one past the longest lease, and one past the handle's own
|
|
2098
|
+
* `expiresAt`, before any request is made.
|
|
2099
|
+
*/
|
|
2100
|
+
leaseUntilOf(op, options) {
|
|
2101
|
+
if (options === void 0 || options === null) return void 0;
|
|
2102
|
+
if (typeof options !== "object" || Array.isArray(options)) {
|
|
2103
|
+
throw new ValidationError2(`${op}: options must be an object such as { leaseUntil }`);
|
|
2104
|
+
}
|
|
2105
|
+
const unknown = Object.keys(options).filter((k) => k !== "leaseUntil");
|
|
2106
|
+
if (unknown.length > 0) {
|
|
2107
|
+
throw new ValidationError2(
|
|
2108
|
+
`${op}: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; this version takes { leaseUntil } only`
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
const until = options.leaseUntil;
|
|
2112
|
+
if (until === void 0) return void 0;
|
|
2113
|
+
const now = this.clock.now();
|
|
2114
|
+
if (typeof until !== "number" || !Number.isSafeInteger(until)) {
|
|
2115
|
+
throw new ValidationError2(
|
|
2116
|
+
`${op}: leaseUntil must be an integer epoch-MILLISECONDS; got ${String(until)}`
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
if (until <= now) {
|
|
2120
|
+
throw new ValidationError2(
|
|
2121
|
+
`${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\``
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
if (until > now + MAX_LEASE_MS) {
|
|
2125
|
+
throw new ValidationError2(
|
|
2126
|
+
`${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`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
if (this.expiresAt !== void 0 && until > this.expiresAt) {
|
|
2130
|
+
throw new ValidationError2(
|
|
2131
|
+
`${op}: leaseUntil outlasts this handle's own expiresAt, which would have it read empty while it still holds the generation`
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
return until;
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* End this handle's lease now, so a load's collection may take its generation. Idempotent: a handle with no lease, or
|
|
2138
|
+
* one already released, does nothing and makes no request. Every read of the handle after it throws
|
|
2139
|
+
* {@link LeaseExpiredError} (reason `'released'`), including one already under way at its next chunk. A lease that has
|
|
2140
|
+
* ended by its own time needs no release: this then makes no request either, and the registry row's entry is pruned
|
|
2141
|
+
* by a later write. Otherwise it is one registry read and one write; a failure of either is thrown as the registry's own
|
|
2142
|
+
* typed error, and the entry then holds until its time ends, `release()` being safe to call again.
|
|
2143
|
+
*/
|
|
2144
|
+
async release() {
|
|
2145
|
+
const hold = this.leaseHold;
|
|
2146
|
+
if (hold === void 0) return;
|
|
2147
|
+
hold.released = true;
|
|
2148
|
+
if (hold.done) return;
|
|
2149
|
+
if (this.clock.now() >= hold.until + LEASE_SKEW_MS) {
|
|
2150
|
+
hold.done = true;
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
await hold.release();
|
|
2154
|
+
hold.done = true;
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* The error a read of this handle must throw, or `undefined` when it holds no lease or the lease is live. One property
|
|
2158
|
+
* read on a handle with no lease. It is not {@link Segment.expired}: that rule reads empty, and a lease must never.
|
|
2159
|
+
*/
|
|
2160
|
+
leaseError() {
|
|
2161
|
+
const hold = this.leaseHold;
|
|
2162
|
+
if (hold === void 0) return void 0;
|
|
2163
|
+
const name = this.ref.namespace ? `${this.ref.namespace}/${this.ref.segment}` : this.ref.segment;
|
|
2164
|
+
if (hold.released) {
|
|
2165
|
+
return new LeaseExpiredError(
|
|
2166
|
+
`the lease on segment "${name}" was released`,
|
|
2167
|
+
hold.until,
|
|
2168
|
+
"released"
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
if (this.clock.now() >= hold.until) {
|
|
2172
|
+
return new LeaseExpiredError(
|
|
2173
|
+
`the lease on segment "${name}" ended; a read of a handle past its lease throws, never reads empty`,
|
|
2174
|
+
hold.until,
|
|
2175
|
+
"expired"
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
return void 0;
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Throw the first lease error among `handles`: this handle and every operand and exclude of a call. It runs before
|
|
2182
|
+
* any `expiresAt` rule and before the engine, so no operand, exclude or cached reader can answer past a lease.
|
|
2183
|
+
*/
|
|
2184
|
+
assertLeases(handles) {
|
|
2185
|
+
for (const h of handles) {
|
|
2186
|
+
const err = h.leaseError();
|
|
2187
|
+
if (err !== void 0) throw err;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Reopen a generation a pin named earlier, as a pinned handle like the one {@link Segment.pin} returns, held at
|
|
2192
|
+
* `at.generation` instead of the current one. Pass what the earlier pin recorded, `{ generation, fingerprint }`
|
|
2193
|
+
* from its {@link Segment.pinnedAt}. A generation number alone is not an identity, since a purged and re-created
|
|
2194
|
+
* name starts again at 0, so the fingerprint is required: a bare number, a malformed fingerprint or a key other
|
|
2195
|
+
* than `generation` and `fingerprint` is a {@link ValidationError}. Identify a pin by its `generation` and
|
|
2196
|
+
* `fingerprint`: the handle's `pinnedAt.version` can differ from the earlier pin's.
|
|
2197
|
+
*
|
|
2198
|
+
* Throws {@link NotFoundError} when the generation is gone (collected, purged, or on a crypto-shredded segment),
|
|
2199
|
+
* is above the row's pointer, or is another object than the fingerprint names, and never reads empty. A rollback
|
|
2200
|
+
* deletes nothing, so once a later load moves the pointer past a generation it rolled back from, that one can be
|
|
2201
|
+
* reopened while its object is stored. Nothing keeps a generation alive unless you lease it: a later load's
|
|
2202
|
+
* collection can delete it, which is sized by `keep` on every writer, or held by `options.leaseUntil` (see
|
|
2203
|
+
* {@link PinOptions} and {@link Segment.pin}). A leased `pinAt` writes the lease before it verifies the object, and
|
|
2204
|
+
* releases it if the verify fails; for a generation below the pointer, a collector that read the row before the lease
|
|
2205
|
+
* landed can still delete the generation inside its own round trip, and the handle then fails with `NotFoundError`
|
|
2206
|
+
* 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,
|
|
2207
|
+
* and a leased `pinAt` one conditional write to the row besides; the handle
|
|
2208
|
+
* then reads as one from `pin()` does, including its failure on a chunk it has not cached once the generation is
|
|
2209
|
+
* swept. Works for an encrypted segment. The first argument is an object so that fields can join it later; the lease
|
|
2210
|
+
* is in `options`, the second.
|
|
2211
|
+
*/
|
|
2212
|
+
async pinAt(at, options) {
|
|
2213
|
+
this.assertLeases([this]);
|
|
2214
|
+
const { generation, fingerprint } = at ?? {};
|
|
2215
|
+
const unknown = typeof at === "object" && at !== null ? Object.keys(at).filter((k) => k !== "generation" && k !== "fingerprint") : [];
|
|
2216
|
+
if (unknown.length > 0) {
|
|
2217
|
+
throw new ValidationError2(
|
|
2218
|
+
`pinAt: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}; this version takes { generation, fingerprint } only`
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
if (typeof generation !== "number" || !Number.isSafeInteger(generation) || generation < 0 || typeof fingerprint !== "string" || !/^\d+:\d+$/.test(fingerprint)) {
|
|
2222
|
+
throw new ValidationError2(
|
|
2223
|
+
"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"
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
const leaseUntil = this.leaseUntilOf("pinAt", options);
|
|
2227
|
+
return this.pinned(this.ref, this.expiresAt, leaseUntil, { generation, fingerprint });
|
|
1856
2228
|
}
|
|
1857
2229
|
/**
|
|
1858
2230
|
* An opaque string that names this handle's segment, namespace included: two handles of one segment have the
|
|
@@ -1896,6 +2268,7 @@ var Segment = class _Segment {
|
|
|
1896
2268
|
* awaiting.
|
|
1897
2269
|
*/
|
|
1898
2270
|
refuseIfExpired(op, dest, operands) {
|
|
2271
|
+
this.assertLeases([this, dest, ...operands]);
|
|
1899
2272
|
const stale = [];
|
|
1900
2273
|
for (const seg of [this, dest, ...operands]) {
|
|
1901
2274
|
if (seg.expired())
|
|
@@ -1907,6 +2280,10 @@ var Segment = class _Segment {
|
|
|
1907
2280
|
);
|
|
1908
2281
|
}
|
|
1909
2282
|
}
|
|
2283
|
+
/** `result` as is, or, when a handle of the call holds a lease, checking that lease each time it reads a chunk. */
|
|
2284
|
+
guarded(out, handles, result) {
|
|
2285
|
+
return handles.some((h) => h.leaseHold !== void 0) ? out.guard(result, () => this.assertLeases(handles)) : result;
|
|
2286
|
+
}
|
|
1910
2287
|
/**
|
|
1911
2288
|
* Time an op with the injected clock and emit an `op` metric on completion (success or throw). Skipped
|
|
1912
2289
|
* entirely when no sink is wired, so the default path pays nothing.
|
|
@@ -1922,6 +2299,8 @@ var Segment = class _Segment {
|
|
|
1922
2299
|
}
|
|
1923
2300
|
/** Membership: one chunk — the cache, else one ranged GET. Throws {@link ValidationError} on a bad id. */
|
|
1924
2301
|
has(id) {
|
|
2302
|
+
const lease = this.leaseError();
|
|
2303
|
+
if (lease !== void 0) return Promise.reject(lease);
|
|
1925
2304
|
if (this.expired()) return Promise.resolve(false);
|
|
1926
2305
|
return this.timed("has", () => this.engine.has(this.ref, id));
|
|
1927
2306
|
}
|
|
@@ -1944,6 +2323,8 @@ var Segment = class _Segment {
|
|
|
1944
2323
|
* structure is checked.
|
|
1945
2324
|
*/
|
|
1946
2325
|
count() {
|
|
2326
|
+
const lease = this.leaseError();
|
|
2327
|
+
if (lease !== void 0) return Promise.reject(lease);
|
|
1947
2328
|
if (this.expired()) return Promise.resolve(0);
|
|
1948
2329
|
return this.timed("count", () => this.engine.count(this.ref));
|
|
1949
2330
|
}
|
|
@@ -1962,6 +2343,7 @@ var Segment = class _Segment {
|
|
|
1962
2343
|
* ```
|
|
1963
2344
|
*/
|
|
1964
2345
|
async stat() {
|
|
2346
|
+
this.assertLeases([this]);
|
|
1965
2347
|
if (this.expired()) return { generation: null, cardinality: 0 };
|
|
1966
2348
|
return this.engine.stat(this.ref);
|
|
1967
2349
|
}
|
|
@@ -1981,16 +2363,70 @@ var Segment = class _Segment {
|
|
|
1981
2363
|
* without checking its options, as every read of one does.
|
|
1982
2364
|
*/
|
|
1983
2365
|
iterate(options) {
|
|
2366
|
+
const lease = this.leaseError();
|
|
2367
|
+
if (lease !== void 0) return failing(lease);
|
|
1984
2368
|
if (this.expired()) return EMPTY_IDS;
|
|
1985
2369
|
const range = options == null ? void 0 : rangeOf(options);
|
|
1986
2370
|
const none = range === void 0 || range.after === void 0 && range.through === void 0;
|
|
1987
2371
|
const { engine, ref } = this;
|
|
1988
2372
|
const bounds = none ? void 0 : range;
|
|
1989
|
-
|
|
2373
|
+
const stream = withBatches(
|
|
2374
|
+
engine.iterate(ref, bounds),
|
|
2375
|
+
() => engine.iterateBatches(ref, bounds)
|
|
2376
|
+
);
|
|
2377
|
+
return this.leaseHold === void 0 ? stream : guardIds(stream, () => this.assertLeases([this]));
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* **Pinned handles only.** The ids at 1-based ranks `n`, `2n`, `3n` …, counted over the ids in `(after, through]`
|
|
2381
|
+
* when a range is given, ascending: `everyNth(1000)` yields the 1,000th id, then the 2,000th, and so on. A last
|
|
2382
|
+
* partial window yields nothing, so a caller cutting a send into windows of `n` appends its own final end.
|
|
2383
|
+
*
|
|
2384
|
+
* It places each boundary from the per-chunk counts the pin already holds and reads only the chunks that hold one,
|
|
2385
|
+
* each once however many boundaries it holds, so it reads at most one chunk per boundary and never more chunks
|
|
2386
|
+
* than the object has. A range with `after` inside a chunk reads that chunk too, to count the ids at or below
|
|
2387
|
+
* `after`, even when fewer than `n` ids remain above it; when the chunks in range hold fewer than `n` ids in all,
|
|
2388
|
+
* 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
|
|
2389
|
+
* yield nothing from it. The reads and the read-ahead window are
|
|
2390
|
+
* those of {@link Segment.iterate}, and the per-op budget is charged before the first fetch, with an upper bound on
|
|
2391
|
+
* the chunks the read can take. A pinned read after the generation is swept throws {@link NotFoundError}.
|
|
2392
|
+
*
|
|
2393
|
+
* The ranks come from the index's counts, which are trusted as {@link Segment.count} trusts them. A chunk that is
|
|
2394
|
+
* read must hold the number of ids its index says, or the read throws {@link IntegrityError}; a chunk that is not
|
|
2395
|
+
* read is not checked, so an index that is wrong about one, and, on an unencrypted object, still adds up to the footer's
|
|
2396
|
+
* total, moves the ids after it to the wrong ranks. An encrypted object's index is authenticated and bound to the
|
|
2397
|
+
* object, so a wrong count there needs the key.
|
|
2398
|
+
*
|
|
2399
|
+
* A live handle is refused with {@link UnsupportedError} at the first read, because its counts and its chunks could
|
|
2400
|
+
* come from two generations and name the wrong id. `n` that is not a positive integer throws
|
|
2401
|
+
* {@link ValidationError} at the first read, as does a bad bound. An expired handle reads empty.
|
|
2402
|
+
*
|
|
2403
|
+
* ```ts
|
|
2404
|
+
* const audience = await store.segment('audience').pin();
|
|
2405
|
+
* const ends: number[] = [];
|
|
2406
|
+
* for await (const id of audience.everyNth(1_000)) ends.push(id); // the end of each window of 1,000
|
|
2407
|
+
* ```
|
|
2408
|
+
*/
|
|
2409
|
+
everyNth(n, options) {
|
|
2410
|
+
const lease = this.leaseError();
|
|
2411
|
+
if (lease !== void 0) return failing(lease);
|
|
2412
|
+
if (this.expired()) return EMPTY_IDS;
|
|
2413
|
+
if (this.pinnedAt === void 0) {
|
|
2414
|
+
return failing(
|
|
2415
|
+
new UnsupportedError(
|
|
2416
|
+
"everyNth needs a pinned handle: a live handle can place a rank by one generation and read it from another. Call `pin()` first."
|
|
2417
|
+
)
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
const stream = this.engine.everyNth(
|
|
2421
|
+
this.ref,
|
|
2422
|
+
n,
|
|
2423
|
+
options == null ? void 0 : rangeOf(options)
|
|
2424
|
+
);
|
|
2425
|
+
return this.leaseHold === void 0 ? stream : guardIdIterable(stream, () => this.assertLeases([this]));
|
|
1990
2426
|
}
|
|
1991
2427
|
/**
|
|
1992
2428
|
* The options a combine hands the engine, read once, when it is called ({@link readOptions}), with the `exclude`
|
|
1993
|
-
* it was given
|
|
2429
|
+
* it was given ({@link excludesOf}) and mapped down to the plain refs
|
|
1994
2430
|
* `core` takes. A method rather than a module function because `ref` is class-private — the encapsulation is
|
|
1995
2431
|
* worth more than the free function.
|
|
1996
2432
|
*
|
|
@@ -2006,18 +2442,35 @@ var Segment = class _Segment {
|
|
|
2006
2442
|
};
|
|
2007
2443
|
}
|
|
2008
2444
|
/**
|
|
2009
|
-
* The handles in `options.exclude
|
|
2010
|
-
*
|
|
2011
|
-
* checked for absence, and never counted in the pin-consistency check. `exclude` is read once, so a getter sees
|
|
2012
|
-
* one call.
|
|
2013
|
-
*
|
|
2014
|
-
* With no `exclude` this is one property read and a shared empty list, and with none expired it returns the
|
|
2015
|
-
* caller's own array, so the common case allocates nothing.
|
|
2445
|
+
* The handles in `options.exclude`, read once so a getter sees one call. With none this is one property read and
|
|
2446
|
+
* a shared empty list; otherwise it is the caller's own array, so the common case allocates nothing.
|
|
2016
2447
|
*/
|
|
2017
|
-
|
|
2448
|
+
excludesOf(options) {
|
|
2018
2449
|
const exclude = options?.exclude;
|
|
2019
|
-
|
|
2020
|
-
|
|
2450
|
+
return exclude == null || exclude.length === 0 ? NO_SEGMENTS : exclude;
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* The refusal for a combine whose **exclusion** has expired, or `undefined` when none has. Unlike an expired
|
|
2454
|
+
* operand, which reads empty, an exclusion is a suppression or opt-out list: left out silently, the result
|
|
2455
|
+
* would include the very ids it was passed to remove. So it is refused with a {@link ValidationError} naming each
|
|
2456
|
+
* expired exclusion, as the `*Into` verbs refuse an expired handle.
|
|
2457
|
+
*
|
|
2458
|
+
* It is judged against the injected clock when the combine is called, before the engine is asked for anything:
|
|
2459
|
+
* no request is made, and an expired exclusion that names a segment that does not exist is refused as expired,
|
|
2460
|
+
* not as an absent operand. It runs ahead of the expired-operand shortcuts, so a combine that would read empty
|
|
2461
|
+
* anyway still refuses a lapsed exclusion. A stream already being read is not re-checked when an exclusion
|
|
2462
|
+
* expires part-way, as an operand expiring part-way does not stop it either.
|
|
2463
|
+
*/
|
|
2464
|
+
expiredExcludes(op, excludes) {
|
|
2465
|
+
let stale;
|
|
2466
|
+
for (const e of excludes) {
|
|
2467
|
+
if (!e.expired()) continue;
|
|
2468
|
+
(stale ??= []).push(e.ref.namespace ? `${e.ref.namespace}/${e.ref.segment}` : e.ref.segment);
|
|
2469
|
+
}
|
|
2470
|
+
if (stale === void 0) return void 0;
|
|
2471
|
+
return new ValidationError2(
|
|
2472
|
+
`${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.`
|
|
2473
|
+
);
|
|
2021
2474
|
}
|
|
2022
2475
|
/**
|
|
2023
2476
|
* Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
|
|
@@ -2032,8 +2485,16 @@ var Segment = class _Segment {
|
|
|
2032
2485
|
return this.intersectAs(AS_IDS, others, options);
|
|
2033
2486
|
}
|
|
2034
2487
|
intersectAs(out, others, options) {
|
|
2488
|
+
const exclude = this.excludesOf(options);
|
|
2489
|
+
const handles = [this, ...others, ...exclude];
|
|
2490
|
+
try {
|
|
2491
|
+
this.assertLeases(handles);
|
|
2492
|
+
} catch (err) {
|
|
2493
|
+
return out.failing(err);
|
|
2494
|
+
}
|
|
2495
|
+
const refused = this.expiredExcludes("intersect", exclude);
|
|
2496
|
+
if (refused) return out.failing(refused);
|
|
2035
2497
|
if (this.expired() || others.some((o) => o.expired())) return out.none;
|
|
2036
|
-
const exclude = this.liveExcludes(options);
|
|
2037
2498
|
let engine;
|
|
2038
2499
|
try {
|
|
2039
2500
|
engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
|
|
@@ -2042,7 +2503,7 @@ var Segment = class _Segment {
|
|
|
2042
2503
|
}
|
|
2043
2504
|
const refs = [this.ref, ...others.map((o) => o.ref)];
|
|
2044
2505
|
const opts = this.refsIn(options, exclude);
|
|
2045
|
-
return out.intersect(engine, refs, opts);
|
|
2506
|
+
return this.guarded(out, handles, out.intersect(engine, refs, opts));
|
|
2046
2507
|
}
|
|
2047
2508
|
/**
|
|
2048
2509
|
* Materialize `this ∩ others…` (minus `exclude`) as a **new generation of `dest`** — `dest`'s previous contents
|
|
@@ -2091,17 +2552,24 @@ var Segment = class _Segment {
|
|
|
2091
2552
|
return this.unionAs(AS_IDS, others, options);
|
|
2092
2553
|
}
|
|
2093
2554
|
unionAs(out, others, options) {
|
|
2555
|
+
const exclude = this.excludesOf(options);
|
|
2556
|
+
const handles = [this, ...others, ...exclude];
|
|
2557
|
+
try {
|
|
2558
|
+
this.assertLeases(handles);
|
|
2559
|
+
} catch (err) {
|
|
2560
|
+
return out.failing(err);
|
|
2561
|
+
}
|
|
2562
|
+
const refused = this.expiredExcludes("union", exclude);
|
|
2563
|
+
if (refused) return out.failing(refused);
|
|
2094
2564
|
const live = others.filter((o) => !o.expired());
|
|
2095
2565
|
if (this.expired()) {
|
|
2096
2566
|
if (live.length === 0) return out.none;
|
|
2097
2567
|
return live[0].unionAs(out, live.slice(1), options);
|
|
2098
2568
|
}
|
|
2099
2569
|
if (live.length === 0 && others.length > 0) {
|
|
2100
|
-
|
|
2101
|
-
return exclude2.length > 0 ? this.andNotAs(out, [...exclude2], options) : this.unionAs(out, [], options);
|
|
2570
|
+
return exclude.length > 0 ? this.andNotAs(out, [...exclude], options) : this.unionAs(out, [], options);
|
|
2102
2571
|
}
|
|
2103
2572
|
if (live.length !== others.length) return this.unionAs(out, live, options);
|
|
2104
|
-
const exclude = this.liveExcludes(options);
|
|
2105
2573
|
let engine;
|
|
2106
2574
|
try {
|
|
2107
2575
|
engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
|
|
@@ -2110,7 +2578,7 @@ var Segment = class _Segment {
|
|
|
2110
2578
|
}
|
|
2111
2579
|
const refs = [this.ref, ...others.map((o) => o.ref)];
|
|
2112
2580
|
const opts = this.refsIn(options, exclude);
|
|
2113
|
-
return out.union(engine, refs, opts);
|
|
2581
|
+
return this.guarded(out, handles, out.union(engine, refs, opts));
|
|
2114
2582
|
}
|
|
2115
2583
|
/** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
2116
2584
|
async unionInto(dest, others, options) {
|
|
@@ -2134,9 +2602,9 @@ var Segment = class _Segment {
|
|
|
2134
2602
|
* suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
|
|
2135
2603
|
* opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
|
|
2136
2604
|
*
|
|
2137
|
-
* An expired handle in `excludes`
|
|
2138
|
-
*
|
|
2139
|
-
* the same rule.
|
|
2605
|
+
* An expired handle in `excludes` is refused, not skipped: the stream throws {@link ValidationError} naming it,
|
|
2606
|
+
* before any request is made, because a lapsed suppression list would otherwise let through the ids it was
|
|
2607
|
+
* passed to remove. An expired `this` is empty. `exclude` on `intersect` and `union` follows the same rule.
|
|
2140
2608
|
*
|
|
2141
2609
|
* To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
|
|
2142
2610
|
* the suppression folds into the same pass rather than materializing an intermediate segment first.
|
|
@@ -2145,12 +2613,15 @@ var Segment = class _Segment {
|
|
|
2145
2613
|
return this.andNotAs(AS_IDS, excludes, options);
|
|
2146
2614
|
}
|
|
2147
2615
|
andNotAs(out, excludes, options) {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2616
|
+
const handles = [this, ...excludes];
|
|
2617
|
+
try {
|
|
2618
|
+
this.assertLeases(handles);
|
|
2619
|
+
} catch (err) {
|
|
2620
|
+
return out.failing(err);
|
|
2152
2621
|
}
|
|
2153
|
-
|
|
2622
|
+
const refused = this.expiredExcludes("andNot", excludes);
|
|
2623
|
+
if (refused) return out.failing(refused);
|
|
2624
|
+
if (this.expired()) return out.none;
|
|
2154
2625
|
let engine;
|
|
2155
2626
|
try {
|
|
2156
2627
|
engine = this.combineEngine([this, ...excludes]) ?? this.engine;
|
|
@@ -2160,7 +2631,7 @@ var Segment = class _Segment {
|
|
|
2160
2631
|
const base = this.ref;
|
|
2161
2632
|
const refs = excludes.map((o) => o.ref);
|
|
2162
2633
|
const opts = options == null ? void 0 : readOptions(options);
|
|
2163
|
-
return out.andNot(engine, base, refs, opts);
|
|
2634
|
+
return this.guarded(out, handles, out.andNot(engine, base, refs, opts));
|
|
2164
2635
|
}
|
|
2165
2636
|
/** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
2166
2637
|
async andNotInto(dest, excludes, options) {
|
|
@@ -2184,6 +2655,7 @@ var Segment = class _Segment {
|
|
|
2184
2655
|
* the lose-zone).
|
|
2185
2656
|
*/
|
|
2186
2657
|
async costReport(options) {
|
|
2658
|
+
this.assertLeases([this]);
|
|
2187
2659
|
const canMeasure = this.engine.supportsStorageSize;
|
|
2188
2660
|
const size = canMeasure ? await this.engine.segmentSize(this.ref) : null;
|
|
2189
2661
|
const refreshMs = this.engine.pointerRefreshMs;
|