distillate 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,41 @@
1
- import { a as writeHeader, c as reduce, i as readHeader, n as assertBodyLength, o as hash128KeyInto, r as assertMinBodyLength, t as SerializationError } from "../serialize-DkkBKDT0.js";
2
- import { a as assertUint32, i as assertProbability, n as assertPositiveFinite, r as assertPositiveInt, t as ParamError } from "../params-ChTRNxM9.js";
1
+ import { _ as reduce, a as bytesEqual, c as toJSONEnvelope, i as assertMinBodyLength, l as writeFrame, m as hash32x2Into, n as UnknownHashVariantError, o as fromJSONEnvelope, r as assertBodyLength, s as readHeader, t as SerializationError } from "../serialize-BnwtzcMw.js";
2
+ import { i as assertProbability, n as assertPositiveFinite, o as assertUint32, r as assertPositiveInt, t as ParamError } from "../params-BrWuBC2A.js";
3
3
  //#region src/blocked/blocked.ts
4
4
  const TYPE = 2;
5
5
  const SALT = Uint32Array.of(1203114875, 1150766481, 2284105051, 2729912477, 1884591559, 770785867, 2667333959, 1550580529);
6
- const scratchHash = {
7
- h1lo: 0,
8
- h1hi: 0,
9
- h2lo: 0,
10
- h2hi: 0
11
- };
6
+ const scratch2 = /* @__PURE__ */ new Uint32Array(2);
7
+ const BLOCK_BITS = 256;
8
+ const LANE_BITS = 32;
9
+ const LANES = 8;
10
+ /**
11
+ * Modeled false-positive rate of a split-block filter at `bitsPerKey`. A block
12
+ * holding `j` keys has FPR `(1 - (1 - 1/32)^j)^8` (8 lanes of 32 bits, one probe
13
+ * each); the filter's rate averages that over the Poisson block load
14
+ * `lambda = 256 / bitsPerKey`. This clustering average is why the blocked curve
15
+ * is not linear in `log10(1/epsilon)`.
16
+ */
17
+ function blockedFprAt(bitsPerKey) {
18
+ const lambda = BLOCK_BITS / bitsPerKey;
19
+ let fpr = 0;
20
+ let p = Math.exp(-lambda);
21
+ for (let j = 0;; j++) {
22
+ if (j > 0) p *= lambda / j;
23
+ fpr += p * (1 - (1 - 1 / LANE_BITS) ** j) ** LANES;
24
+ if (j > lambda && p < 1e-15) break;
25
+ }
26
+ return fpr;
27
+ }
28
+ const MAX_BITS_PER_KEY = 128;
29
+ /**
30
+ * Minimal integer bits-per-key whose modeled split-block FPR is at or below
31
+ * `epsilon`. Throws {@link ParamError} when even the densest supported filter
32
+ * cannot reach the target, so callers get a typed rejection instead of a
33
+ * silently under-provisioned filter.
34
+ */
35
+ function blockedBitsPerKey(epsilon) {
36
+ for (let bpk = 1; bpk <= MAX_BITS_PER_KEY; bpk++) if (blockedFprAt(bpk) <= epsilon) return bpk;
37
+ throw new ParamError(`epsilon ${String(epsilon)} is below the blocked-filter floor; use a classic or fuse filter`);
38
+ }
12
39
  /** Thrown when an operation requires two filters built with identical parameters. */
