@cloudbitmaps/core 0.5.0 → 0.7.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/index.cjs +29 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -5
- package/dist/index.d.ts +44 -5
- package/dist/index.js +29 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -20,9 +20,13 @@ export { o as BufferReader, p as BufferSink, q as GovernanceMeta, r as NO_ROW, s
|
|
|
20
20
|
* **Interface surface = exactly what the engine needs**, kept general enough for the known fast-follow codecs
|
|
21
21
|
* (positional/rank-select access + raw-bitset interop that `@cloudbitmaps/bitset` will add live on that
|
|
22
22
|
* package's own extended value type, not here — the engine never calls them). The one codec-linked concern
|
|
23
|
-
* this seam intentionally leaves in the format layer is the `.crbm` **
|
|
24
|
-
* in `crbm/format.ts`): it is stamped in the footer
|
|
25
|
-
*
|
|
23
|
+
* this seam intentionally leaves in the format layer is the `.crbm` **payload codec id**
|
|
24
|
+
* (`PAYLOAD_CODEC_ROARING_PORTABLE` / `KNOWN_PAYLOAD_CODEC_IDS` in `crbm/format.ts`): it is stamped in the footer
|
|
25
|
+
* and validated on read. That generalization is **done** as of 0.7.0 — the field was `roaringSerializationId`
|
|
26
|
+
* and validated by equality against a single constant, which would have frozen a codec-specific name into the
|
|
27
|
+
* format at `1.0`. It is now a registry the reader checks membership against, so a future codec is a one-line
|
|
28
|
+
* registration rather than a major format version. Still deliberately outside this interface: a codec declares
|
|
29
|
+
* its bytes, the seam does not.
|
|
26
30
|
*/
|
|
27
31
|
/**
|
|
28
32
|
* The value type a codec produces — a mutable set of `u32` with set algebra and portable (de)serialization.
|
|
@@ -61,6 +65,27 @@ interface CodecBitmap {
|
|
|
61
65
|
* Roaring answers it in O(1) from its container index, so the flagship codec implements it.
|
|
62
66
|
*/
|
|
63
67
|
maximum?(): number | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Re-encode for size, in place, immediately before a **cold** write. Representation only — this must never
|
|
70
|
+
* change membership, and `serialize()` afterwards must decode back to exactly the same set.
|
|
71
|
+
*
|
|
72
|
+
* **Optional, like {@link maximum}.** A codec whose encoding has no size decision to make (a plain bitset has
|
|
73
|
+
* one representation and nothing to choose) simply omits it, and the engine skips the call.
|
|
74
|
+
*
|
|
75
|
+
* WHY THIS EXISTS. Roaring picks per container between an array, a bitset and a RUN — but the run choice is
|
|
76
|
+
* not automatic in any implementation: it is a `runOptimize()` pass you have to ask for, and nothing here was
|
|
77
|
+
* asking. So two of the three container types were ever used, and run-shaped data paid list or bitmap prices
|
|
78
|
+
* for a run. Measured on the shipped codec: a contiguous 1,000,000-id range serialized to 128.1 KiB where
|
|
79
|
+
* run-encoding needs **0.2 KiB** (570×), and a 2,000-run shape 536.5 KiB against **8.5 KiB** (63×). Sparse
|
|
80
|
+
* data is unchanged, because there are no runs to find — the pass is not a gamble.
|
|
81
|
+
*
|
|
82
|
+
* WHY ONLY ON THE COLD PATH. This is called where a whole immutable generation is written, so its cost
|
|
83
|
+
* amortizes over a write that is already serializing and checksumming every chunk. It is deliberately NOT
|
|
84
|
+
* called on the warm delta path (`chunk.ts`), which runs per operation: the hot path must not pay for a
|
|
85
|
+
* rare-ish win, per KISS/YAGNI. Warm rows are short-lived and get folded into a cold generation by
|
|
86
|
+
* compaction, where they are optimized then.
|
|
87
|
+
*/
|
|
88
|
+
optimize?(): void;
|
|
64
89
|
}
|
|
65
90
|
/**
|
|
66
91
|
* A pluggable bitmap codec — the factory the codec-agnostic engine constructs {@link CodecBitmap}s through.
|
|
@@ -2021,7 +2046,15 @@ interface DestroyResult {
|
|
|
2021
2046
|
* `segment.erase` audit event keys off — a cleartext "erase" is not an irreversible destruction.
|
|
2022
2047
|
*/
|
|
2023
2048
|
readonly cryptoShredded: boolean;
|
|
2024
|
-
/**
|
|
2049
|
+
/**
|
|
2050
|
+
* Why this segment was not a fresh crypto-shred. `'absent'` (no registry row), `'already'` (already
|
|
2051
|
+
* destroyed), `'cleartext'` (no DEK to shred) — and from `eraseNamespace` only, where per-segment faults are
|
|
2052
|
+
* isolated so one failure cannot discard the ledger: `'contended'` (warm rows were rewritten during every
|
|
2053
|
+
* erase pass) or `` `failed: <message>` `` (any other fault).
|
|
2054
|
+
*
|
|
2055
|
+
* The last two come with `destroyed: false` and mean **the segment still holds data**. A namespace erase
|
|
2056
|
+
* returns them rather than throwing, so its entries have to be inspected — see {@link eraseNamespace}.
|
|
2057
|
+
*/
|
|
2025
2058
|
readonly reason?: string;
|
|
2026
2059
|
/** Warm rows physically deleted as part of the erase. */
|
|
2027
2060
|
readonly warmRowsDeleted: number;
|
|
@@ -2256,7 +2289,13 @@ declare class RetryingRegistryDriver implements IRegistryDriver {
|
|
|
2256
2289
|
interface CrbmWriterOptions {
|
|
2257
2290
|
/** Self-describing generation number (also encoded in the object key). */
|
|
2258
2291
|
readonly generation: number;
|
|
2259
|
-
|
|
2292
|
+
/**
|
|
2293
|
+
* Which codec produced the chunk payloads. Defaults to {@link PAYLOAD_CODEC_ROARING_PORTABLE}.
|
|
2294
|
+
*
|
|
2295
|
+
* Renamed from `roaringSerializationId` in 0.7.0 — same footer field, same offset and width, generalized
|
|
2296
|
+
* because `.crbm` is a shared container and a second codec is expected before the format freezes.
|
|
2297
|
+
*/
|
|
2298
|
+
readonly payloadCodecId?: number;
|
|
2260
2299
|
readonly elementWidth?: number;
|
|
2261
2300
|
/** When set, payloads + index are AES-256-GCM-encrypted, bound to `(segment, generation, scope)` via AAD. */
|
|
2262
2301
|
readonly crypto?: CrbmCrypto;
|
package/dist/index.d.ts
CHANGED
|
@@ -20,9 +20,13 @@ export { o as BufferReader, p as BufferSink, q as GovernanceMeta, r as NO_ROW, s
|
|
|
20
20
|
* **Interface surface = exactly what the engine needs**, kept general enough for the known fast-follow codecs
|
|
21
21
|
* (positional/rank-select access + raw-bitset interop that `@cloudbitmaps/bitset` will add live on that
|
|
22
22
|
* package's own extended value type, not here — the engine never calls them). The one codec-linked concern
|
|
23
|
-
* this seam intentionally leaves in the format layer is the `.crbm` **
|
|
24
|
-
* in `crbm/format.ts`): it is stamped in the footer
|
|
25
|
-
*
|
|
23
|
+
* this seam intentionally leaves in the format layer is the `.crbm` **payload codec id**
|
|
24
|
+
* (`PAYLOAD_CODEC_ROARING_PORTABLE` / `KNOWN_PAYLOAD_CODEC_IDS` in `crbm/format.ts`): it is stamped in the footer
|
|
25
|
+
* and validated on read. That generalization is **done** as of 0.7.0 — the field was `roaringSerializationId`
|
|
26
|
+
* and validated by equality against a single constant, which would have frozen a codec-specific name into the
|
|
27
|
+
* format at `1.0`. It is now a registry the reader checks membership against, so a future codec is a one-line
|
|
28
|
+
* registration rather than a major format version. Still deliberately outside this interface: a codec declares
|
|
29
|
+
* its bytes, the seam does not.
|
|
26
30
|
*/
|
|
27
31
|
/**
|
|
28
32
|
* The value type a codec produces — a mutable set of `u32` with set algebra and portable (de)serialization.
|
|
@@ -61,6 +65,27 @@ interface CodecBitmap {
|
|
|
61
65
|
* Roaring answers it in O(1) from its container index, so the flagship codec implements it.
|
|
62
66
|
*/
|
|
63
67
|
maximum?(): number | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Re-encode for size, in place, immediately before a **cold** write. Representation only — this must never
|
|
70
|
+
* change membership, and `serialize()` afterwards must decode back to exactly the same set.
|
|
71
|
+
*
|
|
72
|
+
* **Optional, like {@link maximum}.** A codec whose encoding has no size decision to make (a plain bitset has
|
|
73
|
+
* one representation and nothing to choose) simply omits it, and the engine skips the call.
|
|
74
|
+
*
|
|
75
|
+
* WHY THIS EXISTS. Roaring picks per container between an array, a bitset and a RUN — but the run choice is
|
|
76
|
+
* not automatic in any implementation: it is a `runOptimize()` pass you have to ask for, and nothing here was
|
|
77
|
+
* asking. So two of the three container types were ever used, and run-shaped data paid list or bitmap prices
|
|
78
|
+
* for a run. Measured on the shipped codec: a contiguous 1,000,000-id range serialized to 128.1 KiB where
|
|
79
|
+
* run-encoding needs **0.2 KiB** (570×), and a 2,000-run shape 536.5 KiB against **8.5 KiB** (63×). Sparse
|
|
80
|
+
* data is unchanged, because there are no runs to find — the pass is not a gamble.
|
|
81
|
+
*
|
|
82
|
+
* WHY ONLY ON THE COLD PATH. This is called where a whole immutable generation is written, so its cost
|
|
83
|
+
* amortizes over a write that is already serializing and checksumming every chunk. It is deliberately NOT
|
|
84
|
+
* called on the warm delta path (`chunk.ts`), which runs per operation: the hot path must not pay for a
|
|
85
|
+
* rare-ish win, per KISS/YAGNI. Warm rows are short-lived and get folded into a cold generation by
|
|
86
|
+
* compaction, where they are optimized then.
|
|
87
|
+
*/
|
|
88
|
+
optimize?(): void;
|
|
64
89
|
}
|
|
65
90
|
/**
|
|
66
91
|
* A pluggable bitmap codec — the factory the codec-agnostic engine constructs {@link CodecBitmap}s through.
|
|
@@ -2021,7 +2046,15 @@ interface DestroyResult {
|
|
|
2021
2046
|
* `segment.erase` audit event keys off — a cleartext "erase" is not an irreversible destruction.
|
|
2022
2047
|
*/
|
|
2023
2048
|
readonly cryptoShredded: boolean;
|
|
2024
|
-
/**
|
|
2049
|
+
/**
|
|
2050
|
+
* Why this segment was not a fresh crypto-shred. `'absent'` (no registry row), `'already'` (already
|
|
2051
|
+
* destroyed), `'cleartext'` (no DEK to shred) — and from `eraseNamespace` only, where per-segment faults are
|
|
2052
|
+
* isolated so one failure cannot discard the ledger: `'contended'` (warm rows were rewritten during every
|
|
2053
|
+
* erase pass) or `` `failed: <message>` `` (any other fault).
|
|
2054
|
+
*
|
|
2055
|
+
* The last two come with `destroyed: false` and mean **the segment still holds data**. A namespace erase
|
|
2056
|
+
* returns them rather than throwing, so its entries have to be inspected — see {@link eraseNamespace}.
|
|
2057
|
+
*/
|
|
2025
2058
|
readonly reason?: string;
|
|
2026
2059
|
/** Warm rows physically deleted as part of the erase. */
|
|
2027
2060
|
readonly warmRowsDeleted: number;
|
|
@@ -2256,7 +2289,13 @@ declare class RetryingRegistryDriver implements IRegistryDriver {
|
|
|
2256
2289
|
interface CrbmWriterOptions {
|
|
2257
2290
|
/** Self-describing generation number (also encoded in the object key). */
|
|
2258
2291
|
readonly generation: number;
|
|
2259
|
-
|
|
2292
|
+
/**
|
|
2293
|
+
* Which codec produced the chunk payloads. Defaults to {@link PAYLOAD_CODEC_ROARING_PORTABLE}.
|
|
2294
|
+
*
|
|
2295
|
+
* Renamed from `roaringSerializationId` in 0.7.0 — same footer field, same offset and width, generalized
|
|
2296
|
+
* because `.crbm` is a shared container and a second codec is expected before the format freezes.
|
|
2297
|
+
*/
|
|
2298
|
+
readonly payloadCodecId?: number;
|
|
2260
2299
|
readonly elementWidth?: number;
|
|
2261
2300
|
/** When set, payloads + index are AES-256-GCM-encrypted, bound to `(segment, generation, scope)` via AAD. */
|
|
2262
2301
|
readonly crypto?: CrbmCrypto;
|
package/dist/index.js
CHANGED
|
@@ -1394,8 +1394,8 @@ var FOOTER = {
|
|
|
1394
1394
|
// u32
|
|
1395
1395
|
flags: 20,
|
|
1396
1396
|
// u32
|
|
1397
|
-
|
|
1398
|
-
// u16
|
|
1397
|
+
payloadCodecId: 24,
|
|
1398
|
+
// u16 — was `roaringSerializationId`; same offset, same width, wider meaning
|
|
1399
1399
|
elementWidth: 26,
|
|
1400
1400
|
// u8
|
|
1401
1401
|
containerCodec: 27,
|
|
@@ -1423,7 +1423,10 @@ var FOOTER_CRC_COVERAGE = FOOTER.footerCrc32c;
|
|
|
1423
1423
|
var FLAG_ENCRYPTED = 1 << 0;
|
|
1424
1424
|
var FLAG_LITTLE_ENDIAN = 1 << 2;
|
|
1425
1425
|
var ELEMENT_WIDTH_32 = 32;
|
|
1426
|
-
var
|
|
1426
|
+
var PAYLOAD_CODEC_ROARING_PORTABLE = 1;
|
|
1427
|
+
var KNOWN_PAYLOAD_CODEC_IDS = /* @__PURE__ */ new Set([
|
|
1428
|
+
PAYLOAD_CODEC_ROARING_PORTABLE
|
|
1429
|
+
]);
|
|
1427
1430
|
var CONTAINER_CODEC_NONE = 0;
|
|
1428
1431
|
var DEFAULT_TAIL_BYTES = 256 * 1024;
|
|
1429
1432
|
var DEFAULT_MAX_INDEX_BYTES = 8 * 1024 * 1024;
|
|
@@ -1520,10 +1523,10 @@ var CrbmReader = class _CrbmReader {
|
|
|
1520
1523
|
`.crbm element_width ${elementWidth} not supported (v1 reads 32-bit ids; 64-bit is a future major version)`
|
|
1521
1524
|
);
|
|
1522
1525
|
}
|
|
1523
|
-
const
|
|
1524
|
-
if (
|
|
1526
|
+
const payloadCodecId = fview.getUint16(FOOTER.payloadCodecId, true);
|
|
1527
|
+
if (!KNOWN_PAYLOAD_CODEC_IDS.has(payloadCodecId)) {
|
|
1525
1528
|
throw new UnsupportedError(
|
|
1526
|
-
`.crbm
|
|
1529
|
+
`.crbm payload_codec_id ${payloadCodecId} not supported by this build (known: ${[...KNOWN_PAYLOAD_CODEC_IDS].join(", ")}; ${PAYLOAD_CODEC_ROARING_PORTABLE}=roaring portable). A generation written by a different codec is rejected rather than decoded \u2014 a store uses one codec throughout.`
|
|
1527
1530
|
);
|
|
1528
1531
|
}
|
|
1529
1532
|
const containerCodec = footer[FOOTER.containerCodec];
|
|
@@ -1818,8 +1821,8 @@ var CrbmWriter = class {
|
|
|
1818
1821
|
view.setUint32(FOOTER.indexCrc32c, crc32c(indexRegion), true);
|
|
1819
1822
|
view.setUint32(FOOTER.flags, FLAG_LITTLE_ENDIAN | (encrypted ? FLAG_ENCRYPTED : 0), true);
|
|
1820
1823
|
view.setUint16(
|
|
1821
|
-
FOOTER.
|
|
1822
|
-
this.options.
|
|
1824
|
+
FOOTER.payloadCodecId,
|
|
1825
|
+
this.options.payloadCodecId ?? PAYLOAD_CODEC_ROARING_PORTABLE,
|
|
1823
1826
|
true
|
|
1824
1827
|
);
|
|
1825
1828
|
footer[FOOTER.elementWidth] = this.options.elementWidth ?? ELEMENT_WIDTH_32;
|
|
@@ -2074,6 +2077,7 @@ function writeCrbmGeneration(driver, key, chunks, options = {}) {
|
|
|
2074
2077
|
const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
|
|
2075
2078
|
for (const { chunkKey, bitmap } of sorted) {
|
|
2076
2079
|
if (bitmap.isEmpty) continue;
|
|
2080
|
+
bitmap.optimize?.();
|
|
2077
2081
|
await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
|
|
2078
2082
|
const pause = tick();
|
|
2079
2083
|
if (pause !== null) await pause;
|
|
@@ -2089,6 +2093,7 @@ async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
|
|
|
2089
2093
|
const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
|
|
2090
2094
|
for await (const { chunkKey, bitmap } of chunks) {
|
|
2091
2095
|
if (bitmap.isEmpty) continue;
|
|
2096
|
+
bitmap.optimize?.();
|
|
2092
2097
|
await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
|
|
2093
2098
|
chunkKeys.push(chunkKey);
|
|
2094
2099
|
cardinality += bitmap.size;
|
|
@@ -3671,7 +3676,22 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
3671
3676
|
const destroyed = [];
|
|
3672
3677
|
let segmentsShredded = 0;
|
|
3673
3678
|
for (const ref of refs) {
|
|
3674
|
-
|
|
3679
|
+
let result;
|
|
3680
|
+
try {
|
|
3681
|
+
result = await shredSegment(ref, deps, options.allowCleartext ?? false);
|
|
3682
|
+
} catch (err) {
|
|
3683
|
+
result = {
|
|
3684
|
+
segment: ref.segment,
|
|
3685
|
+
namespace: ref.namespace,
|
|
3686
|
+
destroyed: false,
|
|
3687
|
+
cryptoShredded: false,
|
|
3688
|
+
reason: isWriteConflictError(err) ? "contended" : `failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3689
|
+
// Deliberately 0, and not a partial count. `eraseWarm` may have deleted rows before it threw, but the
|
|
3690
|
+
// erase did not complete as a unit, so a partial figure on a failed entry would invite reading it as
|
|
3691
|
+
// progress toward a destruction that did not happen. The attestation that matters is `destroyed`.
|
|
3692
|
+
warmRowsDeleted: 0
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3675
3695
|
destroyed.push(result);
|
|
3676
3696
|
if (result.cryptoShredded) {
|
|
3677
3697
|
segmentsShredded += 1;
|