@cloudbitmaps/core 0.4.1 → 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.cjs +38 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -8
- package/dist/index.d.ts +49 -8
- package/dist/index.js +38 -7
- package/dist/index.js.map +1 -1
- package/dist/mysql/index.cjs.map +1 -1
- package/dist/mysql/index.js.map +1 -1
- package/dist/postgres/index.cjs.map +1 -1
- package/dist/postgres/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -7,7 +7,7 @@ export { o as BufferReader, p as BufferSink, q as GovernanceMeta, r as NO_ROW, s
|
|
|
7
7
|
* `core/` is **codec-agnostic**: `SegmentEngine`, compaction, and the `.crbm` read/write helpers only ever
|
|
8
8
|
* construct and combine bitmaps through the {@link CodecInterface} factory + the {@link CodecBitmap} value type
|
|
9
9
|
* defined here — never a concrete implementation. The flagship codec is roaring (`roaringCodec`, today in
|
|
10
|
-
* `core/bitmap.ts`; it moves to `@cloudbitmaps/roaring` when the package split lands); `@cloudbitmaps/
|
|
10
|
+
* `core/bitmap.ts`; it moves to `@cloudbitmaps/roaring` when the package split lands); `@cloudbitmaps/bitset`
|
|
11
11
|
* (plain bitset) and `@cloudbitmaps/soaring` plug in behind the same seam with zero engine or driver changes.
|
|
12
12
|
*
|
|
13
13
|
* **Homogeneity contract:** a single store uses a single codec, so every {@link CodecBitmap} an operation sees
|
|
@@ -18,7 +18,7 @@ export { o as BufferReader, p as BufferSink, q as GovernanceMeta, r as NO_ROW, s
|
|
|
18
18
|
* mints the cache per store, so this cannot arise in normal use.)
|
|
19
19
|
*
|
|
20
20
|
* **Interface surface = exactly what the engine needs**, kept general enough for the known fast-follow codecs
|
|
21
|
-
* (positional/rank-select access + raw-bitset interop that `@cloudbitmaps/
|
|
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
23
|
* this seam intentionally leaves in the format layer is the `.crbm` **serialization id** (`ROARING_PORTABLE_ID`
|
|
24
24
|
* in `crbm/format.ts`): it is stamped in the footer and validated on read, and generalizes to a per-codec id
|
|
@@ -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
|
-
/**
|
|
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;
|
|
@@ -2131,11 +2160,23 @@ declare function runConsistencyCheck(deps: {
|
|
|
2131
2160
|
* I/O of their own); the wrapped driver is responsible for *classifying* its transient faults (raising
|
|
2132
2161
|
* {@link TransientError}); these decorators decide *whether and when* to retry.
|
|
2133
2162
|
*
|
|
2134
|
-
* **Streaming methods
|
|
2135
|
-
*
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2163
|
+
* **Streaming methods split into two shapes**, and the difference is deliberate — a partially-consumed async
|
|
2164
|
+
* iterator cannot be resumed mid-stream (it would re-yield earlier items), so each method picks between
|
|
2165
|
+
* buffering and bounded memory:
|
|
2166
|
+
*
|
|
2167
|
+
* · `RetryingColdDriver.list` / `RetryingRegistryDriver.list` **buffer**, inside `withRetry`, so the whole
|
|
2168
|
+
* enumeration is retried by re-running it from the start. Both are discovery scans over a generation or
|
|
2169
|
+
* namespace listing, where the result set is small and whole-scan retry is worth the memory.
|
|
2170
|
+
* · `RetryingWarmDriver.listChunks` does **not** buffer. It retries only until the first row arrives and
|
|
2171
|
+
* then streams live, because buffering it defeated the engine's resident-memory bound outright — see the
|
|
2172
|
+
* measured account on the method itself. A mid-stream fault therefore propagates rather than being
|
|
2173
|
+
* retried, which is the documented trade there.
|
|
2174
|
+
*
|
|
2175
|
+
* Point methods are retried in place, with nothing to buffer.
|
|
2176
|
+
*
|
|
2177
|
+
* (This paragraph previously said streaming methods buffer, full stop. That described `listChunks` as it was
|
|
2178
|
+
* before the bound was fixed, and the stale wording had also been copied into the Postgres and MySQL warm
|
|
2179
|
+
* drivers — so three comments promised a mid-stream resilience that the hot read path does not have.)
|
|
2139
2180
|
*
|
|
2140
2181
|
* What is **not** retried here: {@link WriteConflictError} (OCC — the engine's read-modify-write loop owns
|
|
2141
2182
|
* that; a blind replay would re-apply against a stale token), and every deterministic error
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { o as BufferReader, p as BufferSink, q as GovernanceMeta, r as NO_ROW, s
|
|
|
7
7
|
* `core/` is **codec-agnostic**: `SegmentEngine`, compaction, and the `.crbm` read/write helpers only ever
|
|
8
8
|
* construct and combine bitmaps through the {@link CodecInterface} factory + the {@link CodecBitmap} value type
|
|
9
9
|
* defined here — never a concrete implementation. The flagship codec is roaring (`roaringCodec`, today in
|
|
10
|
-
* `core/bitmap.ts`; it moves to `@cloudbitmaps/roaring` when the package split lands); `@cloudbitmaps/
|
|
10
|
+
* `core/bitmap.ts`; it moves to `@cloudbitmaps/roaring` when the package split lands); `@cloudbitmaps/bitset`
|
|
11
11
|
* (plain bitset) and `@cloudbitmaps/soaring` plug in behind the same seam with zero engine or driver changes.
|
|
12
12
|
*
|
|
13
13
|
* **Homogeneity contract:** a single store uses a single codec, so every {@link CodecBitmap} an operation sees
|
|
@@ -18,7 +18,7 @@ export { o as BufferReader, p as BufferSink, q as GovernanceMeta, r as NO_ROW, s
|
|
|
18
18
|
* mints the cache per store, so this cannot arise in normal use.)
|
|
19
19
|
*
|
|
20
20
|
* **Interface surface = exactly what the engine needs**, kept general enough for the known fast-follow codecs
|
|
21
|
-
* (positional/rank-select access + raw-bitset interop that `@cloudbitmaps/
|
|
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
23
|
* this seam intentionally leaves in the format layer is the `.crbm` **serialization id** (`ROARING_PORTABLE_ID`
|
|
24
24
|
* in `crbm/format.ts`): it is stamped in the footer and validated on read, and generalizes to a per-codec id
|
|
@@ -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
|
-
/**
|
|
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;
|
|
@@ -2131,11 +2160,23 @@ declare function runConsistencyCheck(deps: {
|
|
|
2131
2160
|
* I/O of their own); the wrapped driver is responsible for *classifying* its transient faults (raising
|
|
2132
2161
|
* {@link TransientError}); these decorators decide *whether and when* to retry.
|
|
2133
2162
|
*
|
|
2134
|
-
* **Streaming methods
|
|
2135
|
-
*
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2163
|
+
* **Streaming methods split into two shapes**, and the difference is deliberate — a partially-consumed async
|
|
2164
|
+
* iterator cannot be resumed mid-stream (it would re-yield earlier items), so each method picks between
|
|
2165
|
+
* buffering and bounded memory:
|
|
2166
|
+
*
|
|
2167
|
+
* · `RetryingColdDriver.list` / `RetryingRegistryDriver.list` **buffer**, inside `withRetry`, so the whole
|
|
2168
|
+
* enumeration is retried by re-running it from the start. Both are discovery scans over a generation or
|
|
2169
|
+
* namespace listing, where the result set is small and whole-scan retry is worth the memory.
|
|
2170
|
+
* · `RetryingWarmDriver.listChunks` does **not** buffer. It retries only until the first row arrives and
|
|
2171
|
+
* then streams live, because buffering it defeated the engine's resident-memory bound outright — see the
|
|
2172
|
+
* measured account on the method itself. A mid-stream fault therefore propagates rather than being
|
|
2173
|
+
* retried, which is the documented trade there.
|
|
2174
|
+
*
|
|
2175
|
+
* Point methods are retried in place, with nothing to buffer.
|
|
2176
|
+
*
|
|
2177
|
+
* (This paragraph previously said streaming methods buffer, full stop. That described `listChunks` as it was
|
|
2178
|
+
* before the bound was fixed, and the stale wording had also been copied into the Postgres and MySQL warm
|
|
2179
|
+
* drivers — so three comments promised a mid-stream resilience that the hot read path does not have.)
|
|
2139
2180
|
*
|
|
2140
2181
|
* What is **not** retried here: {@link WriteConflictError} (OCC — the engine's read-modify-write loop owns
|
|
2141
2182
|
* that; a blind replay would re-apply against a stale token), and every deterministic error
|
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;
|
|
@@ -2108,6 +2110,10 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
2108
2110
|
currentGen: key.generation,
|
|
2109
2111
|
wrappedDeks: options.wrappedDeks
|
|
2110
2112
|
});
|
|
2113
|
+
} else if (record.status === "destroyed") {
|
|
2114
|
+
throw new ValidationError(
|
|
2115
|
+
`segment "${key.segment}" was destroyed (crypto-shredded) while generation ${key.generation} was being written \u2014 refusing to publish it; the written object is unreadable. Use a new segment.`
|
|
2116
|
+
);
|
|
2111
2117
|
} else if (record.currentGen > key.generation) {
|
|
2112
2118
|
return false;
|
|
2113
2119
|
} else if (record.currentGen === key.generation) {
|
|
@@ -3667,7 +3673,22 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
3667
3673
|
const destroyed = [];
|
|
3668
3674
|
let segmentsShredded = 0;
|
|
3669
3675
|
for (const ref of refs) {
|
|
3670
|
-
|
|
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
|
+
}
|
|
3671
3692
|
destroyed.push(result);
|
|
3672
3693
|
if (result.cryptoShredded) {
|
|
3673
3694
|
segmentsShredded += 1;
|
|
@@ -3730,12 +3751,13 @@ async function shredSegment(ref, deps, allowCleartext) {
|
|
|
3730
3751
|
}
|
|
3731
3752
|
async function eraseWarm(warm, ref) {
|
|
3732
3753
|
let deleted = 0;
|
|
3754
|
+
let conflicts = 0;
|
|
3733
3755
|
for (let pass = 0; pass < MAX_WARM_PASSES; pass++) {
|
|
3756
|
+
conflicts = 0;
|
|
3734
3757
|
const rows = [];
|
|
3735
3758
|
for await (const row of warm.listChunks(ref))
|
|
3736
3759
|
rows.push({ chunkKey: row.chunkKey, token: row.token });
|
|
3737
3760
|
if (rows.length === 0) break;
|
|
3738
|
-
let conflicts = 0;
|
|
3739
3761
|
for (const { chunkKey, token } of rows) {
|
|
3740
3762
|
const chunkRef = { namespace: ref.namespace, segment: ref.segment, chunkKey };
|
|
3741
3763
|
try {
|
|
@@ -3748,6 +3770,11 @@ async function eraseWarm(warm, ref) {
|
|
|
3748
3770
|
}
|
|
3749
3771
|
if (conflicts === 0) break;
|
|
3750
3772
|
}
|
|
3773
|
+
if (conflicts > 0) {
|
|
3774
|
+
throw new WriteConflictError(
|
|
3775
|
+
`destroySegment: ${conflicts} warm row(s) of "${ref.segment}" were rewritten during every one of ${MAX_WARM_PASSES} erase passes \u2014 the segment was NOT destroyed and its cleartext warm deltas are still readable. Stop writes to this segment and retry.`
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3751
3778
|
return deleted;
|
|
3752
3779
|
}
|
|
3753
3780
|
|
|
@@ -3878,11 +3905,15 @@ var RetryingWarmDriver = class {
|
|
|
3878
3905
|
this.deps
|
|
3879
3906
|
);
|
|
3880
3907
|
if (started.first.done === true) return;
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3908
|
+
try {
|
|
3909
|
+
yield started.first.value;
|
|
3910
|
+
for (; ; ) {
|
|
3911
|
+
const next = await started.iterator.next();
|
|
3912
|
+
if (next.done === true) return;
|
|
3913
|
+
yield next.value;
|
|
3914
|
+
}
|
|
3915
|
+
} finally {
|
|
3916
|
+
await started.iterator.return?.();
|
|
3886
3917
|
}
|
|
3887
3918
|
}
|
|
3888
3919
|
};
|