13
40
  var BlockedBloomParamMismatchError = class extends Error {
14
41
  /** Discriminates this error from other `Error`s. */
@@ -16,7 +43,7 @@ var BlockedBloomParamMismatchError = class extends Error {
16
43
  };
17
44
  /**
18
45
  * A blocked (split-block) Bloom filter: confines every lookup to a single cache
19
- * line, trading ~20-30% more space for cache-friendly throughput.
46
+ * line, trading ~15% more space for higher lookup throughput and a lower FPR.
20
47
  *
21
48
  * @example
22
49
  * ```ts
@@ -32,11 +59,6 @@ var BlockedBloomFilter = class BlockedBloomFilter {
32
59
  #n;
33
60
  #words = /* @__PURE__ */ new Uint32Array(8);
34
61
  #bits = /* @__PURE__ */ new Uint32Array(8);
35
- static #ANCHORS = [
36
- [2, 10.5],
37
- [3, 16.9],
38
- [4, 26.4]
39
- ];
40
62
  /**
41
63
  * Creates a filter sized for `n` expected keys at a target false-positive rate.
42
64
  *
@@ -47,35 +69,62 @@ var BlockedBloomFilter = class BlockedBloomFilter {
47
69
  static create(n, epsilon) {
48
70
  assertPositiveInt(n, "n");
49
71
  assertProbability(epsilon, "epsilon");
50
- const t = Math.log10(1 / epsilon);
51
- const a = BlockedBloomFilter.#ANCHORS;
52
- let seg = a.findIndex((p) => t <= p[0]);
53
- if (seg < 1) seg = seg === -1 ? a.length - 1 : 1;
54
- const [t0, b0] = a[seg - 1] ?? [0, 0];
55
- const [t1, b1] = a[seg] ?? [0, 0];
56
- const bitsPerKey = b0 + (b1 - b0) / (t1 - t0) * (t - t0);
57
72
  return new BlockedBloomFilter({
58
- bitsPerKey: Math.max(1, Math.ceil(bitsPerKey)),
73
+ bitsPerKey: blockedBitsPerKey(epsilon),
59
74
  capacity: n
60
75
  });
61
76
  }
62
77
  /**
78
+ * Builds a filter from `keys`, sized for their count at the target
79
+ * false-positive rate. The ergonomic entry point when the key set is already
80
+ * in hand; use {@link BlockedBloomFilter.create} to size for a count known
81
+ * ahead.
82
+ *
83
+ * @param keys - The keys to insert.
84
+ * @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
85
+ * @returns A new filter containing every key.
86
+ */
87
+ static from(keys, epsilon) {
88
+ const arr = [...keys];
89
+ const f = BlockedBloomFilter.create(Math.max(1, arr.length), epsilon);
90
+ for (const k of arr) f.add(k);
91
+ return f;
92
+ }
93
+ /**
63
94
  * Constructs a filter from low-level {@link BlockedBloomParams}. Prefer
64
95
  * {@link BlockedBloomFilter.create} unless restoring a specific configuration.
65
96
  */
66
97
  constructor({ bitsPerKey, capacity, seed = 0 }) {
67
98
  assertPositiveFinite(bitsPerKey, "bitsPerKey");
68
99
  assertPositiveInt(capacity, "capacity");
100
+ assertUint32(capacity, "capacity");
69
101
  assertUint32(seed, "seed");
70
102
  this.#numBlocks = Math.max(1, Math.ceil(bitsPerKey * capacity / 256));
71
103
  this.#lanes = new Uint32Array(this.#numBlocks * 8);
72
104
  this.#seed = seed;
73
105
  this.#n = capacity;
74
106
  }
107
+ static #fromNumBlocks(numBlocks, seed, n) {
108
+ const f = new BlockedBloomFilter({
109
+ bitsPerKey: 256,
110
+ capacity: numBlocks,
111
+ seed
112
+ });
113
+ f.#n = n;
114
+ return f;
115
+ }
75
116
  /** Actual bits allocated per key (`total bits / capacity`). */
76
117
  get bitsPerKey() {
77
118
  return this.#numBlocks * 256 / this.#n;
78
119
  }
120
+ /** Number of 256-bit blocks; one of the two fields `union` requires to match. */
121
+ get numBlocks() {
122
+ return this.#numBlocks;
123
+ }
124
+ /** Hash seed; the other field `union` requires to match. */
125
+ get seed() {
126
+ return this.#seed;
127
+ }
79
128
  /** Number of bits currently set across all lanes. */
