@cloudbitmaps/core 0.11.2 → 0.13.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/dist/{chunk-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
- package/dist/chunk-5W3XS2IU.js.map +7 -0
- package/dist/core/audit.d.ts +6 -0
- package/dist/core/chunk-window.d.ts +24 -0
- package/dist/core/codec.d.ts +33 -1
- package/dist/core/consistency.d.ts +21 -2
- package/dist/core/cost.d.ts +91 -10
- package/dist/core/crbm/format.d.ts +42 -3
- package/dist/core/crbm/reader.d.ts +45 -3
- package/dist/core/crbm/writer.d.ts +32 -3
- package/dist/core/crbm-storage-source.d.ts +204 -23
- package/dist/core/crypto.d.ts +11 -7
- package/dist/core/determinism.d.ts +7 -0
- package/dist/core/engine.d.ts +41 -8
- package/dist/core/erase-id.d.ts +23 -3
- package/dist/core/erasure.d.ts +8 -4
- package/dist/core/generation-gc.d.ts +102 -4
- package/dist/core/load-input.d.ts +55 -0
- package/dist/core/load.d.ts +65 -10
- package/dist/core/metadata.d.ts +42 -0
- package/dist/core/metrics.d.ts +7 -1
- package/dist/core/pinned-storage-source.d.ts +2 -1
- package/dist/core/ports.d.ts +119 -13
- package/dist/core/registry-scan.d.ts +6 -0
- package/dist/core/retention-sweep.d.ts +47 -2
- package/dist/core/retry.d.ts +14 -0
- package/dist/core/rollback.d.ts +34 -4
- package/dist/core/summary.d.ts +61 -0
- package/dist/core/token.d.ts +36 -0
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.js +39 -20
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/entropy.d.ts +5 -0
- package/dist/drivers/_shared/object-registry.d.ts +36 -6
- package/dist/drivers/_shared/registry.d.ts +47 -19
- package/dist/drivers/localfs/registry.d.ts +10 -0
- package/dist/drivers/memory.d.ts +9 -3
- package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1878 -501
- package/dist/index.js.map +4 -4
- package/dist/testing/fuzz-core.d.ts +7 -6
- package/package.json +1 -1
- package/dist/chunk-5OMC5U7U.js.map +0 -7
package/dist/core/cost.d.ts
CHANGED
|
@@ -8,9 +8,19 @@ export interface PricingProfile {
|
|
|
8
8
|
readonly putPerMillion: number;
|
|
9
9
|
readonly storagePerGiBMonth: number;
|
|
10
10
|
/**
|
|
11
|
-
* Requests one read costs
|
|
12
|
-
* Default **1**, S3
|
|
13
|
-
*
|
|
11
|
+
* Requests one pointer read costs: a read of a segment's registry row, whose version comes back with its bytes.
|
|
12
|
+
* Default **1**, and 1 on S3, GCS and Azure Blob, each of which answers it with one GET. Charged for each operand
|
|
13
|
+
* of an intersection, for the pointer reads a load makes, and for each pointer refresh. Set it when a
|
|
14
|
+
* registry of your own takes more than one request to read a row.
|
|
15
|
+
*/
|
|
16
|
+
readonly requestsPerPointerRead?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Requests one tail read costs: the read of a segment's index from the end of its generation, which needs the
|
|
19
|
+
* object's size. Default **1**, S3's, whose suffix-range GET returns the size with the bytes, and GCS's, the same.
|
|
20
|
+
* **2** on Azure Blob, which takes no suffix range and reads the properties and then the bytes. Charged for each
|
|
21
|
+
* operand of an intersection. A load reads no index of its own: a row that carries a summary of its current
|
|
22
|
+
* generation gives the load the size it needs, and the checks it makes are one metadata request on every backend.
|
|
23
|
+
* A chunk read knows its range, and is one request everywhere.
|
|
14
24
|
*/
|
|
15
25
|
readonly requestsPerSizedRead?: number;
|
|
16
26
|
};
|
|
@@ -110,7 +120,8 @@ export interface Workload {
|
|
|
110
120
|
/**
|
|
111
121
|
* Segments each intersection reads, `exclude` operands included; at least 1. Default 2. An intersection is priced
|
|
112
122
|
* **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
|
|
123
|
+
* 2 GETs an operand on S3 and GCS, so a cold intersect of two segments sharing `k` chunks is `4 + 2k` GETs there,
|
|
124
|
+
* and 3 on Azure Blob, whose tail read is two requests (see {@link PricingProfile}). `cacheHitRate`
|
|
114
125
|
* does not apply to intersections, so a long-lived reader that answers a repeat from its cache pays less. Other
|
|
115
126
|
* combines read their operands the same way and can be priced here too, with the chunks they fetch.
|
|
116
127
|
*/
|
|
@@ -123,12 +134,23 @@ export interface Workload {
|
|
|
123
134
|
/**
|
|
124
135
|
* PUT-class requests one load's object write issues, each priced at the PUT rate. Default **1** (a single-object
|
|
125
136
|
* 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
|
|
127
|
-
* PUT-class on S3, and
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* pointer
|
|
137
|
+
* object sizes. The model adds what `store.load()` does around the write, at the default `keep` of 1: the pointer's
|
|
138
|
+
* write, PUT-class on S3, and five GETs: the pointer read three times, one check that the next generation number is
|
|
139
|
+
* free, and one that the current generation's object is there, which tells the collection that deletes by name it
|
|
140
|
+
* may. The current generation's size comes from the row's summary of it, so the load reads no index. Collection
|
|
141
|
+
* deletes the generation the window pushed out by name, so it lists only on every 16th generation, which adds a
|
|
142
|
+
* PUT-class request and two pointer reads there and makes no check that the current generation is there, a
|
|
143
|
+
* sixteenth of each on average. That is a segment with two
|
|
144
|
+
* generations behind it, whose row carries a summary; its first two loads make fewer requests and collect nothing,
|
|
145
|
+
* and the first load of a row written before rows carried a summary reads the current generation's index, one tail
|
|
146
|
+
* read, in place of the check that its object is there. A publish that loses a race to another writer
|
|
147
|
+
* reads the pointer again, and a load whose check finds the number taken (a crashed load's object, or the
|
|
148
|
+
* generations a rollback left above the pointer) lists the segment's objects to number past them and to collect,
|
|
149
|
+
* two PUT-class requests on S3 and two more pointer reads. So does every load that keeps two or more generations,
|
|
150
|
+
* which lists to collect: one more PUT-class request and two more pointer reads than the model counts. The counts
|
|
151
|
+
* are a cleartext segment's: an encrypted segment's load reads its row once more, after its ids and before it
|
|
152
|
+
* unwraps the key, one more GET ($0.40 per million at the default prices) that the model leaves out, beside the
|
|
153
|
+
* key-management calls it does not price either.
|
|
132
154
|
*/
|
|
133
155
|
readonly requestsPerLoad?: number;
|
|
134
156
|
/**
|
|
@@ -158,6 +180,26 @@ export interface Workload {
|
|
|
158
180
|
* write invalidates it, none of which this term prices.
|
|
159
181
|
*/
|
|
160
182
|
readonly genTtlMs?: number;
|
|
183
|
+
/**
|
|
184
|
+
* Segments the retention sweep (`retireExpired`) retires a month. Each costs 9 registry reads, 3 writes and a
|
|
185
|
+
* delete with {@link Workload.conditionalDelete} on, and 8 reads and 3 writes with it off, priced at the GET and PUT
|
|
186
|
+
* rates, a delete unbilled, as S3 leaves it. Default
|
|
187
|
+
* **0**.
|
|
188
|
+
*/
|
|
189
|
+
readonly retirementsPerMonth?: number;
|
|
190
|
+
/**
|
|
191
|
+
* Tombstones the sweep purges a month: at a steady state, as many as it retires a month, a `tombstoneGraceMs` later.
|
|
192
|
+
* A purge costs 4 reads and 2 deletes with {@link Workload.conditionalDelete} on, and 3 reads and a write with it
|
|
193
|
+
* off. Default **0**.
|
|
194
|
+
*/
|
|
195
|
+
readonly purgesPerMonth?: number;
|
|
196
|
+
/**
|
|
197
|
+
* Whether the registry's deletes remove a row for good (`RegCaps.conditionalDelete`), which the shipped registries
|
|
198
|
+
* do where the backend applies a delete precondition. Default **true**. `false` prices the sweep of a registry that
|
|
199
|
+
* only tombstones: a purge rewrites the row instead of deleting it, and every later full sweep reads each tombstone
|
|
200
|
+
* left (two reads per purged segment), which this term does not price.
|
|
201
|
+
*/
|
|
202
|
+
readonly conditionalDelete?: boolean;
|
|
161
203
|
}
|
|
162
204
|
/** One (group of) segment(s) for planning. `count` = how many like this (default 1). */
|
|
163
205
|
export interface SegmentSizing {
|
|
@@ -184,6 +226,11 @@ export interface CostReport {
|
|
|
184
226
|
readonly loads: number;
|
|
185
227
|
/** The pointer refresh of {@link Workload.hotSegments}. 0 unless `hotSegments` is set. */
|
|
186
228
|
readonly pointerRefresh: number;
|
|
229
|
+
/**
|
|
230
|
+
* The retention sweep's retirements and purges: each's registry reads at the GET rate and its writes at the PUT
|
|
231
|
+
* rate. 0 unless `retirementsPerMonth` or `purgesPerMonth` is set.
|
|
232
|
+
*/
|
|
233
|
+
readonly retention: number;
|
|
187
234
|
};
|
|
188
235
|
readonly total: number;
|
|
189
236
|
};
|
|
@@ -234,6 +281,40 @@ export interface CostReport {
|
|
|
234
281
|
readonly notes: readonly string[];
|
|
235
282
|
};
|
|
236
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* What the retention sweep makes per segment, counted with a store that counts its requests
|
|
286
|
+
* (`tests/core/retention-hard-purge.test.ts` holds each figure to the engine, and `tests/core/cost.test.ts` holds the
|
|
287
|
+
* estimator to these). With the registry's `conditionalDelete` on, a retirement files a due-index pointer to the
|
|
288
|
+
* tombstone and a purge removes the row and every pointer: a later sweep reads nothing of a purged segment. With it
|
|
289
|
+
* off, a retirement files no pointer, a purge rewrites the row as a tombstone, and every later full sweep reads what
|
|
290
|
+
* is left, two objects per purged segment.
|
|
291
|
+
*/
|
|
292
|
+
export declare const RETENTION_SWEEP_REQUESTS: Readonly<{
|
|
293
|
+
conditionalDelete: {
|
|
294
|
+
retirement: {
|
|
295
|
+
reads: number;
|
|
296
|
+
writes: number;
|
|
297
|
+
deletes: number;
|
|
298
|
+
};
|
|
299
|
+
purge: {
|
|
300
|
+
reads: number;
|
|
301
|
+
writes: number;
|
|
302
|
+
deletes: number;
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
tombstoning: {
|
|
306
|
+
retirement: {
|
|
307
|
+
reads: number;
|
|
308
|
+
writes: number;
|
|
309
|
+
deletes: number;
|
|
310
|
+
};
|
|
311
|
+
purge: {
|
|
312
|
+
reads: number;
|
|
313
|
+
writes: number;
|
|
314
|
+
deletes: number;
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
}>;
|
|
237
318
|
/**
|
|
238
319
|
* **Planning** cost estimate — pure, no instance or live data needed (sizing, sales, what-if). Segment sizes
|
|
239
320
|
* are taken as given (or roughly derived from cardinality); use the grounded `segment.costReport()` for
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Frozen `.crbm` v1
|
|
2
|
+
* Frozen `.crbm` v1 layout constants (format 1.0, with its optional extension block).
|
|
3
3
|
*
|
|
4
4
|
* These byte widths/offsets are pinned by the golden corpus and must never change for v1 —
|
|
5
5
|
* a new layout is a new format version. All multi-byte integers are little-endian (v1 fixes LE).
|
|
@@ -41,6 +41,12 @@ export declare const FOOTER_CRC_COVERAGE: 96;
|
|
|
41
41
|
export declare const FLAG_ENCRYPTED: number;
|
|
42
42
|
export declare const FLAG_INDEX_COMPRESSED: number;
|
|
43
43
|
export declare const FLAG_LITTLE_ENDIAN: number;
|
|
44
|
+
/**
|
|
45
|
+
* The object carries an extension block just before its index (a generation's metadata). Set only when there is
|
|
46
|
+
* something to put in it, so an object without metadata is the same bytes as before the block existed; a reader that
|
|
47
|
+
* does not know the bit refuses the object rather than reading past what it cannot see.
|
|
48
|
+
*/
|
|
49
|
+
export declare const FLAG_EXTENSION: number;
|
|
44
50
|
/** Flag bits a v1 reader understands; any bit outside this mask is an unsupported feature. */
|
|
45
51
|
export declare const KNOWN_FLAGS: number;
|
|
46
52
|
/**
|
|
@@ -98,10 +104,37 @@ export declare const DEFAULT_TAIL_BYTES: number;
|
|
|
98
104
|
* above the ~650 KB full-span worst case, well below a denial-of-wallet allocation.
|
|
99
105
|
*/
|
|
100
106
|
export declare const DEFAULT_MAX_INDEX_BYTES: number;
|
|
101
|
-
/**
|
|
102
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Largest serialized bitmap the engine decodes (1 MiB, per chunk). A well-formed 16-bit chunk serializes to at most
|
|
109
|
+
* about 8.2 KiB, so no legitimate chunk is near it; both the read engine and the erase path decode under this one cap.
|
|
110
|
+
*/
|
|
111
|
+
export declare const DEFAULT_MAX_BITMAP_BYTES: number;
|
|
103
112
|
/** A chunk's cardinality is in `[1, 65536]` (empty chunks are never written). */
|
|
104
113
|
export declare const MAX_CHUNK_CARDINALITY = 65536;
|
|
114
|
+
/**
|
|
115
|
+
* The extension block, present when the footer sets {@link FLAG_EXTENSION}: `section* ‖ u32 sectionsLength ‖ u32
|
|
116
|
+
* crc32c ‖ "CRBX"`, between the last payload and the index, so the index starts right after it. A reader finds it from
|
|
117
|
+
* `indexOffset`: the trailer is the {@link EXT_TRAILER_BYTES} bytes before the index, and the CRC32C covers the sections
|
|
118
|
+
* and the length field. Each section is `u8 type ‖ u32 length ‖ bytes`, in strictly ascending type order; a reader
|
|
119
|
+
* skips a type it does not know, so a section must be safe to ignore.
|
|
120
|
+
*/
|
|
121
|
+
export declare const EXT_MAGIC: Uint8Array<ArrayBuffer>;
|
|
122
|
+
/** The trailer: the sections' length (u32), their CRC32C (u32) and {@link EXT_MAGIC}. */
|
|
123
|
+
export declare const EXT_TRAILER_BYTES = 12;
|
|
124
|
+
/** A section's header: its type (u8) and its length (u32). */
|
|
125
|
+
export declare const EXT_SECTION_HEADER_BYTES = 5;
|
|
126
|
+
/**
|
|
127
|
+
* Cap on the sections of one block, in bytes. A reader refuses a larger block from its trailer alone, before it reads
|
|
128
|
+
* any sections beyond what it already holds (the read that brings the index may already hold up to this much before
|
|
129
|
+
* it), and a writer of any 1.x minor keeps within it, so a later section type stays readable by this reader.
|
|
130
|
+
*/
|
|
131
|
+
export declare const MAX_EXT_BYTES: number;
|
|
132
|
+
/**
|
|
133
|
+
* Section type 1: the generation's metadata, its canonical JSON in UTF-8. On an encrypted object the section is
|
|
134
|
+
* `nonce ‖ ciphertext ‖ tag`, sealed under the segment's key with the metadata scope of the associated data.
|
|
135
|
+
* Type 0 is not a section type: a reader refuses it.
|
|
136
|
+
*/
|
|
137
|
+
export declare const EXT_SECTION_METADATA = 1;
|
|
105
138
|
/** Fixed width of a per-chunk CRC32C field in the index (high-entropy → not varint). */
|
|
106
139
|
export declare const CRC32C_BYTES = 4;
|
|
107
140
|
/**
|
|
@@ -112,3 +145,9 @@ export declare const CRC32C_BYTES = 4;
|
|
|
112
145
|
*/
|
|
113
146
|
export declare const AEAD_NONCE_BYTES = 12;
|
|
114
147
|
export declare const AEAD_TAG_BYTES = 16;
|
|
148
|
+
/**
|
|
149
|
+
* Hard cap on one encrypted chunk's stored payload (a cleartext one is capped at the decode cap): the decode cap plus the AEAD framing, so an
|
|
150
|
+
* entry the engine would refuse to decode is refused when the object is opened, before any payload is read. No writer
|
|
151
|
+
* exceeds it: a stored payload is a serialized 16-bit chunk (at most about 8.2 KiB) plus, when encrypted, that framing.
|
|
152
|
+
*/
|
|
153
|
+
export declare const DEFAULT_MAX_PAYLOAD_BYTES: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BlobReader } from '../blob.js';
|
|
2
2
|
import type { CrbmCrypto } from '../crypto.js';
|
|
3
|
+
import type { GenerationMetadata } from '../ports.js';
|
|
3
4
|
/**
|
|
4
5
|
* A parsed index as parallel typed arrays, one slot per entry, ascending by key. Typed arrays hold each field at
|
|
5
6
|
* its own width with no per-entry object, so the retained heap is the arrays' byte length and nothing else.
|
|
@@ -30,11 +31,15 @@ export declare function indexCapacity(indexLength: number): number;
|
|
|
30
31
|
export interface CrbmReaderOptions {
|
|
31
32
|
/** Speculative tail size in bytes (default 256 KB; clamped up to at least the footer size). */
|
|
32
33
|
readonly tailBytes?: number;
|
|
33
|
-
/** Hard cap on a single chunk payload length (default
|
|
34
|
+
/** Hard cap on a single chunk payload length (default: the 1 MiB decode cap, plus the 28-byte nonce and tag on an encrypted object). The engine decodes under `maxBitmapBytes`; raise this with it. */
|
|
34
35
|
readonly maxPayloadBytes?: number;
|
|
35
36
|
/** Hard cap on the whole index region fetched/parsed from one object (default 8 MB). */
|
|
36
37
|
readonly maxIndexBytes?: number;
|
|
37
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Decryption context for an encrypted object (its DEK's AEAD + AAD builder). Required iff the object's footer sets
|
|
40
|
+
* `FLAG_ENCRYPTED`: an encrypted object opened without one throws {@link ValidationError}, and an object that is
|
|
41
|
+
* not encrypted, opened with one, is refused with {@link IntegrityError}.
|
|
42
|
+
*/
|
|
38
43
|
readonly crypto?: CrbmCrypto;
|
|
39
44
|
/**
|
|
40
45
|
* Opaque marker for the *incarnation of the name* this object belongs to — the caller's own identity for the
|
|
@@ -44,6 +49,15 @@ export interface CrbmReaderOptions {
|
|
|
44
49
|
*/
|
|
45
50
|
readonly lineage?: unknown;
|
|
46
51
|
}
|
|
52
|
+
/** An object's fingerprint, from its size and its footer's CRC (see {@link CrbmReader.fingerprint}). */
|
|
53
|
+
export declare const fingerprintFor: (size: number, footerCrc: number) => string;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the object behind `blob` says it is encrypted, from one tail read of a footer's worth, with no key and no
|
|
56
|
+
* index; a footer that fails its own checks throws, as an open would. For a caller that must not open an object with
|
|
57
|
+
* the key it holds: an erasure looking for an id in a cleartext object under an encrypted segment, and a rollback
|
|
58
|
+
* checking that its target is what the row says.
|
|
59
|
+
*/
|
|
60
|
+
export declare function footerSaysEncrypted(blob: BlobReader): Promise<boolean>;
|
|
47
61
|
export declare class CrbmReader {
|
|
48
62
|
private readonly blob;
|
|
49
63
|
private readonly objectSize;
|
|
@@ -52,12 +66,22 @@ export declare class CrbmReader {
|
|
|
52
66
|
readonly lineage: unknown;
|
|
53
67
|
readonly totalCardinality: number;
|
|
54
68
|
private readonly index;
|
|
55
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* True when `open()` satisfied the index, and any extension block, from the tail GET alone (no second range
|
|
71
|
+
* read).
|
|
72
|
+
*/
|
|
56
73
|
readonly servedFromTail: boolean;
|
|
57
74
|
/** Set iff the object is encrypted — used to decrypt each chunk payload in {@link getChunk}. */
|
|
58
75
|
private readonly crypto;
|
|
59
76
|
/** The footer's own CRC, as verified at open: it covers the index's CRC, the chunk count and the generation. */
|
|
60
77
|
private readonly footerCrc;
|
|
78
|
+
/**
|
|
79
|
+
* The metadata the generation was written with, checked and decrypted at open; `undefined` when it has none
|
|
80
|
+
* (every format 1.0 object). Frozen: every caller shares one copy.
|
|
81
|
+
*/
|
|
82
|
+
readonly metadata: GenerationMetadata | undefined;
|
|
83
|
+
/** What that metadata adds to {@link retainedBytes}; 0 when there is none. */
|
|
84
|
+
private readonly metadataWeight;
|
|
61
85
|
private constructor();
|
|
62
86
|
/**
|
|
63
87
|
* Which object this reader opened, as its size and its footer's CRC. The footer covers the index's CRC, so two
|
|
@@ -73,6 +97,13 @@ export declare class CrbmReader {
|
|
|
73
97
|
* arrays that hold it, {@link RETAINED_BYTES_PER_INDEX_ENTRY} per entry.
|
|
74
98
|
*/
|
|
75
99
|
get retainedIndexBytes(): number;
|
|
100
|
+
/**
|
|
101
|
+
* What this reader holds, the weight the storage reader cache bounds on: its parsed index
|
|
102
|
+
* ({@link retainedIndexBytes}), plus a weight for the metadata it decoded, from its canonical length and its number
|
|
103
|
+
* of keys. Equal to {@link retainedIndexBytes} for a generation with no metadata. A cache heuristic, not a contract:
|
|
104
|
+
* how the metadata is weighed may change in any release.
|
|
105
|
+
*/
|
|
106
|
+
get retainedBytes(): number;
|
|
76
107
|
/** Per-chunk cardinality (`chunkKey → count`) from the parsed index — no payload reads. */
|
|
77
108
|
cardinalities(): Map<number, number>;
|
|
78
109
|
/**
|
|
@@ -100,4 +131,15 @@ export declare class CrbmReader {
|
|
|
100
131
|
*/
|
|
101
132
|
getChunk(chunkKey: number): Promise<Uint8Array | null>;
|
|
102
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Read the sections of an extension block, its bytes before the trailer, once their CRC has passed. Each section is
|
|
136
|
+
* `u8 type ‖ u32 length ‖ bytes`, in strictly ascending type order, and together they fill the region exactly. Type 1
|
|
137
|
+
* is the generation's metadata, held to the metadata rules and its canonical form (and opened first with `crypto`,
|
|
138
|
+
* under the metadata scope, when the object is encrypted); a type this build does not know is a later build's, and
|
|
139
|
+
* is skipped. Returns the metadata, or `undefined` when the block has none. Every other shape is an
|
|
140
|
+
* {@link IntegrityError}.
|
|
141
|
+
*
|
|
142
|
+
* Exported for the coverage-guided fuzz harness, which drives it directly, past the block's CRC; not public API.
|
|
143
|
+
*/
|
|
144
|
+
export declare function parseExtension(sections: Uint8Array, crypto: CrbmCrypto | undefined): GenerationMetadata | undefined;
|
|
103
145
|
export declare function parseIndex(indexBytes: Uint8Array, payloadEnd: number, maxPayloadBytes: number, minPayloadBytes?: number): ParsedIndex;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CrbmCrypto } from '../crypto.js';
|
|
2
|
+
import type { GenerationMetadata } from '../ports.js';
|
|
2
3
|
import type { BlobSink } from '../blob.js';
|
|
3
4
|
export interface CrbmWriterOptions {
|
|
4
5
|
/** Self-describing generation number (also encoded in the object key). */
|
|
@@ -13,6 +14,13 @@ export interface CrbmWriterOptions {
|
|
|
13
14
|
readonly elementWidth?: number;
|
|
14
15
|
/** When set, payloads + index are AES-256-GCM-encrypted, bound to `(segment, generation, scope)` via AAD. */
|
|
15
16
|
readonly crypto?: CrbmCrypto;
|
|
17
|
+
/**
|
|
18
|
+
* The generation's metadata, checked against the metadata rules and copied as its canonical JSON when the writer
|
|
19
|
+
* is constructed, so a later change to the caller's object never reaches the object. A value that breaks a rule
|
|
20
|
+
* throws {@link ValidationError} from the constructor, before a byte is written. None, or the empty object, writes
|
|
21
|
+
* format 1.0.
|
|
22
|
+
*/
|
|
23
|
+
readonly metadata?: GenerationMetadata;
|
|
16
24
|
}
|
|
17
25
|
export declare class CrbmWriter {
|
|
18
26
|
private readonly sink;
|
|
@@ -23,17 +31,38 @@ export declare class CrbmWriter {
|
|
|
23
31
|
private totalCardinality;
|
|
24
32
|
private preambleWritten;
|
|
25
33
|
private finished;
|
|
34
|
+
/** The metadata's canonical JSON, copied at construction; `undefined` leaves the footer's extension flag clear. */
|
|
35
|
+
private readonly metadata;
|
|
36
|
+
/** Bytes written so far, the footer included once {@link finish} has run. */
|
|
37
|
+
private written;
|
|
38
|
+
private footerCrc32c;
|
|
26
39
|
constructor(sink: BlobSink, options: CrbmWriterOptions);
|
|
27
40
|
/**
|
|
28
41
|
* Append one chunk's payload (the roaring-serialized bytes) with its cardinality. `chunkKey` must be
|
|
29
42
|
* strictly greater than the previous one (ascending, no duplicates); `cardinality` is `[1, 65536]`.
|
|
30
43
|
*/
|
|
31
44
|
addChunk(chunkKey: number, payload: Uint8Array, cardinality: number): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Write the extension block (when there is metadata), the index region and the fixed footer. After this the object
|
|
47
|
+
* is complete and immutable.
|
|
48
|
+
*/
|
|
35
49
|
finish(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* The finished object's size and its footer's CRC: what names it, as a reader's fingerprint does. A writer that
|
|
52
|
+
* has to tell its own object from another stored later under the same key compares these with the object's
|
|
53
|
+
* footer. Throws before {@link finish}.
|
|
54
|
+
*/
|
|
55
|
+
get identity(): {
|
|
56
|
+
readonly size: number;
|
|
57
|
+
readonly footerCrc: number;
|
|
58
|
+
};
|
|
36
59
|
private ensurePreamble;
|
|
60
|
+
/**
|
|
61
|
+
* The extension block: one metadata section (`u8 type ‖ u32 length ‖ bytes`), then the trailer
|
|
62
|
+
* (`u32 sectionsLength ‖ u32 crc32c(sections ‖ sectionsLength) ‖ "CRBX"`). Encrypted, the section holds
|
|
63
|
+
* `nonce ‖ ciphertext ‖ tag`, sealed with the metadata scope of the associated data.
|
|
64
|
+
*/
|
|
65
|
+
private extensionBlock;
|
|
37
66
|
private encodeIndex;
|
|
38
67
|
private buildFooter;
|
|
39
68
|
}
|