@cloudbitmaps/core 0.9.0 → 0.10.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.
Files changed (132) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +28 -7
  3. package/dist/chunk-BUPDGZRR.js +461 -0
  4. package/dist/chunk-BUPDGZRR.js.map +7 -0
  5. package/dist/core/audit.d.ts +127 -0
  6. package/dist/core/bit-route.d.ts +21 -0
  7. package/dist/core/blob.d.ts +34 -0
  8. package/dist/core/budget.d.ts +66 -0
  9. package/dist/core/codec.d.ts +114 -0
  10. package/dist/core/concurrency.d.ts +15 -0
  11. package/dist/core/consistency.d.ts +73 -0
  12. package/dist/core/cooperative.d.ts +78 -0
  13. package/dist/core/cost.d.ts +102 -0
  14. package/dist/core/crbm/crc32c.d.ts +10 -0
  15. package/dist/core/crbm/format.d.ts +116 -0
  16. package/dist/core/crbm/reader.d.ts +69 -0
  17. package/dist/core/crbm/varint.d.ts +13 -0
  18. package/dist/core/crbm/writer.d.ts +39 -0
  19. package/dist/core/crbm-storage-source.d.ts +432 -0
  20. package/dist/core/crypto.d.ts +80 -0
  21. package/dist/core/determinism.d.ts +42 -0
  22. package/dist/core/discover.d.ts +75 -0
  23. package/dist/core/due-index.d.ts +55 -0
  24. package/dist/core/engine.d.ts +204 -0
  25. package/dist/core/erase-id.d.ts +141 -0
  26. package/dist/core/erasure.d.ts +179 -0
  27. package/dist/core/errors.d.ts +120 -0
  28. package/dist/core/generation-gc.d.ts +52 -0
  29. package/dist/core/keys.d.ts +23 -0
  30. package/dist/core/load.d.ts +102 -0
  31. package/dist/core/lru.d.ts +69 -0
  32. package/dist/core/metrics.d.ts +132 -0
  33. package/dist/core/name-codec.d.ts +79 -0
  34. package/dist/core/pinned-storage-source.d.ts +69 -0
  35. package/dist/core/ports.d.ts +340 -0
  36. package/dist/core/registry-scan.d.ts +38 -0
  37. package/dist/core/retention-sweep.d.ts +190 -0
  38. package/dist/core/retention.d.ts +67 -0
  39. package/dist/core/retry.d.ts +65 -0
  40. package/dist/core/rollback.d.ts +71 -0
  41. package/dist/core/validate.d.ts +20 -0
  42. package/dist/driver-kit.d.ts +10 -0
  43. package/dist/driver-kit.js +314 -0
  44. package/dist/driver-kit.js.map +7 -0
  45. package/dist/drivers/_shared/aws-errors.d.ts +18 -0
  46. package/dist/drivers/_shared/keys.d.ts +27 -0
  47. package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
  48. package/dist/drivers/_shared/object-registry.d.ts +84 -0
  49. package/dist/drivers/_shared/registry.d.ts +62 -0
  50. package/dist/drivers/backends.d.ts +49 -0
  51. package/dist/drivers/crypto.d.ts +38 -0
  52. package/dist/drivers/localfs/fs-util.d.ts +20 -0
  53. package/dist/drivers/localfs/paths.d.ts +33 -0
  54. package/dist/drivers/localfs/registry.d.ts +28 -0
  55. package/dist/drivers/localfs/storage.d.ts +19 -0
  56. package/dist/drivers/memory.d.ts +57 -0
  57. package/dist/drivers/retry/retrying-drivers.d.ts +90 -0
  58. package/dist/export/index.d.ts +92 -0
  59. package/dist/index.d.ts +85 -2760
  60. package/dist/index.js +2815 -3062
  61. package/dist/index.js.map +7 -1
  62. package/dist/testing/fuzz-core.d.ts +14 -0
  63. package/package.json +19 -115
  64. package/dist/azure/index.cjs +0 -368
  65. package/dist/azure/index.cjs.map +0 -1
  66. package/dist/azure/index.d.cts +0 -48
  67. package/dist/azure/index.d.ts +0 -48
  68. package/dist/azure/index.js +0 -304
  69. package/dist/azure/index.js.map +0 -1
  70. package/dist/cassandra/index.cjs +0 -265
  71. package/dist/cassandra/index.cjs.map +0 -1
  72. package/dist/cassandra/index.d.cts +0 -52
  73. package/dist/cassandra/index.d.ts +0 -52
  74. package/dist/cassandra/index.js +0 -204
  75. package/dist/cassandra/index.js.map +0 -1
  76. package/dist/chunk-7LMLYSVJ.js +0 -43
  77. package/dist/chunk-7LMLYSVJ.js.map +0 -1
  78. package/dist/chunk-BD22WDAN.js +0 -230
  79. package/dist/chunk-BD22WDAN.js.map +0 -1
  80. package/dist/chunk-NUIDEEFZ.js +0 -91
  81. package/dist/chunk-NUIDEEFZ.js.map +0 -1
  82. package/dist/chunk-OZ6LAFV4.js +0 -6
  83. package/dist/chunk-OZ6LAFV4.js.map +0 -1
  84. package/dist/chunk-SNJVZ227.js +0 -35
  85. package/dist/chunk-SNJVZ227.js.map +0 -1
  86. package/dist/dynamodb/index.cjs +0 -767
  87. package/dist/dynamodb/index.cjs.map +0 -1
  88. package/dist/dynamodb/index.d.cts +0 -106
  89. package/dist/dynamodb/index.d.ts +0 -106
  90. package/dist/dynamodb/index.js +0 -469
  91. package/dist/dynamodb/index.js.map +0 -1
  92. package/dist/gcs/index.cjs +0 -343
  93. package/dist/gcs/index.cjs.map +0 -1
  94. package/dist/gcs/index.d.cts +0 -46
  95. package/dist/gcs/index.d.ts +0 -46
  96. package/dist/gcs/index.js +0 -279
  97. package/dist/gcs/index.js.map +0 -1
  98. package/dist/index.cjs +0 -4990
  99. package/dist/index.cjs.map +0 -1
  100. package/dist/index.d.cts +0 -2761
  101. package/dist/mongodb/index.cjs +0 -260
  102. package/dist/mongodb/index.cjs.map +0 -1
  103. package/dist/mongodb/index.d.cts +0 -45
  104. package/dist/mongodb/index.d.ts +0 -45
  105. package/dist/mongodb/index.js +0 -199
  106. package/dist/mongodb/index.js.map +0 -1
  107. package/dist/mysql/index.cjs +0 -281
  108. package/dist/mysql/index.cjs.map +0 -1
  109. package/dist/mysql/index.d.cts +0 -56
  110. package/dist/mysql/index.d.ts +0 -56
  111. package/dist/mysql/index.js +0 -211
  112. package/dist/mysql/index.js.map +0 -1
  113. package/dist/ports-CvcvJWf6.d.cts +0 -395
  114. package/dist/ports-CvcvJWf6.d.ts +0 -395
  115. package/dist/postgres/index.cjs +0 -273
  116. package/dist/postgres/index.cjs.map +0 -1
  117. package/dist/postgres/index.d.cts +0 -54
  118. package/dist/postgres/index.d.ts +0 -54
  119. package/dist/postgres/index.js +0 -203
  120. package/dist/postgres/index.js.map +0 -1
  121. package/dist/redis/index.cjs +0 -257
  122. package/dist/redis/index.cjs.map +0 -1
  123. package/dist/redis/index.d.cts +0 -42
  124. package/dist/redis/index.d.ts +0 -42
  125. package/dist/redis/index.js +0 -197
  126. package/dist/redis/index.js.map +0 -1
  127. package/dist/s3/index.cjs +0 -918
  128. package/dist/s3/index.cjs.map +0 -1
  129. package/dist/s3/index.d.cts +0 -112
  130. package/dist/s3/index.d.ts +0 -112
  131. package/dist/s3/index.js +0 -555
  132. 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 now that both usually live 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 can be
