distillate 0.3.0 → 0.5.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 CHANGED
@@ -43,7 +43,7 @@ Each structure ships as its own subpath, so you only bundle what you import.
43
43
  | Import | Structure | Mutable? | Use for |
44
44
  | -------------------- | ------------- | -------- | ---------------------------------------------------- |
45
45
  | `distillate/bloom` | Classic Bloom | yes | Familiar default, migration from `bloom-filters` |
46
- | `distillate/blocked` | Blocked Bloom | yes | Streaming inserts, speed-first, cache-friendly |
46
+ | `distillate/blocked` | Blocked Bloom | yes | Very large sets that outgrow CPU cache (10M+ keys) |
47
47
  | `distillate/fuse` | Binary Fuse | no | Static set built once and queried a lot; least space |
48
48
 
49
49
  ### Classic Bloom (`distillate/bloom`)
@@ -72,7 +72,7 @@ filter.add("alice");
72
72
  filter.has("alice"); // true
73
73
  ```
74
74
 
75
- Same surface as Classic Bloom (`add` / `has` / `union` / `toBytes` / `fromBytes` / `bitsPerKey`). Confines every lookup to one cache line, trading ~20-30% more space for cache-friendly throughput.
75
+ Same surface as Classic Bloom (`add` / `has` / `union` / `toBytes` / `fromBytes` / `bitsPerKey`). Confines every lookup to a single cache line, which only pays off once the filter outgrows CPU cache. At typical sizes it matches Classic; for very large sets (tens of millions of keys, past L2/L3) it runs about 1.4x Classic's lookup throughput, at the cost of ~20-30% more space. Prefer Classic unless you are memory-bound at scale.
76
76
 
77
77
  ### Binary Fuse (`distillate/fuse`)
78
78
 
@@ -98,10 +98,10 @@ Classic Bloom head-to-head at a **matched 1% false-positive rate** over the same
98
98
 
99
99
  | Classic Bloom | bits/key | measured FPR | `has` throughput |
100
100
  | -------------- | -------- | ------------ | ---------------- |
101
- | **distillate** | 9.59 | 1.03% | ~7.0 M ops/s |
101
+ | **distillate** | 9.59 | 1.03% | ~16 M ops/s |
102
102
  | bloom-filters | 9.59 | 0.99% | ~0.29 M ops/s |
103
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.
104
+ Same space, same accuracy, **~56x 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
105
 
106
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
107
 
@@ -1,15 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_serialize = require("../serialize-CHHDM4TQ.cjs");
2
+ const require_serialize = require("../serialize-fa-pEUGq.cjs");
3
3
  const require_params = require("../params-J8p3bKq5.cjs");
4
4
  //#region src/blocked/blocked.ts
5
5
  const TYPE = 2;
6
6
  const SALT = Uint32Array.of(1203114875, 1150766481, 2284105051, 2729912477, 1884591559, 770785867, 2667333959, 1550580529);
7
- const scratchHash = {
8
- h1lo: 0,
9
- h1hi: 0,
10
- h2lo: 0,
11
- h2hi: 0
12
- };
7
+ const scratch2 = /* @__PURE__ */ new Uint32Array(2);
13
8
  /** Thrown when an operation requires two filters built with identical parameters. */
14
9
  var BlockedBloomParamMismatchError = class extends Error {
15
10
  /** Discriminates this error from other `Error`s. */
@@ -104,11 +99,15 @@ var BlockedBloomFilter = class BlockedBloomFilter {
104
99
  * @returns The reconstructed filter.
105
100
  */
106
101
  static fromBytes(bytes) {
107
- const { body } = require_serialize.readHeader(bytes);
102
+ const { type, flags, body } = require_serialize.readHeader(bytes);
103
+ if (type !== TYPE) throw new require_serialize.SerializationError(`expected AMQF type ${String(TYPE)}, got ${String(type)}`);
104
+ if ((flags & 15) !== 1) throw new require_serialize.UnknownHashVariantError(`unsupported hash variant ${String(flags & 15)}`);
105
+ require_serialize.assertMinBodyLength(body.length, 12, "blocked");
108
106
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
109
107
  const numBlocks = dv.getUint32(0, true);
110
108
  const seed = dv.getUint32(4, true);
111
109
  const n = dv.getUint32(8, true);
110
+ require_serialize.assertBodyLength(body.length, 12 + numBlocks * 32, "blocked");
112
111
  const f = new BlockedBloomFilter({
113
112
  bitsPerKey: 256,
114
113
  capacity: numBlocks,
@@ -132,9 +131,9 @@ var BlockedBloomFilter = class BlockedBloomFilter {
132
131
  dv.setUint32(8, this.#n, true);
133
132
  body.set(lanes, 12);
134
133
  return require_serialize.writeHeader({
135
- version: 1,
134
+ version: 2,
136
135
  type: TYPE,
137
- flags: 0
136
+ flags: 1
138
137
  }, body);
139
138
  }
140
139
  /**
@@ -182,9 +181,9 @@ var BlockedBloomFilter = class BlockedBloomFilter {
182
181
  }
183
182
  };
184
183
  function fillBlock(key, numBlocks, seed, outWords, outBits) {
185
- require_serialize.hash128KeyInto(key, seed, scratchHash);
186
- const block = require_serialize.reduce((scratchHash.h1lo ^ scratchHash.h1hi) >>> 0, numBlocks);
187
- const x = (scratchHash.h2lo ^ scratchHash.h2hi) >>> 0;
184
+ require_serialize.hash32x2Into(key, seed, scratch2);
185
+ const block = require_serialize.reduce(scratch2[0] ?? 0, numBlocks);
186
+ const x = scratch2[1] ?? 0;
188
187
  const base = block * 8;
189
188
  for (let i = 0; i < 8; i++) {
190
189
  outWords[i] = base + i;
@@ -1,14 +1,9 @@
1
- import { i as hash128KeyInto, n as readHeader, o as reduce, r as writeHeader } from "../serialize-5XQ5y_R-.js";
1
+ import { a as readHeader, d as hash32x2Into, i as assertMinBodyLength, m as reduce, n as UnknownHashVariantError, o as writeHeader, r as assertBodyLength, t as SerializationError } from "../serialize-BQ2UzZqq.js";
2
2
  import { a as assertUint32, i as assertProbability, n as assertPositiveFinite, r as assertPositiveInt, t as ParamError } from "../params-ChTRNxM9.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);
12
7
  /** Thrown when an operation requires two filters built with identical parameters. */
13
8
  var BlockedBloomParamMismatchError = class extends Error {
14
9
  /** Discriminates this error from other `Error`s. */
@@ -103,11 +98,15 @@ var BlockedBloomFilter = class BlockedBloomFilter {
103
98
  * @returns The reconstructed filter.
104
99
  */
105
100
  static fromBytes(bytes) {
106
- const { body } = readHeader(bytes);
101
+ const { type, flags, body } = readHeader(bytes);
102
+ if (type !== TYPE) throw new SerializationError(`expected AMQF type ${String(TYPE)}, got ${String(type)}`);
103
+ if ((flags & 15) !== 1) throw new UnknownHashVariantError(`unsupported hash variant ${String(flags & 15)}`);
104
+ assertMinBodyLength(body.length, 12, "blocked");
107
105
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
108
106
  const numBlocks = dv.getUint32(0, true);
109
107
  const seed = dv.getUint32(4, true);
110
108
  const n = dv.getUint32(8, true);
109
+ assertBodyLength(body.length, 12 + numBlocks * 32, "blocked");
111
110
  const f = new BlockedBloomFilter({
112
111
  bitsPerKey: 256,
113
112
  capacity: numBlocks,
@@ -131,9 +130,9 @@ var BlockedBloomFilter = class BlockedBloomFilter {
131
130
  dv.setUint32(8, this.#n, true);
132
131
  body.set(lanes, 12);
133
132
  return writeHeader({
134
- version: 1,
133
+ version: 2,
135
134
  type: TYPE,
136
- flags: 0
135
+ flags: 1
137
136
  }, body);
138
137
  }
139
138
  /**
@@ -181,9 +180,9 @@ var BlockedBloomFilter = class BlockedBloomFilter {
181
180
  }
182
181
  };
183
182
  function fillBlock(key, numBlocks, seed, outWords, outBits) {
184
- hash128KeyInto(key, seed, scratchHash);
185
- const block = reduce((scratchHash.h1lo ^ scratchHash.h1hi) >>> 0, numBlocks);
186
- const x = (scratchHash.h2lo ^ scratchHash.h2hi) >>> 0;
183
+ hash32x2Into(key, seed, scratch2);
184
+ const block = reduce(scratch2[0] ?? 0, numBlocks);
185
+ const x = scratch2[1] ?? 0;
187
186
  const base = block * 8;
188
187
  for (let i = 0; i < 8; i++) {
189
188
  outWords[i] = base + i;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_serialize = require("../serialize-CHHDM4TQ.cjs");
2
+ const require_serialize = require("../serialize-fa-pEUGq.cjs");
3
3
  const require_params = require("../params-J8p3bKq5.cjs");
4
4
  //#region src/core/bitset.ts
5
5
  var BitSetRangeError = class extends RangeError {
@@ -94,17 +94,23 @@ var BloomFilter = class BloomFilter {
94
94
  * @returns The reconstructed filter.
95
95
  */
96
96
  static fromBytes(bytes) {
97
- const { body } = require_serialize.readHeader(bytes);
97
+ const { type, flags, body } = require_serialize.readHeader(bytes);
98
+ if (type !== TYPE) throw new require_serialize.SerializationError(`expected AMQF type ${String(TYPE)}, got ${String(type)}`);
99
+ if ((flags & 15) !== 1) throw new require_serialize.UnknownHashVariantError(`unsupported hash variant ${String(flags & 15)}`);
100
+ require_serialize.assertMinBodyLength(body.length, 14, "bloom");
98
101
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
99
102
  const m = dv.getUint32(0, true);
100
- const k = body[4] ?? 0;
101
- const seed = dv.getUint32(5, true);
103
+ const k = dv.getUint16(4, true);
104
+ const seed = dv.getUint32(6, true);
105
+ const n = dv.getUint32(10, true);
106
+ require_serialize.assertBodyLength(body.length, 14 + Math.ceil(m / 8), "bloom");
102
107
  const f = new BloomFilter({
103
108
  m,
104
109
  k,
105
110
  seed
106
111
  });
107
- f.#bits.bytes.set(body.subarray(9));
112
+ f.#n = n;
113
+ f.#bits.bytes.set(body.subarray(14));
108
114
  return f;
109
115
  }
110
116
  /**
@@ -159,16 +165,17 @@ var BloomFilter = class BloomFilter {
159
165
  */
160
166
  toBytes() {
161
167
  const payload = this.#bits.bytes;
162
- const body = new Uint8Array(9 + payload.length);
168
+ const body = new Uint8Array(14 + payload.length);
163
169
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
164
170
  dv.setUint32(0, this.#m, true);
165
- body[4] = this.#k;
166
- dv.setUint32(5, this.#seed, true);
167
- body.set(payload, 9);
171
+ dv.setUint16(4, this.#k, true);
172
+ dv.setUint32(6, this.#seed, true);
173
+ dv.setUint32(10, this.#n, true);
174
+ body.set(payload, 14);
168
175
  return require_serialize.writeHeader({
169
- version: 1,
176
+ version: 2,
170
177
  type: TYPE,
171
- flags: 0
178
+ flags: 1
172
179
  }, body);
173
180
  }
174
181
  /**
@@ -1,4 +1,4 @@
1
- import { a as probeInto, n as readHeader, r as writeHeader } from "../serialize-5XQ5y_R-.js";
1
+ import { a as readHeader, i as assertMinBodyLength, n as UnknownHashVariantError, o as writeHeader, p as probeInto, r as assertBodyLength, t as SerializationError } from "../serialize-BQ2UzZqq.js";
2
2
  import { a as assertUint32, i as assertProbability, r as assertPositiveInt, t as ParamError } from "../params-ChTRNxM9.js";
3
3
  //#region src/core/bitset.ts
4
4
  var BitSetRangeError = class extends RangeError {
@@ -93,17 +93,23 @@ var BloomFilter = class BloomFilter {
93
93
  * @returns The reconstructed filter.
94
94
  */
95
95
  static fromBytes(bytes) {
96
- const { body } = readHeader(bytes);
96
+ const { type, flags, body } = readHeader(bytes);
97
+ if (type !== TYPE) throw new SerializationError(`expected AMQF type ${String(TYPE)}, got ${String(type)}`);
98
+ if ((flags & 15) !== 1) throw new UnknownHashVariantError(`unsupported hash variant ${String(flags & 15)}`);
99
+ assertMinBodyLength(body.length, 14, "bloom");
97
100
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
98
101
  const m = dv.getUint32(0, true);
99
- const k = body[4] ?? 0;
100
- const seed = dv.getUint32(5, true);
102
+ const k = dv.getUint16(4, true);
103
+ const seed = dv.getUint32(6, true);
104
+ const n = dv.getUint32(10, true);
105
+ assertBodyLength(body.length, 14 + Math.ceil(m / 8), "bloom");
101
106
  const f = new BloomFilter({
102
107
  m,
103
108
  k,
104
109
  seed
105
110
  });
106
- f.#bits.bytes.set(body.subarray(9));
111
+ f.#n = n;
112
+ f.#bits.bytes.set(body.subarray(14));
107
113
  return f;
108
114
  }
109
115
  /**
@@ -158,16 +164,17 @@ var BloomFilter = class BloomFilter {
158
164
  */
159
165
  toBytes() {
160
166
  const payload = this.#bits.bytes;
161
- const body = new Uint8Array(9 + payload.length);
167
+ const body = new Uint8Array(14 + payload.length);
162
168
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
163
169
  dv.setUint32(0, this.#m, true);
164
- body[4] = this.#k;
165
- dv.setUint32(5, this.#seed, true);
166
- body.set(payload, 9);
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);
167
174
  return writeHeader({
168
- version: 1,
175
+ version: 2,
169
176
  type: TYPE,
170
- flags: 0
177
+ flags: 1
171
178
  }, body);
172
179
  }
173
180
  /**
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_serialize = require("../serialize-CHHDM4TQ.cjs");
2
+ const require_serialize = require("../serialize-fa-pEUGq.cjs");
3
3
  //#region src/fuse/fuse.ts
4
4
  const ARITY = 3;
5
5
  const TYPE_FUSE8 = 3;
@@ -9,71 +9,23 @@ var BinaryFuseBuildError = class extends Error {
9
9
  /** Discriminates this error from other `Error`s. */
10
10
  name = "BinaryFuseBuildError";
11
11
  };
12
- let RLO = 0;
13
- let RHI = 0;
14
12
  const scratchHash = {
15
13
  h1lo: 0,
16
14
  h1hi: 0,
17
15
  h2lo: 0,
18
16
  h2hi: 0
19
17
  };
20
- function mul64(alo, ahi, blo, bhi) {
21
- const a0 = alo & 65535;
22
- const a1 = alo >>> 16;
23
- const a2 = ahi & 65535;
24
- const a3 = ahi >>> 16;
25
- const b0 = blo & 65535;
26
- const b1 = blo >>> 16;
27
- const b2 = bhi & 65535;
28
- const b3 = bhi >>> 16;
29
- let c0 = a0 * b0;
30
- let c1 = c0 >>> 16;
31
- c0 &= 65535;
32
- c1 += a1 * b0;
33
- let c2 = c1 >>> 16;
34
- c1 &= 65535;
35
- c1 += a0 * b1;
36
- c2 += c1 >>> 16;
37
- c1 &= 65535;
38
- c2 += a2 * b0;
39
- let c3 = c2 >>> 16;
40
- c2 &= 65535;
41
- c2 += a1 * b1;
42
- c3 += c2 >>> 16;
43
- c2 &= 65535;
44
- c2 += a0 * b2;
45
- c3 += c2 >>> 16;
46
- c2 &= 65535;
47
- c3 += a3 * b0 + a2 * b1 + a1 * b2 + a0 * b3;
48
- c3 &= 65535;
49
- RLO = (c1 << 16 | c0) >>> 0;
50
- RHI = (c3 << 16 | c2) >>> 0;
51
- }
52
- function fmix64(lo, hi) {
53
- let l = (lo ^ hi >>> 1) >>> 0;
54
- let h = hi;
55
- mul64(l, h, 3981806797, 4283543511);
56
- l = RLO;
57
- h = RHI;
58
- l = (l ^ h >>> 1) >>> 0;
59
- mul64(l, h, 444984403, 3301882366);
60
- l = RLO;
61
- h = RHI;
62
- l = (l ^ h >>> 1) >>> 0;
63
- RLO = l;
64
- RHI = h;
65
- }
66
18
  function mixSeed(lo, hi, seed) {
67
19
  const l = (lo >>> 0) + (seed >>> 0);
68
20
  const carry = l > 4294967295 ? 1 : 0;
69
- fmix64(l >>> 0, hi + carry >>> 0);
21
+ require_serialize.fmix64(l >>> 0, hi + carry >>> 0);
70
22
  }
71
23
  function mulhi64(lo, hi, n) {
72
- mul64(hi, 0, n, 0);
73
- const aLo = RLO;
74
- const aHi = RHI;
75
- mul64(lo, 0, n, 0);
76
- const bHi = RHI;
24
+ require_serialize.mul64(hi, 0, n, 0);
25
+ const aLo = require_serialize.RLO;
26
+ const aHi = require_serialize.RHI;
27
+ require_serialize.mul64(lo, 0, n, 0);
28
+ const bHi = require_serialize.RHI;
77
29
  return aHi + ((aLo >>> 0) + (bHi >>> 0) > 4294967295 ? 1 : 0) >>> 0;
78
30
  }
79
31
  function positionsInto(mlo, mhi, seg, segMask, segCountLen, out) {
@@ -130,8 +82,8 @@ function buildFingerprints(fp, hashes, params, maxAttempts = 100) {
130
82
  xorHi.fill(0);
131
83
  for (let i = 0; i < size; i++) {
132
84
  mixSeed(hashes[2 * i] ?? 0, hashes[2 * i + 1] ?? 0, seed);
133
- const mlo = RLO;
134
- const mhi = RHI;
85
+ const mlo = require_serialize.RLO;
86
+ const mhi = require_serialize.RHI;
135
87
  positionsInto(mlo, mhi, seg, segMask, segCountLen, pos);
136
88
  for (let j = 0; j < 3; j++) {
137
89
  const p = pos[j] ?? 0;
@@ -211,13 +163,16 @@ function buildState(keys, alloc) {
211
163
  function fuseStateFromBytes(bytes, expectedType) {
212
164
  const { type, body } = require_serialize.readHeader(bytes);
213
165
  if (type !== expectedType) throw new require_serialize.SerializationError(`expected AMQF type ${String(expectedType)}, got ${String(type)}`);
166
+ require_serialize.assertMinBodyLength(body.length, 16, "fuse");
214
167
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
215
168
  const seed = dv.getUint32(0, true);
216
169
  const seg = dv.getUint32(4, true);
217
170
  const segCountLen = dv.getUint32(8, true);
218
171
  const size = dv.getUint32(12, true);
219
- const laneBytes = body.subarray(16);
220
172
  const bpe = expectedType === TYPE_FUSE8 ? 1 : 2;
173
+ const expectedArrayLength = size === 0 ? 0 : segCountLen + 2 * seg;
174
+ require_serialize.assertBodyLength(body.length, 16 + expectedArrayLength * bpe, "fuse");
175
+ const laneBytes = body.subarray(16);
221
176
  const arrayLength = laneBytes.length / bpe;
222
177
  const fp = bpe === 1 ? new Uint8Array(arrayLength) : new Uint16Array(arrayLength);
223
178
  new Uint8Array(fp.buffer).set(laneBytes);
@@ -276,7 +231,7 @@ var BinaryFuse = class {
276
231
  dv.setUint32(12, this.#size, true);
277
232
  body.set(laneBytes, 16);
278
233
  return require_serialize.writeHeader({
279
- version: 1,
234
+ version: 2,
280
235
  type: this.#fp.BYTES_PER_ELEMENT === 1 ? TYPE_FUSE8 : TYPE_FUSE16,
281
236
  flags: 0
282
237
  }, body);
@@ -291,8 +246,8 @@ var BinaryFuse = class {
291
246
  if (this.#fp.length === 0) return false;
292
247
  require_serialize.hash128KeyInto(key, 0, scratchHash);
293
248
  mixSeed(scratchHash.h1lo >>> 0, scratchHash.h1hi >>> 0, this.#seed);
294
- const mlo = RLO;
295
- const mhi = RHI;
249
+ const mlo = require_serialize.RLO;
250
+ const mhi = require_serialize.RHI;
296
251
  positionsInto(mlo, mhi, this.#seg, this.#segMask, this.#segCountLen, this.#pos);
297
252
  const mask = fpMask(this.#fp);
298
253
  const p0 = this.#pos[0] ?? 0;
@@ -1,4 +1,4 @@
1
- import { i as hash128KeyInto, n as readHeader, r as writeHeader, t as SerializationError } from "../serialize-5XQ5y_R-.js";
1
+ import { a as readHeader, c as RLO, f as mul64, i as assertMinBodyLength, l as fmix64, o as writeHeader, r as assertBodyLength, s as RHI, t as SerializationError, u as hash128KeyInto } from "../serialize-BQ2UzZqq.js";
2
2
  //#region src/fuse/fuse.ts
3
3
  const ARITY = 3;
4
4
  const TYPE_FUSE8 = 3;
@@ -8,60 +8,12 @@ var BinaryFuseBuildError = class extends Error {
8
8
  /** Discriminates this error from other `Error`s. */
9
9
  name = "BinaryFuseBuildError";
10
10
  };
11
- let RLO = 0;
12
- let RHI = 0;
13
11
  const scratchHash = {
14
12
  h1lo: 0,
15
13
  h1hi: 0,
16
14
  h2lo: 0,
17
15
  h2hi: 0
18
16
  };
19
- function mul64(alo, ahi, blo, bhi) {
20
- const a0 = alo & 65535;
21
- const a1 = alo >>> 16;
22
- const a2 = ahi & 65535;
23
- const a3 = ahi >>> 16;
24
- const b0 = blo & 65535;
25
- const b1 = blo >>> 16;
26
- const b2 = bhi & 65535;
27
- const b3 = bhi >>> 16;
28
- let c0 = a0 * b0;
29
- let c1 = c0 >>> 16;
30
- c0 &= 65535;
31
- c1 += a1 * b0;
32
- let c2 = c1 >>> 16;
33
- c1 &= 65535;
34
- c1 += a0 * b1;
35
- c2 += c1 >>> 16;
36
- c1 &= 65535;
37
- c2 += a2 * b0;
38
- let c3 = c2 >>> 16;
39
- c2 &= 65535;
40
- c2 += a1 * b1;
41
- c3 += c2 >>> 16;
42
- c2 &= 65535;
43
- c2 += a0 * b2;
44
- c3 += c2 >>> 16;
45
- c2 &= 65535;
46
- c3 += a3 * b0 + a2 * b1 + a1 * b2 + a0 * b3;
47
- c3 &= 65535;
48
- RLO = (c1 << 16 | c0) >>> 0;
49
- RHI = (c3 << 16 | c2) >>> 0;
50
- }
51
- function fmix64(lo, hi) {
52
- let l = (lo ^ hi >>> 1) >>> 0;
53
- let h = hi;
54
- mul64(l, h, 3981806797, 4283543511);
55
- l = RLO;
56
- h = RHI;
57
- l = (l ^ h >>> 1) >>> 0;
58
- mul64(l, h, 444984403, 3301882366);
59
- l = RLO;
60
- h = RHI;
61
- l = (l ^ h >>> 1) >>> 0;
62
- RLO = l;
63
- RHI = h;
64
- }
65
17
  function mixSeed(lo, hi, seed) {
66
18
  const l = (lo >>> 0) + (seed >>> 0);
67
19
  const carry = l > 4294967295 ? 1 : 0;
@@ -210,13 +162,16 @@ function buildState(keys, alloc) {
210
162
  function fuseStateFromBytes(bytes, expectedType) {
211
163
  const { type, body } = readHeader(bytes);
212
164
  if (type !== expectedType) throw new SerializationError(`expected AMQF type ${String(expectedType)}, got ${String(type)}`);
165
+ assertMinBodyLength(body.length, 16, "fuse");
213
166
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
214
167
  const seed = dv.getUint32(0, true);
215
168
  const seg = dv.getUint32(4, true);
216
169
  const segCountLen = dv.getUint32(8, true);
217
170
  const size = dv.getUint32(12, true);
218
- const laneBytes = body.subarray(16);
219
171
  const bpe = expectedType === TYPE_FUSE8 ? 1 : 2;
172
+ const expectedArrayLength = size === 0 ? 0 : segCountLen + 2 * seg;
173
+ assertBodyLength(body.length, 16 + expectedArrayLength * bpe, "fuse");
174
+ const laneBytes = body.subarray(16);
220
175
  const arrayLength = laneBytes.length / bpe;
221
176
  const fp = bpe === 1 ? new Uint8Array(arrayLength) : new Uint16Array(arrayLength);
222
177
  new Uint8Array(fp.buffer).set(laneBytes);
@@ -275,7 +230,7 @@ var BinaryFuse = class {
275
230
  dv.setUint32(12, this.#size, true);
276
231
  body.set(laneBytes, 16);
277
232
  return writeHeader({
278
- version: 1,
233
+ version: 2,
279
234
  type: this.#fp.BYTES_PER_ELEMENT === 1 ? TYPE_FUSE8 : TYPE_FUSE16,
280
235
  flags: 0
281
236
  }, body);
package/dist/index.cjs CHANGED
@@ -2,6 +2,6 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#endregion
3
3
  //#region src/index.ts
4
4
  /** The installed `distillate` package version. */
5
- const VERSION = "0.3.0";
5
+ const VERSION = "0.5.0";
6
6
  //#endregion
7
7
  exports.VERSION = VERSION;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  //#endregion
2
2
  //#region src/index.ts
3
3
  /** The installed `distillate` package version. */
4
- const VERSION = "0.3.0";
4
+ const VERSION = "0.5.0";
5
5
  //#endregion
6
6
  export { VERSION };
@@ -81,6 +81,37 @@ function fmix64(lo, hi) {
81
81
  RLO = l;
82
82
  RHI = h;
83
83
  }
84
+ function murmur32(bytes, seed, len = bytes.length) {
85
+ let h = seed >>> 0;
86
+ const n = len & -4;
87
+ for (let i = 0; i < n; i += 4) {
88
+ let k = ((bytes[i] ?? 0) | (bytes[i + 1] ?? 0) << 8 | (bytes[i + 2] ?? 0) << 16 | (bytes[i + 3] ?? 0) << 24) >>> 0;
89
+ k = Math.imul(k, 3432918353);
90
+ k = (k << 15 | k >>> 17) >>> 0;
91
+ k = Math.imul(k, 461845907);
92
+ h = (h ^ k) >>> 0;
93
+ h = (h << 13 | h >>> 19) >>> 0;
94
+ h = Math.imul(h, 5) + 3864292196 >>> 0;
95
+ }
96
+ let k1 = 0;
97
+ const rem = len & 3;
98
+ if (rem >= 3) k1 ^= (bytes[n + 2] ?? 0) << 16;
99
+ if (rem >= 2) k1 ^= (bytes[n + 1] ?? 0) << 8;
100
+ if (rem >= 1) {
101
+ k1 ^= bytes[n] ?? 0;
102
+ k1 = Math.imul(k1 >>> 0, 3432918353);
103
+ k1 = (k1 << 15 | k1 >>> 17) >>> 0;
104
+ k1 = Math.imul(k1, 461845907);
105
+ h = (h ^ k1) >>> 0;
106
+ }
107
+ h = (h ^ len) >>> 0;
108
+ h ^= h >>> 16;
109
+ h = Math.imul(h, 2246822507);
110
+ h ^= h >>> 13;
111
+ h = Math.imul(h, 3266489909);
112
+ h ^= h >>> 16;
113
+ return h >>> 0;
114
+ }
84
115
  function computeLanes(bytes, seed, len) {
85
116
  const nblocks = len >>> 4;
86
117
  let h1lo = seed >>> 0;
@@ -186,16 +217,22 @@ function computeLanes(bytes, seed, len) {
186
217
  }
187
218
  const keyEncoder = new TextEncoder();
188
219
  let keyBuf = /* @__PURE__ */ new Uint8Array(256);
189
- function keyToLanes(key, seed) {
220
+ let encBytes = keyBuf;
221
+ let encLen = 0;
222
+ function encodeKey(key) {
190
223
  if (typeof key === "string") {
191
224
  const cap = key.length * 3;
192
225
  if (keyBuf.length < cap) keyBuf = new Uint8Array(cap);
193
- const { written } = keyEncoder.encodeInto(key, keyBuf);
194
- computeLanes(keyBuf, seed, written);
226
+ encLen = keyEncoder.encodeInto(key, keyBuf).written;
227
+ encBytes = keyBuf;
195
228
  return;
196
229
  }
197
- const bytes = normalize(key);
198
- computeLanes(bytes, seed, bytes.length);
230
+ encBytes = normalize(key);
231
+ encLen = encBytes.length;
232
+ }
233
+ function keyToLanes(key, seed) {
234
+ encodeKey(key);
235
+ computeLanes(encBytes, seed, encLen);
199
236
  }
200
237
  function hash128KeyInto(key, seed, out) {
201
238
  keyToLanes(key, seed);
@@ -221,10 +258,16 @@ function reduce(x, range) {
221
258
  * Kirsch-Mitzenmacher enhanced double hashing `g_i = h1 + i*h2 + i^2`
222
259
  * (the RocksDB `+i^2` fix), reduced into range with Lemire multiply-shift.
223
260
  */
261
+ const SEED2 = 2654435761;
262
+ function hash32x2Into(key, seed, out) {
263
+ encodeKey(key);
264
+ out[0] = murmur32(encBytes, seed, encLen);
265
+ out[1] = murmur32(encBytes, (seed ^ SEED2) >>> 0, encLen);
266
+ }
224
267
  function probeInto(key, count, range, seed, out) {
225
- keyToLanes(key, seed);
226
- const a = (LANES.h1lo ^ LANES.h1hi) >>> 0;
227
- const b = (LANES.h2lo ^ LANES.h2hi) >>> 0;
268
+ encodeKey(key);
269
+ const a = murmur32(encBytes, seed, encLen);
270
+ const b = murmur32(encBytes, (seed ^ SEED2) >>> 0, encLen);
228
271
  for (let i = 0; i < count; i++) out[i] = reduce(a + Math.imul(i, b) + i * i >>> 0, range);
229
272
  }
230
273
  //#endregion
@@ -274,14 +317,31 @@ var BadMagicError = class extends SerializationError {
274
317
  var UnknownVersionError = class extends SerializationError {
275
318
  name = "UnknownVersionError";
276
319
  };
320
+ var UnknownHashVariantError = class extends SerializationError {
321
+ name = "UnknownHashVariantError";
322
+ };
277
323
  var ChecksumError = class extends SerializationError {
278
324
  name = "ChecksumError";
279
325
  };
326
+ /**
327
+ * Asserts a frame body is long enough to hold its fixed params block, so the
328
+ * params can be read without running off the end.
329
+ */
330
+ function assertMinBodyLength(actual, min, context) {
331
+ if (actual < min) throw new TruncatedError(`${context}: body of ${String(actual)} bytes is shorter than the ${String(min)}-byte params block`);
332
+ }
333
+ /**
334
+ * Asserts a frame body is exactly the length its declared params imply, so a
335
+ * hostile or truncated frame is rejected before any backing store is allocated.
336
+ */
337
+ function assertBodyLength(actual, expected, context) {
338
+ if (actual !== expected) throw new TruncatedError(`${context}: body of ${String(actual)} bytes does not match the declared params (expected ${String(expected)})`);
339
+ }
280
340
  function readHeader(frame) {
281
341
  if (frame.length < 12) throw new TruncatedError(`frame of ${String(frame.length)} bytes is shorter than the minimum ${String(12)}`);
282
342
  if (frame[0] !== 65 || frame[1] !== 77 || frame[2] !== 81 || frame[3] !== 70) throw new BadMagicError("frame does not start with the AMQF magic");
283
343
  const version = frame[4] ?? 0;
284
- if (version !== 1) throw new UnknownVersionError(`unsupported format version ${String(version)}`);
344
+ if (version !== 2) throw new UnknownVersionError(`unsupported format version ${String(version)}`);
285
345
  if (new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(frame.length - TRAILER_SIZE, true) !== crc32(frame.subarray(0, frame.length - TRAILER_SIZE))) throw new ChecksumError("frame CRC32 does not match its contents");
286
346
  return {
287
347
  version,
@@ -291,39 +351,4 @@ function readHeader(frame) {
291
351
  };
292
352
  }
293
353
  //#endregion
294
- Object.defineProperty(exports, "SerializationError", {
295
- enumerable: true,
296
- get: function() {
297
- return SerializationError;
298
- }
299
- });
300
- Object.defineProperty(exports, "hash128KeyInto", {
301
- enumerable: true,
302
- get: function() {
303
- return hash128KeyInto;
304
- }
305
- });
306
- Object.defineProperty(exports, "probeInto", {
307
- enumerable: true,
308
- get: function() {
309
- return probeInto;
310
- }
311
- });
312
- Object.defineProperty(exports, "readHeader", {
313
- enumerable: true,
314
- get: function() {
315
- return readHeader;
316
- }
317
- });
318
- Object.defineProperty(exports, "reduce", {
319
- enumerable: true,
320
- get: function() {
321
- return reduce;
322
- }
323
- });
324
- Object.defineProperty(exports, "writeHeader", {
325
- enumerable: true,
326
- get: function() {
327
- return writeHeader;
328
- }
329
- });
354
+ export { readHeader as a, RLO as c, hash32x2Into as d, mul64 as f, assertMinBodyLength as i, fmix64 as l, reduce as m, UnknownHashVariantError as n, writeHeader as o, probeInto as p, assertBodyLength as r, RHI as s, SerializationError as t, hash128KeyInto as u };
@@ -81,6 +81,37 @@ function fmix64(lo, hi) {
81
81
  RLO = l;
82
82
  RHI = h;
83
83
  }
84
+ function murmur32(bytes, seed, len = bytes.length) {
85
+ let h = seed >>> 0;
86
+ const n = len & -4;
87
+ for (let i = 0; i < n; i += 4) {
88
+ let k = ((bytes[i] ?? 0) | (bytes[i + 1] ?? 0) << 8 | (bytes[i + 2] ?? 0) << 16 | (bytes[i + 3] ?? 0) << 24) >>> 0;
89
+ k = Math.imul(k, 3432918353);
90
+ k = (k << 15 | k >>> 17) >>> 0;
91
+ k = Math.imul(k, 461845907);
92
+ h = (h ^ k) >>> 0;
93
+ h = (h << 13 | h >>> 19) >>> 0;
94
+ h = Math.imul(h, 5) + 3864292196 >>> 0;
95
+ }
96
+ let k1 = 0;
97
+ const rem = len & 3;
98
+ if (rem >= 3) k1 ^= (bytes[n + 2] ?? 0) << 16;
99
+ if (rem >= 2) k1 ^= (bytes[n + 1] ?? 0) << 8;
100
+ if (rem >= 1) {
101
+ k1 ^= bytes[n] ?? 0;
102
+ k1 = Math.imul(k1 >>> 0, 3432918353);
103
+ k1 = (k1 << 15 | k1 >>> 17) >>> 0;
104
+ k1 = Math.imul(k1, 461845907);
105
+ h = (h ^ k1) >>> 0;
106
+ }
107
+ h = (h ^ len) >>> 0;
108
+ h ^= h >>> 16;
109
+ h = Math.imul(h, 2246822507);
110
+ h ^= h >>> 13;
111
+ h = Math.imul(h, 3266489909);
112
+ h ^= h >>> 16;
113
+ return h >>> 0;
114
+ }
84
115
  function computeLanes(bytes, seed, len) {
85
116
  const nblocks = len >>> 4;
86
117
  let h1lo = seed >>> 0;
@@ -186,16 +217,22 @@ function computeLanes(bytes, seed, len) {
186
217
  }
187
218
  const keyEncoder = new TextEncoder();
188
219
  let keyBuf = /* @__PURE__ */ new Uint8Array(256);
189
- function keyToLanes(key, seed) {
220
+ let encBytes = keyBuf;
221
+ let encLen = 0;
222
+ function encodeKey(key) {
190
223
  if (typeof key === "string") {
191
224
  const cap = key.length * 3;
192
225
  if (keyBuf.length < cap) keyBuf = new Uint8Array(cap);
193
- const { written } = keyEncoder.encodeInto(key, keyBuf);
194
- computeLanes(keyBuf, seed, written);
226
+ encLen = keyEncoder.encodeInto(key, keyBuf).written;
227
+ encBytes = keyBuf;
195
228
  return;
196
229
  }
197
- const bytes = normalize(key);
198
- computeLanes(bytes, seed, bytes.length);
230
+ encBytes = normalize(key);
231
+ encLen = encBytes.length;
232
+ }
233
+ function keyToLanes(key, seed) {
234
+ encodeKey(key);
235
+ computeLanes(encBytes, seed, encLen);
199
236
  }
200
237
  function hash128KeyInto(key, seed, out) {
201
238
  keyToLanes(key, seed);
@@ -221,10 +258,16 @@ function reduce(x, range) {
221
258
  * Kirsch-Mitzenmacher enhanced double hashing `g_i = h1 + i*h2 + i^2`
222
259
  * (the RocksDB `+i^2` fix), reduced into range with Lemire multiply-shift.
223
260
  */
261
+ const SEED2 = 2654435761;
262
+ function hash32x2Into(key, seed, out) {
263
+ encodeKey(key);
264
+ out[0] = murmur32(encBytes, seed, encLen);
265
+ out[1] = murmur32(encBytes, (seed ^ SEED2) >>> 0, encLen);
266
+ }
224
267
  function probeInto(key, count, range, seed, out) {
225
- keyToLanes(key, seed);
226
- const a = (LANES.h1lo ^ LANES.h1hi) >>> 0;
227
- const b = (LANES.h2lo ^ LANES.h2hi) >>> 0;
268
+ encodeKey(key);
269
+ const a = murmur32(encBytes, seed, encLen);
270
+ const b = murmur32(encBytes, (seed ^ SEED2) >>> 0, encLen);
228
271
  for (let i = 0; i < count; i++) out[i] = reduce(a + Math.imul(i, b) + i * i >>> 0, range);
229
272
  }
230
273
  //#endregion
@@ -274,14 +317,31 @@ var BadMagicError = class extends SerializationError {
274
317
  var UnknownVersionError = class extends SerializationError {
275
318
  name = "UnknownVersionError";
276
319
  };
320
+ var UnknownHashVariantError = class extends SerializationError {
321
+ name = "UnknownHashVariantError";
322
+ };
277
323
  var ChecksumError = class extends SerializationError {
278
324
  name = "ChecksumError";
279
325
  };
326
+ /**
327
+ * Asserts a frame body is long enough to hold its fixed params block, so the
328
+ * params can be read without running off the end.
329
+ */
330
+ function assertMinBodyLength(actual, min, context) {
331
+ if (actual < min) throw new TruncatedError(`${context}: body of ${String(actual)} bytes is shorter than the ${String(min)}-byte params block`);
332
+ }
333
+ /**
334
+ * Asserts a frame body is exactly the length its declared params imply, so a
335
+ * hostile or truncated frame is rejected before any backing store is allocated.
336
+ */
337
+ function assertBodyLength(actual, expected, context) {
338
+ if (actual !== expected) throw new TruncatedError(`${context}: body of ${String(actual)} bytes does not match the declared params (expected ${String(expected)})`);
339
+ }
280
340
  function readHeader(frame) {
281
341
  if (frame.length < 12) throw new TruncatedError(`frame of ${String(frame.length)} bytes is shorter than the minimum ${String(12)}`);
282
342
  if (frame[0] !== 65 || frame[1] !== 77 || frame[2] !== 81 || frame[3] !== 70) throw new BadMagicError("frame does not start with the AMQF magic");
283
343
  const version = frame[4] ?? 0;
284
- if (version !== 1) throw new UnknownVersionError(`unsupported format version ${String(version)}`);
344
+ if (version !== 2) throw new UnknownVersionError(`unsupported format version ${String(version)}`);
285
345
  if (new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(frame.length - TRAILER_SIZE, true) !== crc32(frame.subarray(0, frame.length - TRAILER_SIZE))) throw new ChecksumError("frame CRC32 does not match its contents");
286
346
  return {
287
347
  version,
@@ -291,4 +351,87 @@ function readHeader(frame) {
291
351
  };
292
352
  }
293
353
  //#endregion
294
- export { probeInto as a, hash128KeyInto as i, readHeader as n, reduce as o, writeHeader as r, SerializationError as t };
354
+ Object.defineProperty(exports, "RHI", {
355
+ enumerable: true,
356
+ get: function() {
357
+ return RHI;
358
+ }
359
+ });
360
+ Object.defineProperty(exports, "RLO", {
361
+ enumerable: true,
362
+ get: function() {
363
+ return RLO;
364
+ }
365
+ });
366
+ Object.defineProperty(exports, "SerializationError", {
367
+ enumerable: true,
368
+ get: function() {
369
+ return SerializationError;
370
+ }
371
+ });
372
+ Object.defineProperty(exports, "UnknownHashVariantError", {
373
+ enumerable: true,
374
+ get: function() {
375
+ return UnknownHashVariantError;
376
+ }
377
+ });
378
+ Object.defineProperty(exports, "assertBodyLength", {
379
+ enumerable: true,
380
+ get: function() {
381
+ return assertBodyLength;
382
+ }
383
+ });
384
+ Object.defineProperty(exports, "assertMinBodyLength", {
385
+ enumerable: true,
386
+ get: function() {
387
+ return assertMinBodyLength;
388
+ }
389
+ });
390
+ Object.defineProperty(exports, "fmix64", {
391
+ enumerable: true,
392
+ get: function() {
393
+ return fmix64;
394
+ }
395
+ });
396
+ Object.defineProperty(exports, "hash128KeyInto", {
397
+ enumerable: true,
398
+ get: function() {
399
+ return hash128KeyInto;
400
+ }
401
+ });
402
+ Object.defineProperty(exports, "hash32x2Into", {
403
+ enumerable: true,
404
+ get: function() {
405
+ return hash32x2Into;
406
+ }
407
+ });
408
+ Object.defineProperty(exports, "mul64", {
409
+ enumerable: true,
410
+ get: function() {
411
+ return mul64;
412
+ }
413
+ });
414
+ Object.defineProperty(exports, "probeInto", {
415
+ enumerable: true,
416
+ get: function() {
417
+ return probeInto;
418
+ }
419
+ });
420
+ Object.defineProperty(exports, "readHeader", {
421
+ enumerable: true,
422
+ get: function() {
423
+ return readHeader;
424
+ }
425
+ });
426
+ Object.defineProperty(exports, "reduce", {
427
+ enumerable: true,
428
+ get: function() {
429
+ return reduce;
430
+ }
431
+ });
432
+ Object.defineProperty(exports, "writeHeader", {
433
+ enumerable: true,
434
+ get: function() {
435
+ return writeHeader;
436
+ }
437
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "distillate",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Probabilistic data structures for JavaScript. Space-efficient, approximate-membership filters (Bloom, Blocked Bloom, Binary Fuse) with tunable error and a portable binary format; zero dependencies, universal.",
5
5
  "keywords": [
6
6
  "bloom-filter",