@cloudbitmaps/core 0.5.0 → 0.6.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
@@ -61,6 +61,27 @@ interface CodecBitmap {
61
61
  * Roaring answers it in O(1) from its container index, so the flagship codec implements it.
62
62
  */
63
63
  maximum?(): number | undefined;
64
+ /**
65
+ * Re-encode for size, in place, immediately before a **cold** write. Representation only — this must never
66
+ * change membership, and `serialize()` afterwards must decode back to exactly the same set.
67
+ *
68
+ * **Optional, like {@link maximum}.** A codec whose encoding has no size decision to make (a plain bitset has
69
+ * one representation and nothing to choose) simply omits it, and the engine skips the call.
70
+ *
71
+ * WHY THIS EXISTS. Roaring picks per container between an array, a bitset and a RUN — but the run choice is
72
+ * not automatic in any implementation: it is a `runOptimize()` pass you have to ask for, and nothing here was
73
+ * asking. So two of the three container types were ever used, and run-shaped data paid list or bitmap prices
74
+ * for a run. Measured on the shipped codec: a contiguous 1,000,000-id range serialized to 128.1 KiB where
75
+ * run-encoding needs **0.2 KiB** (570×), and a 2,000-run shape 536.5 KiB against **8.5 KiB** (63×). Sparse
76
+ * data is unchanged, because there are no runs to find — the pass is not a gamble.
77
+ *
78
+ * WHY ONLY ON THE COLD PATH. This is called where a whole immutable generation is written, so its cost
79
+ * amortizes over a write that is already serializing and checksumming every chunk. It is deliberately NOT
80
+ * called on the warm delta path (`chunk.ts`), which runs per operation: the hot path must not pay for a
81
+ * rare-ish win, per KISS/YAGNI. Warm rows are short-lived and get folded into a cold generation by
82
+ * compaction, where they are optimized then.
83
+ */
84
+ optimize?(): void;
64
85
  }
65
86
  /**
66
87
  * A pluggable bitmap codec — the factory the codec-agnostic engine constructs {@link CodecBitmap}s through.
@@ -2021,7 +2042,15 @@ interface DestroyResult {
2021
2042
  * `segment.erase` audit event keys off — a cleartext "erase" is not an irreversible destruction.
2022
2043
  */
2023
2044
  readonly cryptoShredded: boolean;
2024
- /** `'absent'` (no registry row), `'already'` (already destroyed), or `'cleartext'` (no DEK to shred). */
2045
+ /**
2046
+ * Why this segment was not a fresh crypto-shred. `'absent'` (no registry row), `'already'` (already
2047
+ * destroyed), `'cleartext'` (no DEK to shred) — and from `eraseNamespace` only, where per-segment faults are
2048
+ * isolated so one failure cannot discard the ledger: `'contended'` (warm rows were rewritten during every
2049
+ * erase pass) or `` `failed: <message>` `` (any other fault).
2050
+ *
2051
+ * The last two come with `destroyed: false` and mean **the segment still holds data**. A namespace erase
2052
+ * returns them rather than throwing, so its entries have to be inspected — see {@link eraseNamespace}.
2053
+ */
2025
2054
  readonly reason?: string;
2026
2055
  /** Warm rows physically deleted as part of the erase. */
2027
2056
  readonly warmRowsDeleted: number;
package/dist/index.d.ts CHANGED
@@ -61,6 +61,27 @@ interface CodecBitmap {
61
61
  * Roaring answers it in O(1) from its container index, so the flagship codec implements it.
62
62
  */
63
63
  maximum?(): number | undefined;
64
+ /**
65
+ * Re-encode for size, in place, immediately before a **cold** write. Representation only — this must never
66
+ * change membership, and `serialize()` afterwards must decode back to exactly the same set.
67
+ *
68
+ * **Optional, like {@link maximum}.** A codec whose encoding has no size decision to make (a plain bitset has
69
+ * one representation and nothing to choose) simply omits it, and the engine skips the call.
70
+ *
71
+ * WHY THIS EXISTS. Roaring picks per container between an array, a bitset and a RUN — but the run choice is
72
+ * not automatic in any implementation: it is a `runOptimize()` pass you have to ask for, and nothing here was
73
+ * asking. So two of the three container types were ever used, and run-shaped data paid list or bitmap prices
74
+ * for a run. Measured on the shipped codec: a contiguous 1,000,000-id range serialized to 128.1 KiB where
75
+ * run-encoding needs **0.2 KiB** (570×), and a 2,000-run shape 536.5 KiB against **8.5 KiB** (63×). Sparse
76
+ * data is unchanged, because there are no runs to find — the pass is not a gamble.
77
+ *
78
+ * WHY ONLY ON THE COLD PATH. This is called where a whole immutable generation is written, so its cost
79
+ * amortizes over a write that is already serializing and checksumming every chunk. It is deliberately NOT
80
+ * called on the warm delta path (`chunk.ts`), which runs per operation: the hot path must not pay for a
81
+ * rare-ish win, per KISS/YAGNI. Warm rows are short-lived and get folded into a cold generation by
82
+ * compaction, where they are optimized then.
83
+ */
84
+ optimize?(): void;
64
85
  }
