@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.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` **serialization id** (`ROARING_PORTABLE_ID`
24
- * in `crbm/format.ts`): it is stamped in the footer and validated on read, and generalizes to a per-codec id
25
- * *with* the second codec's format work — a documented follow-up, not part of this interface.
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
- /** `'absent'` (no registry row), `'already'` (already destroyed), or `'cleartext'` (no DEK to shred). */
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
- readonly roaringSerializationId?: number;
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` **serialization id** (`ROARING_PORTABLE_ID`
24
- * in `crbm/format.ts`): it is stamped in the footer and validated on read, and generalizes to a per-codec id
25
- * *with* the second codec's format work — a documented follow-up, not part of this interface.
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
- /** `'absent'` (no registry row), `'already'` (already destroyed), or `'cleartext'` (no DEK to shred). */
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
- readonly roaringSerializationId?: number;
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
- roaringSerializationId: 24,
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 ROARING_PORTABLE_ID = 1;
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 serializationId = fview.getUint16(FOOTER.roaringSerializationId, true);
1524
- if (serializationId !== ROARING_PORTABLE_ID) {
1526
+ const payloadCodecId = fview.getUint16(FOOTER.payloadCodecId, true);
1527
+ if (!KNOWN_PAYLOAD_CODEC_IDS.has(payloadCodecId)) {
1525
1528
  throw new UnsupportedError(
1526
- `.crbm roaring_serialization_id ${serializationId} not supported (v1 is portable=${ROARING_PORTABLE_ID})`
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.roaringSerializationId,
1822
- this.options.roaringSerializationId ?? ROARING_PORTABLE_ID,
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
- const result = await shredSegment(ref, deps, options.allowCleartext ?? false);
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;