80
129
  get length() {
81
130
  let bits = 0;
@@ -103,20 +152,17 @@ var BlockedBloomFilter = class BlockedBloomFilter {
103
152
  * @returns The reconstructed filter.
104
153
  */
105
154
  static fromBytes(bytes) {
106
- const { type, body } = readHeader(bytes);
155
+ const { type, flags, body } = readHeader(bytes);
107
156
  if (type !== TYPE) throw new SerializationError(`expected AMQF type ${String(TYPE)}, got ${String(type)}`);
157
+ if ((flags & 15) !== 0) throw new UnknownHashVariantError(`unsupported hash variant ${String(flags & 15)}`);
108
158
  assertMinBodyLength(body.length, 12, "blocked");
109
159
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
110
160
  const numBlocks = dv.getUint32(0, true);
111
161
  const seed = dv.getUint32(4, true);
112
162
  const n = dv.getUint32(8, true);
163
+ if (numBlocks === 0 || n === 0) throw new SerializationError(`blocked frame declares numBlocks=${String(numBlocks)}, n=${String(n)}; both must be positive`);
113
164
  assertBodyLength(body.length, 12 + numBlocks * 32, "blocked");
114
- const f = new BlockedBloomFilter({
115
- bitsPerKey: 256,
116
- capacity: numBlocks,
117
- seed
118
- });
119
- f.#n = n;
165
+ const f = BlockedBloomFilter.#fromNumBlocks(numBlocks, seed, n);
120
166
  new Uint8Array(f.#lanes.buffer).set(body.subarray(12));
121
167
  return f;
122
168
  }
@@ -127,17 +173,44 @@ var BlockedBloomFilter = class BlockedBloomFilter {
127
173
  */
128
174
  toBytes() {
129
175
  const lanes = new Uint8Array(this.#lanes.buffer, this.#lanes.byteOffset, this.#lanes.byteLength);
130
- const body = new Uint8Array(12 + lanes.length);
131
- const dv = new DataView(body.buffer);
132
- dv.setUint32(0, this.#numBlocks, true);
133
- dv.setUint32(4, this.#seed, true);
134
- dv.setUint32(8, this.#n, true);
135
- body.set(lanes, 12);
136
- return writeHeader({
137
- version: 2,
176
+ return writeFrame({
177
+ version: 3,
138
178
  type: TYPE,
139
179
  flags: 0
140
- }, body);
180
+ }, 12 + lanes.length, (body, dv) => {
181
+ dv.setUint32(0, this.#numBlocks, true);
182
+ dv.setUint32(4, this.#seed, true);
183
+ dv.setUint32(8, this.#n, true);
184
+ body.set(lanes, 12);
185
+ });
186
+ }
187
+ /**
188
+ * Tests structural equality: `true` when `other` serializes to identical
189
+ * bytes, meaning identical parameters and set bits.
190
+ *
191
+ * @param other - The filter to compare against.
192
+ * @returns `true` if the two filters are byte-for-byte identical.
193
+ */
194
+ equals(other) {
195
+ return bytesEqual(this.toBytes(), other.toBytes());
196
+ }
197
+ /**
198
+ * Serializes the filter to a JSON-friendly envelope wrapping the base64 of
199
+ * {@link BlockedBloomFilter.toBytes}.
200
+ *
201
+ * @returns The envelope, readable by {@link BlockedBloomFilter.fromJSON}.
202
+ */
203
+ toJSON() {
204
+ return toJSONEnvelope(this.toBytes());
205
+ }
206
+ /**
207
+ * Restores a filter from its {@link BlockedBloomFilter.toJSON} envelope.
208
+ *
209
+ * @param value - The JSON envelope.
210
+ * @returns The reconstructed filter.
211
+ */
212
+ static fromJSON(value) {
213
+ return BlockedBloomFilter.fromBytes(fromJSONEnvelope(value));
141
214
  }
142
215
  /**
143
216
  * Returns a new filter containing the union of this filter and `other`.
@@ -148,11 +221,7 @@ var BlockedBloomFilter = class BlockedBloomFilter {
148
221
  */
149
222
  union(other) {
150
223
  if (this.#numBlocks !== other.#numBlocks || this.#seed !== other.#seed) throw new BlockedBloomParamMismatchError("cannot union blocked Bloom filters whose parameters do not match");
151
- const r = new BlockedBloomFilter({
152
- bitsPerKey: this.bitsPerKey,
153
- capacity: this.#n,
154
- seed: this.#seed
155
- });
224
+ const r = BlockedBloomFilter.#fromNumBlocks(this.#numBlocks, this.#seed, this.#n);
156
225
  for (let i = 0; i < this.#lanes.length; i++) r.#lanes[i] = (this.#lanes[i] ?? 0) | (other.#lanes[i] ?? 0);
157
226
  return r;
158
227
  }
@@ -184,9 +253,9 @@ var BlockedBloomFilter = class BlockedBloomFilter {
184
253
  }
185
254
  };
186
255
  function fillBlock(key, numBlocks, seed, outWords, outBits) {
187
- hash128KeyInto(key, seed, scratchHash);
188
- const block = reduce((scratchHash.h1lo ^ scratchHash.h1hi) >>> 0, numBlocks);
189
- const x = (scratchHash.h2lo ^ scratchHash.h2hi) >>> 0;
256
+ hash32x2Into(key, seed, scratch2);
257
+ const block = reduce(scratch2[0] ?? 0, numBlocks);
258
+ const x = scratch2[1] ?? 0;
190
259
  const base = block * 8;
191
260
  for (let i = 0; i < 8; i++) {
192
261
  outWords[i] = base + i;
@@ -1,18 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_serialize = require("../serialize-DPdiySxq.cjs");
3
- const require_params = require("../params-J8p3bKq5.cjs");
2
+ const require_serialize = require("../serialize-BIIKUHH6.cjs");
3
+ const require_params = require("../params--8CNXYWu.cjs");
4
4
  //#region src/core/bitset.ts
5
5
  var BitSetRangeError = class extends RangeError {
6
6
  name = "BitSetRangeError";
7
7
  };
8
8
  const MAX_BITS = 2 ** 32;
9
- var BitSet = class BitSet {
9
+ var BitSet = class {
10
10
  #bits;
11
- static fromBytes(bytes) {
12
- const bs = new BitSet(bytes.length * 8);
13
- bs.#bits.set(bytes);
14
- return bs;
15
- }
16
11
  constructor(nbits) {
17
12
  if (nbits > MAX_BITS) throw new BitSetRangeError(`BitSet capacity ${String(nbits)} exceeds the 2^32-bit limit`);
18
13
  this.#bits = new Uint8Array(Math.ceil(nbits / 8));
@@ -82,9 +77,23 @@ var BloomFilter = class BloomFilter {
82
77
  */
83
78
  static create(n, epsilon) {
84
79
  require_params.assertPositiveInt(n, "n");
80
+ require_params.assertUint32(n, "n");
85
81
  require_params.assertProbability(epsilon, "epsilon");
86
- const f = new BloomFilter(optimal(n, epsilon));
87
- f.#n = n;
82
+ return BloomFilter.#withN(optimal(n, epsilon), n);
83
+ }
84
+ /**
85
+ * Builds a filter from `keys`, sized for their count at the target
86
+ * false-positive rate. The ergonomic entry point when the key set is already
87
+ * in hand; use {@link BloomFilter.create} to size for a count known ahead.
88
+ *
89
+ * @param keys - The keys to insert.
90
+ * @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
91
+ * @returns A new filter containing every key.
92
+ */
93
+ static from(keys, epsilon) {
94
+ const arr = [...keys];
95
+ const f = BloomFilter.create(Math.max(1, arr.length), epsilon);
96
+ for (const k of arr) f.add(k);
88
97
  return f;
89
98
  }
90
99
  /**
@@ -94,8 +103,9 @@ var BloomFilter = class BloomFilter {
94
103
  * @returns The reconstructed filter.
95
104
  */
96
105
  static fromBytes(bytes) {
97
- const { type, body } = require_serialize.readHeader(bytes);
106
+ const { type, flags, body } = require_serialize.readHeader(bytes);
98
107
  if (type !== TYPE) throw new require_serialize.SerializationError(`expected AMQF type ${String(TYPE)}, got ${String(type)}`);
108
+ if ((flags & 15) !== 0) throw new require_serialize.UnknownHashVariantError(`unsupported hash variant ${String(flags & 15)}`);
99
109
  require_serialize.assertMinBodyLength(body.length, 14, "bloom");
100
110
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
101
111
  const m = dv.getUint32(0, true);
@@ -103,12 +113,11 @@ var BloomFilter = class BloomFilter {
103
113
  const seed = dv.getUint32(6, true);
104
114
  const n = dv.getUint32(10, true);
105
115
  require_serialize.assertBodyLength(body.length, 14 + Math.ceil(m / 8), "bloom");
106
- const f = new BloomFilter({
116
+ const f = BloomFilter.#withN({
107
117
  m,
108
118
  k,
109
119
  seed
110
- });
111
- f.#n = n;
120
+ }, n);
112
121
  f.#bits.bytes.set(body.subarray(14));
113
122
  return f;
114
123
  }
@@ -118,7 +127,9 @@ var BloomFilter = class BloomFilter {
118
127
  */
119
128
  constructor({ m, k, seed = 0 }) {
120
129
  require_params.assertPositiveInt(m, "m");
130
+ require_params.assertUint32(m, "m");
121
131
  require_params.assertPositiveInt(k, "k");
132
+ require_params.assertUint16(k, "k");
122
133
  require_params.assertUint32(seed, "seed");
123
134
  this.#bits = new BitSet(m);
124
135
  this.#m = m;
@@ -127,6 +138,11 @@ var BloomFilter = class BloomFilter {
127
138
  this.#scratch = new Uint32Array(k);
128
139
  this.#n = Math.round(m * Math.LN2 / k);
129
140
  }
141
+ static #withN(params, n) {
142
+ const f = new BloomFilter(params);
143
+ f.#n = n;
144
+ return f;
145
+ }
130
146
  /** Number of bits in the filter. */
131
147
  get m() {
132
148
  return this.#m;
@@ -164,18 +180,45 @@ var BloomFilter = class BloomFilter {
164
180
  */
165
181
  toBytes() {
166
182
  const payload = this.#bits.bytes;
167
- const body = new Uint8Array(14 + payload.length);
168
- const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
169
- dv.setUint32(0, this.#m, true);
170
- dv.setUint16(4, this.#k, true);
171
- dv.setUint32(6, this.#seed, true);
172
- dv.setUint32(10, this.#n, true);
173
- body.set(payload, 14);
174
- return require_serialize.writeHeader({
175
- version: 2,
183
+ return require_serialize.writeFrame({
184
+ version: 3,
176
185
  type: TYPE,
177
186
  flags: 0
178
- }, body);
187
+ }, 14 + payload.length, (body, dv) => {
188
+ dv.setUint32(0, this.#m, true);
189
+ dv.setUint16(4, this.#k, true);
190
+ dv.setUint32(6, this.#seed, true);
191
+ dv.setUint32(10, this.#n, true);
192
+ body.set(payload, 14);
193
+ });
194
+ }
195
+ /**
196
+ * Tests structural equality: `true` when `other` serializes to identical
197
+ * bytes, meaning identical parameters and set bits.
198
+ *
199
+ * @param other - The filter to compare against.
200
+ * @returns `true` if the two filters are byte-for-byte identical.
201
+ */
202
+ equals(other) {
203
+ return require_serialize.bytesEqual(this.toBytes(), other.toBytes());
204
+ }
205
+ /**
206
+ * Serializes the filter to a JSON-friendly envelope wrapping the base64 of
207
+ * {@link BloomFilter.toBytes}.
208
+ *
209
+ * @returns The envelope, readable by {@link BloomFilter.fromJSON}.
210
+ */
211
+ toJSON() {
212
+ return require_serialize.toJSONEnvelope(this.toBytes());
213
+ }
214
+ /**
215
+ * Restores a filter from its {@link BloomFilter.toJSON} envelope.
216
+ *
217
+ * @param value - The JSON envelope.
218
+ * @returns The reconstructed filter.
219
+ */
220
+ static fromJSON(value) {
221
+ return BloomFilter.fromBytes(require_serialize.fromJSONEnvelope(value));
179
222
  }
180
223
  /**
181
224
  * Returns a new filter containing the union of this filter and `other`.
@@ -190,11 +233,11 @@ var BloomFilter = class BloomFilter {
190
233
  const b = other.#bits.bytes;
191
234
  const merged = new Uint8Array(a.length);
192
235
  for (let i = 0; i < a.length; i++) merged[i] = (a[i] ?? 0) | (b[i] ?? 0);
193
- const r = new BloomFilter({
236
+ const r = BloomFilter.#withN({
194
237
  m: this.#m,
195
238
  k: this.#k,
196
239
  seed: this.#seed
197
- });
240
+ }, this.#n);
198
241
  r.#bits.bytes.set(merged);
199
242
  return r;
200
243
  }
@@ -1,4 +1,4 @@
1
- import { t as BytesLike } from "../bytes-DCuYtUVS.cjs";
1
+ import { n as BytesLike, t as FilterJSON } from "../serialize-DRKh6QOr.cjs";
2
2
  import { t as ParamError } from "../params-DnqJBqLS.cjs";
3
3
  //#region src/bloom/bloom.d.ts
4
4
  /** Thrown when an operation requires two filters built with identical parameters. */
@@ -37,6 +37,16 @@ declare class BloomFilter {
37
37
  * @returns A new, empty filter.
38
38
  */
39
39
  static create(n: number, epsilon: number): BloomFilter;
40
+ /**
41
+ * Builds a filter from `keys`, sized for their count at the target
42
+ * false-positive rate. The ergonomic entry point when the key set is already
43
+ * in hand; use {@link BloomFilter.create} to size for a count known ahead.
44
+ *
45
+ * @param keys - The keys to insert.
46
+ * @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
47
+ * @returns A new filter containing every key.
48
+ */
49
+ static from(keys: Iterable<BytesLike>, epsilon: number): BloomFilter;
40
50
  /**
41
51
  * Restores a filter from its {@link BloomFilter.toBytes} serialization.
42
52
  *
@@ -73,6 +83,28 @@ declare class BloomFilter {
73
83
  * @returns The serialized filter, readable by {@link BloomFilter.fromBytes}.
74
84
  */
75
85
  toBytes(): Uint8Array;
86
+ /**
87
+ * Tests structural equality: `true` when `other` serializes to identical
88
+ * bytes, meaning identical parameters and set bits.
89
+ *
90
+ * @param other - The filter to compare against.
91
+ * @returns `true` if the two filters are byte-for-byte identical.
92
+ */
93
+ equals(other: BloomFilter): boolean;
94
+ /**
95
+ * Serializes the filter to a JSON-friendly envelope wrapping the base64 of
96
+ * {@link BloomFilter.toBytes}.
97
+ *
98
+ * @returns The envelope, readable by {@link BloomFilter.fromJSON}.
99
+ */
100
+ toJSON(): FilterJSON;
101
+ /**
102
+ * Restores a filter from its {@link BloomFilter.toJSON} envelope.
103
+ *
104
+ * @param value - The JSON envelope.
105
+ * @returns The reconstructed filter.
106
+ */
107
+ static fromJSON(value: unknown): BloomFilter;
76
108
  /**
77
109
  * Returns a new filter containing the union of this filter and `other`.
78
110
  *
@@ -96,4 +128,4 @@ declare class BloomFilter {
96
128
  has(key: BytesLike): boolean;
97
129
  }
98
130
  //#endregion
99
- export { BloomFilter, BloomParamMismatchError, type BloomParams, ParamError };
131
+ export { BloomFilter, BloomParamMismatchError, type BloomParams, type FilterJSON, ParamError };
@@ -1,4 +1,4 @@
1
- import { t as BytesLike } from "../bytes-DCuYtUVS.js";
1
+ import { n as BytesLike, t as FilterJSON } from "../serialize-DRKh6QOr.js";
2
2
  import { t as ParamError } from "../params-DnqJBqLS.js";
3
3
  //#region src/bloom/bloom.d.ts
4
4
  /** Thrown when an operation requires two filters built with identical parameters. */
@@ -37,6 +37,16 @@ declare class BloomFilter {
37
37
  * @returns A new, empty filter.
38
38
  */
39
39
  static create(n: number, epsilon: number): BloomFilter;
40
+ /**
41
+ * Builds a filter from `keys`, sized for their count at the target
42
+ * false-positive rate. The ergonomic entry point when the key set is already
43
+ * in hand; use {@link BloomFilter.create} to size for a count known ahead.
44
+ *
45
+ * @param keys - The keys to insert.
46
+ * @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
47
+ * @returns A new filter containing every key.
48
+ */
49
+ static from(keys: Iterable<BytesLike>, epsilon: number): BloomFilter;
40
50
  /**
41
51
  * Restores a filter from its {@link BloomFilter.toBytes} serialization.
42
52
  *
@@ -73,6 +83,28 @@ declare class BloomFilter {
73
83
  * @returns The serialized filter, readable by {@link BloomFilter.fromBytes}.
74
84
  */
75
85
  toBytes(): Uint8Array;
86
+ /**
87
+ * Tests structural equality: `true` when `other` serializes to identical
88
+ * bytes, meaning identical parameters and set bits.
89
+ *
90
+ * @param other - The filter to compare against.
91
+ * @returns `true` if the two filters are byte-for-byte identical.
92
+ */
93
+ equals(other: BloomFilter): boolean;
94
+ /**
95
+ * Serializes the filter to a JSON-friendly envelope wrapping the base64 of
96
+ * {@link BloomFilter.toBytes}.
97
+ *
98
+ * @returns The envelope, readable by {@link BloomFilter.fromJSON}.
99
+ */
100
+ toJSON(): FilterJSON;
101
+ /**
102
+ * Restores a filter from its {@link BloomFilter.toJSON} envelope.
103
+ *
104
+ * @param value - The JSON envelope.
105
+ * @returns The reconstructed filter.
106
+ */
107
+ static fromJSON(value: unknown): BloomFilter;
76
108
  /**
77
109
  * Returns a new filter containing the union of this filter and `other`.
78
110
  *
@@ -96,4 +128,4 @@ declare class BloomFilter {
96
128
  has(key: BytesLike): boolean;
97
129
  }
98
130
  //#endregion
99
- export { BloomFilter, BloomParamMismatchError, type BloomParams, ParamError };
131
+ export { BloomFilter, BloomParamMismatchError, type BloomParams, type FilterJSON, ParamError };
@@ -1,17 +1,12 @@
1
- import { a as writeHeader, i as readHeader, n as assertBodyLength, r as assertMinBodyLength, s as probeInto, t as SerializationError } from "../serialize-DkkBKDT0.js";
2
- import { a as assertUint32, i as assertProbability, r as assertPositiveInt, t as ParamError } from "../params-ChTRNxM9.js";
1
+ import { a as bytesEqual, c as toJSONEnvelope, g as probeInto, i as assertMinBodyLength, l as writeFrame, n as UnknownHashVariantError, o as fromJSONEnvelope, r as assertBodyLength, s as readHeader, t as SerializationError } from "../serialize-BnwtzcMw.js";
2
+ import { a as assertUint16, i as assertProbability, o as assertUint32, r as assertPositiveInt, t as ParamError } from "../params-BrWuBC2A.js";
3
3
  //#region src/core/bitset.ts
4
4
  var BitSetRangeError = class extends RangeError {
5
5
  name = "BitSetRangeError";
6
6
  };
7
7
  const MAX_BITS = 2 ** 32;
8
- var BitSet = class BitSet {
8
+ var BitSet = class {
9
9
  #bits;
10
- static fromBytes(bytes) {
11
- const bs = new BitSet(bytes.length * 8);
12
- bs.#bits.set(bytes);
13
- return bs;
14
- }
15
10
  constructor(nbits) {
16
11
  if (nbits > MAX_BITS) throw new BitSetRangeError(`BitSet capacity ${String(nbits)} exceeds the 2^32-bit limit`);
17
12
  this.#bits = new Uint8Array(Math.ceil(nbits / 8));
@@ -81,9 +76,23 @@ var BloomFilter = class BloomFilter {
81
76
  */
82
77
  static create(n, epsilon) {
83
78
  assertPositiveInt(n, "n");
79
+ assertUint32(n, "n");
84
80
  assertProbability(epsilon, "epsilon");
85
- const f = new BloomFilter(optimal(n, epsilon));
86
- f.#n = n;
81
+ return BloomFilter.#withN(optimal(n, epsilon), n);
82
+ }
83
+ /**
84
+ * Builds a filter from `keys`, sized for their count at the target
85
+ * false-positive rate. The ergonomic entry point when the key set is already
86
+ * in hand; use {@link BloomFilter.create} to size for a count known ahead.
87
+ *
88
+ * @param keys - The keys to insert.
89
+ * @param epsilon - Target false-positive rate, e.g. `0.01` for 1%.
90
+ * @returns A new filter containing every key.
91
+ */
92
+ static from(keys, epsilon) {
93
+ const arr = [...keys];
94
+ const f = BloomFilter.create(Math.max(1, arr.length), epsilon);
95
+ for (const k of arr) f.add(k);
87
96
  return f;
88
97
  }
89
98
  /**
@@ -93,8 +102,9 @@ var BloomFilter = class BloomFilter {
93
102
  * @returns The reconstructed filter.
94
103
  */
95
104
  static fromBytes(bytes) {
96
- const { type, body } = readHeader(bytes);
105
+ const { type, flags, body } = readHeader(bytes);
97
106
  if (type !== TYPE) throw new SerializationError(`expected AMQF type ${String(TYPE)}, got ${String(type)}`);
107
+ if ((flags & 15) !== 0) throw new UnknownHashVariantError(`unsupported hash variant ${String(flags & 15)}`);
98
108
  assertMinBodyLength(body.length, 14, "bloom");
99
109
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
100
110
  const m = dv.getUint32(0, true);
@@ -102,12 +112,11 @@ var BloomFilter = class BloomFilter {
102
112
  const seed = dv.getUint32(6, true);
103
113
  const n = dv.getUint32(10, true);
104
114
  assertBodyLength(body.length, 14 + Math.ceil(m / 8), "bloom");
105
- const f = new BloomFilter({
115
+ const f = BloomFilter.#withN({
106
116
  m,
107
117
  k,
108
118
  seed
109
- });
110
- f.#n = n;
119
+ }, n);
111
120
  f.#bits.bytes.set(body.subarray(14));
112
121
  return f;
113
122
  }
@@ -117,7 +126,9 @@ var BloomFilter = class BloomFilter {
117
126
  */
118
127
  constructor({ m, k, seed = 0 }) {
119
128
  assertPositiveInt(m, "m");
129
+ assertUint32(m, "m");
120
130
  assertPositiveInt(k, "k");
131
+ assertUint16(k, "k");
121
132
  assertUint32(seed, "seed");
122
133
  this.#bits = new BitSet(m);
123
134
  this.#m = m;
@@ -126,6 +137,11 @@ var BloomFilter = class BloomFilter {
126
137
  this.#scratch = new Uint32Array(k);
127
138
  this.#n = Math.round(m * Math.LN2 / k);
128
139
  }
140
+ static #withN(params, n) {
141
+ const f = new BloomFilter(params);
142
+ f.#n = n;
143
+ return f;
144
+ }
129
145
  /** Number of bits in the filter. */
130
146
  get m() {
131
147
  return this.#m;
@@ -163,18 +179,45 @@ var BloomFilter = class BloomFilter {
163
179
  */
164
180
  toBytes() {
165
181
  const payload = this.#bits.bytes;
166
- const body = new Uint8Array(14 + payload.length);
167
- const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
168
- dv.setUint32(0, this.#m, true);
169
- dv.setUint16(4, this.#k, true);
170
- dv.setUint32(6, this.#seed, true);
171
- dv.setUint32(10, this.#n, true);
172
- body.set(payload, 14);
173
- return writeHeader({
174
- version: 2,
182
+ return writeFrame({
183
+ version: 3,
175
184
  type: TYPE,
176
185
  flags: 0
177
- }, body);
186
+ }, 14 + payload.length, (body, dv) => {
187
+ dv.setUint32(0, this.#m, true);
188
+ dv.setUint16(4, this.#k, true);
189
+ dv.setUint32(6, this.#seed, true);
190
+ dv.setUint32(10, this.#n, true);
191
+ body.set(payload, 14);
192
+ });
193
+ }
194
+ /**
195
+ * Tests structural equality: `true` when `other` serializes to identical
196
+ * bytes, meaning identical parameters and set bits.
197
+ *
198
+ * @param other - The filter to compare against.
199
+ * @returns `true` if the two filters are byte-for-byte identical.
200
+ */
201
+ equals(other) {
202
+ return bytesEqual(this.toBytes(), other.toBytes());
203
+ }
204
+ /**
205
+ * Serializes the filter to a JSON-friendly envelope wrapping the base64 of
206
+ * {@link BloomFilter.toBytes}.
207
+ *
208
+ * @returns The envelope, readable by {@link BloomFilter.fromJSON}.
209
+ */
210
+ toJSON() {
211
+ return toJSONEnvelope(this.toBytes());
212
+ }
213
+ /**
214
+ * Restores a filter from its {@link BloomFilter.toJSON} envelope.
215
+ *
216
+ * @param value - The JSON envelope.
217
+ * @returns The reconstructed filter.
218
+ */
219
+ static fromJSON(value) {
220
+ return BloomFilter.fromBytes(fromJSONEnvelope(value));
178
221
  }
179
222
  /**
180
223
  * Returns a new filter containing the union of this filter and `other`.
@@ -189,11 +232,11 @@ var BloomFilter = class BloomFilter {
189
232
  const b = other.#bits.bytes;
190
233
  const merged = new Uint8Array(a.length);
191
234
  for (let i = 0; i < a.length; i++) merged[i] = (a[i] ?? 0) | (b[i] ?? 0);
192
- const r = new BloomFilter({
235
+ const r = BloomFilter.#withN({
193
236
  m: this.#m,
194
237
  k: this.#k,
195
238
  seed: this.#seed
196
- });
239
+ }, this.#n);
197
240
  r.#bits.bytes.set(merged);
198
241
  return r;
199
242
  }