@cloudbitmaps/core 0.9.0 → 0.11.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/NOTICE +4 -4
- package/README.md +33 -16
- package/dist/chunk-5OMC5U7U.js +508 -0
- package/dist/chunk-5OMC5U7U.js.map +7 -0
- package/dist/core/audit.d.ts +128 -0
- package/dist/core/bit-route.d.ts +21 -0
- package/dist/core/blob.d.ts +34 -0
- package/dist/core/budget.d.ts +68 -0
- package/dist/core/codec.d.ts +122 -0
- package/dist/core/concurrency.d.ts +15 -0
- package/dist/core/consistency.d.ts +73 -0
- package/dist/core/cooperative.d.ts +78 -0
- package/dist/core/cost.d.ts +254 -0
- package/dist/core/crbm/crc32c.d.ts +10 -0
- package/dist/core/crbm/format.d.ts +114 -0
- package/dist/core/crbm/reader.d.ts +86 -0
- package/dist/core/crbm/varint.d.ts +13 -0
- package/dist/core/crbm/writer.d.ts +39 -0
- package/dist/core/crbm-storage-source.d.ts +522 -0
- package/dist/core/crypto.d.ts +80 -0
- package/dist/core/determinism.d.ts +42 -0
- package/dist/core/discover.d.ts +77 -0
- package/dist/core/due-index.d.ts +53 -0
- package/dist/core/engine.d.ts +246 -0
- package/dist/core/erase-id.d.ts +176 -0
- package/dist/core/erasure.d.ts +185 -0
- package/dist/core/errors.d.ts +113 -0
- package/dist/core/generation-gc.d.ts +56 -0
- package/dist/core/keys.d.ts +23 -0
- package/dist/core/load.d.ts +121 -0
- package/dist/core/lru.d.ts +69 -0
- package/dist/core/metrics.d.ts +136 -0
- package/dist/core/name-codec.d.ts +69 -0
- package/dist/core/pinned-storage-source.d.ts +90 -0
- package/dist/core/ports.d.ts +423 -0
- package/dist/core/reader-defaults.d.ts +18 -0
- package/dist/core/registry-scan.d.ts +47 -0
- package/dist/core/retention-sweep.d.ts +198 -0
- package/dist/core/retention.d.ts +67 -0
- package/dist/core/retry.d.ts +69 -0
- package/dist/core/rollback.d.ts +75 -0
- package/dist/core/validate.d.ts +44 -0
- package/dist/driver-kit.d.ts +9 -0
- package/dist/driver-kit.js +275 -0
- package/dist/driver-kit.js.map +7 -0
- package/dist/drivers/_shared/keys.d.ts +26 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
- package/dist/drivers/_shared/object-registry.d.ts +89 -0
- package/dist/drivers/_shared/registry.d.ts +66 -0
- package/dist/drivers/backends.d.ts +43 -0
- package/dist/drivers/crypto.d.ts +38 -0
- package/dist/drivers/localfs/fs-util.d.ts +21 -0
- package/dist/drivers/localfs/paths.d.ts +33 -0
- package/dist/drivers/localfs/registry.d.ts +28 -0
- package/dist/drivers/localfs/storage.d.ts +19 -0
- package/dist/drivers/memory.d.ts +49 -0
- package/dist/drivers/retry/retrying-chunk-source.d.ts +59 -0
- package/dist/export/index.d.ts +93 -0
- package/dist/index.d.ts +76 -2760
- package/dist/index.js +3527 -3133
- package/dist/index.js.map +7 -1
- package/dist/testing/fuzz-core.d.ts +14 -0
- package/package.json +19 -115
- package/dist/azure/index.cjs +0 -368
- package/dist/azure/index.cjs.map +0 -1
- package/dist/azure/index.d.cts +0 -48
- package/dist/azure/index.d.ts +0 -48
- package/dist/azure/index.js +0 -304
- package/dist/azure/index.js.map +0 -1
- package/dist/cassandra/index.cjs +0 -265
- package/dist/cassandra/index.cjs.map +0 -1
- package/dist/cassandra/index.d.cts +0 -52
- package/dist/cassandra/index.d.ts +0 -52
- package/dist/cassandra/index.js +0 -204
- package/dist/cassandra/index.js.map +0 -1
- package/dist/chunk-7LMLYSVJ.js +0 -43
- package/dist/chunk-7LMLYSVJ.js.map +0 -1
- package/dist/chunk-BD22WDAN.js +0 -230
- package/dist/chunk-BD22WDAN.js.map +0 -1
- package/dist/chunk-NUIDEEFZ.js +0 -91
- package/dist/chunk-NUIDEEFZ.js.map +0 -1
- package/dist/chunk-OZ6LAFV4.js +0 -6
- package/dist/chunk-OZ6LAFV4.js.map +0 -1
- package/dist/chunk-SNJVZ227.js +0 -35
- package/dist/chunk-SNJVZ227.js.map +0 -1
- package/dist/dynamodb/index.cjs +0 -767
- package/dist/dynamodb/index.cjs.map +0 -1
- package/dist/dynamodb/index.d.cts +0 -106
- package/dist/dynamodb/index.d.ts +0 -106
- package/dist/dynamodb/index.js +0 -469
- package/dist/dynamodb/index.js.map +0 -1
- package/dist/gcs/index.cjs +0 -343
- package/dist/gcs/index.cjs.map +0 -1
- package/dist/gcs/index.d.cts +0 -46
- package/dist/gcs/index.d.ts +0 -46
- package/dist/gcs/index.js +0 -279
- package/dist/gcs/index.js.map +0 -1
- package/dist/index.cjs +0 -4990
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2761
- package/dist/mongodb/index.cjs +0 -260
- package/dist/mongodb/index.cjs.map +0 -1
- package/dist/mongodb/index.d.cts +0 -45
- package/dist/mongodb/index.d.ts +0 -45
- package/dist/mongodb/index.js +0 -199
- package/dist/mongodb/index.js.map +0 -1
- package/dist/mysql/index.cjs +0 -281
- package/dist/mysql/index.cjs.map +0 -1
- package/dist/mysql/index.d.cts +0 -56
- package/dist/mysql/index.d.ts +0 -56
- package/dist/mysql/index.js +0 -211
- package/dist/mysql/index.js.map +0 -1
- package/dist/ports-CvcvJWf6.d.cts +0 -395
- package/dist/ports-CvcvJWf6.d.ts +0 -395
- package/dist/postgres/index.cjs +0 -273
- package/dist/postgres/index.cjs.map +0 -1
- package/dist/postgres/index.d.cts +0 -54
- package/dist/postgres/index.d.ts +0 -54
- package/dist/postgres/index.js +0 -203
- package/dist/postgres/index.js.map +0 -1
- package/dist/redis/index.cjs +0 -257
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -42
- package/dist/redis/index.d.ts +0 -42
- package/dist/redis/index.js +0 -197
- package/dist/redis/index.js.map +0 -1
- package/dist/s3/index.cjs +0 -918
- package/dist/s3/index.cjs.map +0 -1
- package/dist/s3/index.d.cts +0 -112
- package/dist/s3/index.d.ts +0 -112
- package/dist/s3/index.js +0 -555
- package/dist/s3/index.js.map +0 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-safe cross-store disaster-recovery check. The registry (`currentGen`) and the immutable `.crbm`
|
|
3
|
+
* generations can be restored **independently**, so a failover can recover the registry *ahead of* the storage
|
|
4
|
+
* objects — leaving `currentGen` pointing at a generation whose `.crbm` isn't present yet. That is likelier
|
|
5
|
+
* than it sounds even with both in one bucket: a restore scoped to a prefix, or replayed
|
|
6
|
+
* per-object from a version history, recovers the two prefixes at different points. That's a torn restore: reads of the affected segment then throw. This scan
|
|
7
|
+
* detects it up front (run it at startup after a restore) instead of discovering it on the first read.
|
|
8
|
+
*
|
|
9
|
+
* Read-only; bounded fan-out. `destroyed` (crypto-shredded) segments are skipped — their Storage is intentionally
|
|
10
|
+
* gone/unreadable, not a torn restore. A segment whose Storage/registry can't be read this pass is recorded in
|
|
11
|
+
* `errored` (never aborts the scan). Each segment is checked against its **authoritative live pointer** — one
|
|
12
|
+
* strong `registry.get` per segment — never the enumeration snapshot from `registry.list`, which a driver may
|
|
13
|
+
* serve eventually-consistently and so lag a recent in-place pointer advance: trusting it would both
|
|
14
|
+
* miss a torn *live* generation and cry torn on a generation the pointer has already advanced past (GC'd during
|
|
15
|
+
* the scan). Residual: a load's publish plus a GC landing in the tiny per-segment get→list gap can still yield a
|
|
16
|
+
* transient false positive — run the scan against a quiesced fleet (the documented restore procedure), or re-run
|
|
17
|
+
* to confirm a reported tear.
|
|
18
|
+
*/
|
|
19
|
+
import type { IStorageDriver, IRegistryDriver } from './ports.js';
|
|
20
|
+
/** Default in-flight fan-out for the consistency scan — bounded, no thundering herd. */
|
|
21
|
+
/**
|
|
22
|
+
* Default ceiling on how many registry records one consistency check may hold resident: 250,000.
|
|
23
|
+
*
|
|
24
|
+
* **Named `maxScanSegments`, not `maxSegments`, on purpose.** A `maxSegments`-style option caps how much work
|
|
25
|
+
* one cycle does and then *continues normally* (the retention sweep's `limit` is that shape). This one caps how
|
|
26
|
+
* much a scan may hold and *refuses* past it. Same-sounding names with opposite behaviour at the limit is a
|
|
27
|
+
* trap; the name says what it is — a ceiling on a scan that fails closed.
|
|
28
|
+
*
|
|
29
|
+
* Generous — fleets of 100K+ segments are the design target — while still bounding a DR drill's memory to
|
|
30
|
+
* something a modest operator box survives. Raisable, because a ceiling you cannot lift is a landmine.
|
|
31
|
+
*/
|
|
32
|
+
export { DEFAULT_MAX_SCAN_SEGMENTS } from './registry-scan.js';
|
|
33
|
+
export interface ConsistencyIssue {
|
|
34
|
+
readonly segment: string;
|
|
35
|
+
readonly namespace?: string;
|
|
36
|
+
/** The registry's `currentGen` for the segment — the generation whose `.crbm` is missing from Storage. */
|
|
37
|
+
readonly currentGen: number;
|
|
38
|
+
/** The only issue class today: `currentGen` references a Storage generation that is not present (torn restore). */
|
|
39
|
+
readonly issue: 'missing-storage-generation';
|
|
40
|
+
}
|
|
41
|
+
/** A segment that could not be checked this pass (Storage/registry read fault) — not proof of a torn restore. */
|
|
42
|
+
export interface ConsistencyErrorEntry {
|
|
43
|
+
readonly segment: string;
|
|
44
|
+
readonly namespace?: string;
|
|
45
|
+
/** The read error's message (never the raw error, so the report stays serializable/loggable). */
|
|
46
|
+
readonly error: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ConsistencyReport {
|
|
49
|
+
/** Registered segments scanned (a `destroyed` segment counts as scanned but is never an issue). */
|
|
50
|
+
readonly checked: number;
|
|
51
|
+
/** Segments whose `currentGen` `.crbm` is absent — recover the object store (or restore to a coherent point). */
|
|
52
|
+
readonly inconsistent: ConsistencyIssue[];
|
|
53
|
+
/**
|
|
54
|
+
* Segments that couldn't be read this pass (a transient/partial object store during a restore is exactly when
|
|
55
|
+
* this runs) — **triage these too**: an unread segment is neither proven-coherent nor proven-torn. Empty on a
|
|
56
|
+
* clean pass.
|
|
57
|
+
*/
|
|
58
|
+
readonly errored: ConsistencyErrorEntry[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Verify every registered segment's `currentGen` `.crbm` actually exists in Storage. Enumerates the registry
|
|
62
|
+
* (optionally one namespace) and, for each non-`destroyed` segment, checks the object store lists that
|
|
63
|
+
* generation. Returns the torn segments in `inconsistent` (empty ⇒ coherent) and any unreadable segments in
|
|
64
|
+
* `errored`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runConsistencyCheck(deps: {
|
|
67
|
+
readonly storage: IStorageDriver;
|
|
68
|
+
readonly registry: IRegistryDriver;
|
|
69
|
+
}, options?: {
|
|
70
|
+
namespace?: string;
|
|
71
|
+
concurrency?: number;
|
|
72
|
+
maxScanSegments?: number;
|
|
73
|
+
}): Promise<ConsistencyReport>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cooperative yielding for the long CPU-bound loops.
|
|
3
|
+
*
|
|
4
|
+
* Everything this library does on the hot read path is small. Bulk-load is the exception: it is the one place
|
|
5
|
+
* where a single call legitimately occupies the CPU for hundreds of milliseconds, and Node has exactly one thread
|
|
6
|
+
* to occupy. Measured on a 1M-id load of 65,536 chunks onto in-memory storage, on a loaded Apple M3 Pro: **518 ms
|
|
7
|
+
* wall, and 519 ms during which the event loop did not turn at all** (medians of 20 fresh-process runs). Wire that to a request handler and every other in-flight request on the instance — health
|
|
8
|
+
* checks included — waits out the whole load.
|
|
9
|
+
*
|
|
10
|
+
* The fix is not to make the work asynchronous. That was measured too, and it is a 7x *regression*: handing each
|
|
11
|
+
* chunk's insert to the threadpool costs ~9 µs of dispatch against ~1.5 µs of actual work once ids are spread
|
|
12
|
+
* across ~62,000 chunks (636 ms versus 92 ms). The work is genuinely CPU-bound and genuinely small per chunk;
|
|
13
|
+
* the only thing wrong with it is that it never stops to let anyone else go. So the fix is to keep the work
|
|
14
|
+
* synchronous and periodically hand the loop back.
|
|
15
|
+
*
|
|
16
|
+
* TWO TRAPS, both of which make a plausible implementation measure as no change at all:
|
|
17
|
+
*
|
|
18
|
+
* 1. **`await Promise.resolve()` — and equally `Clock.sleep(0)` — do nothing.** Both resolve on a microtask, and
|
|
19
|
+
* the microtask queue drains to empty before the event loop advances a single phase. A loop awaiting them
|
|
20
|
+
* yields to other *microtasks* and to nothing else; no pending I/O callback runs. Measured: 555 ms of
|
|
21
|
+
* starvation against a 568 ms unyielded baseline **on the synthetic yield-primitive benchmark below** —
|
|
22
|
+
* a different, smaller workload than the 442 ms end-to-end load above, which is why the two baselines
|
|
23
|
+
* differ. The yield must be a **macrotask** — hence
|
|
24
|
+
* `Clock.yieldNow`, backed by `setImmediate` in production wiring.
|
|
25
|
+
* 2. **Yielding per unit of work costs more than the work.** The yield has to be periodic, and `everyN` has to be
|
|
26
|
+
* large enough that its cost disappears into the batch it interrupts.
|
|
27
|
+
*
|
|
28
|
+
* At `everyN = 1024` over that same synthetic workload the trade is close to free: 569 ms wall against a 568 ms
|
|
29
|
+
* baseline, with the worst event-loop gap down from 568 ms to 13.8 ms.
|
|
30
|
+
*
|
|
31
|
+
* **Three experiments, three sets of numbers — do not mix them.** (a) The end-to-end 1M-id bulk load:
|
|
32
|
+
* 518 ms wall / 519 ms blocked unyielded, 526 ms / 24 ms yielded. (b) This synthetic yield-primitive comparison:
|
|
33
|
+
* 568 ms / 568 ms unyielded, 569 ms / 13.8 ms with `setImmediate`. (c) A per-chunk insert microbenchmark:
|
|
34
|
+
* 92 ms sync versus 636 ms via the threadpool. Quoting a baseline from one against a result from another describes
|
|
35
|
+
* work none of them did, such as a 92 ms operation with 450 ms of starvation inside it.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* The only part of a `Clock` that yielding actually needs.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately looser than `Clock` so a write path can opt in with whatever it has: a bulk load or a rewrite
|
|
41
|
+
* yields when its caller supplies a clock that can, and simply runs unbroken when handed nothing (or a bare
|
|
42
|
+
* `{ now }`). Requiring a full `Clock` would make every out-of-process caller wire one just to write a
|
|
43
|
+
* generation.
|
|
44
|
+
*/
|
|
45
|
+
export interface Yielder {
|
|
46
|
+
yieldNow?(): Promise<void>;
|
|
47
|
+
sleep?(ms: number): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* How many units of work pass between yields. Sized so the yield is unmeasurable against the batch it interrupts
|
|
51
|
+
* (~60 yields across a 1M-id load) while still capping any single blocking stretch in the low tens of ms.
|
|
52
|
+
*/
|
|
53
|
+
export declare const YIELD_EVERY = 1024;
|
|
54
|
+
/**
|
|
55
|
+
* Build a periodic yield.
|
|
56
|
+
*
|
|
57
|
+
* Call the returned function **once per unit of work**. It returns `null` on the vast majority of calls — nothing
|
|
58
|
+
* to await, nothing allocated — and a promise only on the `everyN`th, so the caller pays for a yield only when
|
|
59
|
+
* one actually happens:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const tick = yieldEvery(clock);
|
|
63
|
+
* for (const item of millions) {
|
|
64
|
+
* work(item);
|
|
65
|
+
* const pause = tick();
|
|
66
|
+
* if (pause !== null) await pause;
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* The two-line shape is deliberate. `await tick()` would read better and would force a microtask turn on every
|
|
71
|
+
* single iteration — an unconditional cost on the very loop this exists to keep fast.
|
|
72
|
+
*
|
|
73
|
+
* With no `clock` the result always returns `null`: the loop runs unbroken, so adding a yield point to a code
|
|
74
|
+
* path can never change behaviour for a caller who did not opt in. `core/` cannot supply a clock itself — it is
|
|
75
|
+
* timer-free by lint, which is the whole reason scheduling goes through the seam — so `@cloudbitmaps/roaring`
|
|
76
|
+
* pre-binds a real one and flavor users get this with no wiring at all.
|
|
77
|
+
*/
|
|
78
|
+
export declare function yieldEvery(clock: Yielder | undefined, everyN?: number): () => Promise<void> | null;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/** Pluggable rate card. Rates differ by cloud/region/term and drift over time; the formulas don't. */
|
|
2
|
+
export interface PricingProfile {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly storage: {
|
|
5
|
+
/** Object GET (per **million** requests). A ranged GET bills as a full GET. */
|
|
6
|
+
readonly getPerMillion: number;
|
|
7
|
+
/** Object PUT (per million) — what a load pays, per request. */
|
|
8
|
+
readonly putPerMillion: number;
|
|
9
|
+
readonly storagePerGiBMonth: number;
|
|
10
|
+
/**
|
|
11
|
+
* Requests one read costs when it needs the object's size first: a pointer read, and a segment's tail read.
|
|
12
|
+
* Default **1**, S3's, whose suffix-range GET returns the size with the bytes. **2** on GCS and Azure Blob,
|
|
13
|
+
* which read the metadata and then the bytes. A chunk read knows its range, and is one request everywhere.
|
|
14
|
+
*/
|
|
15
|
+
readonly requestsPerSizedRead?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The always-on Redis the verdict compares against: exactly one of the two. `sizedToData`, the default's, prices
|
|
19
|
+
* the cheapest cluster that holds the report's stored bytes; `monthlyUSD` is one cluster you name, whatever the
|
|
20
|
+
* data size. A profile carrying both is refused rather than read one way.
|
|
21
|
+
*/
|
|
22
|
+
readonly redis: {
|
|
23
|
+
readonly monthlyUSD: number;
|
|
24
|
+
readonly sizedToData?: never;
|
|
25
|
+
} | {
|
|
26
|
+
readonly sizedToData: RedisSizing;
|
|
27
|
+
readonly monthlyUSD?: never;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** One node type a Redis cluster can be built from, as its cloud prices it. */
|
|
31
|
+
export interface RedisNodeType {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/** Memory, in GiB, as the cloud publishes it for the node type. */
|
|
34
|
+
readonly memoryGiB: number;
|
|
35
|
+
/**
|
|
36
|
+
* SSD, in GiB, on a data-tiering node (ElastiCache's `r6gd`): keys stay in memory, and the values read least
|
|
37
|
+
* recently move to the SSD. Counted in full beside the memory, as AWS counts a data-tiering node's capacity.
|
|
38
|
+
*/
|
|
39
|
+
readonly ssdGiB?: number;
|
|
40
|
+
/** Price per node-hour, in USD. */
|
|
41
|
+
readonly hourlyUSD: number;
|
|
42
|
+
/**
|
|
43
|
+
* The most shards a cluster of this type is priced at. The default catalogue prices its burstable nodes as one
|
|
44
|
+
* shard: a cluster sharded across them is not how data anyone queries is held. Default: no limit.
|
|
45
|
+
*/
|
|
46
|
+
readonly maxShards?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Redis sized to hold the data: enough shards for the bytes, each a primary and its replicas, on whichever node
|
|
50
|
+
* type in `nodeTypes` makes that cheapest. Prices within a millionth of a dollar of the cheapest are a tie, which
|
|
51
|
+
* fewer nodes win, then the lower price, then the name, so the answer never depends on the order of the rows. The data is held at its stored,
|
|
52
|
+
* compressed size, which is a floor on the memory Redis needs for it: a native Redis bitmap is sized by its highest
|
|
53
|
+
* id, not by how many ids it holds, so sparse ids need more.
|
|
54
|
+
*/
|
|
55
|
+
export interface RedisSizing {
|
|
56
|
+
/** Where the prices come from, and when; the report's notes quote it. */
|
|
57
|
+
readonly source: string;
|
|
58
|
+
readonly nodeTypes: readonly RedisNodeType[];
|
|
59
|
+
/** Replicas per shard. AWS's best practice is 2; Multi-AZ needs at least 1. */
|
|
60
|
+
readonly replicasPerShard: number;
|
|
61
|
+
/** The share of each node's memory kept back from data: ElastiCache's `reserved-memory-percent`, 25% by default. */
|
|
62
|
+
readonly reservedMemoryFraction: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* **ElastiCache for Redis OSS in us-east-1, on-demand**: node prices from AWS's public price list, version
|
|
66
|
+
* 20260914063714 (published 2026-09-14, effective 2026-09-01,
|
|
67
|
+
* https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonElastiCache/20260914063714/us-east-1/index.json), and the
|
|
68
|
+
* memory AWS publishes for each node type. Every shard is a primary and two replicas, AWS's best practice, with
|
|
69
|
+
* ElastiCache's default 25% of each node's memory reserved, and the burstable `t4g` nodes are priced as one shard.
|
|
70
|
+
*
|
|
71
|
+
* It lists only rows that are the cheapest fit for some size of data: the data-tiering `r6gd` rows above about
|
|
72
|
+
* 30 GiB, and the larger in-memory `r6g` rows, `2xlarge` and up, when those are left out. The current `m7g` and `r7g` nodes are not here,
|
|
73
|
+
* because an `m6g` or `r6g` with the same memory costs less. It is the cheapest cluster of THIS kind; Redis bought
|
|
74
|
+
* another way costs less — one replica a shard a third less, ElastiCache for Valkey 20% less a node, reserved nodes
|
|
75
|
+
* less again — so against those the saving the verdict reports is smaller. Pass their prices to compare with them.
|
|
76
|
+
*/
|
|
77
|
+
export declare const ELASTICACHE_REDIS_US_EAST_1_ONDEMAND: RedisSizing;
|
|
78
|
+
/**
|
|
79
|
+
* One ElastiCache HA cluster, a primary and two replicas of cache.m7g.large — the m7g family's smallest node —
|
|
80
|
+
* at $0.158 an hour each: 3 × 730 h × $0.158 = $346.02, published to the dollar. The per-request crossovers on the
|
|
81
|
+
* benchmarks page are drawn against this one cluster, whatever the data size; pass it as `pricing.redis` to compare
|
|
82
|
+
* with it. It is not the cheapest cluster for any size of data: three cache.m6g.large hold the same memory for less.
|
|
83
|
+
*/
|
|
84
|
+
export declare const ONE_REDIS_HA_CLUSTER: Readonly<{
|
|
85
|
+
monthlyUSD: number;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Default profile — **AWS us-east-1, on-demand**, mid-2026, from the fact-checked published pricing rather than
|
|
89
|
+
* copied from a blog post, with Redis sized to the data. Override it for your region, cloud, or committed term.
|
|
90
|
+
*/
|
|
91
|
+
export declare const AWS_US_EAST_1_ONDEMAND: PricingProfile;
|
|
92
|
+
/** Sustained access pattern. All rates default to 0; unspecified ⇒ that op contributes nothing. */
|
|
93
|
+
export interface Workload {
|
|
94
|
+
/**
|
|
95
|
+
* Point reads (`has`) per second. Each cache miss is one object GET, once the segment's pointer and index are
|
|
96
|
+
* read.
|
|
97
|
+
*/
|
|
98
|
+
readonly readsPerSec?: number;
|
|
99
|
+
readonly intersectsPerSec?: number;
|
|
100
|
+
/** Cache hit rate in `[0, 1]` — hits are free; only misses cost. Default 0. */
|
|
101
|
+
readonly cacheHitRate?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Storage chunks one intersection fetches, summed over its operands: the chunk-skipping survivors. Default 1.
|
|
104
|
+
* Two segments sharing `k` chunks fetch `2k`. The model adds each operand's pointer and index reads itself (see
|
|
105
|
+
* {@link Workload.operandsPerIntersect}), so count chunks only. One more GET per operand whose index outgrows
|
|
106
|
+
* the reader's tail read (256 KiB by default), which then reads the index whole, belongs here too, and so does a
|
|
107
|
+
* pointer re-read by an intersect slow enough to outlive {@link Workload.genTtlMs}.
|
|
108
|
+
*/
|
|
109
|
+
readonly chunksPerIntersect?: number;
|
|
110
|
+
/**
|
|
111
|
+
* Segments each intersection reads, `exclude` operands included; at least 1. Default 2. An intersection is priced
|
|
112
|
+
* **cold**: before its chunks, each operand's pointer is read, then its index, in one read of the object's tail —
|
|
113
|
+
* 2 GETs an operand, so a cold intersect of two segments sharing `k` chunks is `4 + 2k` GETs. `cacheHitRate`
|
|
114
|
+
* does not apply to intersections, so a long-lived reader that answers a repeat from its cache pays less. Other
|
|
115
|
+
* combines read their operands the same way and can be priced here too, with the chunks they fetch.
|
|
116
|
+
*/
|
|
117
|
+
readonly operandsPerIntersect?: number;
|
|
118
|
+
/**
|
|
119
|
+
* Generations published per month across the modeled data — the write side of a loaded store. Default **0**
|
|
120
|
+
* ⇒ loads are not modeled and the report *discloses* the omission rather than silently under-reporting.
|
|
121
|
+
*/
|
|
122
|
+
readonly loadsPerMonth?: number;
|
|
123
|
+
/**
|
|
124
|
+
* PUT-class requests one load's object write issues, each priced at the PUT rate. Default **1** (a single-object
|
|
125
|
+
* PUT). A multipart write of `P` parts bills `P + 2` (initiate, the parts, complete) — set it when you know your
|
|
126
|
+
* object sizes. The model adds what `store.load()` does around the write: two listings and the pointer's write,
|
|
127
|
+
* PUT-class on S3, and nine GETs, the pointer read eight times and the current generation's index once. That is
|
|
128
|
+
* a segment with two generations behind it; its first load makes two fewer GETs, and its second one fewer. On
|
|
129
|
+
* S3 at the default prices a single-part `store.load()` is then about $23.60 per million. A segment whose index
|
|
130
|
+
* outgrows the tail read makes one more GET, and a publish that loses a race to another writer reads the
|
|
131
|
+
* pointer again.
|
|
132
|
+
*/
|
|
133
|
+
readonly requestsPerLoad?: number;
|
|
134
|
+
/**
|
|
135
|
+
* Segments each long-lived reader process keeps reading. A reader re-reads a segment's pointer when it reads the
|
|
136
|
+
* segment after {@link Workload.genTtlMs} has passed, so each costs at most one GET per `genTtlMs` in each process
|
|
137
|
+
* that reads it — 1,314,000 GETs a month at the default 2 s — and the whole term at most one GET per point read
|
|
138
|
+
* (`readsPerSec`, across the fleet). Intersections are priced cold and pay their own pointer reads. Default **0**
|
|
139
|
+
* ⇒ the refresh is not modeled, and the report *discloses* the omission when there are reads to refresh for.
|
|
140
|
+
*
|
|
141
|
+
* It assumes each hot segment stays open in the reader's cache (`cache.readerMax`, 1,024 segments by default,
|
|
142
|
+
* and `cache.readerMaxBytes`, 64 MiB of parsed index). A read of a segment the cache evicted opens it again, a
|
|
143
|
+
* pointer read and a tail read, which is not priced here, and neither is the re-open every reader makes after
|
|
144
|
+
* each load, for the new generation's index. Size those caches to keep the hot set open; the report says so when
|
|
145
|
+
* `hotSegments` is more than a store keeps open by default.
|
|
146
|
+
*/
|
|
147
|
+
readonly hotSegments?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Long-lived reader processes, each keeping its own {@link Workload.hotSegments} open and refreshing their
|
|
150
|
+
* pointers on its own: ten processes reading the same hundred segments pay ten times one process's refresh.
|
|
151
|
+
* At least 1. Default 1.
|
|
152
|
+
*/
|
|
153
|
+
readonly readerProcesses?: number;
|
|
154
|
+
/**
|
|
155
|
+
* How long the reader trusts a pointer, in ms: the store's `cache.genTtlMs`. Default 2000, the store's own
|
|
156
|
+
* default; `segment.costReport()` uses the store's. `0` turns the timed refresh off, and the model bills none: a
|
|
157
|
+
* reader then re-reads a pointer only when its cache evicts the segment, a read finds its generation swept, or a
|
|
158
|
+
* write invalidates it, none of which this term prices.
|
|
159
|
+
*/
|
|
160
|
+
readonly genTtlMs?: number;
|
|
161
|
+
}
|
|
162
|
+
/** One (group of) segment(s) for planning. `count` = how many like this (default 1). */
|
|
163
|
+
export interface SegmentSizing {
|
|
164
|
+
readonly sizeBytes?: number;
|
|
165
|
+
readonly cardinality?: number;
|
|
166
|
+
/** Number of segments with these characteristics. Default 1. */
|
|
167
|
+
readonly count?: number;
|
|
168
|
+
}
|
|
169
|
+
export interface EstimateInput {
|
|
170
|
+
readonly segments: readonly SegmentSizing[];
|
|
171
|
+
readonly workload?: Workload;
|
|
172
|
+
readonly pricing?: PricingProfile;
|
|
173
|
+
}
|
|
174
|
+
export interface CostReport {
|
|
175
|
+
readonly monthlyUSD: {
|
|
176
|
+
readonly byOp: {
|
|
177
|
+
readonly reads: number;
|
|
178
|
+
readonly intersects: number;
|
|
179
|
+
readonly storage: number;
|
|
180
|
+
/**
|
|
181
|
+
* Loads: each its object's `requestsPerLoad` PUT-class requests plus what `store.load()` adds around them.
|
|
182
|
+
* 0 unless `loadsPerMonth` is set.
|
|
183
|
+
*/
|
|
184
|
+
readonly loads: number;
|
|
185
|
+
/** The pointer refresh of {@link Workload.hotSegments}. 0 unless `hotSegments` is set. */
|
|
186
|
+
readonly pointerRefresh: number;
|
|
187
|
+
};
|
|
188
|
+
readonly total: number;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* The Redis the verdict compares against. Sized, the default, it is the cheapest cluster in
|
|
192
|
+
* `pricing.redis.sizedToData` that holds this report's stored bytes; fixed, it is `pricing.redis.monthlyUSD` as
|
|
193
|
+
* given.
|
|
194
|
+
*
|
|
195
|
+
* **It is not additive across reports.** Each report sizes Redis to its own bytes, so a per-segment report
|
|
196
|
+
* (`segment.costReport()`) compares with a cluster holding that one segment alone, and the baselines of a store's
|
|
197
|
+
* segments do not sum to the store's. To judge a store, price all its segments in one `estimateCost`. To alarm on
|
|
198
|
+
* one, sum `monthlyUSD.total` over its segments and compare the sum with the Redis you would run for it: a
|
|
199
|
+
* per-segment verdict against that whole price fires only when one segment alone costs more than all of it.
|
|
200
|
+
*/
|
|
201
|
+
readonly redisBaseline: {
|
|
202
|
+
readonly basis: 'fixed';
|
|
203
|
+
readonly monthlyUSD: number;
|
|
204
|
+
} | {
|
|
205
|
+
readonly basis: 'sized-to-data';
|
|
206
|
+
readonly monthlyUSD: number;
|
|
207
|
+
/** The node type, the shards, the nodes (every shard's primary and replicas), and whether they tier to SSD. */
|
|
208
|
+
readonly cluster: {
|
|
209
|
+
readonly nodeType: string;
|
|
210
|
+
readonly shards: number;
|
|
211
|
+
readonly nodes: number;
|
|
212
|
+
readonly dataTiering: boolean;
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Sustained read rate at which the pay-per-use model's cost passes {@link CostReport.redisBaseline}, **evaluated
|
|
217
|
+
* at this report's `cacheHitRate`** — so a higher cache-hit rate raises it (cache hits are free) — with the other
|
|
218
|
+
* request axes at 0, and with what keeping the data readable costs taken out of the baseline first: storage, and
|
|
219
|
+
* this report's pointer refresh. Loads, the write side, are left out of it. `Infinity` means it never crosses (a
|
|
220
|
+
* 100% cache-hit rate). The published anchor (~329 reads/s) is against {@link ONE_REDIS_HA_CLUSTER}, at
|
|
221
|
+
* `cacheHitRate: 0` with no refresh modeled.
|
|
222
|
+
*/
|
|
223
|
+
readonly redisCrossover: {
|
|
224
|
+
readonly readsPerSec: number;
|
|
225
|
+
};
|
|
226
|
+
readonly verdict: 'win-big' | 'win' | 'lose-zone';
|
|
227
|
+
readonly rationale: string;
|
|
228
|
+
readonly assumptions: {
|
|
229
|
+
readonly cacheHitRate: number;
|
|
230
|
+
readonly pricingName: string;
|
|
231
|
+
/** True when segment **sizes** were real (grounded `costReport`), false for a pure `estimateCost`. */
|
|
232
|
+
readonly grounded: boolean;
|
|
233
|
+
/** What the model assumed and what it left out, in words. The last says how the Redis was priced. */
|
|
234
|
+
readonly notes: readonly string[];
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* **Planning** cost estimate — pure, no instance or live data needed (sizing, sales, what-if). Segment sizes
|
|
239
|
+
* are taken as given (or roughly derived from cardinality); use the grounded `segment.costReport()` for
|
|
240
|
+
* exact, real sizes. See {@link CostReport}.
|
|
241
|
+
*/
|
|
242
|
+
export declare function estimateCost(input: EstimateInput): CostReport;
|
|
243
|
+
/**
|
|
244
|
+
* **Grounded** report from a real segment byte total (from the `.crbm` index) + a supplied workload. Used by
|
|
245
|
+
* `Segment.costReport()` in the facade. `grounded` defaults to true (the size is exact, not estimated); the
|
|
246
|
+
* caller passes `grounded: false` + a note when the Storage source can't measure size.
|
|
247
|
+
*/
|
|
248
|
+
export declare function groundedReport(input: {
|
|
249
|
+
readonly storageBytes: number;
|
|
250
|
+
readonly grounded?: boolean;
|
|
251
|
+
readonly workload?: Workload;
|
|
252
|
+
readonly pricing?: PricingProfile;
|
|
253
|
+
readonly extraNotes?: readonly string[];
|
|
254
|
+
}): CostReport;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRC32C (Castagnoli, polynomial 0x1EDC6F41) — the pre-deserialize integrity screen for `.crbm`
|
|
3
|
+
* payloads, index, and footer.
|
|
4
|
+
*
|
|
5
|
+
* Pure TypeScript, table-driven: `core/` may not import `crypto`/`node:crypto` (the determinism lint
|
|
6
|
+
* enforces it), and a from-scratch implementation is what the cross-language golden corpus will be pinned
|
|
7
|
+
* against. CRC32C (not CRC32) is the locked choice — it matches the hardware-accelerated checksum used
|
|
8
|
+
* by SSE/iSCSI/Btrfs, and it is the algorithm the `.crbm` format locks in.
|
|
9
|
+
*/
|
|
10
|
+
export declare function crc32c(bytes: Uint8Array, seed?: number): number;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen `.crbm` v1.0 layout constants.
|
|
3
|
+
*
|
|
4
|
+
* These byte widths/offsets are pinned by the golden corpus and must never change for v1 —
|
|
5
|
+
* a new layout is a new format version. All multi-byte integers are little-endian (v1 fixes LE).
|
|
6
|
+
*/
|
|
7
|
+
/** 4-byte magic at both file ends: "CRBM". */
|
|
8
|
+
export declare const MAGIC: Uint8Array<ArrayBuffer>;
|
|
9
|
+
export declare const VERSION_MAJOR = 1;
|
|
10
|
+
export declare const VERSION_MINOR = 0;
|
|
11
|
+
/** Front preamble: magic(4) + version_major(1) + version_minor(1) + reserved(2). */
|
|
12
|
+
export declare const PREAMBLE_BYTES = 8;
|
|
13
|
+
/** Payloads begin immediately after the preamble. */
|
|
14
|
+
export declare const PAYLOAD_START = 8;
|
|
15
|
+
/** Fixed footer size (v1.0). */
|
|
16
|
+
export declare const FOOTER_BYTES = 104;
|
|
17
|
+
/** Byte offsets of each field within the 104-byte footer. */
|
|
18
|
+
export declare const FOOTER: {
|
|
19
|
+
readonly indexOffset: 0;
|
|
20
|
+
readonly indexLength: 8;
|
|
21
|
+
readonly indexCrc32c: 16;
|
|
22
|
+
readonly flags: 20;
|
|
23
|
+
readonly payloadCodecId: 24;
|
|
24
|
+
readonly elementWidth: 26;
|
|
25
|
+
readonly containerCodec: 27;
|
|
26
|
+
readonly versionMajor: 28;
|
|
27
|
+
readonly versionMinor: 29;
|
|
28
|
+
readonly reserved2: 30;
|
|
29
|
+
readonly generation: 32;
|
|
30
|
+
readonly indexNonce: 40;
|
|
31
|
+
readonly indexTag: 52;
|
|
32
|
+
readonly keyId: 68;
|
|
33
|
+
readonly chunkCount: 84;
|
|
34
|
+
readonly totalCardinality: 88;
|
|
35
|
+
readonly footerCrc32c: 96;
|
|
36
|
+
readonly endMagic: 100;
|
|
37
|
+
};
|
|
38
|
+
/** Footer bytes covered by `footer_crc32c` (everything before the CRC field). */
|
|
39
|
+
export declare const FOOTER_CRC_COVERAGE: 96;
|
|
40
|
+
/** `flags` bit positions. */
|
|
41
|
+
export declare const FLAG_ENCRYPTED: number;
|
|
42
|
+
export declare const FLAG_INDEX_COMPRESSED: number;
|
|
43
|
+
export declare const FLAG_LITTLE_ENDIAN: number;
|
|
44
|
+
/** Flag bits a v1 reader understands; any bit outside this mask is an unsupported feature. */
|
|
45
|
+
export declare const KNOWN_FLAGS: number;
|
|
46
|
+
/**
|
|
47
|
+
* Default element width: 32-bit ids (the u32 member space). `64` is the *reserved* escape above the
|
|
48
|
+
* ~4.29 B-ids/segment u32 ceiling; a v1 (32-bit) reader must **reject** a 64-bit generation rather than feed
|
|
49
|
+
* it to the 32-bit deserializer (which would mis-count fleet-wide). A future 64-bit generation is therefore a
|
|
50
|
+
* **major**-version bump (auto-rejected by old readers), never a silent minor one — see
|
|
51
|
+
* {@link CrbmReader.open} validates this.
|
|
52
|
+
*/
|
|
53
|
+
export declare const ELEMENT_WIDTH_32 = 32;
|
|
54
|
+
/**
|
|
55
|
+
* **Which codec produced the chunk payloads.** Recorded in the footer, validated on read.
|
|
56
|
+
*
|
|
57
|
+
* `.crbm` is a shared container: the index, the CRC32Cs, the AEAD framing and the generation model are all
|
|
58
|
+
* codec-independent, and **only the chunk payload bytes belong to a flavor** (hence *Chunked Remote BitMap* —
|
|
59
|
+
* see the `.crbm` format section of the API reference). This field is what lets one container hold either, the same way ZIP tags each member
|
|
60
|
+
* with a compression method.
|
|
61
|
+
*
|
|
62
|
+
* **A codec id, not a roaring-specific one.** A second codec is genuinely expected — `soaring` is a planned
|
|
63
|
+
* Roaring *variant*, so its serialized bytes are unlikely to be roaring-portable, and it lands *after* the `1.0`
|
|
64
|
+
* format freeze. A field frozen under a codec-specific meaning could not be reinterpreted later without a major
|
|
65
|
+
* format version.
|
|
66
|
+
*
|
|
67
|
+
* **Ids are permanent once published.** Add to {@link KNOWN_PAYLOAD_CODEC_IDS} when a codec ships; never
|
|
68
|
+
* reuse or renumber. Ids are deliberately *not* pre-allocated for codecs that do not exist — a reserved number
|
|
69
|
+
* for an unbuilt codec is a guess about a format nobody has designed.
|
|
70
|
+
*/
|
|
71
|
+
export declare const PAYLOAD_CODEC_ROARING_PORTABLE = 1;
|
|
72
|
+
/**
|
|
73
|
+
* Every payload codec id this reader can decode.
|
|
74
|
+
*
|
|
75
|
+
* The reader validates membership rather than equality with a single constant. That is the whole point of an
|
|
76
|
+
* id registry: an unknown id is rejected with a typed error naming it, so an old reader meeting a
|
|
77
|
+
* future-codec generation **fails closed** — the correct direction, and the reason a store built on one codec
|
|
78
|
+
* can never silently misread another's bytes as its own. (The homogeneity contract means one store is one
|
|
79
|
+
* codec, so meeting a foreign generation implies misconfiguration, and a loud rejection is exactly what you
|
|
80
|
+
* want there.)
|
|
81
|
+
*/
|
|
82
|
+
export declare const KNOWN_PAYLOAD_CODEC_IDS: ReadonlySet<number>;
|
|
83
|
+
/**
|
|
84
|
+
* The only container codec v1 defines: `0` = none (payloads are stored as the portable roaring serialization
|
|
85
|
+
* with no extra container-level transform). A non-zero codec is a future format feature; a v1 reader rejects
|
|
86
|
+
* it ({@link UnsupportedError}) rather than mis-decode. Stamped by the writer, validated by the reader.
|
|
87
|
+
*/
|
|
88
|
+
export declare const CONTAINER_CODEC_NONE = 0;
|
|
89
|
+
/**
|
|
90
|
+
* Default speculative-tail-read size. The spec's worst-case index (a full 16-bit span) is ~400–650 KB,
|
|
91
|
+
* so 256 KB collapses the read to a single GET for the vast majority of real (sparse/medium) segments;
|
|
92
|
+
* only near-full-span segments take the documented second GET.
|
|
93
|
+
*/
|
|
94
|
+
export declare const DEFAULT_TAIL_BYTES: number;
|
|
95
|
+
/**
|
|
96
|
+
* Hard cap on the index region a reader will fetch/parse from one (untrusted) object. The index is read
|
|
97
|
+
* whole on the cheap `open()`/`count()` path, so it must be bounded before allocation — generously
|
|
98
|
+
* above the ~650 KB full-span worst case, well below a denial-of-wallet allocation.
|
|
99
|
+
*/
|
|
100
|
+
export declare const DEFAULT_MAX_INDEX_BYTES: number;
|
|
101
|
+
/** Hard cap on a single chunk payload, defending the native deserializer against oversized input. */
|
|
102
|
+
export declare const DEFAULT_MAX_PAYLOAD_BYTES: number;
|
|
103
|
+
/** A chunk's cardinality is in `[1, 65536]` (empty chunks are never written). */
|
|
104
|
+
export declare const MAX_CHUNK_CARDINALITY = 65536;
|
|
105
|
+
/** Fixed width of a per-chunk CRC32C field in the index (high-entropy → not varint). */
|
|
106
|
+
export declare const CRC32C_BYTES = 4;
|
|
107
|
+
/**
|
|
108
|
+
* v1 AEAD framing sizes (AES-256-GCM), fixed by the format exactly like the payload codec id — a
|
|
109
|
+
* different cipher is a new format version. An encrypted chunk payload is stored as `nonce ‖ ciphertext ‖ tag`;
|
|
110
|
+
* the encrypted **index**'s nonce/tag live in the footer's reserved `indexNonce`/`indexTag` slots. Matches
|
|
111
|
+
* {@link FOOTER.indexNonce} (12 B) and {@link FOOTER.indexTag} (16 B).
|
|
112
|
+
*/
|
|
113
|
+
export declare const AEAD_NONCE_BYTES = 12;
|
|
114
|
+
export declare const AEAD_TAG_BYTES = 16;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { BlobReader } from '../blob.js';
|
|
2
|
+
import type { CrbmCrypto } from '../crypto.js';
|
|
3
|
+
export interface CrbmIndexEntry {
|
|
4
|
+
readonly chunkKey: number;
|
|
5
|
+
readonly offset: number;
|
|
6
|
+
readonly length: number;
|
|
7
|
+
readonly cardinality: number;
|
|
8
|
+
readonly crc32c: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CrbmReaderOptions {
|
|
11
|
+
/** Speculative tail size in bytes (default 256 KB; clamped up to at least the footer size). */
|
|
12
|
+
readonly tailBytes?: number;
|
|
13
|
+
/** Hard cap on a single chunk payload length (default 16 MB). */
|
|
14
|
+
readonly maxPayloadBytes?: number;
|
|
15
|
+
/** Hard cap on the whole index region fetched/parsed from one object (default 8 MB). */
|
|
16
|
+
readonly maxIndexBytes?: number;
|
|
17
|
+
/** Decryption context for an encrypted object (its DEK's AEAD + AAD builder). Required iff `FLAG_ENCRYPTED`. */
|
|
18
|
+
readonly crypto?: CrbmCrypto;
|
|
19
|
+
/**
|
|
20
|
+
* Opaque marker for the *incarnation of the name* this object belongs to — the caller's own identity for the
|
|
21
|
+
* registry row it resolved. The reader never interprets it; it carries it so a caller holding an open reader
|
|
22
|
+
* can tell "the same generation of the same segment" from "the same generation NUMBER of a segment that was
|
|
23
|
+
* deleted and re-created", which the number alone cannot express.
|
|
24
|
+
*/
|
|
25
|
+
readonly lineage?: unknown;
|
|
26
|
+
}
|
|
27
|
+
export declare class CrbmReader {
|
|
28
|
+
private readonly blob;
|
|
29
|
+
private readonly objectSize;
|
|
30
|
+
readonly generation: number;
|
|
31
|
+
/** See {@link CrbmReaderOptions.lineage} — carried, never interpreted. */
|
|
32
|
+
readonly lineage: unknown;
|
|
33
|
+
readonly totalCardinality: number;
|
|
34
|
+
private readonly entries;
|
|
35
|
+
private readonly orderedKeys;
|
|
36
|
+
/** True when `open()` satisfied the index from the tail GET alone (no second range read). */
|
|
37
|
+
readonly servedFromTail: boolean;
|
|
38
|
+
/** Set iff the object is encrypted — used to decrypt each chunk payload in {@link getChunk}. */
|
|
39
|
+
private readonly crypto;
|
|
40
|
+
/** The footer's own CRC, as verified at open: it covers the index's CRC, the chunk count and the generation. */
|
|
41
|
+
private readonly footerCrc;
|
|
42
|
+
private constructor();
|
|
43
|
+
/**
|
|
44
|
+
* Which object this reader opened, as its size and its footer's CRC. The footer covers the index's CRC, so two
|
|
45
|
+
* objects with the same fingerprint hold the same chunks, as far as a CRC can tell; a pin compares it to know that
|
|
46
|
+
* the object it reopens is the one it pinned, since a purged and reloaded name reuses the generation number.
|
|
47
|
+
*/
|
|
48
|
+
get fingerprint(): string;
|
|
49
|
+
/** Total object bytes (from the one-GET tail read) — for grounded storage cost. */
|
|
50
|
+
get sizeBytes(): number;
|
|
51
|
+
/**
|
|
52
|
+
* Estimated retained JS heap of this reader's parsed index — the weight the storage reader cache bounds on
|
|
53
|
+
* (a wide segment's parsed index, not its payloads, dominates the reader's footprint). `entries.size` scales with the number
|
|
54
|
+
* of resident chunks (≤ 65536), so this is `entries.size × {@link RETAINED_BYTES_PER_INDEX_ENTRY}`.
|
|
55
|
+
*/
|
|
56
|
+
get retainedIndexBytes(): number;
|
|
57
|
+
/** Per-chunk cardinality (`chunkKey → count`) from the parsed index — no payload reads. */
|
|
58
|
+
cardinalities(): Map<number, number>;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the object behind `blob` is the one `fingerprint` names ({@link fingerprint}), from one tail read of a
|
|
61
|
+
* footer's worth, with no key and no index, since the footer is stored in the clear and checked by its own CRC.
|
|
62
|
+
* Another size is another object, whatever its bytes hold: a short one that is no `.crbm` at all is not the one
|
|
63
|
+
* pinned. At the same size the footer's CRC says, and a footer that fails its own checks says nothing about which
|
|
64
|
+
* object is there, so this throws, as an open would. What a pin asks, to tell the object it opened from one that
|
|
65
|
+
* has since been stored under its key, whoever wrote it.
|
|
66
|
+
*/
|
|
67
|
+
static sameObject(blob: BlobReader, fingerprint: string): Promise<boolean>;
|
|
68
|
+
static open(blob: BlobReader, options?: CrbmReaderOptions): Promise<CrbmReader>;
|
|
69
|
+
/** Chunk keys present in this generation, ascending. */
|
|
70
|
+
chunkKeys(): number[];
|
|
71
|
+
/** Segment cardinality from the footer (no payload reads) — the cheap `count()` path. */
|
|
72
|
+
count(): number;
|
|
73
|
+
has(chunkKey: number): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Range-GET one chunk's payload, verifying its CRC32C before returning. Returns `null` if the chunk is
|
|
76
|
+
* absent. Throws `IntegrityError` on a CRC mismatch (the bytes must never reach the native
|
|
77
|
+
* deserializer). The returned buffer is a **read-only view** owned by the reader/driver — callers must
|
|
78
|
+
* not mutate it.
|
|
79
|
+
*/
|
|
80
|
+
getChunk(chunkKey: number): Promise<Uint8Array | null>;
|
|
81
|
+
}
|
|
82
|
+
export declare function parseIndex(indexBytes: Uint8Array, payloadEnd: number, maxPayloadBytes: number, minPayloadBytes?: number): {
|
|
83
|
+
entries: Map<number, CrbmIndexEntry>;
|
|
84
|
+
orderedKeys: number[];
|
|
85
|
+
cardinalitySum: number;
|
|
86
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Append the LEB128 encoding of a u32 `value` to `out`. */
|
|
2
|
+
export declare function writeVarint(out: number[], value: number): void;
|
|
3
|
+
export interface VarintRead {
|
|
4
|
+
readonly value: number;
|
|
5
|
+
/** Offset just past the bytes consumed. */
|
|
6
|
+
readonly next: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Decode a LEB128 varint from `bytes` starting at `offset`. Throws `IntegrityError` on a truncated or
|
|
10
|
+
* over-long (> 5-byte / > u32) encoding — the bytes are untrusted, so a malformed index must fail
|
|
11
|
+
* cleanly, never loop or overflow.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readVarint(bytes: Uint8Array, offset: number): VarintRead;
|