65
86
  /**
66
87
  * A pluggable bitmap codec — the factory the codec-agnostic engine constructs {@link CodecBitmap}s through.
@@ -2021,7 +2042,15 @@ interface DestroyResult {
2021
2042
  * `segment.erase` audit event keys off — a cleartext "erase" is not an irreversible destruction.
2022
2043
  */
2023
2044
  readonly cryptoShredded: boolean;
2024
- /** `'absent'` (no registry row), `'already'` (already destroyed), or `'cleartext'` (no DEK to shred). */
2045
+ /**
2046
+ * Why this segment was not a fresh crypto-shred. `'absent'` (no registry row), `'already'` (already
2047
+ * destroyed), `'cleartext'` (no DEK to shred) — and from `eraseNamespace` only, where per-segment faults are
2048
+ * isolated so one failure cannot discard the ledger: `'contended'` (warm rows were rewritten during every
2049
+ * erase pass) or `` `failed: <message>` `` (any other fault).
2050
+ *
2051
+ * The last two come with `destroyed: false` and mean **the segment still holds data**. A namespace erase
2052
+ * returns them rather than throwing, so its entries have to be inspected — see {@link eraseNamespace}.
2053
+ */
2025
2054
  readonly reason?: string;
2026
2055
  /** Warm rows physically deleted as part of the erase. */
2027
2056
  readonly warmRowsDeleted: number;
package/dist/index.js CHANGED
@@ -2074,6 +2074,7 @@ function writeCrbmGeneration(driver, key, chunks, options = {}) {
2074
2074
  const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
2075
2075
  for (const { chunkKey, bitmap } of sorted) {
2076
2076
  if (bitmap.isEmpty) continue;
2077
+ bitmap.optimize?.();
2077
2078
  await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
2078
2079
  const pause = tick();
2079
2080
  if (pause !== null) await pause;
@@ -2089,6 +2090,7 @@ async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
2089
2090
  const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
2090
2091
  for await (const { chunkKey, bitmap } of chunks) {
2091
2092
  if (bitmap.isEmpty) continue;
2093
+ bitmap.optimize?.();
2092
2094
  await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
2093
2095
  chunkKeys.push(chunkKey);
2094
2096
  cardinality += bitmap.size;
@@ -3671,7 +3673,22 @@ async function eraseNamespace(namespace, deps, options) {
3671
3673
  const destroyed = [];
3672
3674
  let segmentsShredded = 0;
3673
3675
  for (const ref of refs) {
3674
- const result = await shredSegment(ref, deps, options.allowCleartext ?? false);
3676
+ let result;
3677
+ try {
3678
+ result = await shredSegment(ref, deps, options.allowCleartext ?? false);
3679
+ } catch (err) {
3680
+ result = {
3681
+ segment: ref.segment,
3682
+ namespace: ref.namespace,
3683
+ destroyed: false,
3684
+ cryptoShredded: false,
3685
+ reason: isWriteConflictError(err) ? "contended" : `failed: ${err instanceof Error ? err.message : String(err)}`,
3686
+ // Deliberately 0, and not a partial count. `eraseWarm` may have deleted rows before it threw, but the
3687
+ // erase did not complete as a unit, so a partial figure on a failed entry would invite reading it as
3688
+ // progress toward a destruction that did not happen. The attestation that matters is `destroyed`.
3689
+ warmRowsDeleted: 0
3690
+ };
3691
+ }
3675
3692
  destroyed.push(result);
3676
3693
  if (result.cryptoShredded) {
3677
3694
  segmentsShredded += 1;