13
+ * eventually-consistent (an unindexed Scan) and 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, 62,455 chunks: **442 ms wall, and 450 ms during which the event loop did
7
+ * not turn at all**. 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
+ * 442 ms wall / 450 ms blocked before, 256 ms / 19 ms after. (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 is
35
+ * how this file previously published a 92 ms operation with 819 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 exactly as it did before this existed, so
74
+ * adding a yield point to a code path can never change behaviour for a caller who did not opt in. `core/` cannot
75
+ * supply a clock itself — it is timer-free by lint, which is the whole reason scheduling goes through the seam —
76
+ * so `@cloudbitmaps/roaring` 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,102 @@
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
+ /** The always-on baseline to compare against (e.g. an ElastiCache HA cluster). */
12
+ readonly redis: {
13
+ readonly monthlyUSD: number;
14
+ };
15
+ }
16
+ /**
17
+ * Default profile — **AWS us-east-1, on-demand**, mid-2026, from the fact-checked
18
+ * published pricing rather than copied from a blog post. Override it for your region, cloud, or committed term.
19
+ */
20
+ export declare const AWS_US_EAST_1_ONDEMAND: PricingProfile;
21
+ export declare const DEFAULT_PRICING: PricingProfile;
22
+ /** Sustained access pattern. All rates default to 0; unspecified ⇒ that op contributes nothing. */
23
+ export interface Workload {
24
+ /** Point reads (`has`) per second. Each cache miss is one object GET. */
25
+ readonly readsPerSec?: number;
26
+ readonly intersectsPerSec?: number;
27
+ /** CACHE-cache hit rate in `[0, 1]` — hits are free; only misses cost. Default 0. */
28
+ readonly cacheHitRate?: number;
29
+ /** Storage chunks fetched per intersection (the chunk-skipping survivors). Default 1. */
30
+ readonly chunksPerIntersect?: number;
31
+ /**
32
+ * Generations published per month across the modeled data — the write side of a loaded store. Default **0**
33
+ * ⇒ loads are not modeled and the report *discloses* the omission rather than silently under-reporting.
34
+ */
35
+ readonly loadsPerMonth?: number;
36
+ /**
37
+ * PUT-class requests one load issues. Default **1** (a single-object PUT). A multipart load of `P` parts bills
38
+ * `P + 2` (initiate, the parts, complete) — set it when you know your object sizes. Still small money: at
39
+ * $5/million, a thousand 100-part loads a month is $0.51.
40
+ */
41
+ readonly requestsPerLoad?: number;
42
+ }
43
+ /** One (group of) segment(s) for planning. `count` = how many like this (default 1). */
44
+ export interface SegmentSizing {
45
+ readonly sizeBytes?: number;
46
+ readonly cardinality?: number;
47
+ /** Number of segments with these characteristics. Default 1. */
48
+ readonly count?: number;
49
+ }
50
+ export interface EstimateInput {
51
+ readonly segments: readonly SegmentSizing[];
52
+ readonly workload?: Workload;
53
+ readonly pricing?: PricingProfile;
54
+ }
55
+ export interface CostReport {
56
+ readonly monthlyUSD: {
57
+ readonly byOp: {
58
+ readonly reads: number;
59
+ readonly intersects: number;
60
+ readonly storage: number;
61
+ /** Loads (object PUTs). 0 unless `loadsPerMonth` is set. */
62
+ readonly loads: number;
63
+ };
64
+ readonly total: number;
65
+ };
66
+ /**
67
+ * Sustained read rate at which the pay-per-use model's cost passes the flat Redis baseline (with every other
68
+ * axis at 0), **evaluated at this report's `cacheHitRate`** — so a higher cache-hit rate raises it (cache hits
69
+ * are free). `Infinity` means it never crosses (a 100% cache-hit rate). The published anchor (~329 reads/s) is
70
+ * at `cacheHitRate: 0`.
71
+ */
72
+ readonly redisCrossover: {
73
+ readonly readsPerSec: number;
74
+ };
75
+ readonly verdict: 'win-big' | 'win' | 'lose-zone';
76
+ readonly rationale: string;
77
+ readonly assumptions: {
78
+ readonly cacheHitRate: number;
79
+ readonly pricingName: string;
80
+ /** True when segment **sizes** were real (grounded `costReport`), false for a pure `estimateCost`. */
81
+ readonly grounded: boolean;
82
+ readonly notes: readonly string[];
83
+ };
84
+ }
85
+ /**
86
+ * **Planning** cost estimate — pure, no instance or live data needed (sizing, sales, what-if). Segment sizes
87
+ * are taken as given (or roughly derived from cardinality); use the grounded `segment.costReport()` for
88
+ * exact, real sizes. See {@link CostReport}.
89
+ */
90
+ export declare function estimateCost(input: EstimateInput): CostReport;
91
+ /**
92
+ * **Grounded** report from a real segment byte total (from the `.crbm` index) + a supplied workload. Used by
93
+ * `Segment.costReport()` in the facade. `grounded` defaults to true (the size is exact, not estimated); the
94
+ * caller passes `grounded: false` + a note when the Storage source can't measure size.
95
+ */
96
+ export declare function groundedReport(input: {
97
+ readonly storageBytes: number;
98
+ readonly grounded?: boolean;
99
+ readonly workload?: Workload;
100
+ readonly pricing?: PricingProfile;
101
+ readonly extraNotes?: readonly string[];
102
+ }): 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,116 @@
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
+ * **Why this is not named `roaringSerializationId` any more.** It was, and the name was a trap waiting for the
63
+ * `1.0` format freeze. A second codec is genuinely expected — `soaring` is a planned Roaring *variant*, so its
64
+ * serialized bytes are unlikely to be roaring-portable, and it lands *after* `1.0`. A field frozen under a
65
+ * codec-specific name cannot be reinterpreted later without a major format version, so generalizing it is a
66
+ * one-line change now and an expensive one after the freeze. The byte layout is untouched: same offset (24),
67
+ * same width (u16), same golden corpus.
68
+ *
69
+ * **Ids are permanent once published.** Add to {@link KNOWN_PAYLOAD_CODEC_IDS} when a codec ships; never
70
+ * reuse or renumber. Ids are deliberately *not* pre-allocated for codecs that do not exist — a reserved number
71
+ * for an unbuilt codec is a guess about a format nobody has designed.
72
+ */
73
+ export declare const PAYLOAD_CODEC_ROARING_PORTABLE = 1;
74
+ /**
75
+ * Every payload codec id this reader can decode.
76
+ *
77
+ * The reader validates membership rather than equality with a single constant. That is the whole point of the
78
+ * generalization: an unknown id is rejected with a typed error naming it, so an old reader meeting a
79
+ * future-codec generation **fails closed** — the correct direction, and the reason a store built on one codec
80
+ * can never silently misread another's bytes as its own. (The homogeneity contract means one store is one
81
+ * codec, so meeting a foreign generation implies misconfiguration, and a loud rejection is exactly what you
82
+ * want there.)
83
+ */
84
+ export declare const KNOWN_PAYLOAD_CODEC_IDS: ReadonlySet<number>;
85
+ /**
86
+ * The only container codec v1 defines: `0` = none (payloads are stored as the portable roaring serialization
87
+ * with no extra container-level transform). A non-zero codec is a future format feature; a v1 reader rejects
88
+ * it ({@link UnsupportedError}) rather than mis-decode. Stamped by the writer, validated by the reader.
89
+ */
90
+ export declare const CONTAINER_CODEC_NONE = 0;
91
+ /**
92
+ * Default speculative-tail-read size. The spec's worst-case index (a full 16-bit span) is ~400–650 KB,
93
+ * so 256 KB collapses the read to a single GET for the vast majority of real (sparse/medium) segments;
94
+ * only near-full-span segments take the documented second GET.
95
+ */
96
+ export declare const DEFAULT_TAIL_BYTES: number;
97
+ /**
98
+ * Hard cap on the index region a reader will fetch/parse from one (untrusted) object. The index is read
99
+ * whole on the cheap `open()`/`count()` path, so it must be bounded before allocation — generously
100
+ * above the ~650 KB full-span worst case, well below a denial-of-wallet allocation.
101
+ */
102
+ export declare const DEFAULT_MAX_INDEX_BYTES: number;
103
+ /** Hard cap on a single chunk payload, defending the native deserializer against oversized input. */
104
+ export declare const DEFAULT_MAX_PAYLOAD_BYTES: number;
105
+ /** A chunk's cardinality is in `[1, 65536]` (empty chunks are never written). */
106
+ export declare const MAX_CHUNK_CARDINALITY = 65536;
107
+ /** Fixed width of a per-chunk CRC32C field in the index (high-entropy → not varint). */
108
+ export declare const CRC32C_BYTES = 4;
109
+ /**
110
+ * v1 AEAD framing sizes (AES-256-GCM), fixed by the format exactly like the roaring serialization id — a
111
+ * different cipher is a new format version. An encrypted chunk payload is stored as `nonce ‖ ciphertext ‖ tag`;
112
+ * the encrypted **index**'s nonce/tag live in the footer's reserved `indexNonce`/`indexTag` slots. Matches
113
+ * {@link FOOTER.indexNonce} (12 B) and {@link FOOTER.indexTag} (16 B).
114
+ */
115
+ export declare const AEAD_NONCE_BYTES = 12;
116
+ export declare const AEAD_TAG_BYTES = 16;
@@ -0,0 +1,69 @@
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
+ private constructor();
41
+ /** Total object bytes (from the one-GET tail read) — for grounded storage cost. */
42
+ get sizeBytes(): number;
43
+ /**
44
+ * Estimated retained JS heap of this reader's parsed index — the weight the storage reader cache bounds on
45
+ * (a wide segment's parsed index, not its payloads, dominates the reader's footprint). `entries.size` scales with the number
46
+ * of resident chunks (≤ 65536), so this is `entries.size × {@link RETAINED_BYTES_PER_INDEX_ENTRY}`.
47
+ */
48
+ get retainedIndexBytes(): number;
49
+ /** Per-chunk cardinality (`chunkKey → count`) from the parsed index — no payload reads. */
50
+ cardinalities(): Map<number, number>;
51
+ static open(blob: BlobReader, options?: CrbmReaderOptions): Promise<CrbmReader>;
52
+ /** Chunk keys present in this generation, ascending. */
53
+ chunkKeys(): number[];
54
+ /** Segment cardinality from the footer (no payload reads) — the cheap `count()` path. */
55
+ count(): number;
56
+ has(chunkKey: number): boolean;
57
+ /**
58
+ * Range-GET one chunk's payload, verifying its CRC32C before returning. Returns `null` if the chunk is
59
+ * absent. Throws `IntegrityError` on a CRC mismatch (the bytes must never reach the native
60
+ * deserializer). The returned buffer is a **read-only view** owned by the reader/driver — callers must
61
+ * not mutate it.
62
+ */
63
+ getChunk(chunkKey: number): Promise<Uint8Array | null>;
64
+ }
65
+ export declare function parseIndex(indexBytes: Uint8Array, objectSize: number, maxPayloadBytes: number): {
66
+ entries: Map<number, CrbmIndexEntry>;
67
+ orderedKeys: number[];
68
+ cardinalitySum: number;
69
+ };
@@ -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;
@@ -0,0 +1,39 @@
1
+ import type { CrbmCrypto } from '../crypto.js';
2
+ import type { BlobSink } from '../blob.js';
3
+ export interface CrbmWriterOptions {
4
+ /** Self-describing generation number (also encoded in the object key). */
5
+ readonly generation: number;
6
+ /**
7
+ * Which codec produced the chunk payloads. Defaults to {@link PAYLOAD_CODEC_ROARING_PORTABLE}.
8
+ *
9
+ * Renamed from `roaringSerializationId` in 0.7.0 — same footer field, same offset and width, generalized
10
+ * because `.crbm` is a shared container and a second codec is expected before the format freezes.
11
+ */
12
+ readonly payloadCodecId?: number;
13
+ readonly elementWidth?: number;
14
+ /** When set, payloads + index are AES-256-GCM-encrypted, bound to `(segment, generation, scope)` via AAD. */
15
+ readonly crypto?: CrbmCrypto;
16
+ }
17
+ export declare class CrbmWriter {
18
+ private readonly sink;
19
+ private readonly options;
20
+ private readonly entries;
21
+ private offset;
22
+ private lastKey;
23
+ private totalCardinality;
24
+ private preambleWritten;
25
+ private finished;
26
+ constructor(sink: BlobSink, options: CrbmWriterOptions);
27
+ /**
28
+ * Append one chunk's payload (the roaring-serialized bytes) with its cardinality. `chunkKey` must be
29
+ * strictly greater than the previous one (ascending, no duplicates); `cardinality` is `[1, 65536]`.
30
+ */
31
+ addChunk(chunkKey: number, payload: Uint8Array, cardinality: number): Promise<void>;
32
+ /** Seal one chunk payload into the on-disk frame `nonce ‖ ciphertext ‖ tag`, bound to its chunkKey. */
33
+ private sealChunk;
34
+ /** Write the index region + the fixed footer. After this the object is complete and immutable. */
35
+ finish(): Promise<void>;
36
+ private ensurePreamble;
37
+ private encodeIndex;
38
+ private buildFooter;
39
+ }