@cloudbitmaps/roaring 0.26.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/dist/bin/export-segments.js +43 -16
- package/dist/bin/export-segments.js.map +3 -3
- package/dist/index.d.ts +27 -11
- package/dist/index.js +21 -10
- package/dist/index.js.map +2 -2
- 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 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. 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
|
|
|
@@ -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
|
};
|
|
@@ -4499,7 +4499,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
4499
4499
|
* duration, or fix the source first and load after. A writer that lands *during* the rewrite is caught: the
|
|
4500
4500
|
* rewrite's publish is refused **by the fence** — the publish lands its compare-and-swap only
|
|
4501
4501
|
* while the pointer is still on the generation the rewrite streamed — and the entry says `note: 'superseded'`,
|
|
4502
|
-
* 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,
|
|
4503
4503
|
* so the rewrite would out-rank the newer generation and then collect it.
|
|
4504
4504
|
*
|
|
4505
4505
|
* A racing **erasure** is caught before that, and reported the same way. It collects with `keep: 0`, taking
|
|
@@ -4737,7 +4737,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
4737
4737
|
* It deletes nothing. The generations above the new pointer stay put — which is what makes this reversible —
|
|
4738
4738
|
* and are then *above* `currentGen`, where collection never looks, and still known to the row, so rolling forward onto
|
|
4739
4739
|
* one needs no flag. They remain until loads pass them (the first load whose
|
|
4740
|
-
* 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
|
|
4741
4742
|
* generation that load superseded first), {@link CloudRoaring.dropSegment}
|
|
4742
4743
|
* deletes them, or {@link CloudRoaring.eraseSubject} does: all of those present when it rewrites, and, when the
|
|
4743
4744
|
* current generation does not hold the id, only those holding it and any no read of the segment can open, whatever
|
|
@@ -5114,7 +5115,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
5114
5115
|
* the unwrapped DEK, and decrypts with them, chunks it had never fetched before the shred included, until its
|
|
5115
5116
|
* snapshot re-resolves: within `cache.genTtlMs` when the store has a registry and a `genTtlMs` above 0, and
|
|
5116
5117
|
* otherwise only when its caches let the segment go or a read has to fetch from a generation a sweep collected.
|
|
5117
|
-
* 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.
|
|
5118
5120
|
* - **Another process.** Erasing on one box invalidates nothing on the others; each store bounds its own
|
|
5119
5121
|
* staleness by `cache.genTtlMs`, and a store with no registry (a bare `IStorageDriver`), with
|
|
5120
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
|
|
@@ -5196,6 +5198,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
5196
5198
|
throw err;
|
|
5197
5199
|
}
|
|
5198
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);
|
|
5199
5202
|
const pins = /* @__PURE__ */ new Map([[segmentKey(ref), pinnedAt]]);
|
|
5200
5203
|
return makeSegment({
|
|
5201
5204
|
engine: this.engineWithPins(pins),
|
|
@@ -5563,7 +5566,9 @@ var Segment = class _Segment {
|
|
|
5563
5566
|
* `*Into` of one throws before it reads anything. Materialise one side first, with `materializeMany({ operands: { src: seg }, outputs: [{ dest, expr: { or: ['src'] } }], keep })`.
|
|
5564
5567
|
*
|
|
5565
5568
|
* **A pin knows its object, not only its number.** A name purged and loaded again starts again at generation
|
|
5566
|
-
* 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
|
|
5567
5572
|
* instant it pinned, and anything it would have to fetch fails with `NotFoundError`, as a swept pin's does.
|
|
5568
5573
|
*
|
|
5569
5574
|
* **It is a hold, and `pin({ leaseUntil })` makes it a bounded one.** Without a lease nothing stops a collection
|
|
@@ -5585,7 +5590,11 @@ var Segment = class _Segment {
|
|
|
5585
5590
|
* segment whose row is later dropped or destroyed fails once it must open its object again, rather than go empty
|
|
5586
5591
|
* part-way through a call. **A pin
|
|
5587
5592
|
* keeps the key its reader unwrapped for as long as that reader stays open, and answers from the chunks it
|
|
5588
|
-
* 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:
|
|
5589
5598
|
*
|
|
5590
5599
|
* - a `load`, a `rollback` or an `*Into` invalidates a pin of the segment it writes;
|
|
5591
5600
|
* - `dropSegment` invalidates a pin of the segment it drops, and `retireExpired` a pin of each segment its ledger
|
|
@@ -5598,16 +5607,18 @@ var Segment = class _Segment {
|
|
|
5598
5607
|
* where the object is still there, and fails with `NotFoundError` where it is gone. It caches nothing more of what it
|
|
5599
5608
|
* began reading before the invalidation, so the pin's next read opens the object again too.
|
|
5600
5609
|
* Anything else leaves the pin as it is: after a `destroySegment` beside this store, or an erasure, a drop or a
|
|
5601
|
-
* 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
|
|
5602
5612
|
* store's reader cache evicts the pin's reader and its chunk cache evicts the chunks the pin decoded, or
|
|
5603
5613
|
* {@link CloudRoaring.invalidate} is called on this store. A combine on another store refuses the pin. Where the
|
|
5604
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
|
|
5605
|
-
* once.
|
|
5615
|
+
* once. With no timed refresh nothing bounds any of that, as nothing bounds anything else a pin holds.
|
|
5606
5616
|
*
|
|
5607
|
-
* 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
|
|
5608
5618
|
* forgets it: an invalidation of the segment does, as does a later `pin()` of the same version that opens the object
|
|
5609
|
-
* again
|
|
5610
|
-
* 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.
|
|
5611
5622
|
*
|
|
5612
5623
|
* `pin()` reads the registry row and opens the generation at once, so the pin knows its object before its first
|
|
5613
5624
|
* read. With a registry, pins of one generation taken while its row is unchanged share one reader while the store
|
|
@@ -6327,7 +6338,7 @@ Exports every registered segment's current generation from a local-filesystem st
|
|
|
6327
6338
|
Settings come from the environment:
|
|
6328
6339
|
|
|
6329
6340
|
CR_EXPORT_ROOT (required) the LocalFsStorage root, holding storage/ and registry/
|
|
6330
|
-
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)
|
|
6331
6342
|
CR_EXPORT_FORMAT roaring | ndjson (default: roaring)
|
|
6332
6343
|
CR_EXPORT_NAMESPACE (optional) scope the export to one namespace
|
|
6333
6344
|
|
|
@@ -6384,6 +6395,7 @@ function parseConfig(env) {
|
|
|
6384
6395
|
return { root, out, format, namespace };
|
|
6385
6396
|
}
|
|
6386
6397
|
async function ensureOwnDirectory(out, dir) {
|
|
6398
|
+
await checkOutDirectory(out);
|
|
6387
6399
|
await mkdir(out, { recursive: true, mode: 448 });
|
|
6388
6400
|
try {
|
|
6389
6401
|
await mkdir(dir, { mode: 448 });
|
|
@@ -6402,6 +6414,20 @@ async function ensureOwnDirectory(out, dir) {
|
|
|
6402
6414
|
if ((found.mode & 63) !== 0) refuse("a namespace directory is open to group or others");
|
|
6403
6415
|
}
|
|
6404
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
|
+
}
|
|
6405
6431
|
var MANIFEST_NAME = "manifest.json";
|
|
6406
6432
|
function fsSink(out) {
|
|
6407
6433
|
return {
|
|
@@ -6470,6 +6496,7 @@ async function main(env = process.env, now = () => Date.now()) {
|
|
|
6470
6496
|
await requireStoreRoot(config.root);
|
|
6471
6497
|
const storage = new LocalFsStorage(config.root);
|
|
6472
6498
|
const store = new CloudRoaring({ storage });
|
|
6499
|
+
await checkOutDirectory(config.out);
|
|
6473
6500
|
const priorManifest = await lstat(join(config.out, MANIFEST_NAME)).catch(() => null);
|
|
6474
6501
|
if (priorManifest?.isDirectory() === true) {
|
|
6475
6502
|
throw new ValidationError6(
|