bloomkit 0.1.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/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/index.cjs +376 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +214 -0
- package/dist/index.d.ts +214 -0
- package/dist/index.js +341 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 trananhtung
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# bloomkit
|
|
2
|
+
|
|
3
|
+
[](#contributors-)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/bloomkit)
|
|
6
|
+
[](https://github.com/trananhtung/bloomkit/actions)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Zero-dependency TypeScript Bloom filter — standard, counting, and scalable variants. Probabilistic set membership with tunable false-positive rate.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install bloomkit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Inspired by Python's [pybloom-live](https://pypi.org/project/pybloom-live/), Java's [Guava BloomFilter](https://guava.dev/releases/33.4.6-jre/api/docs/com/google/common/hash/BloomFilter.html), and Go's [bits-and-blooms/bloom](https://github.com/bits-and-blooms/bloom).
|
|
16
|
+
|
|
17
|
+
## Why bloomkit?
|
|
18
|
+
|
|
19
|
+
- **`bloomfilter`** on npm: last published **2013**, no TypeScript types.
|
|
20
|
+
- **`bloom-filters`** on npm: active, but ships **8 runtime dependencies** (lodash, xxhashjs, seedrandom, long, reflect-metadata…).
|
|
21
|
+
- **bloomkit**: zero runtime dependencies, native TypeScript, three filter variants, base64 serialization.
|
|
22
|
+
|
|
23
|
+
## What is a Bloom filter?
|
|
24
|
+
|
|
25
|
+
A Bloom filter is a **space-efficient probabilistic data structure** for set membership:
|
|
26
|
+
|
|
27
|
+
- `has(item)` can return **false positives** (item not added, but `has` returns `true`) at a configurable rate.
|
|
28
|
+
- `has(item)` **never returns false negatives** — if it says `false`, the item is definitely not in the set.
|
|
29
|
+
- Memory: a 1M-item filter with 1% FPR uses ~1.2 MB (vs. storing strings directly).
|
|
30
|
+
|
|
31
|
+
**Common uses:** deduplication pipelines, cache pre-screening, spam detection, database query optimization, network packet routing.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { BloomFilter } from "bloomkit";
|
|
37
|
+
|
|
38
|
+
// 1 million items, 1% false-positive rate
|
|
39
|
+
const bf = new BloomFilter({ capacity: 1_000_000, errorRate: 0.01 });
|
|
40
|
+
|
|
41
|
+
bf.add("user:42");
|
|
42
|
+
bf.has("user:42"); // true (definitely in the set)
|
|
43
|
+
bf.has("user:99"); // false (with 99% probability)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### `BloomFilter` — standard (no deletion)
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const bf = new BloomFilter({ capacity: 10_000, errorRate: 0.01 });
|
|
52
|
+
|
|
53
|
+
bf.add("item"); // add to filter
|
|
54
|
+
bf.has("item"); // true — may be a false positive
|
|
55
|
+
bf.size; // number of items added
|
|
56
|
+
bf.m; // bit-array size (auto-computed)
|
|
57
|
+
bf.k; // number of hash functions
|
|
58
|
+
bf.errorRate; // configured FPR target
|
|
59
|
+
bf.currentFPR; // estimated actual FPR given items inserted
|
|
60
|
+
bf.clear(); // reset
|
|
61
|
+
|
|
62
|
+
// Serialize / deserialize
|
|
63
|
+
const json = bf.toJSON();
|
|
64
|
+
const bf2 = BloomFilter.fromJSON(json);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `CountingBloomFilter` — supports deletion
|
|
68
|
+
|
|
69
|
+
Maintains per-cell counters so items can be removed. Uses 4-bit counters (default) or 8-bit for higher multiplicity.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { CountingBloomFilter } from "bloomkit";
|
|
73
|
+
|
|
74
|
+
const cbf = new CountingBloomFilter({ capacity: 10_000, errorRate: 0.01 });
|
|
75
|
+
|
|
76
|
+
cbf.add("session:abc");
|
|
77
|
+
cbf.has("session:abc"); // true
|
|
78
|
+
cbf.remove("session:abc");
|
|
79
|
+
cbf.has("session:abc"); // false
|
|
80
|
+
|
|
81
|
+
// 8-bit counters for items added many times
|
|
82
|
+
const cbf8 = new CountingBloomFilter({ capacity: 1000, counterBits: 8 });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `ScalableBloomFilter` — grows automatically
|
|
86
|
+
|
|
87
|
+
Use when you don't know the final set size upfront. Creates sub-filters as needed, maintaining the target FPR.
|
|
88
|
+
|
|
89
|
+
Port of [pybloom-live's `ScalableBloomFilter`](https://github.com/jaybaird/python-bloomfilter).
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { ScalableBloomFilter } from "bloomkit";
|
|
93
|
+
|
|
94
|
+
const sbf = new ScalableBloomFilter({ errorRate: 0.01 });
|
|
95
|
+
|
|
96
|
+
// Add any number of items — filter grows as needed
|
|
97
|
+
for (const id of millionIds) sbf.add(id);
|
|
98
|
+
sbf.has(id); // reliable
|
|
99
|
+
|
|
100
|
+
sbf.filterCount; // number of sub-filters created
|
|
101
|
+
sbf.bitsAllocated; // total bits across all sub-filters
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Utility exports
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { murmur3, fnv1a, hashPositions, optimalM, optimalK, BitArray } from "bloomkit";
|
|
108
|
+
|
|
109
|
+
murmur3("hello"); // MurmurHash3 (32-bit)
|
|
110
|
+
fnv1a("hello"); // FNV-1a (32-bit)
|
|
111
|
+
hashPositions("hello", 7, 1000); // 7 positions in [0, 1000)
|
|
112
|
+
optimalM(1000, 0.01); // optimal bit count for 1k items at 1% FPR
|
|
113
|
+
optimalK(9585, 1000); // optimal k for m=9585, n=1000
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
### URL deduplication (web crawler)
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { BloomFilter } from "bloomkit";
|
|
122
|
+
|
|
123
|
+
const seen = new BloomFilter({ capacity: 10_000_000, errorRate: 0.001 });
|
|
124
|
+
|
|
125
|
+
async function crawl(url: string) {
|
|
126
|
+
if (seen.has(url)) return; // skip if probably seen
|
|
127
|
+
seen.add(url);
|
|
128
|
+
await fetch(url);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Cache stampede prevention
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { BloomFilter } from "bloomkit";
|
|
136
|
+
|
|
137
|
+
const popularKeys = new BloomFilter({ capacity: 100_000, errorRate: 0.01 });
|
|
138
|
+
|
|
139
|
+
function getCached(key: string) {
|
|
140
|
+
if (!popularKeys.has(key)) {
|
|
141
|
+
// Likely a cache miss — skip to DB directly
|
|
142
|
+
return db.get(key);
|
|
143
|
+
}
|
|
144
|
+
return cache.get(key) ?? db.get(key);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Session tracking with expiry
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { CountingBloomFilter } from "bloomkit";
|
|
152
|
+
|
|
153
|
+
const activeSessions = new CountingBloomFilter({ capacity: 50_000 });
|
|
154
|
+
|
|
155
|
+
function login(sessionId: string) { activeSessions.add(sessionId); }
|
|
156
|
+
function logout(sessionId: string) { activeSessions.remove(sessionId); }
|
|
157
|
+
function isActive(sessionId: string) { return activeSessions.has(sessionId); }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Unknown-size dataset
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { ScalableBloomFilter } from "bloomkit";
|
|
164
|
+
|
|
165
|
+
const seen = new ScalableBloomFilter({ errorRate: 0.01 });
|
|
166
|
+
|
|
167
|
+
for await (const record of streamRecords()) {
|
|
168
|
+
if (!seen.has(record.id)) {
|
|
169
|
+
seen.add(record.id);
|
|
170
|
+
await process(record);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Comparison
|
|
176
|
+
|
|
177
|
+
| Package | Weekly downloads | Last release | TypeScript | Zero-dep | Variants |
|
|
178
|
+
|---------|-----------------|--------------|------------|----------|----------|
|
|
179
|
+
| **bloomkit** | — | 2024 | ✅ native | ✅ | Standard + Counting + Scalable |
|
|
180
|
+
| bloomfilter | ~12k | **2013** | ❌ | ✅ | Standard only |
|
|
181
|
+
| bloom-filters | ~500k | 2024 (active) | ❌ | ❌ (8 deps) | Many |
|
|
182
|
+
|
|
183
|
+
## Contributors ✨
|
|
184
|
+
|
|
185
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the [emoji key](https://allcontributors.org/docs/en/emoji-key) for how each contribution is recognized, and open a PR or issue to get involved.
|
|
186
|
+
|
|
187
|
+
Thanks goes to these wonderful people:
|
|
188
|
+
|
|
189
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
190
|
+
<!-- prettier-ignore-start -->
|
|
191
|
+
<!-- markdownlint-disable -->
|
|
192
|
+
<table>
|
|
193
|
+
<tbody>
|
|
194
|
+
<tr>
|
|
195
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/trananhtung"><img src="https://avatars.githubusercontent.com/u/30992229?v=4?s=100" width="100px;" alt="Tung Tran"/><br /><sub><b>Tung Tran</b></sub></a><br /><a href="https://github.com/trananhtung/./commits?author=trananhtung" title="Code">💻</a> <a href="#maintenance-trananhtung" title="Maintenance">🚧</a></td>
|
|
196
|
+
</tr>
|
|
197
|
+
</tbody>
|
|
198
|
+
</table>
|
|
199
|
+
|
|
200
|
+
<!-- markdownlint-restore -->
|
|
201
|
+
<!-- prettier-ignore-end -->
|
|
202
|
+
|
|
203
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BitArray: () => BitArray,
|
|
24
|
+
BloomFilter: () => BloomFilter,
|
|
25
|
+
CountingBloomFilter: () => CountingBloomFilter,
|
|
26
|
+
ScalableBloomFilter: () => ScalableBloomFilter,
|
|
27
|
+
fnv1a: () => fnv1a,
|
|
28
|
+
hashPositions: () => hashPositions,
|
|
29
|
+
murmur3: () => murmur3,
|
|
30
|
+
optimalK: () => optimalK,
|
|
31
|
+
optimalM: () => optimalM
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/BitArray.ts
|
|
36
|
+
var BitArray = class _BitArray {
|
|
37
|
+
constructor(size) {
|
|
38
|
+
this.size = size;
|
|
39
|
+
this._buf = new Uint32Array(Math.ceil(size / 32));
|
|
40
|
+
}
|
|
41
|
+
set(index) {
|
|
42
|
+
this._buf[index >>> 5] |= 1 << (index & 31);
|
|
43
|
+
}
|
|
44
|
+
get(index) {
|
|
45
|
+
return (this._buf[index >>> 5] >>> (index & 31) & 1) === 1;
|
|
46
|
+
}
|
|
47
|
+
clear() {
|
|
48
|
+
this._buf.fill(0);
|
|
49
|
+
}
|
|
50
|
+
/** Number of bits set to 1 (popcount). */
|
|
51
|
+
popcount() {
|
|
52
|
+
let n = 0;
|
|
53
|
+
for (const w of this._buf) {
|
|
54
|
+
let v = w;
|
|
55
|
+
v = v - (v >>> 1 & 1431655765);
|
|
56
|
+
v = (v & 858993459) + (v >>> 2 & 858993459);
|
|
57
|
+
n += (v + (v >>> 4) & 252645135) * 16843009 >>> 24;
|
|
58
|
+
}
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
/** Export as a base64-encoded string for serialization. */
|
|
62
|
+
toBase64() {
|
|
63
|
+
const bytes = new Uint8Array(this._buf.buffer);
|
|
64
|
+
let s = "";
|
|
65
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
66
|
+
return btoa(s);
|
|
67
|
+
}
|
|
68
|
+
/** Restore from a base64 string produced by `toBase64`. */
|
|
69
|
+
static fromBase64(b64, size) {
|
|
70
|
+
const arr = new _BitArray(size);
|
|
71
|
+
const raw = atob(b64);
|
|
72
|
+
const bytes = new Uint8Array(arr._buf.buffer);
|
|
73
|
+
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
|
|
74
|
+
return arr;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/hash.ts
|
|
79
|
+
function murmur3(key, seed = 0) {
|
|
80
|
+
let h = seed >>> 0;
|
|
81
|
+
const len = key.length;
|
|
82
|
+
let i = 0;
|
|
83
|
+
while (i <= len - 4) {
|
|
84
|
+
let k = key.charCodeAt(i) & 255 | (key.charCodeAt(i + 1) & 255) << 8 | (key.charCodeAt(i + 2) & 255) << 16 | (key.charCodeAt(i + 3) & 255) << 24;
|
|
85
|
+
k = Math.imul(k, 3432918353);
|
|
86
|
+
k = k << 15 | k >>> 17;
|
|
87
|
+
k = Math.imul(k, 461845907);
|
|
88
|
+
h ^= k;
|
|
89
|
+
h = h << 13 | h >>> 19;
|
|
90
|
+
h = Math.imul(h, 5) + 3864292196 >>> 0;
|
|
91
|
+
i += 4;
|
|
92
|
+
}
|
|
93
|
+
let remaining = 0;
|
|
94
|
+
switch (len & 3) {
|
|
95
|
+
case 3:
|
|
96
|
+
remaining ^= (key.charCodeAt(i + 2) & 255) << 16;
|
|
97
|
+
// fall through
|
|
98
|
+
case 2:
|
|
99
|
+
remaining ^= (key.charCodeAt(i + 1) & 255) << 8;
|
|
100
|
+
// fall through
|
|
101
|
+
case 1:
|
|
102
|
+
remaining ^= key.charCodeAt(i) & 255;
|
|
103
|
+
remaining = Math.imul(remaining, 3432918353);
|
|
104
|
+
remaining = remaining << 15 | remaining >>> 17;
|
|
105
|
+
remaining = Math.imul(remaining, 461845907);
|
|
106
|
+
h ^= remaining;
|
|
107
|
+
}
|
|
108
|
+
h ^= len;
|
|
109
|
+
h ^= h >>> 16;
|
|
110
|
+
h = Math.imul(h, 2246822507);
|
|
111
|
+
h ^= h >>> 13;
|
|
112
|
+
h = Math.imul(h, 3266489909);
|
|
113
|
+
h ^= h >>> 16;
|
|
114
|
+
return h >>> 0;
|
|
115
|
+
}
|
|
116
|
+
function fnv1a(key, seed = 2166136261) {
|
|
117
|
+
let h = seed >>> 0;
|
|
118
|
+
for (let i = 0; i < key.length; i++) {
|
|
119
|
+
h ^= key.charCodeAt(i) & 255;
|
|
120
|
+
h = Math.imul(h, 16777619);
|
|
121
|
+
}
|
|
122
|
+
return h >>> 0;
|
|
123
|
+
}
|
|
124
|
+
function hashPositions(key, k, m) {
|
|
125
|
+
const h1 = murmur3(key);
|
|
126
|
+
const h2 = fnv1a(key);
|
|
127
|
+
const positions = new Array(k);
|
|
128
|
+
for (let i = 0; i < k; i++) {
|
|
129
|
+
positions[i] = (h1 + i * h2 >>> 0) % m;
|
|
130
|
+
}
|
|
131
|
+
return positions;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/BloomFilter.ts
|
|
135
|
+
function optimalM(n, fpr) {
|
|
136
|
+
return Math.ceil(-n * Math.log(fpr) / (Math.LN2 * Math.LN2));
|
|
137
|
+
}
|
|
138
|
+
function optimalK(m, n) {
|
|
139
|
+
return Math.max(1, Math.round(m / n * Math.LN2));
|
|
140
|
+
}
|
|
141
|
+
var BloomFilter = class _BloomFilter {
|
|
142
|
+
constructor(options) {
|
|
143
|
+
this._count = 0;
|
|
144
|
+
const { capacity, errorRate = 0.01 } = options;
|
|
145
|
+
if (capacity <= 0 || !Number.isFinite(capacity)) throw new RangeError("capacity must be a positive finite number");
|
|
146
|
+
if (errorRate <= 0 || errorRate >= 1) throw new RangeError("errorRate must be in (0, 1)");
|
|
147
|
+
this.capacity = capacity;
|
|
148
|
+
this.errorRate = errorRate;
|
|
149
|
+
this.m = optimalM(capacity, errorRate);
|
|
150
|
+
this.k = optimalK(this.m, capacity);
|
|
151
|
+
this._bits = new BitArray(this.m);
|
|
152
|
+
}
|
|
153
|
+
/** Add an item to the filter. */
|
|
154
|
+
add(item) {
|
|
155
|
+
for (const pos of hashPositions(item, this.k, this.m)) {
|
|
156
|
+
this._bits.set(pos);
|
|
157
|
+
}
|
|
158
|
+
this._count++;
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Test membership. Returns `true` if item *may* have been added,
|
|
163
|
+
* `false` if it *definitely* has not.
|
|
164
|
+
*/
|
|
165
|
+
has(item) {
|
|
166
|
+
for (const pos of hashPositions(item, this.k, this.m)) {
|
|
167
|
+
if (!this._bits.get(pos)) return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
/** Number of items added (may exceed capacity). */
|
|
172
|
+
get size() {
|
|
173
|
+
return this._count;
|
|
174
|
+
}
|
|
175
|
+
/** Current estimated false-positive rate based on items inserted. */
|
|
176
|
+
get currentFPR() {
|
|
177
|
+
const fillRatio = this._bits.popcount() / this.m;
|
|
178
|
+
return Math.pow(fillRatio, this.k);
|
|
179
|
+
}
|
|
180
|
+
/** Reset the filter. */
|
|
181
|
+
clear() {
|
|
182
|
+
this._bits.clear();
|
|
183
|
+
this._count = 0;
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
/** Serialize to a plain object for JSON.stringify. */
|
|
187
|
+
toJSON() {
|
|
188
|
+
return {
|
|
189
|
+
type: "BloomFilter",
|
|
190
|
+
capacity: this.capacity,
|
|
191
|
+
errorRate: this.errorRate,
|
|
192
|
+
m: this.m,
|
|
193
|
+
k: this.k,
|
|
194
|
+
bits: this._bits.toBase64()
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Restore a BloomFilter from the output of `toJSON`. */
|
|
198
|
+
static fromJSON(data) {
|
|
199
|
+
const bf = new _BloomFilter({ capacity: data.capacity, errorRate: data.errorRate });
|
|
200
|
+
bf["_bits"] = BitArray.fromBase64(data.bits, data.m);
|
|
201
|
+
return bf;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/CountingBloomFilter.ts
|
|
206
|
+
var CountingBloomFilter = class {
|
|
207
|
+
constructor(options) {
|
|
208
|
+
this._count = 0;
|
|
209
|
+
const { capacity, errorRate = 0.01, counterBits = 4 } = options;
|
|
210
|
+
if (capacity <= 0 || !Number.isFinite(capacity)) throw new RangeError("capacity must be a positive finite number");
|
|
211
|
+
if (errorRate <= 0 || errorRate >= 1) throw new RangeError("errorRate must be in (0, 1)");
|
|
212
|
+
this.capacity = capacity;
|
|
213
|
+
this.errorRate = errorRate;
|
|
214
|
+
this.m = optimalM(capacity, errorRate);
|
|
215
|
+
this.k = optimalK(this.m, capacity);
|
|
216
|
+
this._counterBits = counterBits;
|
|
217
|
+
if (counterBits === 4) {
|
|
218
|
+
this._counters = new Uint8Array(Math.ceil(this.m / 2));
|
|
219
|
+
this._maxCount = 15;
|
|
220
|
+
} else {
|
|
221
|
+
this._counters = new Uint8Array(this.m);
|
|
222
|
+
this._maxCount = 255;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
_get(pos) {
|
|
226
|
+
if (this._counterBits === 4) {
|
|
227
|
+
const byte = this._counters[pos >>> 1];
|
|
228
|
+
return pos & 1 ? byte >>> 4 : byte & 15;
|
|
229
|
+
}
|
|
230
|
+
return this._counters[pos];
|
|
231
|
+
}
|
|
232
|
+
_increment(pos) {
|
|
233
|
+
if (this._counterBits === 4) {
|
|
234
|
+
const idx = pos >>> 1;
|
|
235
|
+
const byte = this._counters[idx];
|
|
236
|
+
if (pos & 1) {
|
|
237
|
+
const hi = byte >>> 4;
|
|
238
|
+
if (hi < this._maxCount) this._counters[idx] = byte & 15 | hi + 1 << 4;
|
|
239
|
+
} else {
|
|
240
|
+
const lo = byte & 15;
|
|
241
|
+
if (lo < this._maxCount) this._counters[idx] = byte & 240 | lo + 1;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
const v = this._counters[pos];
|
|
245
|
+
if (v < this._maxCount) this._counters[pos] = v + 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
_decrement(pos) {
|
|
249
|
+
if (this._counterBits === 4) {
|
|
250
|
+
const idx = pos >>> 1;
|
|
251
|
+
const byte = this._counters[idx];
|
|
252
|
+
if (pos & 1) {
|
|
253
|
+
const hi = byte >>> 4;
|
|
254
|
+
if (hi > 0) this._counters[idx] = byte & 15 | hi - 1 << 4;
|
|
255
|
+
} else {
|
|
256
|
+
const lo = byte & 15;
|
|
257
|
+
if (lo > 0) this._counters[idx] = byte & 240 | lo - 1;
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
const v = this._counters[pos];
|
|
261
|
+
if (v > 0) this._counters[pos] = v - 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** Add an item to the filter. */
|
|
265
|
+
add(item) {
|
|
266
|
+
for (const pos of hashPositions(item, this.k, this.m)) {
|
|
267
|
+
this._increment(pos);
|
|
268
|
+
}
|
|
269
|
+
this._count++;
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Remove an item from the filter.
|
|
274
|
+
* Only remove items you previously added — removing items never added leads
|
|
275
|
+
* to false negatives.
|
|
276
|
+
*/
|
|
277
|
+
remove(item) {
|
|
278
|
+
for (const pos of hashPositions(item, this.k, this.m)) {
|
|
279
|
+
this._decrement(pos);
|
|
280
|
+
}
|
|
281
|
+
this._count = Math.max(0, this._count - 1);
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
/** Test membership. */
|
|
285
|
+
has(item) {
|
|
286
|
+
for (const pos of hashPositions(item, this.k, this.m)) {
|
|
287
|
+
if (this._get(pos) === 0) return false;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
/** Number of items currently in the filter (approximate). */
|
|
292
|
+
get size() {
|
|
293
|
+
return this._count;
|
|
294
|
+
}
|
|
295
|
+
/** Reset the filter. */
|
|
296
|
+
clear() {
|
|
297
|
+
this._counters.fill(0);
|
|
298
|
+
this._count = 0;
|
|
299
|
+
return this;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// src/ScalableBloomFilter.ts
|
|
304
|
+
var ScalableBloomFilter = class {
|
|
305
|
+
constructor(options = {}) {
|
|
306
|
+
this._count = 0;
|
|
307
|
+
this._initialCapacity = options.initialCapacity ?? 1e3;
|
|
308
|
+
this._errorRate = options.errorRate ?? 0.01;
|
|
309
|
+
this._scaleFactor = options.scaleFactor ?? 2;
|
|
310
|
+
this._tighteningRatio = options.tighteningRatio ?? 0.9;
|
|
311
|
+
if (this._errorRate <= 0 || this._errorRate >= 1) throw new RangeError("errorRate must be in (0, 1)");
|
|
312
|
+
if (this._scaleFactor < 2) throw new RangeError("scaleFactor must be >= 2");
|
|
313
|
+
if (this._tighteningRatio <= 0 || this._tighteningRatio >= 1) throw new RangeError("tighteningRatio must be in (0, 1)");
|
|
314
|
+
this._filters = [this._createFilter(0)];
|
|
315
|
+
}
|
|
316
|
+
_createFilter(index) {
|
|
317
|
+
const capacity = Math.ceil(this._initialCapacity * Math.pow(this._scaleFactor, index));
|
|
318
|
+
const errorRate = this._errorRate * Math.pow(this._tighteningRatio, index);
|
|
319
|
+
return new BloomFilter({ capacity, errorRate });
|
|
320
|
+
}
|
|
321
|
+
/** Add an item. The filter grows automatically when the current slice is full. */
|
|
322
|
+
add(item) {
|
|
323
|
+
const current = this._filters[this._filters.length - 1];
|
|
324
|
+
if (current.size >= current.capacity) {
|
|
325
|
+
this._filters.push(this._createFilter(this._filters.length));
|
|
326
|
+
}
|
|
327
|
+
this._filters[this._filters.length - 1].add(item);
|
|
328
|
+
this._count++;
|
|
329
|
+
return this;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Test membership — checks all sub-filters.
|
|
333
|
+
* Returns `true` if item may have been added, `false` if definitely not.
|
|
334
|
+
*/
|
|
335
|
+
has(item) {
|
|
336
|
+
for (const filter of this._filters) {
|
|
337
|
+
if (filter.has(item)) return true;
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
/** Total number of items added (approximate). */
|
|
342
|
+
get size() {
|
|
343
|
+
return this._count;
|
|
344
|
+
}
|
|
345
|
+
/** Number of internal sub-filters created so far. */
|
|
346
|
+
get filterCount() {
|
|
347
|
+
return this._filters.length;
|
|
348
|
+
}
|
|
349
|
+
/** Total bits allocated across all sub-filters. */
|
|
350
|
+
get bitsAllocated() {
|
|
351
|
+
return this._filters.reduce((s, f) => s + f.m, 0);
|
|
352
|
+
}
|
|
353
|
+
/** Target false-positive rate. */
|
|
354
|
+
get errorRate() {
|
|
355
|
+
return this._errorRate;
|
|
356
|
+
}
|
|
357
|
+
/** Reset the filter. */
|
|
358
|
+
clear() {
|
|
359
|
+
this._filters = [this._createFilter(0)];
|
|
360
|
+
this._count = 0;
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
365
|
+
0 && (module.exports = {
|
|
366
|
+
BitArray,
|
|
367
|
+
BloomFilter,
|
|
368
|
+
CountingBloomFilter,
|
|
369
|
+
ScalableBloomFilter,
|
|
370
|
+
fnv1a,
|
|
371
|
+
hashPositions,
|
|
372
|
+
murmur3,
|
|
373
|
+
optimalK,
|
|
374
|
+
optimalM
|
|
375
|
+
});
|
|
376
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/BitArray.ts","../src/hash.ts","../src/BloomFilter.ts","../src/CountingBloomFilter.ts","../src/ScalableBloomFilter.ts"],"sourcesContent":["export { BloomFilter } from \"./BloomFilter.js\";\nexport type { BloomFilterOptions, BloomFilterJSON } from \"./BloomFilter.js\";\nexport { CountingBloomFilter } from \"./CountingBloomFilter.js\";\nexport type { CountingBloomFilterOptions } from \"./CountingBloomFilter.js\";\nexport { ScalableBloomFilter } from \"./ScalableBloomFilter.js\";\nexport type { ScalableBloomFilterOptions } from \"./ScalableBloomFilter.js\";\nexport { murmur3, fnv1a, hashPositions } from \"./hash.js\";\nexport { BitArray } from \"./BitArray.js\";\nexport { optimalM, optimalK } from \"./BloomFilter.js\";\n","/** Compact fixed-size bit array backed by Uint32Array. */\nexport class BitArray {\n private readonly _buf: Uint32Array;\n readonly size: number;\n\n constructor(size: number) {\n this.size = size;\n this._buf = new Uint32Array(Math.ceil(size / 32));\n }\n\n set(index: number): void {\n this._buf[index >>> 5]! |= 1 << (index & 31);\n }\n\n get(index: number): boolean {\n return (this._buf[index >>> 5]! >>> (index & 31) & 1) === 1;\n }\n\n clear(): void {\n this._buf.fill(0);\n }\n\n /** Number of bits set to 1 (popcount). */\n popcount(): number {\n let n = 0;\n for (const w of this._buf) {\n let v = w;\n v = v - ((v >>> 1) & 0x55555555);\n v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);\n n += (((v + (v >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;\n }\n return n;\n }\n\n /** Export as a base64-encoded string for serialization. */\n toBase64(): string {\n const bytes = new Uint8Array(this._buf.buffer);\n let s = \"\";\n for (const b of bytes) s += String.fromCharCode(b);\n return btoa(s);\n }\n\n /** Restore from a base64 string produced by `toBase64`. */\n static fromBase64(b64: string, size: number): BitArray {\n const arr = new BitArray(size);\n const raw = atob(b64);\n const bytes = new Uint8Array(arr._buf.buffer);\n for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);\n return arr;\n }\n}\n","/**\n * MurmurHash3 (32-bit, x86) — fast non-cryptographic hash.\n * Returns an unsigned 32-bit integer.\n */\nexport function murmur3(key: string, seed = 0): number {\n let h = seed >>> 0;\n const len = key.length;\n let i = 0;\n\n while (i <= len - 4) {\n let k =\n ((key.charCodeAt(i) & 0xff)) |\n ((key.charCodeAt(i + 1) & 0xff) << 8) |\n ((key.charCodeAt(i + 2) & 0xff) << 16) |\n ((key.charCodeAt(i + 3) & 0xff) << 24);\n k = Math.imul(k, 0xcc9e2d51);\n k = (k << 15) | (k >>> 17);\n k = Math.imul(k, 0x1b873593);\n h ^= k;\n h = (h << 13) | (h >>> 19);\n h = (Math.imul(h, 5) + 0xe6546b64) >>> 0;\n i += 4;\n }\n\n let remaining = 0;\n switch (len & 3) {\n case 3: remaining ^= (key.charCodeAt(i + 2) & 0xff) << 16; // fall through\n case 2: remaining ^= (key.charCodeAt(i + 1) & 0xff) << 8; // fall through\n case 1:\n remaining ^= key.charCodeAt(i) & 0xff;\n remaining = Math.imul(remaining, 0xcc9e2d51);\n remaining = (remaining << 15) | (remaining >>> 17);\n remaining = Math.imul(remaining, 0x1b873593);\n h ^= remaining;\n }\n\n h ^= len;\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return h >>> 0;\n}\n\n/**\n * FNV-1a (32-bit) — second independent hash for double-hashing.\n */\nexport function fnv1a(key: string, seed = 0x811c9dc5): number {\n let h = seed >>> 0;\n for (let i = 0; i < key.length; i++) {\n h ^= key.charCodeAt(i) & 0xff;\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n\n/**\n * Generate `k` hash positions in `[0, m)` using double hashing.\n * gi(x) = (h1(x) + i * h2(x)) mod m\n */\nexport function hashPositions(key: string, k: number, m: number): number[] {\n const h1 = murmur3(key);\n const h2 = fnv1a(key);\n const positions: number[] = new Array(k);\n for (let i = 0; i < k; i++) {\n positions[i] = ((h1 + i * h2) >>> 0) % m;\n }\n return positions;\n}\n","import { BitArray } from \"./BitArray.js\";\nimport { hashPositions } from \"./hash.js\";\n\n/** @internal Compute optimal bit-array size m given n items and fpr. */\nexport function optimalM(n: number, fpr: number): number {\n return Math.ceil(-n * Math.log(fpr) / (Math.LN2 * Math.LN2));\n}\n\n/** @internal Compute optimal number of hash functions k given m bits and n items. */\nexport function optimalK(m: number, n: number): number {\n return Math.max(1, Math.round((m / n) * Math.LN2));\n}\n\nexport interface BloomFilterOptions {\n /**\n * Expected number of items that will be inserted.\n * Used to size the filter optimally.\n */\n capacity: number;\n /**\n * Target false-positive rate (0 < fpr < 1). Default: 0.01 (1%).\n */\n errorRate?: number;\n}\n\nexport interface BloomFilterJSON {\n type: \"BloomFilter\";\n capacity: number;\n errorRate: number;\n m: number;\n k: number;\n bits: string;\n}\n\n/**\n * Standard Bloom filter — space-efficient probabilistic set membership.\n *\n * - `add(item)` always works correctly.\n * - `has(item)` may return `true` for items not added (false positive, rate ≤ `errorRate`).\n * - `has(item)` never returns `false` for items that were added.\n * - Items cannot be removed (use `CountingBloomFilter` for deletions).\n *\n * @example\n * const bf = new BloomFilter({ capacity: 1_000_000, errorRate: 0.01 });\n * bf.add(\"hello\");\n * bf.has(\"hello\"); // true\n * bf.has(\"world\"); // false (with high probability)\n */\nexport class BloomFilter {\n readonly capacity: number;\n readonly errorRate: number;\n /** Bit-array size. */\n readonly m: number;\n /** Number of hash functions. */\n readonly k: number;\n\n private readonly _bits: BitArray;\n private _count = 0;\n\n constructor(options: BloomFilterOptions) {\n const { capacity, errorRate = 0.01 } = options;\n if (capacity <= 0 || !Number.isFinite(capacity)) throw new RangeError(\"capacity must be a positive finite number\");\n if (errorRate <= 0 || errorRate >= 1) throw new RangeError(\"errorRate must be in (0, 1)\");\n\n this.capacity = capacity;\n this.errorRate = errorRate;\n this.m = optimalM(capacity, errorRate);\n this.k = optimalK(this.m, capacity);\n this._bits = new BitArray(this.m);\n }\n\n /** Add an item to the filter. */\n add(item: string): this {\n for (const pos of hashPositions(item, this.k, this.m)) {\n this._bits.set(pos);\n }\n this._count++;\n return this;\n }\n\n /**\n * Test membership. Returns `true` if item *may* have been added,\n * `false` if it *definitely* has not.\n */\n has(item: string): boolean {\n for (const pos of hashPositions(item, this.k, this.m)) {\n if (!this._bits.get(pos)) return false;\n }\n return true;\n }\n\n /** Number of items added (may exceed capacity). */\n get size(): number {\n return this._count;\n }\n\n /** Current estimated false-positive rate based on items inserted. */\n get currentFPR(): number {\n const fillRatio = this._bits.popcount() / this.m;\n return Math.pow(fillRatio, this.k);\n }\n\n /** Reset the filter. */\n clear(): this {\n this._bits.clear();\n this._count = 0;\n return this;\n }\n\n /** Serialize to a plain object for JSON.stringify. */\n toJSON(): BloomFilterJSON {\n return {\n type: \"BloomFilter\",\n capacity: this.capacity,\n errorRate: this.errorRate,\n m: this.m,\n k: this.k,\n bits: this._bits.toBase64(),\n };\n }\n\n /** Restore a BloomFilter from the output of `toJSON`. */\n static fromJSON(data: BloomFilterJSON): BloomFilter {\n const bf = new BloomFilter({ capacity: data.capacity, errorRate: data.errorRate });\n (bf as unknown as { _bits: BitArray })[\"_bits\"] = BitArray.fromBase64(data.bits, data.m);\n return bf;\n }\n}\n","import { hashPositions } from \"./hash.js\";\nimport { optimalM, optimalK } from \"./BloomFilter.js\";\n\nexport interface CountingBloomFilterOptions {\n /** Expected number of items. */\n capacity: number;\n /** Target false-positive rate (default: 0.01). */\n errorRate?: number;\n /** Counter width in bits — 4 (default) supports up to 15 adds per cell. */\n counterBits?: 4 | 8;\n}\n\n/**\n * Counting Bloom filter — supports deletion by maintaining per-cell counters\n * instead of single bits.\n *\n * - `add(item)` increments k counters.\n * - `remove(item)` decrements k counters. Do NOT remove items that were never added.\n * - `has(item)` returns `true` iff all k counters are > 0.\n *\n * @example\n * const cbf = new CountingBloomFilter({ capacity: 10_000 });\n * cbf.add(\"user:42\");\n * cbf.has(\"user:42\"); // true\n * cbf.remove(\"user:42\");\n * cbf.has(\"user:42\"); // false\n */\nexport class CountingBloomFilter {\n readonly capacity: number;\n readonly errorRate: number;\n readonly m: number;\n readonly k: number;\n private readonly _counters: Uint8Array | Uint16Array;\n private readonly _counterBits: number;\n private readonly _maxCount: number;\n private _count = 0;\n\n constructor(options: CountingBloomFilterOptions) {\n const { capacity, errorRate = 0.01, counterBits = 4 } = options;\n if (capacity <= 0 || !Number.isFinite(capacity)) throw new RangeError(\"capacity must be a positive finite number\");\n if (errorRate <= 0 || errorRate >= 1) throw new RangeError(\"errorRate must be in (0, 1)\");\n\n this.capacity = capacity;\n this.errorRate = errorRate;\n this.m = optimalM(capacity, errorRate);\n this.k = optimalK(this.m, capacity);\n this._counterBits = counterBits;\n\n if (counterBits === 4) {\n // Pack two 4-bit counters per byte\n this._counters = new Uint8Array(Math.ceil(this.m / 2));\n this._maxCount = 15;\n } else {\n this._counters = new Uint8Array(this.m);\n this._maxCount = 255;\n }\n }\n\n private _get(pos: number): number {\n if (this._counterBits === 4) {\n const byte = this._counters[pos >>> 1]!;\n return pos & 1 ? byte >>> 4 : byte & 0x0f;\n }\n return (this._counters as Uint8Array)[pos]!;\n }\n\n private _increment(pos: number): void {\n if (this._counterBits === 4) {\n const idx = pos >>> 1;\n const byte = this._counters[idx]!;\n if (pos & 1) {\n const hi = byte >>> 4;\n if (hi < this._maxCount) this._counters[idx] = (byte & 0x0f) | ((hi + 1) << 4);\n } else {\n const lo = byte & 0x0f;\n if (lo < this._maxCount) this._counters[idx] = (byte & 0xf0) | (lo + 1);\n }\n } else {\n const v = (this._counters as Uint8Array)[pos]!;\n if (v < this._maxCount) (this._counters as Uint8Array)[pos] = v + 1;\n }\n }\n\n private _decrement(pos: number): void {\n if (this._counterBits === 4) {\n const idx = pos >>> 1;\n const byte = this._counters[idx]!;\n if (pos & 1) {\n const hi = byte >>> 4;\n if (hi > 0) this._counters[idx] = (byte & 0x0f) | ((hi - 1) << 4);\n } else {\n const lo = byte & 0x0f;\n if (lo > 0) this._counters[idx] = (byte & 0xf0) | (lo - 1);\n }\n } else {\n const v = (this._counters as Uint8Array)[pos]!;\n if (v > 0) (this._counters as Uint8Array)[pos] = v - 1;\n }\n }\n\n /** Add an item to the filter. */\n add(item: string): this {\n for (const pos of hashPositions(item, this.k, this.m)) {\n this._increment(pos);\n }\n this._count++;\n return this;\n }\n\n /**\n * Remove an item from the filter.\n * Only remove items you previously added — removing items never added leads\n * to false negatives.\n */\n remove(item: string): this {\n for (const pos of hashPositions(item, this.k, this.m)) {\n this._decrement(pos);\n }\n this._count = Math.max(0, this._count - 1);\n return this;\n }\n\n /** Test membership. */\n has(item: string): boolean {\n for (const pos of hashPositions(item, this.k, this.m)) {\n if (this._get(pos) === 0) return false;\n }\n return true;\n }\n\n /** Number of items currently in the filter (approximate). */\n get size(): number {\n return this._count;\n }\n\n /** Reset the filter. */\n clear(): this {\n this._counters.fill(0);\n this._count = 0;\n return this;\n }\n}\n","import { BloomFilter } from \"./BloomFilter.js\";\n\nexport interface ScalableBloomFilterOptions {\n /**\n * Initial capacity (items before first resize). Default: 1000.\n */\n initialCapacity?: number;\n /**\n * Target overall false-positive rate. Default: 0.01.\n * Each sub-filter uses `errorRate * tighteningRatio^i` so the series\n * converges to a total FPR ≤ `errorRate`.\n */\n errorRate?: number;\n /**\n * Scale factor: each new sub-filter has `scaleFactor × previous capacity`.\n * Common values: 2 (default) or 4.\n */\n scaleFactor?: number;\n /**\n * Ratio by which each sub-filter tightens its FPR. Default: 0.9.\n * Must be in (0, 1).\n */\n tighteningRatio?: number;\n}\n\n/**\n * Scalable Bloom filter — grows automatically as items are added, so you\n * don't need to know the final set size upfront.\n *\n * Maintains a series of standard BloomFilters; when the current one is full,\n * a new one is created with higher capacity and tighter FPR.\n *\n * Port of Python pybloom-live's `ScalableBloomFilter`.\n *\n * @example\n * const sbf = new ScalableBloomFilter({ errorRate: 0.01 });\n * for (const id of millionIds) sbf.add(id);\n * sbf.has(id); // reliable, even with 1M+ items\n */\nexport class ScalableBloomFilter {\n private readonly _initialCapacity: number;\n private readonly _errorRate: number;\n private readonly _scaleFactor: number;\n private readonly _tighteningRatio: number;\n private _filters: BloomFilter[];\n private _count = 0;\n\n constructor(options: ScalableBloomFilterOptions = {}) {\n this._initialCapacity = options.initialCapacity ?? 1000;\n this._errorRate = options.errorRate ?? 0.01;\n this._scaleFactor = options.scaleFactor ?? 2;\n this._tighteningRatio = options.tighteningRatio ?? 0.9;\n\n if (this._errorRate <= 0 || this._errorRate >= 1) throw new RangeError(\"errorRate must be in (0, 1)\");\n if (this._scaleFactor < 2) throw new RangeError(\"scaleFactor must be >= 2\");\n if (this._tighteningRatio <= 0 || this._tighteningRatio >= 1) throw new RangeError(\"tighteningRatio must be in (0, 1)\");\n\n this._filters = [this._createFilter(0)];\n }\n\n private _createFilter(index: number): BloomFilter {\n const capacity = Math.ceil(this._initialCapacity * Math.pow(this._scaleFactor, index));\n const errorRate = this._errorRate * Math.pow(this._tighteningRatio, index);\n return new BloomFilter({ capacity, errorRate });\n }\n\n /** Add an item. The filter grows automatically when the current slice is full. */\n add(item: string): this {\n const current = this._filters[this._filters.length - 1]!;\n\n // Grow when we'd exceed capacity (keeps FPR ≤ target)\n if (current.size >= current.capacity) {\n this._filters.push(this._createFilter(this._filters.length));\n }\n\n this._filters[this._filters.length - 1]!.add(item);\n this._count++;\n return this;\n }\n\n /**\n * Test membership — checks all sub-filters.\n * Returns `true` if item may have been added, `false` if definitely not.\n */\n has(item: string): boolean {\n for (const filter of this._filters) {\n if (filter.has(item)) return true;\n }\n return false;\n }\n\n /** Total number of items added (approximate). */\n get size(): number {\n return this._count;\n }\n\n /** Number of internal sub-filters created so far. */\n get filterCount(): number {\n return this._filters.length;\n }\n\n /** Total bits allocated across all sub-filters. */\n get bitsAllocated(): number {\n return this._filters.reduce((s, f) => s + f.m, 0);\n }\n\n /** Target false-positive rate. */\n get errorRate(): number {\n return this._errorRate;\n }\n\n /** Reset the filter. */\n clear(): this {\n this._filters = [this._createFilter(0)];\n this._count = 0;\n return this;\n }\n}\n"],"mappings":"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","names":[]}
|