distillate 0.1.1 → 0.2.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/README.md +15 -0
- package/dist/blocked/index.cjs +62 -7
- package/dist/blocked/index.d.cts +60 -7
- package/dist/blocked/index.d.ts +60 -7
- package/dist/blocked/index.js +62 -8
- package/dist/bloom/index.cjs +61 -0
- package/dist/bloom/index.d.cts +60 -1
- package/dist/bloom/index.d.ts +60 -1
- package/dist/bloom/index.js +61 -1
- package/dist/fuse/index.cjs +66 -0
- package/dist/fuse/index.d.cts +66 -0
- package/dist/fuse/index.d.ts +66 -0
- package/dist/fuse/index.js +66 -0
- package/dist/index.cjs +3 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/params-ChTRNxM9.js +24 -0
- package/dist/params-DnqJBqLS.d.cts +8 -0
- package/dist/params-DnqJBqLS.d.ts +8 -0
- package/dist/params-J8p3bKq5.cjs +53 -0
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -92,6 +92,19 @@ const precise = BinaryFuse16.from(["alice", "bob", "carol"]);
|
|
|
92
92
|
|
|
93
93
|
Also: `toBytes` / `fromBytes`. No `add` / `delete`; rebuild `from` the new set to change membership.
|
|
94
94
|
|
|
95
|
+
## Performance
|
|
96
|
+
|
|
97
|
+
Classic Bloom head-to-head at a **matched 1% false-positive rate** over the same 100k keys, measured by identical code (Node, Apple M5):
|
|
98
|
+
|
|
99
|
+
| Classic Bloom | bits/key | measured FPR | `has` throughput |
|
|
100
|
+
| -------------- | -------- | ------------ | ---------------- |
|
|
101
|
+
| **distillate** | 9.59 | 1.03% | ~7.0 M ops/s |
|
|
102
|
+
| bloom-filters | 9.59 | 0.99% | ~0.29 M ops/s |
|
|
103
|
+
|
|
104
|
+
Same space, same accuracy, **~24x the lookup throughput** of [`bloom-filters`](https://www.npmjs.com/package/bloom-filters) (the package distillate replaces), while hashing UTF-8 bytes with MurmurHash3 so filters stay portable and cross-language readable.
|
|
105
|
+
|
|
106
|
+
These are a point-in-time snapshot on one machine. The full report (blocked/fuse, 1M capacity, the `bloomfilter` micro-package) and exactly how it is measured live in the [distillate-bench](https://github.com/akshay-xp/distillate-bench) repo: [RESULTS.md](https://github.com/akshay-xp/distillate-bench/blob/main/RESULTS.md), [METHODOLOGY.md](https://github.com/akshay-xp/distillate-bench/blob/main/METHODOLOGY.md).
|
|
107
|
+
|
|
95
108
|
## Docs
|
|
96
109
|
|
|
97
110
|
Design notes, the structure decision matrix, hashing, and the binary format live in [`docs/`](./docs):
|
|
@@ -99,6 +112,8 @@ Design notes, the structure decision matrix, hashing, and the binary format live
|
|
|
99
112
|
- [overview](./docs/overview.md): what and why
|
|
100
113
|
- [structures](./docs/structures.md): decision matrix and the full lineup
|
|
101
114
|
- [architecture](./docs/architecture.md), [hashing](./docs/hashing.md), [serialization](./docs/serialization.md)
|
|
115
|
+
- [versioning](./docs/versioning.md): SemVer policy and supported-runtime baseline
|
|
116
|
+
- [API reference](./docs/api): generated from TSDoc (per entry point)
|
|
102
117
|
|
|
103
118
|
## License
|
|
104
119
|
|
package/dist/blocked/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_serialize = require("../serialize-CHHDM4TQ.cjs");
|
|
3
|
+
const require_params = require("../params-J8p3bKq5.cjs");
|
|
3
4
|
//#region src/blocked/blocked.ts
|
|
4
5
|
const TYPE = 2;
|
|
5
6
|
const SALT = Uint32Array.of(1203114875, 1150766481, 2284105051, 2729912477, 1884591559, 770785867, 2667333959, 1550580529);
|
|
@@ -9,15 +10,22 @@ const scratchHash = {
|
|
|
9
10
|
h2lo: 0,
|
|
10
11
|
h2hi: 0
|
|
11
12
|
};
|
|
12
|
-
/**
|
|
13
|
-
* Split-block probe: derive one block index and 8 single-bit lane masks for
|
|
14
|
-
* `key`. Writes lane word indices into `outWords` and their masks into
|
|
15
|
-
* `outBits` (both length 8, caller-owned so no per-call allocation). A block is
|
|
16
|
-
* 256 bits = 8 contiguous 32-bit lanes; word `block*8 + i` gets one bit set.
|
|
17
|
-
*/
|
|
13
|
+
/** Thrown when an operation requires two filters built with identical parameters. */
|
|
18
14
|
var BlockedBloomParamMismatchError = class extends Error {
|
|
15
|
+
/** Discriminates this error from other `Error`s. */
|
|
19
16
|
name = "BlockedBloomParamMismatchError";
|
|
20
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* A blocked (split-block) Bloom filter: confines every lookup to a single cache
|
|
20
|
+
* line, trading ~20-30% more space for cache-friendly throughput.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const filter = BlockedBloomFilter.create(100_000, 0.01);
|
|
25
|
+
* filter.add("alice");
|
|
26
|
+
* filter.has("alice"); // true
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
21
29
|
var BlockedBloomFilter = class BlockedBloomFilter {
|
|
22
30
|
#lanes;
|
|
23
31
|
#numBlocks;
|
|
@@ -30,7 +38,16 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
30
38
|
[3, 16.9],
|
|
31
39
|
[4, 26.4]
|
|
32
40
|
];
|
|
41
|
+
/**
|
|
42
|
+
* Creates a filter sized for `n` expected keys at a target false-positive rate.
|
|
43
|
+
*
|
|
44
|
+
* @param n - Expected number of keys.
|
|
45
|
+
* @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
|
|
46
|
+
* @returns A new, empty filter.
|
|
47
|
+
*/
|
|
33
48
|
static create(n, epsilon) {
|
|
49
|
+
require_params.assertPositiveInt(n, "n");
|
|
50
|
+
require_params.assertProbability(epsilon, "epsilon");
|
|
34
51
|
const t = Math.log10(1 / epsilon);
|
|
35
52
|
const a = BlockedBloomFilter.#ANCHORS;
|
|
36
53
|
let seg = a.findIndex((p) => t <= p[0]);
|
|
@@ -39,19 +56,33 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
39
56
|
const [t1, b1] = a[seg] ?? [0, 0];
|
|
40
57
|
const bitsPerKey = b0 + (b1 - b0) / (t1 - t0) * (t - t0);
|
|
41
58
|
return new BlockedBloomFilter({
|
|
42
|
-
bitsPerKey: Math.ceil(bitsPerKey),
|
|
59
|
+
bitsPerKey: Math.max(1, Math.ceil(bitsPerKey)),
|
|
43
60
|
capacity: n
|
|
44
61
|
});
|
|
45
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Constructs a filter from low-level {@link BlockedBloomParams}. Prefer
|
|
65
|
+
* {@link BlockedBloomFilter.create} unless restoring a specific configuration.
|
|
66
|
+
*/
|
|
46
67
|
constructor({ bitsPerKey, capacity, seed = 0 }) {
|
|
68
|
+
require_params.assertPositiveFinite(bitsPerKey, "bitsPerKey");
|
|
69
|
+
require_params.assertPositiveInt(capacity, "capacity");
|
|
70
|
+
require_params.assertUint32(seed, "seed");
|
|
47
71
|
this.#numBlocks = Math.max(1, Math.ceil(bitsPerKey * capacity / 256));
|
|
48
72
|
this.#lanes = new Uint32Array(this.#numBlocks * 8);
|
|
49
73
|
this.#seed = seed;
|
|
50
74
|
this.#n = capacity;
|
|
51
75
|
}
|
|
76
|
+
/** Actual bits allocated per key (`total bits / capacity`). */
|
|
52
77
|
get bitsPerKey() {
|
|
53
78
|
return this.#numBlocks * 256 / this.#n;
|
|
54
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Restores a filter from its {@link BlockedBloomFilter.toBytes} serialization.
|
|
82
|
+
*
|
|
83
|
+
* @param bytes - The serialized filter.
|
|
84
|
+
* @returns The reconstructed filter.
|
|
85
|
+
*/
|
|
55
86
|
static fromBytes(bytes) {
|
|
56
87
|
const { body } = require_serialize.readHeader(bytes);
|
|
57
88
|
const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
|
|
@@ -67,6 +98,11 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
67
98
|
new Uint8Array(f.#lanes.buffer).set(body.subarray(12));
|
|
68
99
|
return f;
|
|
69
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Serializes the filter to a portable little-endian byte layout.
|
|
103
|
+
*
|
|
104
|
+
* @returns The serialized filter, readable by {@link BlockedBloomFilter.fromBytes}.
|
|
105
|
+
*/
|
|
70
106
|
toBytes() {
|
|
71
107
|
const lanes = new Uint8Array(this.#lanes.buffer, this.#lanes.byteOffset, this.#lanes.byteLength);
|
|
72
108
|
const body = new Uint8Array(12 + lanes.length);
|
|
@@ -81,6 +117,13 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
81
117
|
flags: 0
|
|
82
118
|
}, body);
|
|
83
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Returns a new filter containing the union of this filter and `other`.
|
|
122
|
+
*
|
|
123
|
+
* @param other - A filter built with identical parameters.
|
|
124
|
+
* @returns A new filter reporting membership for keys in either input.
|
|
125
|
+
* @throws {@link BlockedBloomParamMismatchError} if the parameters differ.
|
|
126
|
+
*/
|
|
84
127
|
union(other) {
|
|
85
128
|
if (this.#numBlocks !== other.#numBlocks || this.#seed !== other.#seed) throw new BlockedBloomParamMismatchError("cannot union blocked Bloom filters whose parameters do not match");
|
|
86
129
|
const r = new BlockedBloomFilter({
|
|
@@ -91,6 +134,11 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
91
134
|
for (let i = 0; i < this.#lanes.length; i++) r.#lanes[i] = (this.#lanes[i] ?? 0) | (other.#lanes[i] ?? 0);
|
|
92
135
|
return r;
|
|
93
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Adds a key to the set.
|
|
139
|
+
*
|
|
140
|
+
* @param key - The key to insert, as a string or bytes.
|
|
141
|
+
*/
|
|
94
142
|
add(key) {
|
|
95
143
|
fillBlock(key, this.#numBlocks, this.#seed, this.#words, this.#bits);
|
|
96
144
|
for (let i = 0; i < 8; i++) {
|
|
@@ -98,6 +146,12 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
98
146
|
this.#lanes[w] = (this.#lanes[w] ?? 0) | (this.#bits[i] ?? 0);
|
|
99
147
|
}
|
|
100
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Tests whether a key is in the set.
|
|
151
|
+
*
|
|
152
|
+
* @param key - The key to test.
|
|
153
|
+
* @returns `true` if present (possibly a false positive); `false` guarantees absence.
|
|
154
|
+
*/
|
|
101
155
|
has(key) {
|
|
102
156
|
fillBlock(key, this.#numBlocks, this.#seed, this.#words, this.#bits);
|
|
103
157
|
for (let i = 0; i < 8; i++) {
|
|
@@ -120,3 +174,4 @@ function fillBlock(key, numBlocks, seed, outWords, outBits) {
|
|
|
120
174
|
//#endregion
|
|
121
175
|
exports.BlockedBloomFilter = BlockedBloomFilter;
|
|
122
176
|
exports.BlockedBloomParamMismatchError = BlockedBloomParamMismatchError;
|
|
177
|
+
exports.ParamError = require_params.ParamError;
|
package/dist/blocked/index.d.cts
CHANGED
|
@@ -1,29 +1,82 @@
|
|
|
1
1
|
import { t as BytesLike } from "../bytes-DCuYtUVS.cjs";
|
|
2
|
+
import { t as ParamError } from "../params-DnqJBqLS.cjs";
|
|
2
3
|
//#region src/blocked/blocked.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Split-block probe: derive one block index and 8 single-bit lane masks for
|
|
5
|
-
* `key`. Writes lane word indices into `outWords` and their masks into
|
|
6
|
-
* `outBits` (both length 8, caller-owned so no per-call allocation). A block is
|
|
7
|
-
* 256 bits = 8 contiguous 32-bit lanes; word `block*8 + i` gets one bit set.
|
|
8
|
-
*/
|
|
4
|
+
/** Thrown when an operation requires two filters built with identical parameters. */
|
|
9
5
|
declare class BlockedBloomParamMismatchError extends Error {
|
|
6
|
+
/** Discriminates this error from other `Error`s. */
|
|
10
7
|
override readonly name = "BlockedBloomParamMismatchError";
|
|
11
8
|
}
|
|
9
|
+
/** Low-level blocked Bloom filter parameters. */
|
|
12
10
|
interface BlockedBloomParams {
|
|
11
|
+
/** Bits allocated per key; higher lowers the false-positive rate. */
|
|
13
12
|
bitsPerKey: number;
|
|
13
|
+
/** Expected number of keys. */
|
|
14
14
|
capacity: number;
|
|
15
|
+
/** Hash seed; defaults to `0`. */
|
|
15
16
|
seed?: number;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A blocked (split-block) Bloom filter: confines every lookup to a single cache
|
|
20
|
+
* line, trading ~20-30% more space for cache-friendly throughput.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const filter = BlockedBloomFilter.create(100_000, 0.01);
|
|
25
|
+
* filter.add("alice");
|
|
26
|
+
* filter.has("alice"); // true
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
17
29
|
declare class BlockedBloomFilter {
|
|
18
30
|
#private;
|
|
31
|
+
/**
|
|
32
|
+
* Creates a filter sized for `n` expected keys at a target false-positive rate.
|
|
33
|
+
*
|
|
34
|
+
* @param n - Expected number of keys.
|
|
35
|
+
* @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
|
|
36
|
+
* @returns A new, empty filter.
|
|
37
|
+
*/
|
|
19
38
|
static create(n: number, epsilon: number): BlockedBloomFilter;
|
|
39
|
+
/**
|
|
40
|
+
* Constructs a filter from low-level {@link BlockedBloomParams}. Prefer
|
|
41
|
+
* {@link BlockedBloomFilter.create} unless restoring a specific configuration.
|
|
42
|
+
*/
|
|
20
43
|
constructor({ bitsPerKey, capacity, seed }: BlockedBloomParams);
|
|
44
|
+
/** Actual bits allocated per key (`total bits / capacity`). */
|
|
21
45
|
get bitsPerKey(): number;
|
|
46
|
+
/**
|
|
47
|
+
* Restores a filter from its {@link BlockedBloomFilter.toBytes} serialization.
|
|
48
|
+
*
|
|
49
|
+
* @param bytes - The serialized filter.
|
|
50
|
+
* @returns The reconstructed filter.
|
|
51
|
+
*/
|
|
22
52
|
static fromBytes(bytes: Uint8Array): BlockedBloomFilter;
|
|
53
|
+
/**
|
|
54
|
+
* Serializes the filter to a portable little-endian byte layout.
|
|
55
|
+
*
|
|
56
|
+
* @returns The serialized filter, readable by {@link BlockedBloomFilter.fromBytes}.
|
|
57
|
+
*/
|
|
23
58
|
toBytes(): Uint8Array;
|
|
59
|
+
/**
|
|
60
|
+
* Returns a new filter containing the union of this filter and `other`.
|
|
61
|
+
*
|
|
62
|
+
* @param other - A filter built with identical parameters.
|
|
63
|
+
* @returns A new filter reporting membership for keys in either input.
|
|
64
|
+
* @throws {@link BlockedBloomParamMismatchError} if the parameters differ.
|
|
65
|
+
*/
|
|
24
66
|
union(other: BlockedBloomFilter): BlockedBloomFilter;
|
|
67
|
+
/**
|
|
68
|
+
* Adds a key to the set.
|
|
69
|
+
*
|
|
70
|
+
* @param key - The key to insert, as a string or bytes.
|
|
71
|
+
*/
|
|
25
72
|
add(key: BytesLike): void;
|
|
73
|
+
/**
|
|
74
|
+
* Tests whether a key is in the set.
|
|
75
|
+
*
|
|
76
|
+
* @param key - The key to test.
|
|
77
|
+
* @returns `true` if present (possibly a false positive); `false` guarantees absence.
|
|
78
|
+
*/
|
|
26
79
|
has(key: BytesLike): boolean;
|
|
27
80
|
}
|
|
28
81
|
//#endregion
|
|
29
|
-
export { BlockedBloomFilter, BlockedBloomParamMismatchError, type BlockedBloomParams };
|
|
82
|
+
export { BlockedBloomFilter, BlockedBloomParamMismatchError, type BlockedBloomParams, ParamError };
|
package/dist/blocked/index.d.ts
CHANGED
|
@@ -1,29 +1,82 @@
|
|
|
1
1
|
import { t as BytesLike } from "../bytes-DCuYtUVS.js";
|
|
2
|
+
import { t as ParamError } from "../params-DnqJBqLS.js";
|
|
2
3
|
//#region src/blocked/blocked.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Split-block probe: derive one block index and 8 single-bit lane masks for
|
|
5
|
-
* `key`. Writes lane word indices into `outWords` and their masks into
|
|
6
|
-
* `outBits` (both length 8, caller-owned so no per-call allocation). A block is
|
|
7
|
-
* 256 bits = 8 contiguous 32-bit lanes; word `block*8 + i` gets one bit set.
|
|
8
|
-
*/
|
|
4
|
+
/** Thrown when an operation requires two filters built with identical parameters. */
|
|
9
5
|
declare class BlockedBloomParamMismatchError extends Error {
|
|
6
|
+
/** Discriminates this error from other `Error`s. */
|
|
10
7
|
override readonly name = "BlockedBloomParamMismatchError";
|
|
11
8
|
}
|
|
9
|
+
/** Low-level blocked Bloom filter parameters. */
|
|
12
10
|
interface BlockedBloomParams {
|
|
11
|
+
/** Bits allocated per key; higher lowers the false-positive rate. */
|
|
13
12
|
bitsPerKey: number;
|
|
13
|
+
/** Expected number of keys. */
|
|
14
14
|
capacity: number;
|
|
15
|
+
/** Hash seed; defaults to `0`. */
|
|
15
16
|
seed?: number;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A blocked (split-block) Bloom filter: confines every lookup to a single cache
|
|
20
|
+
* line, trading ~20-30% more space for cache-friendly throughput.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const filter = BlockedBloomFilter.create(100_000, 0.01);
|
|
25
|
+
* filter.add("alice");
|
|
26
|
+
* filter.has("alice"); // true
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
17
29
|
declare class BlockedBloomFilter {
|
|
18
30
|
#private;
|
|
31
|
+
/**
|
|
32
|
+
* Creates a filter sized for `n` expected keys at a target false-positive rate.
|
|
33
|
+
*
|
|
34
|
+
* @param n - Expected number of keys.
|
|
35
|
+
* @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
|
|
36
|
+
* @returns A new, empty filter.
|
|
37
|
+
*/
|
|
19
38
|
static create(n: number, epsilon: number): BlockedBloomFilter;
|
|
39
|
+
/**
|
|
40
|
+
* Constructs a filter from low-level {@link BlockedBloomParams}. Prefer
|
|
41
|
+
* {@link BlockedBloomFilter.create} unless restoring a specific configuration.
|
|
42
|
+
*/
|
|
20
43
|
constructor({ bitsPerKey, capacity, seed }: BlockedBloomParams);
|
|
44
|
+
/** Actual bits allocated per key (`total bits / capacity`). */
|
|
21
45
|
get bitsPerKey(): number;
|
|
46
|
+
/**
|
|
47
|
+
* Restores a filter from its {@link BlockedBloomFilter.toBytes} serialization.
|
|
48
|
+
*
|
|
49
|
+
* @param bytes - The serialized filter.
|
|
50
|
+
* @returns The reconstructed filter.
|
|
51
|
+
*/
|
|
22
52
|
static fromBytes(bytes: Uint8Array): BlockedBloomFilter;
|
|
53
|
+
/**
|
|
54
|
+
* Serializes the filter to a portable little-endian byte layout.
|
|
55
|
+
*
|
|
56
|
+
* @returns The serialized filter, readable by {@link BlockedBloomFilter.fromBytes}.
|
|
57
|
+
*/
|
|
23
58
|
toBytes(): Uint8Array;
|
|
59
|
+
/**
|
|
60
|
+
* Returns a new filter containing the union of this filter and `other`.
|
|
61
|
+
*
|
|
62
|
+
* @param other - A filter built with identical parameters.
|
|
63
|
+
* @returns A new filter reporting membership for keys in either input.
|
|
64
|
+
* @throws {@link BlockedBloomParamMismatchError} if the parameters differ.
|
|
65
|
+
*/
|
|
24
66
|
union(other: BlockedBloomFilter): BlockedBloomFilter;
|
|
67
|
+
/**
|
|
68
|
+
* Adds a key to the set.
|
|
69
|
+
*
|
|
70
|
+
* @param key - The key to insert, as a string or bytes.
|
|
71
|
+
*/
|
|
25
72
|
add(key: BytesLike): void;
|
|
73
|
+
/**
|
|
74
|
+
* Tests whether a key is in the set.
|
|
75
|
+
*
|
|
76
|
+
* @param key - The key to test.
|
|
77
|
+
* @returns `true` if present (possibly a false positive); `false` guarantees absence.
|
|
78
|
+
*/
|
|
26
79
|
has(key: BytesLike): boolean;
|
|
27
80
|
}
|
|
28
81
|
//#endregion
|
|
29
|
-
export { BlockedBloomFilter, BlockedBloomParamMismatchError, type BlockedBloomParams };
|
|
82
|
+
export { BlockedBloomFilter, BlockedBloomParamMismatchError, type BlockedBloomParams, ParamError };
|
package/dist/blocked/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { i as hash128KeyInto, n as readHeader, o as reduce, r as writeHeader } from "../serialize-5XQ5y_R-.js";
|
|
2
|
+
import { a as assertUint32, i as assertProbability, n as assertPositiveFinite, r as assertPositiveInt, t as ParamError } from "../params-ChTRNxM9.js";
|
|
2
3
|
//#region src/blocked/blocked.ts
|
|
3
4
|
const TYPE = 2;
|
|
4
5
|
const SALT = Uint32Array.of(1203114875, 1150766481, 2284105051, 2729912477, 1884591559, 770785867, 2667333959, 1550580529);
|
|
@@ -8,15 +9,22 @@ const scratchHash = {
|
|
|
8
9
|
h2lo: 0,
|
|
9
10
|
h2hi: 0
|
|
10
11
|
};
|
|
11
|
-
/**
|
|
12
|
-
* Split-block probe: derive one block index and 8 single-bit lane masks for
|
|
13
|
-
* `key`. Writes lane word indices into `outWords` and their masks into
|
|
14
|
-
* `outBits` (both length 8, caller-owned so no per-call allocation). A block is
|
|
15
|
-
* 256 bits = 8 contiguous 32-bit lanes; word `block*8 + i` gets one bit set.
|
|
16
|
-
*/
|
|
12
|
+
/** Thrown when an operation requires two filters built with identical parameters. */
|
|
17
13
|
var BlockedBloomParamMismatchError = class extends Error {
|
|
14
|
+
/** Discriminates this error from other `Error`s. */
|
|
18
15
|
name = "BlockedBloomParamMismatchError";
|
|
19
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* A blocked (split-block) Bloom filter: confines every lookup to a single cache
|
|
19
|
+
* line, trading ~20-30% more space for cache-friendly throughput.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const filter = BlockedBloomFilter.create(100_000, 0.01);
|
|
24
|
+
* filter.add("alice");
|
|
25
|
+
* filter.has("alice"); // true
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
20
28
|
var BlockedBloomFilter = class BlockedBloomFilter {
|
|
21
29
|
#lanes;
|
|
22
30
|
#numBlocks;
|
|
@@ -29,7 +37,16 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
29
37
|
[3, 16.9],
|
|
30
38
|
[4, 26.4]
|
|
31
39
|
];
|
|
40
|
+
/**
|
|
41
|
+
* Creates a filter sized for `n` expected keys at a target false-positive rate.
|
|
42
|
+
*
|
|
43
|
+
* @param n - Expected number of keys.
|
|
44
|
+
* @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
|
|
45
|
+
* @returns A new, empty filter.
|
|
46
|
+
*/
|
|
32
47
|
static create(n, epsilon) {
|
|
48
|
+
assertPositiveInt(n, "n");
|
|
49
|
+
assertProbability(epsilon, "epsilon");
|
|
33
50
|
const t = Math.log10(1 / epsilon);
|
|
34
51
|
const a = BlockedBloomFilter.#ANCHORS;
|
|
35
52
|
let seg = a.findIndex((p) => t <= p[0]);
|
|
@@ -38,19 +55,33 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
38
55
|
const [t1, b1] = a[seg] ?? [0, 0];
|
|
39
56
|
const bitsPerKey = b0 + (b1 - b0) / (t1 - t0) * (t - t0);
|
|
40
57
|
return new BlockedBloomFilter({
|
|
41
|
-
bitsPerKey: Math.ceil(bitsPerKey),
|
|
58
|
+
bitsPerKey: Math.max(1, Math.ceil(bitsPerKey)),
|
|
42
59
|
capacity: n
|
|
43
60
|
});
|
|
44
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Constructs a filter from low-level {@link BlockedBloomParams}. Prefer
|
|
64
|
+
* {@link BlockedBloomFilter.create} unless restoring a specific configuration.
|
|
65
|
+
*/
|
|
45
66
|
constructor({ bitsPerKey, capacity, seed = 0 }) {
|
|
67
|
+
assertPositiveFinite(bitsPerKey, "bitsPerKey");
|
|
68
|
+
assertPositiveInt(capacity, "capacity");
|
|
69
|
+
assertUint32(seed, "seed");
|
|
46
70
|
this.#numBlocks = Math.max(1, Math.ceil(bitsPerKey * capacity / 256));
|
|
47
71
|
this.#lanes = new Uint32Array(this.#numBlocks * 8);
|
|
48
72
|
this.#seed = seed;
|
|
49
73
|
this.#n = capacity;
|
|
50
74
|
}
|
|
75
|
+
/** Actual bits allocated per key (`total bits / capacity`). */
|
|
51
76
|
get bitsPerKey() {
|
|
52
77
|
return this.#numBlocks * 256 / this.#n;
|
|
53
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Restores a filter from its {@link BlockedBloomFilter.toBytes} serialization.
|
|
81
|
+
*
|
|
82
|
+
* @param bytes - The serialized filter.
|
|
83
|
+
* @returns The reconstructed filter.
|
|
84
|
+
*/
|
|
54
85
|
static fromBytes(bytes) {
|
|
55
86
|
const { body } = readHeader(bytes);
|
|
56
87
|
const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
|
|
@@ -66,6 +97,11 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
66
97
|
new Uint8Array(f.#lanes.buffer).set(body.subarray(12));
|
|
67
98
|
return f;
|
|
68
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Serializes the filter to a portable little-endian byte layout.
|
|
102
|
+
*
|
|
103
|
+
* @returns The serialized filter, readable by {@link BlockedBloomFilter.fromBytes}.
|
|
104
|
+
*/
|
|
69
105
|
toBytes() {
|
|
70
106
|
const lanes = new Uint8Array(this.#lanes.buffer, this.#lanes.byteOffset, this.#lanes.byteLength);
|
|
71
107
|
const body = new Uint8Array(12 + lanes.length);
|
|
@@ -80,6 +116,13 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
80
116
|
flags: 0
|
|
81
117
|
}, body);
|
|
82
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns a new filter containing the union of this filter and `other`.
|
|
121
|
+
*
|
|
122
|
+
* @param other - A filter built with identical parameters.
|
|
123
|
+
* @returns A new filter reporting membership for keys in either input.
|
|
124
|
+
* @throws {@link BlockedBloomParamMismatchError} if the parameters differ.
|
|
125
|
+
*/
|
|
83
126
|
union(other) {
|
|
84
127
|
if (this.#numBlocks !== other.#numBlocks || this.#seed !== other.#seed) throw new BlockedBloomParamMismatchError("cannot union blocked Bloom filters whose parameters do not match");
|
|
85
128
|
const r = new BlockedBloomFilter({
|
|
@@ -90,6 +133,11 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
90
133
|
for (let i = 0; i < this.#lanes.length; i++) r.#lanes[i] = (this.#lanes[i] ?? 0) | (other.#lanes[i] ?? 0);
|
|
91
134
|
return r;
|
|
92
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Adds a key to the set.
|
|
138
|
+
*
|
|
139
|
+
* @param key - The key to insert, as a string or bytes.
|
|
140
|
+
*/
|
|
93
141
|
add(key) {
|
|
94
142
|
fillBlock(key, this.#numBlocks, this.#seed, this.#words, this.#bits);
|
|
95
143
|
for (let i = 0; i < 8; i++) {
|
|
@@ -97,6 +145,12 @@ var BlockedBloomFilter = class BlockedBloomFilter {
|
|
|
97
145
|
this.#lanes[w] = (this.#lanes[w] ?? 0) | (this.#bits[i] ?? 0);
|
|
98
146
|
}
|
|
99
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Tests whether a key is in the set.
|
|
150
|
+
*
|
|
151
|
+
* @param key - The key to test.
|
|
152
|
+
* @returns `true` if present (possibly a false positive); `false` guarantees absence.
|
|
153
|
+
*/
|
|
100
154
|
has(key) {
|
|
101
155
|
fillBlock(key, this.#numBlocks, this.#seed, this.#words, this.#bits);
|
|
102
156
|
for (let i = 0; i < 8; i++) {
|
|
@@ -117,4 +171,4 @@ function fillBlock(key, numBlocks, seed, outWords, outBits) {
|
|
|
117
171
|
}
|
|
118
172
|
}
|
|
119
173
|
//#endregion
|
|
120
|
-
export { BlockedBloomFilter, BlockedBloomParamMismatchError };
|
|
174
|
+
export { BlockedBloomFilter, BlockedBloomParamMismatchError, ParamError };
|
package/dist/bloom/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_serialize = require("../serialize-CHHDM4TQ.cjs");
|
|
3
|
+
const require_params = require("../params-J8p3bKq5.cjs");
|
|
3
4
|
//#region src/core/bitset.ts
|
|
4
5
|
var BitSetRangeError = class extends RangeError {
|
|
5
6
|
name = "BitSetRangeError";
|
|
@@ -48,9 +49,23 @@ function optimal(n, epsilon) {
|
|
|
48
49
|
//#endregion
|
|
49
50
|
//#region src/bloom/bloom.ts
|
|
50
51
|
const TYPE = 1;
|
|
52
|
+
/** Thrown when an operation requires two filters built with identical parameters. */
|
|
51
53
|
var BloomParamMismatchError = class extends Error {
|
|
54
|
+
/** Discriminates this error from other `Error`s. */
|
|
52
55
|
name = "BloomParamMismatchError";
|
|
53
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* A classic Bloom filter: a space-efficient set with a tunable false-positive
|
|
59
|
+
* rate and zero false negatives.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const filter = BloomFilter.create(100_000, 0.01);
|
|
64
|
+
* filter.add("alice");
|
|
65
|
+
* filter.has("alice"); // true
|
|
66
|
+
* filter.has("bob"); // false (or a ~1% false positive)
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
54
69
|
var BloomFilter = class BloomFilter {
|
|
55
70
|
#bits;
|
|
56
71
|
#m;
|
|
@@ -58,11 +73,26 @@ var BloomFilter = class BloomFilter {
|
|
|
58
73
|
#seed;
|
|
59
74
|
#scratch;
|
|
60
75
|
#n;
|
|
76
|
+
/**
|
|
77
|
+
* Creates a filter sized for `n` expected keys at a target false-positive rate.
|
|
78
|
+
*
|
|
79
|
+
* @param n - Expected number of keys.
|
|
80
|
+
* @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
|
|
81
|
+
* @returns A new, empty filter.
|
|
82
|
+
*/
|
|
61
83
|
static create(n, epsilon) {
|
|
84
|
+
require_params.assertPositiveInt(n, "n");
|
|
85
|
+
require_params.assertProbability(epsilon, "epsilon");
|
|
62
86
|
const f = new BloomFilter(optimal(n, epsilon));
|
|
63
87
|
f.#n = n;
|
|
64
88
|
return f;
|
|
65
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Restores a filter from its {@link BloomFilter.toBytes} serialization.
|
|
92
|
+
*
|
|
93
|
+
* @param bytes - The serialized filter.
|
|
94
|
+
* @returns The reconstructed filter.
|
|
95
|
+
*/
|
|
66
96
|
static fromBytes(bytes) {
|
|
67
97
|
const { body } = require_serialize.readHeader(bytes);
|
|
68
98
|
const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
|
|
@@ -77,7 +107,14 @@ var BloomFilter = class BloomFilter {
|
|
|
77
107
|
f.#bits.bytes.set(body.subarray(9));
|
|
78
108
|
return f;
|
|
79
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Constructs a filter from low-level {@link BloomParams}. Prefer
|
|
112
|
+
* {@link BloomFilter.create} unless restoring a specific configuration.
|
|
113
|
+
*/
|
|
80
114
|
constructor({ m, k, seed = 0 }) {
|
|
115
|
+
require_params.assertPositiveInt(m, "m");
|
|
116
|
+
require_params.assertPositiveInt(k, "k");
|
|
117
|
+
require_params.assertUint32(seed, "seed");
|
|
81
118
|
this.#bits = new BitSet(m);
|
|
82
119
|
this.#m = m;
|
|
83
120
|
this.#k = k;
|
|
@@ -89,6 +126,11 @@ var BloomFilter = class BloomFilter {
|
|
|
89
126
|
get bitsPerKey() {
|
|
90
127
|
return this.#m / this.#n;
|
|
91
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Serializes the filter to a portable little-endian byte layout.
|
|
131
|
+
*
|
|
132
|
+
* @returns The serialized filter, readable by {@link BloomFilter.fromBytes}.
|
|
133
|
+
*/
|
|
92
134
|
toBytes() {
|
|
93
135
|
const payload = this.#bits.bytes;
|
|
94
136
|
const body = new Uint8Array(9 + payload.length);
|
|
@@ -103,6 +145,13 @@ var BloomFilter = class BloomFilter {
|
|
|
103
145
|
flags: 0
|
|
104
146
|
}, body);
|
|
105
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Returns a new filter containing the union of this filter and `other`.
|
|
150
|
+
*
|
|
151
|
+
* @param other - A filter built with identical parameters.
|
|
152
|
+
* @returns A new filter reporting membership for keys in either input.
|
|
153
|
+
* @throws {@link BloomParamMismatchError} if the parameters differ.
|
|
154
|
+
*/
|
|
106
155
|
union(other) {
|
|
107
156
|
if (this.#m !== other.#m || this.#k !== other.#k || this.#seed !== other.#seed) throw new BloomParamMismatchError("cannot union Bloom filters whose parameters do not match");
|
|
108
157
|
const a = this.#bits.bytes;
|
|
@@ -117,10 +166,21 @@ var BloomFilter = class BloomFilter {
|
|
|
117
166
|
r.#bits.bytes.set(merged);
|
|
118
167
|
return r;
|
|
119
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Adds a key to the set.
|
|
171
|
+
*
|
|
172
|
+
* @param key - The key to insert, as a string or bytes.
|
|
173
|
+
*/
|
|
120
174
|
add(key) {
|
|
121
175
|
require_serialize.probeInto(key, this.#k, this.#m, this.#seed, this.#scratch);
|
|
122
176
|
for (let i = 0; i < this.#k; i++) this.#bits.set(this.#scratch[i] ?? 0);
|
|
123
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Tests whether a key is in the set.
|
|
180
|
+
*
|
|
181
|
+
* @param key - The key to test.
|
|
182
|
+
* @returns `true` if present (possibly a false positive); `false` guarantees absence.
|
|
183
|
+
*/
|
|
124
184
|
has(key) {
|
|
125
185
|
require_serialize.probeInto(key, this.#k, this.#m, this.#seed, this.#scratch);
|
|
126
186
|
for (let i = 0; i < this.#k; i++) if (!this.#bits.get(this.#scratch[i] ?? 0)) return false;
|
|
@@ -130,3 +190,4 @@ var BloomFilter = class BloomFilter {
|
|
|
130
190
|
//#endregion
|
|
131
191
|
exports.BloomFilter = BloomFilter;
|
|
132
192
|
exports.BloomParamMismatchError = BloomParamMismatchError;
|
|
193
|
+
exports.ParamError = require_params.ParamError;
|