@cloudbitmaps/roaring 0.25.0 → 0.27.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 +15 -9
- package/README.md +2 -2
- package/dist/bin/export-segments.js +175 -17
- package/dist/bin/export-segments.js.map +3 -3
- package/dist/index.d.ts +80 -11
- package/dist/index.js +153 -11
- package/dist/index.js.map +3 -3
- package/package.json +2 -2
package/PRIVACY.md
CHANGED
|
@@ -42,7 +42,7 @@ from constructing a cross-region topology. The points where personal data moves
|
|
|
42
42
|
|
|
43
43
|
| Location | What's there | Residency note |
|
|
44
44
|
|---|---|---|
|
|
45
|
-
| **Storage** (object store) | immutable `.crbm` generations — every generation a segment has had, until a superseded one is collected. Every generation object a cloud driver writes (S3, GCS and Azure Blob) also carries a random write id in its metadata (`cbwid`; `x-amz-meta-cbwid` on S3): 128 random bits, no data from the bitmap or the source. A write that fails or is killed part way can leave the uploaded part of a generation behind (S3 multipart parts, Azure Blob uncommitted blocks, an unfinished GCS resumable session, a `LocalFsStorage` `.tmp` file); no listing shows it, so no erasure or drop reaches it. What each backend leaves, and
|
|
45
|
+
| **Storage** (object store) | immutable `.crbm` generations — every generation a segment has had, until a superseded one is collected. Every generation object a cloud driver writes (S3, GCS and Azure Blob) also carries a random write id in its metadata (`cbwid`; `x-amz-meta-cbwid` on S3): 128 random bits, no data from the bitmap or the source. A write that fails or is killed part way can leave the uploaded part of a generation behind (S3 multipart parts, Azure Blob uncommitted blocks, an unfinished GCS resumable session, a `LocalFsStorage` `.tmp` file); no listing shows it, so no erasure or drop reaches it. What each backend leaves, and how to remove it, is in [the backup checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/disaster-recovery.md#backup-checklist). The library sweeps a `LocalFsStorage` `.tmp` leftover more than 24 hours old only when something writes in that directory again, and after a drop or an erasure nothing does: delete those yourself | the region of the bucket you wire |
|
|
46
46
|
| **Registry** (S3 / GCS / Azure Blob / local) | one row per segment: its namespace and name, its status and timestamps, the current-generation pointer, wrapped keys and any key id, its `retention` and `residency` metadata, the current generation's id count, its object's size, footer checksum and tail size, and your metadata (those sealed when the segment is encrypted), a ledger of the generations the segment knows (each one's number and its object's footer checksum, in the clear even on an encrypted segment), any leases, and a token and `pointerId` — no IDs, unless you put one in the metadata or a name, which you must not. An Azure Blob row also carries the random write id in its metadata. On `LocalFsStorage`, a write killed part way leaves a `.tmp` copy of the row beside it, wrapped keys included, until a later write in that directory removes it once it is more than a day old, so a crypto-shred there can be outlived by that copy for that long | the region of the bucket you wire |
|
|
47
47
|
| **cache** (process RAM) | decoded chunks, and the stored chunk bytes (ciphertext on an encrypted segment) a reader keeps of a small generation it read whole; bounded LRU | **wherever your process/Lambda runs** — an EU segment queried from a US function is processed in the US |
|
|
48
48
|
| **Loads and rewrites** (`store.load()`, the `*Into` verbs, `store.materializeMany()`, `eraseSubject`) | read your source (or existing generations), write a new generation; a fed operand of `materializeMany` is read from your own process's memory | run wherever you run them — a loader in one region writing to a bucket in another is a transfer |
|
|
@@ -60,7 +60,7 @@ CloudBitmaps gives you three levers with different guarantees. Use them delibera
|
|
|
60
60
|
|
|
61
61
|
| Lever | API | Guarantee | Use for |
|
|
62
62
|
|---|---|---|---|
|
|
63
|
-
| **Subject erasure** | `store.eraseSubject(id, { namespace })` | *Physical on return* — the segment's current generation is rewritten without the id (every chunk streamed through, one bit cleared), published **fenced on the generation it streamed**, and **the generation that held the bit is deleted before the call returns**. So is every other generation still holding the id: a retained *superseded* one (an ex-member dropped by a re-seed), and each one above the pointer after a `rollback` — generations a rollback can move the pointer onto again — while, when the current generation does not hold the id, the ones there that never held it stay as rollback targets, but for any object no read of the segment can open, which goes whatever the id. (When the current generation holds the id, the rewrite is numbered above
|
|
63
|
+
| **Subject erasure** | `store.eraseSubject(id, { namespace })` | *Physical on return* — the segment's current generation is rewritten without the id (every chunk streamed through, one bit cleared), published **fenced on the generation it streamed**, and **the generation that held the bit is deleted before the call returns**. So is every other generation still holding the id: a retained *superseded* one (an ex-member dropped by a re-seed), and each one above the pointer after a `rollback` — generations a rollback can move the pointer onto again — while, when the current generation does not hold the id, the ones there that never held it stay as rollback targets, but for any object no read of the segment can open, which goes whatever the id. (When the current generation holds the id, the rewrite is numbered above every object near the row's own numbers, so its `keep: 0` collection takes every older generation, above the pointer or below. A holder found only above the pointer likewise takes every generation below the pointer with it, clean ones included ([every holder goes](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#every-holder-goes)); above the pointer, a generation with no copy of the id stays, and so does the current one.) Before it says `erased: true` the call lists the bucket and reads what is left, so **`erased: true` means no generation of the segment holds the id**, above the pointer or below it. `eraseSubject` reports a per-segment fault as an `error: …` ledger entry rather than throwing it, and a call that a racing writer overtook (a load, another erasure, or a rollback) as `erased: false, note: 'superseded'`, so "on return" is a claim about every segment whose entry says **`erased: true`** (a fault is an `error: …` entry, never `erased: true`). The store that performs the call stops serving the id immediately; **other processes converge on their own read TTL** — see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). Does **not** reach backups, replicas or noncurrent object versions — those hold the old generation until their own lifecycle removes it. It carries a generation's metadata over unchanged and does **not** scan it, so **never put a subject's id in metadata**. The scope is one `namespace` or `{ allNamespaces: true }`, never both (both throws `ValidationError`); a segment loaded with no `namespace` is reached only by `{ allNamespaces: true }`, and `'_default'` is an ordinary namespace name, not those segments. | "forget this person" — GDPR Art. 17 |
|
|
64
64
|
| **Dispose** | `store.dropSegment(ref, { confirmSegment })` | *Immediate* — the segment is tombstoned and its Storage generations deleted, reclaiming the space. **Check `generationsRemaining`:** if it is non-empty the space was *not* fully reclaimed and the drop should be re-run (a load that had read the segment before the tombstone landed, even one still consuming its ids, or an `eraseSubject` rewrite already under way, can write its object after the drop's sweep; it deletes that object itself once its publish is refused, and only one whose process stops in between, or whose publish fails without a definite answer (a lost response, a timeout), leaves it, for a re-run of the drop). Works on cleartext; on an encrypted segment it *also* crypto-shreds, on the terms in the next row. Does **not** reach noncurrent versions / replicas / PITR snapshots — deleting an object is weaker than removing a key. | retiring a dated bucket; rolling-window retention |
|
|
65
65
|
| **Crypto-shred** | `destroySegment` / `eraseNamespace` | *At rest, complete once no copy of the wrapped key survives* — removes the segment's wrapped DEK(s) from its registry row in one compare-and-swap, without touching the bytes or any KEK. **Every** copy of the objects (current, prior generations, backups, WORM-locked objects) is then unreadable once no retained copy of that row still holds the wrapped key — noncurrent object versions, backups and PITR copies of the row included — or once every KEK that wrapped it is destroyed. Until then, a retained copy of the row and a KEK that wrapped it decrypt the segment, and a registry restore to a point before the shred makes it readable again; see *Erasure vs. backups / WORM* below, and [an optional registry expiry rule](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/disaster-recovery.md#optional-make-a-shred-durable-with-a-registry-expiry-rule) that makes a shred durable after a set number of days. Requires the segment to be encrypted. A process that had already opened the segment holds the **unwrapped** key in memory and keeps reading until it is told — call `store.invalidate(ref)` there; see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). | whole-segment / tenant offboarding; erasure under immutable backups (see below) |
|
|
66
66
|
|
|
@@ -173,7 +173,7 @@ A call with a feed reads operands the library never stores, which no erasure can
|
|
|
173
173
|
Erasure and crypto-shred are **immediate in storage and immediate in the store whose verb performed them**, for
|
|
174
174
|
every read that starts after the verb returns. A read already in progress in that store can still yield an erased id
|
|
175
175
|
from a chunk it had requested before the verb ran: up to 32 chunks for `iterate` and `count`, and up to `concurrency` + 1
|
|
176
|
-
keys (33 by default) for a combine. That is the bound: the chunk or key a read is handing out and what it had already requested, which a `count` that reads chunks (on a source with no index of its own) reaches. A running `iterate` or combine on the library's own sources stops sooner: the verb invalidates the segment, so before the next key it takes the read lists its chunk keys again from the generation current then and drops what it had requested, and so yields the id only from the chunk or key it is handing out. A combine or `iterate` reads ranges of the object, and resolves the segment again before it serves each chunk, one held in its cache included, as a read of that chunk alone does, so the ranges it had already requested are dropped, not served. A pinned read in progress does not move to the rewritten generation: before its next chunk it opens its object again, and fails with `NotFoundError` once the erasure has deleted it, so it yields no more than that bound either. They are not immediate in any *other* store, in the same process or another, and this library ships nothing that could
|
|
176
|
+
keys (33 by default) for a combine or a count of a combine. That is the bound: the chunk or key a read is handing out and what it had already requested, which a `count` that reads chunks (on a source with no index of its own) reaches. A running `iterate` or combine on the library's own sources stops sooner: the verb invalidates the segment, so before the next key it takes the read lists its chunk keys again from the generation current then and drops what it had requested, and so yields the id only from the chunk or key it is handing out. A combine or `iterate` reads ranges of the object, and resolves the segment again before it serves each chunk, one held in its cache included, as a read of that chunk alone does, so the ranges it had already requested are dropped, not served. A pinned read in progress does not move to the rewritten generation: before its next chunk it opens its object again, and fails with `NotFoundError` once the erasure has deleted it, so it yields no more than that bound either. In another store it stops at its next chunk once `cache.genTtlMs` has passed since the pin last looked at its segment's row and the segment has been destroyed or its generation erased. They are not immediate in any *other* store, in the same process or another, and this library ships nothing that could
|
|
177
177
|
make them so — there is no daemon, no bus, and no connection between two stores that happen to point at the same
|
|
178
178
|
bucket.
|
|
179
179
|
|
|
@@ -185,8 +185,7 @@ bucket.
|
|
|
185
185
|
| another store with **no registry** (a bare `IStorageDriver`), with `cache: { genTtlMs: 0 }`, or on a `StorageChunkSource` of your own, which the store times no refresh on | **no bound** — only when its caches happen to let the segment go, or something tells it |
|
|
186
186
|
| `seg.pinAt` of a generation the erasure rewrote, in any store | on return it fails with `NotFoundError`, since the object is deleted (and, with a registry, the row has moved on); during the erasure, between the rewrite's publish and its delete of the old generation, it can still open that generation, and a handle opened then is a pin, covered by the rows for the store whose verb made the call and for a pinned handle in another store |
|
|
187
187
|
| a `materializeMany` call with a feed, and an erasure by any path but this store's `eraseSubject` | **no bound**: the call is refused only by this store's `eraseSubject` |
|
|
188
|
-
| a pinned handle (`seg.pin()`) in another store |
|
|
189
|
-
| a leased pinned handle (`seg.pin({ leaseUntil })`) in another store | until its lease ends, at most 14 days after it was taken: its reads then throw `LeaseExpiredError`, whether or not its reader still holds the chunks. Until then, as for any pinned handle in the row above |
|
|
188
|
+
| a pinned handle (`seg.pin()`) in another store, leased or not | within `cache.genTtlMs` (default 2 s), while the registry can be read and storage answers the read of the pinned object's footer, whether or not its reader still holds the chunks; an outage of the registry, or a transient storage fault on that read, stretches it. A lease does not lengthen it: once a lease has lapsed, reads throw `LeaseExpiredError` first. A pin of a generation the erasure could not remove (its ledger entry has `erased: false`) reads it, the id included, until a re-run of the erasure removes it |
|
|
190
189
|
|
|
191
190
|
**An outage of the registry extends that bound.** A refresh that cannot read the row because of a transient fault
|
|
192
191
|
(throttling, a 5xx, a dropped connection) keeps serving what the store resolved before the outage, for a segment
|
|
@@ -202,12 +201,13 @@ that error, and the reader is dropped with the key it held.
|
|
|
202
201
|
`destroySegment` and `eraseNamespace` are free functions over raw drivers, not verbs of a store, so for them every
|
|
203
202
|
store is another store, one in the same process included.
|
|
204
203
|
|
|
205
|
-
The
|
|
204
|
+
The no-timed-refresh row and the feed row are the ones to design around. `cache: { genTtlMs: 0 }` turns the timed refresh off, and is a
|
|
206
205
|
legitimate setting for a read-only replica of immutable data — but a store set that way has no bound on when it
|
|
207
206
|
observes a shred: until its reader cache lets the segment go, it keeps decrypting with the key it already unwrapped.
|
|
208
|
-
A pinned handle
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
A pinned handle in such a store is that case too: its reader keeps the key until the reader cache evicts it, and after
|
|
208
|
+
that the pin still answers `has()` for the ids in chunks it decoded, until the chunk cache drops them. A pinned handle
|
|
209
|
+
in a store with a timed refresh is bounded like any other reader: it re-reads its segment's row every `cache.genTtlMs`,
|
|
210
|
+
counted from the moment it was taken, and fails with `NotFoundError` once the segment is destroyed or its generation erased. If a
|
|
211
211
|
compliance deadline depends on every reader converging, fan the reference out to your fleet and have each
|
|
212
212
|
process call `store.invalidate(ref)`; that is the hook, and delivering it is yours because the transport is
|
|
213
213
|
yours.
|
|
@@ -451,6 +451,12 @@ it (the request and response it kept, and a credential the service quotes back o
|
|
|
451
451
|
in another form reaches you as the SDK wrote it; and `eraseSubject` copies each message into its ledger's `note`.
|
|
452
452
|
Treat an error message, and the ledger, as carrying those names wherever you report or keep them.
|
|
453
453
|
|
|
454
|
+
The names also reach your metrics sink: every `storage.get` event carries the segment and namespace of the read, as the
|
|
455
|
+
audit events do (ids never appear in either). **The library does not redact a name.** If a name carries tenant or customer
|
|
456
|
+
meaning, pass a name that does not: for example your own keyed hash of the tenant id (an HMAC under a key you hold) as the
|
|
457
|
+
namespace or segment, with the mapping from a hash back to the tenant kept in your own system. Then every error, event and
|
|
458
|
+
registry row names only the hash.
|
|
459
|
+
|
|
454
460
|
|
|
455
461
|
## Shared-responsibility matrix
|
|
456
462
|
|
package/README.md
CHANGED
|
@@ -105,8 +105,8 @@ name.
|
|
|
105
105
|
|
|
106
106
|
## What it costs
|
|
107
107
|
|
|
108
|
-
A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks made 6 GETs and took
|
|
109
|
-
million** at list prices: measured on S3 in-region, in the run of 2026-10-
|
|
108
|
+
A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks made 6 GETs and took 107.87 ms at the median, **$2.40 per
|
|
109
|
+
million** at list prices: measured on S3 in-region, in the run of 2026-10-30, whose client had 128 sockets. On a spread layout it reads more
|
|
110
110
|
bytes than it needs, which cross-region can cost more than the requests it saves. A segment's first single-part `store.load()` made
|
|
111
111
|
2 PUT + 3 GET, **$11.20 per million** at the default prices, pointer included: the requests are measured on S3 in-region, in the same run. A steady single-part `store.load()` at `keep: 12` that deletes a generation by name costs **$12.00 per million**, for 2 PUT + 5 GET and a free delete, the requests measured on S3 in-region, in the same run. A cold `count()` is one
|
|
112
112
|
pointer read: it reads no payload and no object.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { realpathSync } from "node:fs";
|
|
6
6
|
import { lstat, mkdir, open, readdir, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
7
|
-
import { join } from "node:path";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
9
|
import { encodeNameForPath, namespacePathPart } from "@cloudbitmaps/core/driver-kit";
|
|
10
10
|
|
|
@@ -3172,8 +3172,8 @@ var SystemClock = class {
|
|
|
3172
3172
|
}
|
|
3173
3173
|
sleep(ms) {
|
|
3174
3174
|
if (ms <= 0) return Promise.resolve();
|
|
3175
|
-
return new Promise((
|
|
3176
|
-
setTimeout(
|
|
3175
|
+
return new Promise((resolve2) => {
|
|
3176
|
+
setTimeout(resolve2, ms);
|
|
3177
3177
|
});
|
|
3178
3178
|
}
|
|
3179
3179
|
/**
|
|
@@ -3200,8 +3200,8 @@ var SystemClock = class {
|
|
|
3200
3200
|
* "there is outstanding work" — and `setImmediate` cannot outlive the turn that scheduled it anyway.
|
|
3201
3201
|
*/
|
|
3202
3202
|
yieldNow() {
|
|
3203
|
-
return new Promise((
|
|
3204
|
-
setImmediate(
|
|
3203
|
+
return new Promise((resolve2) => {
|
|
3204
|
+
setImmediate(resolve2);
|
|
3205
3205
|
});
|
|
3206
3206
|
}
|
|
3207
3207
|
};
|
|
@@ -3572,6 +3572,29 @@ var CALL_KEYS = {
|
|
|
3572
3572
|
"allowAbsentOperands",
|
|
3573
3573
|
"exclude"
|
|
3574
3574
|
]),
|
|
3575
|
+
intersectCount: keysOf()([
|
|
3576
|
+
"after",
|
|
3577
|
+
"through",
|
|
3578
|
+
"concurrency",
|
|
3579
|
+
"budget",
|
|
3580
|
+
"allowAbsentOperands",
|
|
3581
|
+
"exclude"
|
|
3582
|
+
]),
|
|
3583
|
+
unionCount: keysOf()([
|
|
3584
|
+
"after",
|
|
3585
|
+
"through",
|
|
3586
|
+
"concurrency",
|
|
3587
|
+
"budget",
|
|
3588
|
+
"allowAbsentOperands",
|
|
3589
|
+
"exclude"
|
|
3590
|
+
]),
|
|
3591
|
+
andNotCount: keysOf()([
|
|
3592
|
+
"after",
|
|
3593
|
+
"through",
|
|
3594
|
+
"concurrency",
|
|
3595
|
+
"budget",
|
|
3596
|
+
"allowAbsentOperands"
|
|
3597
|
+
]),
|
|
3575
3598
|
into: keysOf()([
|
|
3576
3599
|
"after",
|
|
3577
3600
|
"through",
|
|
@@ -3649,6 +3672,9 @@ var MISPLACED = {
|
|
|
3649
3672
|
range: outside(CALL_KEYS.into, CALL_KEYS.range),
|
|
3650
3673
|
andNot: outside(CALL_KEYS.into, CALL_KEYS.andNot),
|
|
3651
3674
|
combine: outside(CALL_KEYS.into, CALL_KEYS.combine),
|
|
3675
|
+
intersectCount: outside(CALL_KEYS.into, CALL_KEYS.intersectCount),
|
|
3676
|
+
unionCount: outside(CALL_KEYS.into, CALL_KEYS.unionCount),
|
|
3677
|
+
andNotCount: outside(CALL_KEYS.into, CALL_KEYS.andNotCount),
|
|
3652
3678
|
andNotInto: outside(CALL_KEYS.into, CALL_KEYS.andNotInto),
|
|
3653
3679
|
exportSegments: ["codec"]
|
|
3654
3680
|
};
|
|
@@ -4473,7 +4499,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
4473
4499
|
* duration, or fix the source first and load after. A writer that lands *during* the rewrite is caught: the
|
|
4474
4500
|
* rewrite's publish is refused **by the fence** — the publish lands its compare-and-swap only
|
|
4475
4501
|
* while the pointer is still on the generation the rewrite streamed — and the entry says `note: 'superseded'`,
|
|
4476
|
-
* so re-run. Forward-only alone would NOT refuse it: the rewrite's number goes above everything
|
|
4502
|
+
* so re-run. Forward-only alone would NOT refuse it: the rewrite's number goes above everything listed near the row's own numbers,
|
|
4477
4503
|
* so the rewrite would out-rank the newer generation and then collect it.
|
|
4478
4504
|
*
|
|
4479
4505
|
* A racing **erasure** is caught before that, and reported the same way. It collects with `keep: 0`, taking
|
|
@@ -4711,7 +4737,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
4711
4737
|
* It deletes nothing. The generations above the new pointer stay put — which is what makes this reversible —
|
|
4712
4738
|
* and are then *above* `currentGen`, where collection never looks, and still known to the row, so rolling forward onto
|
|
4713
4739
|
* one needs no flag. They remain until loads pass them (the first load whose
|
|
4714
|
-
* number one of them holds numbers above them all,
|
|
4740
|
+
* number one of them holds numbers above them all, those within 1,024 of what the row names and any contiguous run
|
|
4741
|
+
* touching them, and collection then keeps `keep` of what is below its pointer, the
|
|
4715
4742
|
* generation that load superseded first), {@link CloudRoaring.dropSegment}
|
|
4716
4743
|
* deletes them, or {@link CloudRoaring.eraseSubject} does: all of those present when it rewrites, and, when the
|
|
4717
4744
|
* current generation does not hold the id, only those holding it and any no read of the segment can open, whatever
|
|
@@ -5088,7 +5115,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
5088
5115
|
* the unwrapped DEK, and decrypts with them, chunks it had never fetched before the shred included, until its
|
|
5089
5116
|
* snapshot re-resolves: within `cache.genTtlMs` when the store has a registry and a `genTtlMs` above 0, and
|
|
5090
5117
|
* otherwise only when its caches let the segment go or a read has to fetch from a generation a sweep collected.
|
|
5091
|
-
* A pin
|
|
5118
|
+
* A pin on a store with a timed refresh fails within `cache.genTtlMs` once its segment is destroyed or its
|
|
5119
|
+
* generation erased; on a store with none it answers until it is told or both caches let it go.
|
|
5092
5120
|
* - **Another process.** Erasing on one box invalidates nothing on the others; each store bounds its own
|
|
5093
5121
|
* staleness by `cache.genTtlMs`, and a store with no registry (a bare `IStorageDriver`), with
|
|
5094
5122
|
* `cache.genTtlMs: 0`, or on a `StorageChunkSource` of your own has no bound at all: it converges only when a read happens to miss its caches. If a compliance deadline depends on every
|
|
@@ -5170,6 +5198,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
5170
5198
|
throw err;
|
|
5171
5199
|
}
|
|
5172
5200
|
const pinnedAt = at === null ? { generation: null, version: null, fingerprint: null } : { generation: at.generation, version: at.version, fingerprint: at.fingerprint };
|
|
5201
|
+
if (at !== null) crbm.startPinChecks(pinnedAt, at.checkedAtMs);
|
|
5173
5202
|
const pins = /* @__PURE__ */ new Map([[segmentKey(ref), pinnedAt]]);
|
|
5174
5203
|
return makeSegment({
|
|
5175
5204
|
engine: this.engineWithPins(pins),
|
|
@@ -5415,7 +5444,7 @@ function operandsLabel(verb) {
|
|
|
5415
5444
|
}
|
|
5416
5445
|
function refuseNoOperands(verb) {
|
|
5417
5446
|
throw new ValidationError5(
|
|
5418
|
-
verb === "intersect" ? "intersect requires at least one segment to intersect with; to read every id of one segment, use iterate()" : "intersectInto requires at least one segment to intersect with; to copy this segment into dest, call materializeMany({ operands: { src: thisSegment }, outputs: [{ dest, expr: { or: ['src'] } }], keep })"
|
|
5447
|
+
verb === "intersect" ? "intersect requires at least one segment to intersect with; to read every id of one segment, use iterate()" : verb === "intersectCount" ? "intersectCount requires at least one segment to intersect with; to count one segment, use count()" : "intersectInto requires at least one segment to intersect with; to copy this segment into dest, call materializeMany({ operands: { src: thisSegment }, outputs: [{ dest, expr: { or: ['src'] } }], keep })"
|
|
5419
5448
|
);
|
|
5420
5449
|
}
|
|
5421
5450
|
var AS_IDS = {
|
|
@@ -5537,7 +5566,9 @@ var Segment = class _Segment {
|
|
|
5537
5566
|
* `*Into` of one throws before it reads anything. Materialise one side first, with `materializeMany({ operands: { src: seg }, outputs: [{ dest, expr: { or: ['src'] } }], keep })`.
|
|
5538
5567
|
*
|
|
5539
5568
|
* **A pin knows its object, not only its number.** A name purged and loaded again starts again at generation
|
|
5540
|
-
* 0, so a pin of the old segment never reads the new one
|
|
5569
|
+
* 0, so a pin of the old segment never reads the new one. On a store with a timed refresh the pin fails with
|
|
5570
|
+
* `NotFoundError` at its next check, within `cache.genTtlMs`, what it had already read included, and `invalidate`
|
|
5571
|
+
* does not bring it back: take a new pin. On a store with none, what it has already read still answers, as the
|
|
5541
5572
|
* instant it pinned, and anything it would have to fetch fails with `NotFoundError`, as a swept pin's does.
|
|
5542
5573
|
*
|
|
5543
5574
|
* **It is a hold, and `pin({ leaseUntil })` makes it a bounded one.** Without a lease nothing stops a collection
|
|
@@ -5559,7 +5590,11 @@ var Segment = class _Segment {
|
|
|
5559
5590
|
* segment whose row is later dropped or destroyed fails once it must open its object again, rather than go empty
|
|
5560
5591
|
* part-way through a call. **A pin
|
|
5561
5592
|
* keeps the key its reader unwrapped for as long as that reader stays open, and answers from the chunks it
|
|
5562
|
-
* decoded for as long as they stay cached.**
|
|
5593
|
+
* decoded for as long as they stay cached, until its next check.** On a store with a timed refresh a pin checks its
|
|
5594
|
+
* segment's row every `cache.genTtlMs` (from the moment it was taken, whether or not it was read) and fails with
|
|
5595
|
+
* `NotFoundError` once the segment is destroyed, is another incarnation, or its generation is collected or recorded by its row as another object (an object lost or replaced out of band
|
|
5596
|
+
* under a row that still names it is found only when the pin must fetch). This
|
|
5597
|
+
* store invalidates a pin:
|
|
5563
5598
|
*
|
|
5564
5599
|
* - a `load`, a `rollback` or an `*Into` invalidates a pin of the segment it writes;
|
|
5565
5600
|
* - `dropSegment` invalidates a pin of the segment it drops, and `retireExpired` a pin of each segment its ledger
|
|
@@ -5572,16 +5607,18 @@ var Segment = class _Segment {
|
|
|
5572
5607
|
* where the object is still there, and fails with `NotFoundError` where it is gone. It caches nothing more of what it
|
|
5573
5608
|
* began reading before the invalidation, so the pin's next read opens the object again too.
|
|
5574
5609
|
* Anything else leaves the pin as it is: after a `destroySegment` beside this store, or an erasure, a drop or a
|
|
5575
|
-
* retirement through another store, in this process or another, the pin answers from what it holds
|
|
5610
|
+
* retirement through another store, in this process or another, the pin answers from what it holds for at most
|
|
5611
|
+
* `cache.genTtlMs` on a store with a timed refresh, and then fails. On a store with none it answers until this
|
|
5576
5612
|
* store's reader cache evicts the pin's reader and its chunk cache evicts the chunks the pin decoded, or
|
|
5577
5613
|
* {@link CloudRoaring.invalidate} is called on this store. A combine on another store refuses the pin. Where the
|
|
5578
5614
|
* object the pin reads has been deleted, by an erasure, a drop or a sweep, a chunk the pin has not cached fails at
|
|
5579
|
-
* once.
|
|
5615
|
+
* once. With no timed refresh nothing bounds any of that, as nothing bounds anything else a pin holds.
|
|
5580
5616
|
*
|
|
5581
|
-
* A replacement a pin
|
|
5617
|
+
* A replacement a read of the pin found is remembered, so its later chunk reads fail with no request, until the store
|
|
5582
5618
|
* forgets it: an invalidation of the segment does, as does a later `pin()` of the same version that opens the object
|
|
5583
|
-
* again
|
|
5584
|
-
* call {@link CloudRoaring.invalidate} on this store: the pin then reads the object again.
|
|
5619
|
+
* again; a replacement the pin's own check found is forgotten only by an invalidation. The store remembers at most `cache.readerMax` of them. So once a restore puts the pinned object back
|
|
5620
|
+
* into the same row, call {@link CloudRoaring.invalidate} on this store: the pin then reads the object again. A
|
|
5621
|
+
* restore into a purged and re-created row does not revive a pin on a store with a timed refresh: take a new pin.
|
|
5585
5622
|
*
|
|
5586
5623
|
* `pin()` reads the registry row and opens the generation at once, so the pin knows its object before its first
|
|
5587
5624
|
* read. With a registry, pins of one generation taken while its row is unchanged share one reader while the store
|
|
@@ -6125,6 +6162,111 @@ var Segment = class _Segment {
|
|
|
6125
6162
|
);
|
|
6126
6163
|
return this.combineInto("andNotInto", dest, excludes, options);
|
|
6127
6164
|
}
|
|
6165
|
+
/**
|
|
6166
|
+
* How many ids `this ∩ others…` holds (minus `exclude`): the length of what {@link intersect} streams, without
|
|
6167
|
+
* streaming it. Every key all the operands hold is decoded and counted as the stream reads it, and a key only some
|
|
6168
|
+
* operands hold is not read, so the call makes the requests {@link intersect} makes and returns a number instead of
|
|
6169
|
+
* the ids. It is exact: it never answers a key from an index, so it trusts nothing {@link count} trusts.
|
|
6170
|
+
*
|
|
6171
|
+
* The range, `concurrency`, `budget`, `allowAbsentOperands` and `exclude` are {@link intersect}'s; a write option fails
|
|
6172
|
+
* the call with {@link ValidationError} before any request, and so does an empty `others` (to count one segment, use
|
|
6173
|
+
* {@link count}).
|
|
6174
|
+
*
|
|
6175
|
+
* Like a stream, a count that runs while a segment is erased or reloaded can describe two instants, within the bound
|
|
6176
|
+
* the streams state. A union or a difference, and an intersection with an `exclude`, looks at its operands'
|
|
6177
|
+
* generations every `concurrency` keys; an intersection with no `exclude` makes no such look, since it decodes every
|
|
6178
|
+
* key and so shows a move in the chunks it decodes, as its stream does. Either way it plans again above the last key
|
|
6179
|
+
* it took when one moved. A pinned handle counts the generation it pinned, and each pinned operand's object is
|
|
6180
|
+
* looked up once before the first key (one footer read each, which `budget` does not charge, and which the stream
|
|
6181
|
+
* does not make), and again at each look where there is one, so an erasure of what it pinned fails the count with
|
|
6182
|
+
* {@link NotFoundError}; a
|
|
6183
|
+
* fault the look meets other than a transient one, which is retried, is raised as it is. A call that involves a
|
|
6184
|
+
* handle whose lease has ended, or has ended by the next look, throws {@link LeaseExpiredError}.
|
|
6185
|
+
*/
|
|
6186
|
+
async intersectCount(others, options) {
|
|
6187
|
+
checkOptions(
|
|
6188
|
+
options,
|
|
6189
|
+
"intersectCount",
|
|
6190
|
+
CALL_KEYS.intersectCount,
|
|
6191
|
+
["allowAbsentOperands"],
|
|
6192
|
+
MISPLACED.intersectCount
|
|
6193
|
+
);
|
|
6194
|
+
return this.countCombine("intersect", others, options ?? void 0);
|
|
6195
|
+
}
|
|
6196
|
+
/**
|
|
6197
|
+
* How many ids `this ∪ others…` holds (minus `exclude`): the length of what {@link union} streams, without streaming
|
|
6198
|
+
* it. A chunk key only one operand holds, that no `exclude` holds and the range does not cut, is answered from that
|
|
6199
|
+
* operand's index, so a union of segments that mostly do not overlap reads little of them; every other key is
|
|
6200
|
+
* decoded and counted. On handles that are not pinned it makes no more requests than {@link union} does, and reads
|
|
6201
|
+
* fewer bytes when the shared keys are few. An empty `others`, with no `exclude` and no range, is {@link count} of
|
|
6202
|
+
* `this`, answered from the registry row when it can be; when that figure is 0 it adds one registry read, to refuse
|
|
6203
|
+
* a segment that does not exist as the stream does.
|
|
6204
|
+
*
|
|
6205
|
+
* What it trusts: the index, for each key it answers without decoding, as {@link count} does, so a party who can write
|
|
6206
|
+
* an object's index can make that key's figure wrong; a decoded key is exact. A source with no per-chunk counts
|
|
6207
|
+
* decodes every key. It takes the options {@link intersectCount} takes; the erasure bound, the lease checks and the
|
|
6208
|
+
* pinned handles are as for {@link intersectCount}, a pinned operand adding a footer read before the first key and at
|
|
6209
|
+
* each look. An empty `others` on a pinned handle is {@link count} of the pin, with no look.
|
|
6210
|
+
*/
|
|
6211
|
+
async unionCount(others, options) {
|
|
6212
|
+
checkOptions(
|
|
6213
|
+
options,
|
|
6214
|
+
"unionCount",
|
|
6215
|
+
CALL_KEYS.unionCount,
|
|
6216
|
+
["allowAbsentOperands"],
|
|
6217
|
+
MISPLACED.unionCount
|
|
6218
|
+
);
|
|
6219
|
+
return this.countCombine("union", others, options ?? void 0);
|
|
6220
|
+
}
|
|
6221
|
+
/**
|
|
6222
|
+
* How many ids `this \ (excludes…)` holds: the length of what {@link andNot} streams, without streaming it. A chunk
|
|
6223
|
+
* key of `this` that no exclude holds and the range does not cut is answered from its index and not read; a key an
|
|
6224
|
+
* exclude shares is decoded and counted. On handles that are not pinned it makes no more requests than {@link andNot}
|
|
6225
|
+
* does, and reads fewer bytes when the excludes overlap little of `this`. An empty `excludes`, with no range, is
|
|
6226
|
+
* {@link count} of `this` (the stream of it is the whole of `this`), answered from the registry row when it can be,
|
|
6227
|
+
* with one registry read added when that figure is 0.
|
|
6228
|
+
*
|
|
6229
|
+
* It takes the options {@link andNot} takes (every one of {@link intersectCount}'s but `exclude`, which a count of a
|
|
6230
|
+
* difference has no use for, since `excludes` is its subtraction) and trusts what {@link unionCount} trusts. The
|
|
6231
|
+
* erasure bound, the lease checks and the pinned handles are as for {@link intersectCount}.
|
|
6232
|
+
*/
|
|
6233
|
+
async andNotCount(excludes, options) {
|
|
6234
|
+
checkOptions(
|
|
6235
|
+
options,
|
|
6236
|
+
"andNotCount",
|
|
6237
|
+
CALL_KEYS.andNotCount,
|
|
6238
|
+
["allowAbsentOperands"],
|
|
6239
|
+
MISPLACED.andNotCount
|
|
6240
|
+
);
|
|
6241
|
+
return this.countCombine("andNot", excludes, options ?? void 0);
|
|
6242
|
+
}
|
|
6243
|
+
/** The one body of the three count methods, after each has checked its own options. */
|
|
6244
|
+
async countCombine(verb, operands, options) {
|
|
6245
|
+
const name = `${verb}Count`;
|
|
6246
|
+
const exclude = verb === "andNot" ? NO_SEGMENTS : this.excludesOf(options);
|
|
6247
|
+
const others = segmentList(
|
|
6248
|
+
operands,
|
|
6249
|
+
verb === "andNot" ? `${name}: \`excludes\`` : `${name}: \`others\``
|
|
6250
|
+
);
|
|
6251
|
+
if (verb === "intersect" && others.length === 0) refuseNoOperands("intersectCount");
|
|
6252
|
+
const handles = [this, ...others, ...exclude];
|
|
6253
|
+
this.assertSegments(name, verb === "andNot" ? "an exclude" : "an operand", others);
|
|
6254
|
+
this.assertSegments(name, "an exclude", exclude);
|
|
6255
|
+
this.assertLeases(handles);
|
|
6256
|
+
const engine = this.combineEngine(handles) ?? this.engine;
|
|
6257
|
+
const read = options == null ? void 0 : readOptions(options);
|
|
6258
|
+
const guard2 = handles.some((h) => h.leaseHold !== void 0) ? () => this.assertLeases(handles) : void 0;
|
|
6259
|
+
const opts = guard2 === void 0 ? read : { ...read, guard: guard2 };
|
|
6260
|
+
const refs = others.map((o) => o.ref);
|
|
6261
|
+
if (verb === "andNot") return engine.combineCount([this.ref], refs, "all", verb, opts);
|
|
6262
|
+
return engine.combineCount(
|
|
6263
|
+
[this.ref, ...refs],
|
|
6264
|
+
exclude.map((o) => o.ref),
|
|
6265
|
+
verb === "union" ? "any" : "all",
|
|
6266
|
+
verb,
|
|
6267
|
+
opts
|
|
6268
|
+
);
|
|
6269
|
+
}
|
|
6128
6270
|
/**
|
|
6129
6271
|
* The one body of the three combines, read as ids or as chunks. `operands` is `others` for `intersect` and
|
|
6130
6272
|
* `union`, and `excludes` for `andNot`, which takes no `exclude` option.
|
|
@@ -6196,7 +6338,7 @@ Exports every registered segment's current generation from a local-filesystem st
|
|
|
6196
6338
|
Settings come from the environment:
|
|
6197
6339
|
|
|
6198
6340
|
CR_EXPORT_ROOT (required) the LocalFsStorage root, holding storage/ and registry/
|
|
6199
|
-
CR_EXPORT_OUT (required) the output directory for the dump
|
|
6341
|
+
CR_EXPORT_OUT (required) the output directory for the dump: new or existing (never a symlink)
|
|
6200
6342
|
CR_EXPORT_FORMAT roaring | ndjson (default: roaring)
|
|
6201
6343
|
CR_EXPORT_NAMESPACE (optional) scope the export to one namespace
|
|
6202
6344
|
|
|
@@ -6253,6 +6395,7 @@ function parseConfig(env) {
|
|
|
6253
6395
|
return { root, out, format, namespace };
|
|
6254
6396
|
}
|
|
6255
6397
|
async function ensureOwnDirectory(out, dir) {
|
|
6398
|
+
await checkOutDirectory(out);
|
|
6256
6399
|
await mkdir(out, { recursive: true, mode: 448 });
|
|
6257
6400
|
try {
|
|
6258
6401
|
await mkdir(dir, { mode: 448 });
|
|
@@ -6271,6 +6414,20 @@ async function ensureOwnDirectory(out, dir) {
|
|
|
6271
6414
|
if ((found.mode & 63) !== 0) refuse("a namespace directory is open to group or others");
|
|
6272
6415
|
}
|
|
6273
6416
|
}
|
|
6417
|
+
async function checkOutDirectory(out) {
|
|
6418
|
+
let found;
|
|
6419
|
+
try {
|
|
6420
|
+
found = await lstat(resolve(out));
|
|
6421
|
+
} catch (err) {
|
|
6422
|
+
if (err.code === "ENOENT") return;
|
|
6423
|
+
throw err;
|
|
6424
|
+
}
|
|
6425
|
+
const refuse = (why) => {
|
|
6426
|
+
throw new ValidationError6(`the output directory is refused: ${why}`);
|
|
6427
|
+
};
|
|
6428
|
+
if (found.isSymbolicLink()) refuse("it is a symbolic link");
|
|
6429
|
+
if (!found.isDirectory()) refuse("it is not a directory");
|
|
6430
|
+
}
|
|
6274
6431
|
var MANIFEST_NAME = "manifest.json";
|
|
6275
6432
|
function fsSink(out) {
|
|
6276
6433
|
return {
|
|
@@ -6339,6 +6496,7 @@ async function main(env = process.env, now = () => Date.now()) {
|
|
|
6339
6496
|
await requireStoreRoot(config.root);
|
|
6340
6497
|
const storage = new LocalFsStorage(config.root);
|
|
6341
6498
|
const store = new CloudRoaring({ storage });
|
|
6499
|
+
await checkOutDirectory(config.out);
|
|
6342
6500
|
const priorManifest = await lstat(join(config.out, MANIFEST_NAME)).catch(() => null);
|
|
6343
6501
|
if (priorManifest?.isDirectory() === true) {
|
|
6344
6502
|
throw new ValidationError6(
|