@cloudbitmaps/roaring 0.19.1 → 0.20.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 +59 -34
- package/README.md +3 -1
- package/dist/bin/export-segments.js +91 -110
- package/dist/bin/export-segments.js.map +2 -2
- package/dist/index.d.ts +110 -73
- package/dist/index.js +89 -112
- package/dist/index.js.map +2 -2
- package/dist/open-charging-storage.d.ts +3 -2
- package/dist/testing/conformance.d.ts +15 -10
- package/package.json +2 -2
package/PRIVACY.md
CHANGED
|
@@ -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 (
|
|
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 everything, so its `keep: 0` collection takes every older generation, above the pointer or below.) 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**. | "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
|
|
|
@@ -76,9 +76,16 @@ It reuses the store's own drivers (so build the store with a backend). It return
|
|
|
76
76
|
**erasure ledger** — one entry per segment the id was found in, `{ segment, namespace?, erased, fromGeneration,
|
|
77
77
|
generation, note? }` — as your proof of deletion; persist it or route it to your audit sink, which also receives
|
|
78
78
|
one `segment.rewrite { fromGeneration, generation }` event per rewrite when you pass `audit` — an id found only
|
|
79
|
-
outside the current generation (a retained *superseded* one,
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
outside the current generation (a retained *superseded* one, one above the pointer after a rollback, an object left
|
|
80
|
+
under a tombstone, or a first load's object on a segment with no generation yet) is collected rather than rewritten,
|
|
81
|
+
so it emits `segment.collect { fromGeneration, collected }`
|
|
82
|
+
instead and its ledger entry carries no `generation`. Each `segment.*` event also names the segment's `incarnation`
|
|
83
|
+
when its registry token carries one, so a segment created again under the same name is told apart from the one
|
|
84
|
+
erased. Segments the id is
|
|
85
|
+
not in are not listed. A segment where the erasure found the id in nothing it could search, and deleted only objects
|
|
86
|
+
no read of it can open, is not listed either: the audit sink gets `segment.collect { collected }` for it, with no
|
|
87
|
+
`fromGeneration`, and that event is the only record of the deletion. `store.subjectReport(id, { namespace })` answers
|
|
88
|
+
the read side (Art. 15 — which segments
|
|
82
89
|
an id is in).
|
|
83
90
|
|
|
84
91
|
Two rules. **Do not load a segment while erasing from it**: a load that lands after the rewrite carries whatever
|
|
@@ -117,11 +124,22 @@ sweep run (it collects nothing without a row) reaches. List those objects with `
|
|
|
117
124
|
`store.dropSegment(ref, { confirmSegment: ref.segment })`, which leaves a tombstone row fencing the name. An empty
|
|
118
125
|
ledger is not by itself proof the id is gone. A tombstoned segment is searched too: a
|
|
119
126
|
cleartext object left under it that holds the id is deleted with everything under the tombstone, and the entry reads
|
|
120
|
-
`erased: true`. A segment whose row names no generation yet (a retention policy set before its first load) is
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
`erased: true`. A segment whose row names no generation yet (a retention policy set before its first load) is searched
|
|
128
|
+
too: an object a first load wrote and never published that holds the id is deleted, and the entry reads `erased:
|
|
129
|
+
true`. Before it deletes anything the erasure renews the row's `pointerId`, so a load still running that wrote such an
|
|
130
|
+
object is refused at its publish and never names an object that is gone. Each delete names the object the erasure
|
|
131
|
+
read: on a storage driver that reports `conditionalDelete`, another erasure of the id that deletes it first, followed
|
|
132
|
+
by a load that takes the number freed and publishes, does not lose that load's generation to this erasure's delete; on
|
|
133
|
+
one that does not, it can, and the row then names a missing object, which `store.checkConsistency()` reports
|
|
134
|
+
([a number taken again](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/erasure.md#how-it-stays-correct)). On an encrypted store such an object cannot
|
|
135
|
+
be searched until its load publishes the key, so it is deleted whatever id is erased: an erasure of any id refuses an
|
|
136
|
+
encrypted first load in flight that has written its object onto a segment whose row was made ahead of its data, and
|
|
137
|
+
that load is re-run ([the erasure guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/erasure.md#two-rules-while-you-erase)). On an encrypted segment,
|
|
138
|
+
an object sealed under a key its row does not hold, and on a cleartext segment with a generation, an encrypted object,
|
|
139
|
+
open for no read of the segment (the object of a first load that lost the race to the one that published, or
|
|
140
|
+
crashed): each is deleted the same way, whatever id is erased, wherever an erasure meets it, except as the current
|
|
141
|
+
generation, which is never deleted for this, and under a tombstone, where it is no holder and goes only with
|
|
142
|
+
everything there, when another object under the tombstone holds the id.
|
|
125
143
|
|
|
126
144
|
**Do not roll a segment back while erasing from it.** A rollback that lands mid-erasure can move the pointer onto a
|
|
127
145
|
generation the erasure did not rewrite, so the id can be current again. Roll back before the erasure starts or
|
|
@@ -137,8 +155,8 @@ A call with a feed reads operands the library never stores, which no erasure can
|
|
|
137
155
|
|
|
138
156
|
Erasure and crypto-shred are **immediate in storage and immediate in the store whose verb performed them**, for
|
|
139
157
|
every read that starts after the verb returns. A read already in progress in that store can still yield an erased id
|
|
140
|
-
from a chunk it had requested before the verb ran: up to 32 chunks for `iterate` and `count`, and up to `concurrency`
|
|
141
|
-
keys (
|
|
158
|
+
from a chunk it had requested before the verb ran: up to 32 chunks for `iterate` and `count`, and up to `concurrency` + 1
|
|
159
|
+
keys (33 by default) for a combine, the key it is handing out and the `concurrency` keys it had already requested. 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. They are not immediate in any *other* store, in the same process or another, and this library ships nothing that could
|
|
142
160
|
make them so — there is no daemon, no bus, and no connection between two stores that happen to point at the same
|
|
143
161
|
bucket.
|
|
144
162
|
|
|
@@ -154,11 +172,15 @@ bucket.
|
|
|
154
172
|
| 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 |
|
|
155
173
|
|
|
156
174
|
**An outage of the registry extends that bound.** A refresh that cannot read the row because of a transient fault
|
|
157
|
-
(throttling, a 5xx, a dropped connection) keeps serving the
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
175
|
+
(throttling, a 5xx, a dropped connection) keeps serving what the store resolved before the outage, for a segment
|
|
176
|
+
still in its reader cache: its open reader and the key that reader unwrapped, or, where no reader is open (a
|
|
177
|
+
`count()` answered from the row opens none), the row it resolved. A read during the outage that needs the key,
|
|
178
|
+
where none was unwrapped before it, unwraps it from that row's wrapped keys. The refresh asks again 500 ms later, or
|
|
179
|
+
after `cache.genTtlMs` if that is shorter. So while the registry cannot be read, a shred or a drop is not seen, and
|
|
180
|
+
the store converges within one retry of the registry answering. A segment the reader cache has let go is served only
|
|
181
|
+
until its resolution lapses: after that its next read fails with the fault, as a cold read does. Any other failure
|
|
182
|
+
of the refresh (an access denial, a row that will not parse) is not ridden out: the read that finds it fails with
|
|
183
|
+
that error, and the reader is dropped with the key it held.
|
|
162
184
|
|
|
163
185
|
`destroySegment` and `eraseNamespace` are free functions over raw drivers, not verbs of a store, so for them every
|
|
164
186
|
store is another store, one in the same process included.
|
|
@@ -261,10 +283,10 @@ to run, and the deletion is ours to perform correctly.** Practical patterns:
|
|
|
261
283
|
The registry row's `retention` is untouched by a load, an `*Into` materialization or an erasure rewrite, so the
|
|
262
284
|
`expiresAt` you set stays put across all of them.
|
|
263
285
|
- Surface segment age and size so unbounded growth is visible, not silent. The **metrics sink** carries neither (its `storage.get` events count the bytes of each read, not a segment's size).
|
|
264
|
-
`seg.count()` gives a segment's cardinality; `seg.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
`seg.count()` gives a segment's cardinality; `seg.stat()` gives its current generation's size in bytes (`sizeBytes`,
|
|
287
|
+
`null` when the storage source cannot measure it), and does not count superseded generations still in the bucket,
|
|
288
|
+
which `store.generations(ref)` lists; and its registry row carries `createdAt` and `updatedAt`, which the
|
|
289
|
+
backend's `registry.list()` returns for every row.
|
|
268
290
|
|
|
269
291
|
**Be precise about what "drop the oldest" involves**, because the three levers differ in what they guarantee:
|
|
270
292
|
|
|
@@ -276,10 +298,10 @@ to run, and the deletion is ours to perform correctly.** Practical patterns:
|
|
|
276
298
|
- **A deleted row keeps its record only where the registry cannot remove it.** Where the registry reports
|
|
277
299
|
`conditionalDelete` (the default for S3 when its client sends to an AWS S3 host, for
|
|
278
300
|
Azure Blob, the local filesystem and memory), `registry.delete` and the retention sweep's purge of a tombstone remove
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
`conditionalDelete: false`),
|
|
282
|
-
|
|
301
|
+
the row from the bucket, and a re-create's random incarnation id keeps it apart from the removed row. Where it does not
|
|
302
|
+
(an S3 client that sends to an S3-compatible store or an emulator, or a GCS client, by default, or
|
|
303
|
+
`conditionalDelete: false`), a delete writes a deleted marker that holds the record, so the row's token counter
|
|
304
|
+
survives a re-create. A tombstone left by `dropSegment` or `destroySegment`
|
|
283
305
|
holds no wrapped key and no summary, since both clear them, so a purge that leaves a tombstone keeps the name, the
|
|
284
306
|
pointer, the retention policy and the timestamps. A **live** row deleted directly with `registry.delete` keeps its
|
|
285
307
|
summary, the current generation's id count and your metadata, in a marker where one is written.
|
|
@@ -308,12 +330,11 @@ So the two are complements, not alternatives: **`dropSegment` for "stop paying f
|
|
|
308
330
|
> ⚠️ **Never let a lifecycle rule expire a registry row's current version, a tombstone, or a due-index pointer** —
|
|
309
331
|
> a separate trap, and a worse one. A rule cannot tell a live row from a tombstone, and an expired live row is a
|
|
310
332
|
> segment whose pointer is gone: its generations look unreferenced, and nothing reads or collects them. The library
|
|
311
|
-
> removes a row only by a delete the store applies to the exact version it judged, and
|
|
312
|
-
>
|
|
313
|
-
>
|
|
314
|
-
>
|
|
315
|
-
>
|
|
316
|
-
> whether two observations describe the same segment. Re-issue one and they can all answer "yes" about a segment that
|
|
333
|
+
> removes a row only by a delete the store applies to the exact version it judged, and every row's token carries a
|
|
334
|
+
> random incarnation id, so a name re-created later is told apart from it with overwhelming probability. That token is
|
|
335
|
+
> the segment's **identity**: a fenced publish and a generation-collection pass compare it, and a cached reader
|
|
336
|
+
> compares the row's `pointerId`, itself one of the row's tokens, to decide whether two observations describe the same
|
|
337
|
+
> segment. Re-issue one and they can all answer "yes" about a segment that
|
|
317
338
|
> no longer exists — serving a deleted incarnation's data, or collecting a live one's objects. A rule on
|
|
318
339
|
> *noncurrent* versions of the registry prefix touches neither the current row nor its tombstone; what it shortens is
|
|
319
340
|
> the history a registry restore picks from, and the time a crypto-shred takes to complete (above).
|
|
@@ -332,8 +353,7 @@ response, a removal the registry refused (`purgeFaults` counts it), or a pointer
|
|
|
332
353
|
it reads, and an unscoped fleet scan, one given no `namespace`, from every day. A deployment that scopes every sweep to a
|
|
333
354
|
namespace, or runs only index scans, keeps such a pointer until an unscoped fleet scan runs. Where the registry does not
|
|
334
355
|
report `conditionalDelete` (an S3 client that sends to an S3-compatible store or an emulator, or a GCS client, by default, or
|
|
335
|
-
`conditionalDelete: false`),
|
|
336
|
-
instead: every one of those fields, in the bucket, indefinitely, read by every full listing. A registry that does not
|
|
356
|
+
`conditionalDelete: false`), the purge leaves a tombstone instead: every one of those fields, in the bucket, indefinitely, read by every full listing. A registry that does not
|
|
337
357
|
report it files no pointer to begin with. A removed
|
|
338
358
|
row stays recoverable wherever the storage keeps a copy of it: with object versioning on, its earlier versions stay until
|
|
339
359
|
a noncurrent-version rule expires them, as an overwritten row's do; with soft delete on, it stays for the retention
|
|
@@ -382,8 +402,13 @@ Wire the **audit sink** (`IAuditSink`) to get an append-only, vendor-neutral rec
|
|
|
382
402
|
state changes — `segment.publish` (a loaded generation became current), `segment.load-refused` (a load that did
|
|
383
403
|
not publish, because its guard refused it or another writer got there first), `segment.rollback` (an operator moved
|
|
384
404
|
the pointer to a generation it named), `segment.rewrite` (a subject-erasure
|
|
385
|
-
rewrite: `fromGeneration` → `generation`), `segment.
|
|
386
|
-
|
|
405
|
+
rewrite: `fromGeneration` → `generation`), `segment.collect` (a subject erasure that rewrote nothing and deleted what
|
|
406
|
+
no reader resolves: the generations holding the id, when it found the id only outside the current generation, or, with
|
|
407
|
+
no `fromGeneration`, only objects no read of the segment can open, which it deletes whatever the id), `segment.erase`
|
|
408
|
+
(a genuine
|
|
409
|
+
crypto-shred), `segment.dispose` (a `dropSegment`, including every retirement the sweep performs) and
|
|
410
|
+
`namespace.erase` — for your audit log / SIEM. Each `segment.*` event names the segment's `incarnation` when its
|
|
411
|
+
registry token carries one, so a segment created again under the same name is told apart from the one before.
|
|
387
412
|
It is off by default and exception-safe. See the [dashboards guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/dashboards.md), which also says
|
|
388
413
|
which event is the receipt for which claim. An erasure *ledger* — per-subject-request proof of physical deletion
|
|
389
414
|
— is returned by **`eraseSubject`**. `subjectReport` is the read side and returns only which segments an id is
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ file, then ask `has`, `count`, `iterate`, `intersect`, `union` and `andNot`, wit
|
|
|
14
14
|
```bash
|
|
15
15
|
pnpm add @cloudbitmaps/roaring # the store, with in-memory and local-disk backends
|
|
16
16
|
pnpm add @cloudbitmaps/s3 # the storage you have: or @cloudbitmaps/gcs, or @cloudbitmaps/azure-blob
|
|
17
|
+
pnpm add @cloudbitmaps/tools # optional: price a store, offline
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
On npm 12 and pnpm 10 and later, allow `roaring`'s one install script first, or the install exits 0 and the package throws at `import`. Put this in your `package.json`: `{ "allowScripts": { "roaring": true }, "pnpm": { "onlyBuiltDependencies": ["roaring"] } }`. npm 11 runs the script but warns until you allow it the same way; pnpm 9 needs nothing extra. [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
|
|
@@ -83,7 +84,8 @@ pointer read: it reads no payload and no object.
|
|
|
83
84
|
The trade is stated plainly. A membership check that misses the cache costs a ranged GET against object storage,
|
|
84
85
|
where an in-process store costs a memory read. If you need sub-millisecond answers on a working set that fits a
|
|
85
86
|
bounded cache, use Redis. [The benchmarks](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/benchmarks.md)
|
|
86
|
-
give the method and what the numbers do not establish.
|
|
87
|
+
give the method and what the numbers do not establish. To price your own store, from the sizes `seg.stat()` reports, use
|
|
88
|
+
`@cloudbitmaps/tools` ([the cost guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/cost.md)).
|
|
87
89
|
|
|
88
90
|
## Before production
|
|
89
91
|
|
|
@@ -33,8 +33,6 @@ import {
|
|
|
33
33
|
excludingReservedRows,
|
|
34
34
|
dropSegment,
|
|
35
35
|
eraseIdFromSegment,
|
|
36
|
-
estimateCost,
|
|
37
|
-
groundedReport,
|
|
38
36
|
judgeLoad,
|
|
39
37
|
loadSegment,
|
|
40
38
|
loadSegmentChunks,
|
|
@@ -55,7 +53,7 @@ import {
|
|
|
55
53
|
splitId,
|
|
56
54
|
takeLease
|
|
57
55
|
} from "@cloudbitmaps/core";
|
|
58
|
-
import { validateSegmentRef } from "@cloudbitmaps/core/driver-kit";
|
|
56
|
+
import { pointerIdOf, validateSegmentRef } from "@cloudbitmaps/core/driver-kit";
|
|
59
57
|
|
|
60
58
|
// src/open-charging-storage.ts
|
|
61
59
|
var OpenChargingStorage = class {
|
|
@@ -88,8 +86,9 @@ var OpenChargingStorage = class {
|
|
|
88
86
|
if (!this.free.has(key.generation)) this.charge();
|
|
89
87
|
return this.inner.getTail(key, maxBytes);
|
|
90
88
|
}
|
|
91
|
-
delete
|
|
92
|
-
|
|
89
|
+
/** Handed through whole: the erasure's delete of a holder is conditioned on the object it searched. */
|
|
90
|
+
delete(key, options) {
|
|
91
|
+
return this.inner.delete(key, options);
|
|
93
92
|
}
|
|
94
93
|
list(ref) {
|
|
95
94
|
return this.inner.list(ref);
|
|
@@ -655,10 +654,7 @@ import {
|
|
|
655
654
|
BufferReader,
|
|
656
655
|
CountingMetricsSink,
|
|
657
656
|
RecordingAuditSink,
|
|
658
|
-
DEFAULT_RETRY_POLICY as DEFAULT_RETRY_POLICY2
|
|
659
|
-
AWS_US_EAST_1_ONDEMAND,
|
|
660
|
-
ELASTICACHE_REDIS_US_EAST_1_ONDEMAND,
|
|
661
|
-
ONE_REDIS_HA_CLUSTER
|
|
657
|
+
DEFAULT_RETRY_POLICY as DEFAULT_RETRY_POLICY2
|
|
662
658
|
} from "@cloudbitmaps/core";
|
|
663
659
|
var SystemRng = class {
|
|
664
660
|
/** A float in `[0, 1)`. Jitter only — never key material, never anything a caller can observe. */
|
|
@@ -780,23 +776,7 @@ var CALL_KEYS = {
|
|
|
780
776
|
"concurrency",
|
|
781
777
|
"summaries"
|
|
782
778
|
]),
|
|
783
|
-
exportSegments: keysOf()(["format", "codec", "namespace", "ndjsonBatchBytes"])
|
|
784
|
-
costReport: keysOf()(["pricing", "workload"]),
|
|
785
|
-
workload: keysOf()([
|
|
786
|
-
"readsPerSec",
|
|
787
|
-
"intersectsPerSec",
|
|
788
|
-
"cacheHitRate",
|
|
789
|
-
"chunksPerIntersect",
|
|
790
|
-
"operandsPerIntersect",
|
|
791
|
-
"loadsPerMonth",
|
|
792
|
-
"requestsPerLoad",
|
|
793
|
-
"hotSegments",
|
|
794
|
-
"readerProcesses",
|
|
795
|
-
"genTtlMs",
|
|
796
|
-
"retirementsPerMonth",
|
|
797
|
-
"purgesPerMonth",
|
|
798
|
-
"conditionalDelete"
|
|
799
|
-
])
|
|
779
|
+
exportSegments: keysOf()(["format", "codec", "namespace", "ndjsonBatchBytes"])
|
|
800
780
|
};
|
|
801
781
|
var kindOf = (v) => v === null ? "null" : Array.isArray(v) ? "an array" : `a ${typeof v}`;
|
|
802
782
|
function optionsError(options, op, known, booleans = []) {
|
|
@@ -1148,22 +1128,24 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1148
1128
|
}
|
|
1149
1129
|
/**
|
|
1150
1130
|
* Get a handle to a segment. Validates the name (non-empty, well-formed, within the encoded-length cap). The options
|
|
1151
|
-
* are `{ namespace }` and nothing else:
|
|
1152
|
-
* misspelt `namespace` would otherwise address the default namespace
|
|
1153
|
-
*
|
|
1154
|
-
* `undefined` is read as absent, so a
|
|
1131
|
+
* are a plain object holding `{ namespace }` and nothing else: any other own key, enumerable or not, is refused with
|
|
1132
|
+
* {@link ValidationError}, by name, since a misspelt `namespace` would otherwise address the default namespace, and a
|
|
1133
|
+
* deadline such as `expiresAt` would be left unread. A class instance, or an object built on another, is refused too,
|
|
1134
|
+
* since it can carry an option no scan of its own keys sees. A key whose value is `undefined` is read as absent, so a
|
|
1135
|
+
* spread of options keeps working.
|
|
1155
1136
|
*/
|
|
1156
1137
|
segment(name, options) {
|
|
1157
1138
|
if (options !== void 0 && options !== null) {
|
|
1158
1139
|
if (typeof options !== "object" || Array.isArray(options)) {
|
|
1159
1140
|
throw new ValidationError2("segment: options must be an object such as { namespace }");
|
|
1160
1141
|
}
|
|
1161
|
-
|
|
1142
|
+
const proto = Object.getPrototypeOf(options);
|
|
1143
|
+
if (proto !== null && Object.getPrototypeOf(proto) !== null) {
|
|
1162
1144
|
throw new ValidationError2(
|
|
1163
|
-
"segment:
|
|
1145
|
+
"segment: options must be a plain object such as { namespace }, not a class instance or an object built on another"
|
|
1164
1146
|
);
|
|
1165
1147
|
}
|
|
1166
|
-
const unknown = Object.
|
|
1148
|
+
const unknown = Object.getOwnPropertyNames(options).filter(
|
|
1167
1149
|
(k) => k !== "namespace" && options[k] !== void 0
|
|
1168
1150
|
);
|
|
1169
1151
|
if (unknown.length > 0) {
|
|
@@ -1197,13 +1179,20 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1197
1179
|
* of it. A materialisation is a load whose ids happen to come from a combine instead of from upstream, so
|
|
1198
1180
|
* everything `load()` learned the hard way applies unchanged: the generation is written UNPUBLISHED, the
|
|
1199
1181
|
* guard runs while the old generation is still authoritative, the publish is fenced as above, and a refused
|
|
1200
|
-
* object is reclaimed only when a re-read finds the row unchanged (the same token
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1205
|
-
* the
|
|
1206
|
-
*
|
|
1182
|
+
* object is reclaimed only when a re-read finds the row unchanged (the same token, or one that differs only in
|
|
1183
|
+
* its leases), `destroyed`, or gone, or finds key material on it when the load wrote cleartext; under any other
|
|
1184
|
+
* row it is kept (hard invariant 1: deleting it after a purge-and-recreate would put a live row over a missing
|
|
1185
|
+
* generation).
|
|
1186
|
+
*
|
|
1187
|
+
* Where the row is gone, or keyed over a cleartext write, the object is proved the load's own by its footer, and
|
|
1188
|
+
* the delete passes the version that read reported, so on a storage driver that reports `conditionalDelete` an
|
|
1189
|
+
* object put under the number since is kept. Where the row is unchanged or `destroyed`, the check narrows the
|
|
1190
|
+
* window rather than closing it: the row read and the delete are two round trips, and the delete is by number,
|
|
1191
|
+
* since the write reports no version of the object it made. The other deletes a load leads to are by number too: its
|
|
1192
|
+
* collection deletes what a listing, or the row's list of kept generations, names below the pointer, and a drop's
|
|
1193
|
+
* sweep and a tombstoned segment's collection delete what a listing names under a `destroyed` row. The failure the
|
|
1194
|
+
* checks leave when they refuse is an orphan object, which costs storage until something collects it —
|
|
1195
|
+
* deliberately the cheaper side of the trade.
|
|
1207
1196
|
*
|
|
1208
1197
|
* Written and published in one step, with no guard, an empty combine — a typo'd operand, an `exclude` that
|
|
1209
1198
|
* swallowed everything, an operand that had not loaded yet — would silently replace `dest` with an empty
|
|
@@ -1568,7 +1557,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1568
1557
|
}
|
|
1569
1558
|
}
|
|
1570
1559
|
const row = await this.withRetries(() => registry.get(ref));
|
|
1571
|
-
return row === null ? null : { generation: row.currentGen, token: row
|
|
1560
|
+
return row === null ? null : { generation: row.currentGen, token: pointerIdOf(row) };
|
|
1572
1561
|
}
|
|
1573
1562
|
/** Throw what a read of, or a publish to, `handle` must throw now: its lease error, if it has one. */
|
|
1574
1563
|
assertLease(handle) {
|
|
@@ -1613,7 +1602,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1613
1602
|
const ref = { segment: rec.segment, namespace: rec.namespace };
|
|
1614
1603
|
if (this.crbmSource !== void 0) {
|
|
1615
1604
|
const held = await this.crbmSource.currentVersion(ref);
|
|
1616
|
-
const listed = rec.currentGen === null ? null : `${rec.currentGen}:${
|
|
1605
|
+
const listed = rec.currentGen === null ? null : `${rec.currentGen}:${pointerIdOf(rec)}`;
|
|
1617
1606
|
if (rowVersionOfLive(held) !== listed) this.engine.invalidate(ref);
|
|
1618
1607
|
}
|
|
1619
1608
|
return await this.engine.has(ref, id) ? { segment: rec.segment, namespace: rec.namespace } : null;
|
|
@@ -1633,10 +1622,14 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1633
1622
|
* can still hold it, and so can one above the pointer after a {@link CloudRoaring.rollback}, which the rollback
|
|
1634
1623
|
* could make current again. Each holder is deleted — below the pointer by a `keep: 0` collection, above it one
|
|
1635
1624
|
* by one, re-proved against the row first — and the generations above the pointer that never held the id stay
|
|
1636
|
-
* as rollback targets
|
|
1637
|
-
*
|
|
1638
|
-
*
|
|
1639
|
-
*
|
|
1625
|
+
* as rollback targets, but for any object no read of the segment can open, which goes whatever the id. An entry
|
|
1626
|
+
* says `erased: true` only once the call has listed the bucket and read what is left: **no generation of the
|
|
1627
|
+
* segment holds the id**. The returned per-segment record is your **erasure ledger** —
|
|
1628
|
+
* persist it / route it to your audit sink as the proof of deletion (when you pass `audit`, an audit event is also
|
|
1629
|
+
* emitted per segment erased: `segment.rewrite` for a rewrite, `segment.collect` where only other generations held
|
|
1630
|
+
* the id and nothing was rewritten). A segment where the erasure deleted only objects no read of it can open (a first
|
|
1631
|
+
* load's sealed under a key no row holds) found the id in none, so the ledger does not list it; the audit sink gets
|
|
1632
|
+
* `segment.collect` for it with no `fromGeneration`, which is the only record of that deletion.
|
|
1640
1633
|
*
|
|
1641
1634
|
* Uses the backend's **own** two halves, so the membership check and the rewrite provably run
|
|
1642
1635
|
* over the same generation. Requires the store built with a **backend** (throws
|
|
@@ -1658,13 +1651,21 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1658
1651
|
* an error, read off the row: a moved pointer is a `'superseded'` entry, which a re-run settles, and a segment
|
|
1659
1652
|
* that a concurrent `dropSegment` tombstoned or a retention sweep purged is left out of the ledger. A tombstoned
|
|
1660
1653
|
* segment is still searched: a cleartext object left under it that holds the id is deleted, with everything under
|
|
1661
|
-
* the tombstone, and the entry reads `erased: true`.
|
|
1654
|
+
* the tombstone, and the entry reads `erased: true`. So is a segment whose row names no generation yet (a retention
|
|
1655
|
+
* policy set before its first load): a first load's object that holds the id is deleted, once the row's `pointerId`
|
|
1656
|
+
* is renewed so that the load, if still running, is refused at its publish, and the entry reads `erased: true`. An
|
|
1657
|
+
* encrypted first load's object there cannot be searched, so it is deleted whatever the id: on an encrypted store,
|
|
1658
|
+
* an erasure of any id refuses an encrypted first load in flight that has written its object onto a segment made
|
|
1659
|
+
* ahead of its data. Each such delete, and each of a holder above the pointer, names the object the erasure read:
|
|
1660
|
+
* on a storage driver that reports `conditionalDelete`, another erasure of the id that deletes it first, followed by
|
|
1661
|
+
* a load that takes the number freed and publishes, does not lose that load's generation to this erasure's delete,
|
|
1662
|
+
* which is refused, and the entry says what the row says; on one that does not, it can.
|
|
1662
1663
|
*
|
|
1663
1664
|
* **Read `note` on any `erased: false` entry — the two reasons mean different things.** `'superseded'` means
|
|
1664
|
-
* another writer (a load, another erasure, or a rollback) moved the pointer
|
|
1665
|
-
* erase the id. Re-run: it erases the id if it is still there, and lists
|
|
1666
|
-
* erasure of the same id already removed it. Do not read `'superseded'` as
|
|
1667
|
-
* read it as "not done by this call, and the re-run settles it".
|
|
1665
|
+
* another writer (a load, another erasure, or a rollback) moved the pointer, or replaced an object the call meant to
|
|
1666
|
+
* delete, mid-call, so **this call** did not erase the id. Re-run: it erases the id if it is still there, and lists
|
|
1667
|
+
* nothing for the segment if a racing erasure of the same id already removed it. Do not read `'superseded'` as
|
|
1668
|
+
* "the id is still present" — read it as "not done by this call, and the re-run settles it".
|
|
1668
1669
|
* `` `error: …` `` is a per-segment fault (caught so one segment can't discard the whole ledger) and it can land
|
|
1669
1670
|
* on either side of the publish: if the rewrite had not published, the id is still there and a re-run erases
|
|
1670
1671
|
* it; if the publish succeeded and only the **collection** of the old generation failed, the id is already
|
|
@@ -1859,8 +1860,9 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1859
1860
|
* and are then *above* `currentGen`, where collection never looks. They remain until loads pass them (the first
|
|
1860
1861
|
* load whose number one of them holds numbers above them all, and collection then keeps the newest `keep` of what is
|
|
1861
1862
|
* below its pointer), {@link CloudRoaring.dropSegment}
|
|
1862
|
-
* deletes them, or {@link CloudRoaring.eraseSubject} does: all of those present when it rewrites, and
|
|
1863
|
-
*
|
|
1863
|
+
* deletes them, or {@link CloudRoaring.eraseSubject} does: all of those present when it rewrites, and, when the
|
|
1864
|
+
* current generation does not hold the id, only those holding it and any no read of the segment can open, whatever
|
|
1865
|
+
* the id. An operator who has just undone a bad load should not have the
|
|
1864
1866
|
* evidence collected out from under them, while a rollback target that still holds erased data would make the
|
|
1865
1867
|
* erasure undoable.
|
|
1866
1868
|
*
|
|
@@ -1929,10 +1931,12 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1929
1931
|
* published, a retention change, a rollback or an erasure wrote the row, or the row was deleted.
|
|
1930
1932
|
*
|
|
1931
1933
|
* A refused load deletes the object it wrote before returning — it sits above `currentGen`, where generation
|
|
1932
|
-
* collection deliberately never looks — but only while the segment's registry row is unchanged or
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
1934
|
+
* collection deliberately never looks — but only while the segment's registry row is unchanged (or differs only
|
|
1935
|
+
* in its leases), `destroyed` or gone, or has key material when the load wrote cleartext, which has no place in an
|
|
1936
|
+
* encrypted segment's bucket. Under a row that is gone or keyed, a read of the object's footer must prove it the
|
|
1937
|
+
* load's own first. Once another write has otherwise changed the row, the generation number it holds may name
|
|
1938
|
+
* another incarnation's live object, so it leaves the orphan rather than risk deleting live data. No row's list
|
|
1939
|
+
* names it, so it takes no place in a window, and a listing deletes it once a later generation is current above it.
|
|
1936
1940
|
*
|
|
1937
1941
|
* **Collection is by name at any `keep` up to 64.** The segment's row records the generations a load keeps, so a load
|
|
1938
1942
|
* that found nothing above the pointer deletes the generations its publish pushed out of the window and lists
|
|
@@ -2145,8 +2149,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
2145
2149
|
* Pass `purgeTombstones: false` to keep every tombstone, that row included. On a backend whose registry reports
|
|
2146
2150
|
* `conditionalDelete` (AWS S3, Azure Blob, the local filesystem and memory, by default; GCS when you set `conditionalDelete: true`)
|
|
2147
2151
|
* the purge removes the row from the bucket for good, by a delete the store applies only to the version it judged,
|
|
2148
|
-
* so a full sweep reads what is live or inside its grace rather than every name a namespace ever held; elsewhere
|
|
2149
|
-
*
|
|
2152
|
+
* so a full sweep reads what is live or inside its grace rather than every name a namespace ever held; elsewhere it
|
|
2153
|
+
* leaves a tombstone. Each retirement files a pointer in the due index
|
|
2150
2154
|
* under the day its tombstone's grace ends, so `scan: 'index'` purges as well as retires.
|
|
2151
2155
|
*
|
|
2152
2156
|
* Needs the store built with a **backend** (throws {@link UnsupportedError} otherwise),
|
|
@@ -2370,7 +2374,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
2370
2374
|
* A segment whose pointer names a missing object is the torn restore this reports; a cold `count()` of it still
|
|
2371
2375
|
* answers the number its row records, while a read of the object throws, so the count alone never shows it. With
|
|
2372
2376
|
* `summaries: true` it also opens each segment's current object (one tail read each) and reports a segment whose
|
|
2373
|
-
* row's summary says another id count or metadata than the object holds
|
|
2377
|
+
* row's summary names another object by its fingerprint, or says another id count or metadata than the object holds,
|
|
2378
|
+
* as `summary-mismatch`, which is what a
|
|
2374
2379
|
* restore of the registry from another point than the bucket leaves a count to answer. A sealed summary is held
|
|
2375
2380
|
* against its object when this store has the key, and counted in `summariesUnchecked` when it does not.
|
|
2376
2381
|
*
|
|
@@ -2406,15 +2411,6 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
2406
2411
|
const registry = this.requireRegistry("exportSegments");
|
|
2407
2412
|
return runExport(this, registry, sink, { ...opts, codec: opts.codec ?? roaringCodec });
|
|
2408
2413
|
}
|
|
2409
|
-
/**
|
|
2410
|
-
* Planning cost estimate — pure, no instance/data needed: sizing, sales, what-if. For a real, grounded report
|
|
2411
|
-
* from live segment sizes, use `store.segment(name).costReport()`. See {@link CostReport}.
|
|
2412
|
-
* @deprecated Moves to `@cloudbitmaps/tools`, a package of its own, with a `size` on `stat()` so a grounded report
|
|
2413
|
-
* needs nothing internal. It is a planning tool, and its price list is as old as the release that ships it.
|
|
2414
|
-
*/
|
|
2415
|
-
static estimateCost(input) {
|
|
2416
|
-
return estimateCost(input);
|
|
2417
|
-
}
|
|
2418
2414
|
};
|
|
2419
2415
|
var withBatches = (gen, batches) => Object.assign(gen, { batches });
|
|
2420
2416
|
var failing = (err) => {
|
|
@@ -2811,8 +2807,8 @@ var Segment = class _Segment {
|
|
|
2811
2807
|
* Cardinality of the generation this handle reads: **one registry read when cold, none when warm, and no read of the
|
|
2812
2808
|
* object**, with **zero payload reads**. The registry row records the id count of the generation it names, written
|
|
2813
2809
|
* by the write that made it current, and a count answers from that. A row with no summary it can use (one written
|
|
2814
|
-
*
|
|
2815
|
-
* `.crbm` index, summed with a tail read of the object. A segment whose pointer names a missing object (a torn
|
|
2810
|
+
* without it, as a registry of your own may, one that names another generation, a sealed one that does not open)
|
|
2811
|
+
* sends the count to the `.crbm` index, summed with a tail read of the object. A segment whose pointer names a missing object (a torn
|
|
2816
2812
|
* restore) still counts the row's number, while a read of the object throws; `checkConsistency` is what finds it.
|
|
2817
2813
|
*
|
|
2818
2814
|
* What this trusts: the row's summary, or the index's sum, and no payload is decoded to confirm it. The summary is
|
|
@@ -2831,17 +2827,30 @@ var Segment = class _Segment {
|
|
|
2831
2827
|
return this.timed("count", () => this.engine.count(this.ref));
|
|
2832
2828
|
}
|
|
2833
2829
|
/**
|
|
2834
|
-
* What the generation this handle reads is, from one resolution: its number, its id count
|
|
2835
|
-
*
|
|
2836
|
-
*
|
|
2837
|
-
*
|
|
2838
|
-
*
|
|
2839
|
-
*
|
|
2840
|
-
*
|
|
2841
|
-
*
|
|
2830
|
+
* What the generation this handle reads is, from one resolution: its number, its id count, the metadata it was
|
|
2831
|
+
* loaded with (absent when it has none), and `sizeBytes`, its object's bytes in storage. All four come from the
|
|
2832
|
+
* registry row when it has a summary this store can use, as `count()`'s do: the summary records the object's
|
|
2833
|
+
* fingerprint, its size with its checksum. That is **one registry read when cold, none when warm, and no read of the
|
|
2834
|
+
* object**. With no summary it can use, they come from the generation's object, opened once: its footer and index,
|
|
2835
|
+
* with no payload read, which is a tail read of the object more when cold (and a range read for an index longer than
|
|
2836
|
+
* the tail) and keeps the generation in the reader cache as a read does. On a pinned handle it answers for the
|
|
2837
|
+
* generation it pinned, from the pinned object. A segment with no generation answers
|
|
2838
|
+
* `{ generation: null, cardinality: 0, sizeBytes: null }`, and a store whose source cannot report a size answers
|
|
2839
|
+
* `sizeBytes: null`. A pointer that names a missing object (a torn restore) still answers from the row's summary, as
|
|
2840
|
+
* `count()` does, while a read of the object throws `NotFoundError`; with no summary to use, `stat()` throws it too.
|
|
2841
|
+
* `checkConsistency` is what finds such a row.
|
|
2842
|
+
*
|
|
2843
|
+
* Trust is as for `count()`: what the row's summary says is believed until the object is next opened. Every open of
|
|
2844
|
+
* the generation holds the object to the fingerprint the summary records, so another object under the number is
|
|
2845
|
+
* refused when it is opened, and holds its count against the summary's; a disagreement in count makes this process
|
|
2846
|
+
* stop using that summary, so `stat()` then answers from the object, whose index is checked for internal consistency
|
|
2847
|
+
* and whose count is the index's sum. A reader already open is not checked again until the row changes, it is
|
|
2848
|
+
* evicted or it is invalidated.
|
|
2849
|
+
*
|
|
2850
|
+
* `sizeBytes` is what `groundedReport` in `@cloudbitmaps/tools` prices storage from.
|
|
2842
2851
|
*
|
|
2843
2852
|
* ```ts
|
|
2844
|
-
* const { generation, cardinality, metadata } = await store.segment('active-30d').stat();
|
|
2853
|
+
* const { generation, cardinality, metadata, sizeBytes } = await store.segment('active-30d').stat();
|
|
2845
2854
|
* ```
|
|
2846
2855
|
*/
|
|
2847
2856
|
async stat() {
|
|
@@ -3131,35 +3140,6 @@ var Segment = class _Segment {
|
|
|
3131
3140
|
)
|
|
3132
3141
|
);
|
|
3133
3142
|
}
|
|
3134
|
-
/**
|
|
3135
|
-
* Grounded cost report for this segment: storage cost from its **real** `.crbm` size (exact, no payload
|
|
3136
|
-
* reads); request cost from the supplied `workload` rates. A segment with no Storage generation reports zero
|
|
3137
|
-
* storage. The pointer refresh is priced at the store's own `cache.genTtlMs`, or at none when the store never
|
|
3138
|
-
* refreshes, unless the workload sets `genTtlMs`. See {@link CostReport} — it always includes a verdict (incl.
|
|
3139
|
-
* the lose-zone).
|
|
3140
|
-
* @deprecated Moves to `@cloudbitmaps/tools`, a package of its own, with a `size` on `stat()` so a grounded report
|
|
3141
|
-
* needs nothing internal. It is a planning tool, and its price list is as old as the release that ships it.
|
|
3142
|
-
*/
|
|
3143
|
-
async costReport(options) {
|
|
3144
|
-
checkOptions(options, "costReport", CALL_KEYS.costReport);
|
|
3145
|
-
const shape = options?.workload;
|
|
3146
|
-
if (shape !== void 0 && shape !== null && (typeof shape !== "object" || Array.isArray(shape))) {
|
|
3147
|
-
throw new ValidationError2("costReport: workload must be an object");
|
|
3148
|
-
}
|
|
3149
|
-
this.assertLeases([this]);
|
|
3150
|
-
const canMeasure = this.engine.supportsStorageSize;
|
|
3151
|
-
const size = canMeasure ? await this.engine.segmentSize(this.ref) : null;
|
|
3152
|
-
const refreshMs = this.engine.pointerRefreshMs;
|
|
3153
|
-
const given = options?.workload === void 0 || options.workload === null ? void 0 : picked(options.workload, CALL_KEYS.workload);
|
|
3154
|
-
const workload = refreshMs === void 0 || given?.genTtlMs !== void 0 ? given : { ...given, genTtlMs: refreshMs };
|
|
3155
|
-
return groundedReport({
|
|
3156
|
-
storageBytes: size?.sizeBytes ?? 0,
|
|
3157
|
-
grounded: canMeasure,
|
|
3158
|
-
workload,
|
|
3159
|
-
pricing: options?.pricing,
|
|
3160
|
-
extraNotes: canMeasure ? void 0 : ["storage source has no sizeOf() \u2014 storage not measured, reported as $0."]
|
|
3161
|
-
});
|
|
3162
|
-
}
|
|
3163
3143
|
};
|
|
3164
3144
|
|
|
3165
3145
|
// src/bin/export-segments.ts
|
|
@@ -3282,7 +3262,8 @@ async function main(env = process.env, now = () => Date.now()) {
|
|
|
3282
3262
|
format: manifest.format,
|
|
3283
3263
|
totalSegments: manifest.totalSegments,
|
|
3284
3264
|
totalIds: manifest.totalIds,
|
|
3285
|
-
failed: manifest.failed.length
|
|
3265
|
+
failed: manifest.failed.length,
|
|
3266
|
+
skipped: manifest.skipped.length
|
|
3286
3267
|
});
|
|
3287
3268
|
return manifest;
|
|
3288
3269
|
}
|