probe-filters 1.0.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.
@@ -0,0 +1,307 @@
1
+ import { PointFilter } from './pointFilter.js';
2
+ import { RangeFilter } from './rangeFilter.js';
3
+ import { SpatialFilter } from './spatialFilter.js';
4
+ import { TemporalFilter } from './temporalFilter.js';
5
+ import { FILTER_MERGE_OPERATORS, normalizeFilterMergeOperator } from './mergeOperators.js';
6
+ import { BinaryWriter, BinaryReader, MAGIC, wrapCRC32, unwrapCRC32 } from './serialization.js';
7
+
8
+
9
+ function isLiveAlephState(state) {
10
+ return state !== 0 && state !== 3;
11
+ }
12
+
13
+ function insertMotherHash(targetFilter, motherHash, state = 1) {
14
+ const fingerprintBits = Math.min(targetFilter.currentFingerprintLengthForNewEntries, 32 - targetFilter.bitsForSlotAddress);
15
+ if (fingerprintBits <= 0) {
16
+ throw new Error('Cannot merge into target filter with zero fingerprint bits.');
17
+ }
18
+
19
+ const addressSpace = 2 ** targetFilter.bitsForSlotAddress;
20
+ const canonicalSlot = motherHash % addressSpace;
21
+ const shiftedHash = Math.floor(motherHash / addressSpace);
22
+ const fingerprint = shiftedHash % (2 ** fingerprintBits);
23
+ targetFilter._performInsertion(canonicalSlot, fingerprint, state, fingerprintBits, motherHash);
24
+ }
25
+
26
+ function mergeAlephFilters(targetFilter, sourceFilter) {
27
+ for (let i = 0; i < sourceFilter.currentCapacity; i++) {
28
+ const state = sourceFilter.states[i];
29
+ if (!isLiveAlephState(state)) continue;
30
+ insertMotherHash(targetFilter, sourceFilter.motherHashes[i], state);
31
+ }
32
+ }
33
+
34
+ function mergeRangeFilters(targetFilter, sourceFilter) {
35
+ for (const partition of sourceFilter.partitions.keys()) {
36
+ targetFilter.insert(partition * sourceFilter.partitionSize);
37
+ }
38
+ }
39
+
40
+ function mergeSpatialFilters(targetFilter, sourceFilter) {
41
+ const source = sourceFilter.rangeFilter;
42
+ const target = targetFilter.rangeFilter;
43
+
44
+ for (const partition of source.partitions.keys()) {
45
+ target.insert(partition * source.partitionSize);
46
+ }
47
+ }
48
+
49
+ function mergeTemporalFilters(targetFilter, sourceFilter) {
50
+ for (const bucket of sourceFilter.buckets) {
51
+ if (!bucket.filter || Number.isNaN(bucket.bucketId)) continue;
52
+ const targetBucket = targetFilter._getOrCreateBucket(bucket.bucketId);
53
+ mergeAlephFilters(targetBucket.filter, bucket.filter);
54
+ }
55
+ }
56
+
57
+ export class MultiFilter {
58
+ constructor(options = {}) {
59
+ this.point = options.point === false ? null : new PointFilter(options.pointOptions ?? options);
60
+ this.range = options.range === false ? null : new RangeFilter(options.rangeOptions ?? {});
61
+ this.spatial = options.spatial === false ? null : new SpatialFilter(options.spatialOptions ?? {});
62
+ this.temporal = options.temporal === false ? null : new TemporalFilter(options.temporalOptions ?? {});
63
+ }
64
+
65
+ insert(key) {
66
+ if (!this.point) throw new Error('Point filtering is disabled.');
67
+ this.point.insert(key);
68
+ }
69
+
70
+ set(key) {
71
+ return this.insert(key);
72
+ }
73
+
74
+ delete(key) {
75
+ if (!this.point) throw new Error('Point filtering is disabled.');
76
+ return this.point.delete(key);
77
+ }
78
+
79
+ remove(key) {
80
+ return this.delete(key);
81
+ }
82
+
83
+ query(key) {
84
+ if (!this.point) throw new Error('Point filtering is disabled.');
85
+ return this.point.query(key);
86
+ }
87
+
88
+ has(key) {
89
+ return this.query(key);
90
+ }
91
+
92
+ insertRangeKey(key) {
93
+ if (!this.range) throw new Error('Range filtering is disabled.');
94
+ this.range.insert(key);
95
+ }
96
+
97
+ setRangeKey(key) {
98
+ return this.insertRangeKey(key);
99
+ }
100
+
101
+ deleteRangeKey(key) {
102
+ if (!this.range) throw new Error('Range filtering is disabled.');
103
+ return this.range.delete(key);
104
+ }
105
+
106
+ removeRangeKey(key) {
107
+ return this.deleteRangeKey(key);
108
+ }
109
+
110
+ queryRange(start, end) {
111
+ if (!this.range) throw new Error('Range filtering is disabled.');
112
+ return this.range.queryRange(start, end);
113
+ }
114
+
115
+ hasRange(start, end) {
116
+ return this.queryRange(start, end);
117
+ }
118
+
119
+ insertSpatialPoint(point) {
120
+ if (!this.spatial) throw new Error('Spatial filtering is disabled.');
121
+ this.spatial.insert(point);
122
+ }
123
+
124
+ setSpatialPoint(point) {
125
+ return this.insertSpatialPoint(point);
126
+ }
127
+
128
+ deleteSpatialPoint(point) {
129
+ if (!this.spatial) throw new Error('Spatial filtering is disabled.');
130
+ return this.spatial.delete(point);
131
+ }
132
+
133
+ removeSpatialPoint(point) {
134
+ return this.deleteSpatialPoint(point);
135
+ }
136
+
137
+ queryBox(min, max) {
138
+ if (!this.spatial) throw new Error('Spatial filtering is disabled.');
139
+ return this.spatial.queryBox(min, max);
140
+ }
141
+
142
+ hasBox(min, max) {
143
+ return this.queryBox(min, max);
144
+ }
145
+
146
+ insertSpatialBox(min, max, id = null) {
147
+ if (!this.spatial) throw new Error('Spatial filtering is disabled.');
148
+ this.spatial.insertBox(min, max, id);
149
+ }
150
+
151
+ setSpatialBox(min, max, id = null) {
152
+ return this.insertSpatialBox(min, max, id);
153
+ }
154
+
155
+ insertSpatialCircle(center, radius, id = null) {
156
+ if (!this.spatial) throw new Error('Spatial filtering is disabled.');
157
+ this.spatial.insertCircle(center, radius, id);
158
+ }
159
+
160
+ setSpatialCircle(center, radius, id = null) {
161
+ return this.insertSpatialCircle(center, radius, id);
162
+ }
163
+
164
+ insertTemporalKey(key, timestampMs = Date.now()) {
165
+ if (!this.temporal) throw new Error('Temporal filtering is disabled.');
166
+ this.temporal.insertAt(key, timestampMs);
167
+ }
168
+
169
+ setTemporalKey(key, timestampMs = Date.now()) {
170
+ return this.insertTemporalKey(key, timestampMs);
171
+ }
172
+
173
+ queryWithinLast(key, durationMs, nowMs = Date.now()) {
174
+ if (!this.temporal) throw new Error('Temporal filtering is disabled.');
175
+ return this.temporal.queryWithinLast(key, durationMs, nowMs);
176
+ }
177
+
178
+ queryAgo(key, ageMs, toleranceMs, nowMs = Date.now()) {
179
+ if (!this.temporal) throw new Error('Temporal filtering is disabled.');
180
+ return this.temporal.queryAgo(key, ageMs, toleranceMs, nowMs);
181
+ }
182
+
183
+ queryBetweenAges(key, minAgeMs, maxAgeMs, nowMs = Date.now()) {
184
+ if (!this.temporal) throw new Error('Temporal filtering is disabled.');
185
+ return this.temporal.queryBetweenAges(key, minAgeMs, maxAgeMs, nowMs);
186
+ }
187
+
188
+ adaptTemporalFalsePositiveWithinLast(key, durationMs, nowMs = Date.now()) {
189
+ if (!this.temporal) throw new Error('Temporal filtering is disabled.');
190
+ return this.temporal.adaptFalsePositiveWithinLast(key, durationMs, nowMs);
191
+ }
192
+
193
+ adaptTemporalFalsePositiveAgo(key, ageMs, toleranceMs, nowMs = Date.now()) {
194
+ if (!this.temporal) throw new Error('Temporal filtering is disabled.');
195
+ return this.temporal.adaptFalsePositiveAgo(key, ageMs, toleranceMs, nowMs);
196
+ }
197
+
198
+ adaptTemporalFalsePositiveBetweenAges(key, minAgeMs, maxAgeMs, nowMs = Date.now()) {
199
+ if (!this.temporal) throw new Error('Temporal filtering is disabled.');
200
+ return this.temporal.adaptFalsePositiveBetweenAges(key, minAgeMs, maxAgeMs, nowMs);
201
+ }
202
+
203
+ adaptFalsePositive(query, kind = 'point') {
204
+ if (kind === 'point') {
205
+ if (!this.point) throw new Error('Point filtering is disabled.');
206
+ return this.point.rejuvenate(query);
207
+ }
208
+
209
+ if (kind === 'range') {
210
+ if (!this.range) throw new Error('Range filtering is disabled.');
211
+ if (Array.isArray(query) && query.length === 2) {
212
+ return this.range.adaptFalsePositive(query[0], query[1]);
213
+ }
214
+ return this.range.adaptFalsePositive(query, query);
215
+ }
216
+
217
+ if (kind === 'spatial') {
218
+ if (!this.spatial) throw new Error('Spatial filtering is disabled.');
219
+ return this.spatial.adaptFalsePositiveBox(query.min, query.max);
220
+ }
221
+
222
+ if (kind === 'temporal') {
223
+ if (!this.temporal) throw new Error('Temporal filtering is disabled.');
224
+ if (query.durationMs !== undefined) {
225
+ return this.temporal.adaptFalsePositiveWithinLast(query.key, query.durationMs, query.nowMs ?? Date.now());
226
+ }
227
+ if (query.ageMs !== undefined) {
228
+ return this.temporal.adaptFalsePositiveAgo(query.key, query.ageMs, query.toleranceMs, query.nowMs ?? Date.now());
229
+ }
230
+ if (query.minAgeMs !== undefined && query.maxAgeMs !== undefined) {
231
+ return this.temporal.adaptFalsePositiveBetweenAges(query.key, query.minAgeMs, query.maxAgeMs, query.nowMs ?? Date.now());
232
+ }
233
+ throw new Error('Temporal adaptation requires durationMs, ageMs, or minAgeMs/maxAgeMs.');
234
+ }
235
+
236
+ throw new Error(`Unknown filter kind: ${kind}`);
237
+ }
238
+
239
+ mergeFrom(other, operator = FILTER_MERGE_OPERATORS.UNION) {
240
+ const normalizedOperator = normalizeFilterMergeOperator(operator);
241
+ if (!(other instanceof MultiFilter)) {
242
+ throw new Error('mergeFrom expects another MultiFilter instance.');
243
+ }
244
+
245
+ if (normalizedOperator === FILTER_MERGE_OPERATORS.UNION) {
246
+ if (this.point && other.point) mergeAlephFilters(this.point, other.point);
247
+ if (this.range && other.range) mergeRangeFilters(this.range, other.range);
248
+ if (this.spatial && other.spatial) mergeSpatialFilters(this.spatial, other.spatial);
249
+ if (this.temporal && other.temporal) mergeTemporalFilters(this.temporal, other.temporal);
250
+ return this;
251
+ }
252
+
253
+ throw new Error(`Unsupported merge operator: ${normalizedOperator}`);
254
+ }
255
+
256
+ getStats() {
257
+ return {
258
+ point: this.point?.getStats() ?? null,
259
+ range: this.range?.getStats() ?? null,
260
+ spatial: this.spatial?.getStats() ?? null,
261
+ temporal: this.temporal?.getStats?.() ?? null,
262
+ };
263
+ }
264
+
265
+ serialize() {
266
+ const parts = [];
267
+ if (this.point) parts.push({ type: 'point', filter: this.point });
268
+ if (this.range) parts.push({ type: 'range', filter: this.range });
269
+ if (this.spatial) parts.push({ type: 'spatial', filter: this.spatial });
270
+ if (this.temporal) parts.push({ type: 'temporal', filter: this.temporal });
271
+
272
+ const writer = new BinaryWriter();
273
+ writer.uint32(MAGIC.MULTI);
274
+ writer.uint32(parts.length);
275
+ for (const { type, filter } of parts) {
276
+ writer.uint32(type.length);
277
+ for (let i = 0; i < type.length; i++) writer.uint8(type.charCodeAt(i));
278
+ const data = filter.serialize();
279
+ writer.bytes(new Uint8Array(data));
280
+ }
281
+ return wrapCRC32(writer.toArrayBuffer());
282
+ }
283
+
284
+ static deserialize(buffer, options = {}) {
285
+ buffer = unwrapCRC32(buffer);
286
+ const reader = new BinaryReader(buffer);
287
+ if (reader.uint32() !== MAGIC.MULTI) throw new Error('Not a MultiFilter binary.');
288
+
289
+ const filter = new MultiFilter(options);
290
+ const partCount = reader.uint32();
291
+ for (let i = 0; i < partCount; i++) {
292
+ const typeLen = reader.uint32();
293
+ let type = '';
294
+ for (let j = 0; j < typeLen; j++) type += String.fromCharCode(reader.uint8());
295
+ const data = reader.bytes();
296
+
297
+ switch (type) {
298
+ case 'point': filter.point = PointFilter.deserialize(data.buffer); break;
299
+ case 'range': filter.range = RangeFilter.deserialize(data.buffer); break;
300
+ case 'spatial': filter.spatial = SpatialFilter.deserialize(data.buffer); break;
301
+ case 'temporal': filter.temporal = TemporalFilter.deserialize(data.buffer); break;
302
+ default: throw new Error(`Unknown serialized filter type: ${type}`);
303
+ }
304
+ }
305
+ return filter;
306
+ }
307
+ }
@@ -0,0 +1,74 @@
1
+ const WORD_BITS = 32;
2
+
3
+ export class PackedBitset {
4
+ constructor(size) {
5
+ this.size = size;
6
+ this.words = new Uint32Array(Math.ceil(size / WORD_BITS));
7
+ }
8
+
9
+ get(index) {
10
+ return (this.words[index >>> 5] >>> (index & 31)) & 1;
11
+ }
12
+
13
+ set(index, value) {
14
+ const wordIndex = index >>> 5;
15
+ const mask = 1 << (index & 31);
16
+
17
+ if (value) {
18
+ this.words[wordIndex] |= mask;
19
+ } else {
20
+ this.words[wordIndex] &= ~mask;
21
+ }
22
+ }
23
+
24
+ clear() {
25
+ this.words.fill(0);
26
+ }
27
+
28
+ rank1(index) {
29
+ if (index < 0) return 0;
30
+ const capped = Math.min(index, this.size - 1);
31
+ const fullWords = capped >>> 5;
32
+ let count = 0;
33
+
34
+ for (let word = 0; word < fullWords; word++) {
35
+ count += popcount32(this.words[word]);
36
+ }
37
+
38
+ const bitsInLastWord = (capped & 31) + 1;
39
+ const mask = bitsInLastWord === WORD_BITS ? 0xffffffff : (2 ** bitsInLastWord) - 1;
40
+ return count + popcount32(this.words[fullWords] & mask);
41
+ }
42
+
43
+ select1(rank) {
44
+ if (rank <= 0) return -1;
45
+
46
+ let remaining = rank;
47
+ for (let wordIndex = 0; wordIndex < this.words.length; wordIndex++) {
48
+ const word = this.words[wordIndex];
49
+ const count = popcount32(word);
50
+ if (remaining > count) {
51
+ remaining -= count;
52
+ continue;
53
+ }
54
+
55
+ for (let bit = 0; bit < WORD_BITS; bit++) {
56
+ const index = (wordIndex * WORD_BITS) + bit;
57
+ if (index >= this.size) return -1;
58
+ if (((word >>> bit) & 1) === 1) {
59
+ remaining--;
60
+ if (remaining === 0) return index;
61
+ }
62
+ }
63
+ }
64
+
65
+ return -1;
66
+ }
67
+ }
68
+
69
+ export function popcount32(value) {
70
+ let v = value >>> 0;
71
+ v -= (v >>> 1) & 0x55555555;
72
+ v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
73
+ return (((v + (v >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
74
+ }
@@ -0,0 +1,302 @@
1
+ import { djb2 } from './pointFilter.js';
2
+ import { PackedBitset } from './packedBitset.js';
3
+
4
+ const EMPTY = 0xffffffff;
5
+
6
+ export class PackedRSQF {
7
+ constructor(options = {}) {
8
+ this.capacity = options.capacity ?? 256;
9
+ this.remainderBits = options.remainderBits ?? 12;
10
+ this.hashFunction = options.hashFunction ?? djb2;
11
+ this.maxLoadFactor = options.maxLoadFactor ?? 0.9;
12
+
13
+ if (Math.log2(this.capacity) % 1 !== 0) {
14
+ throw new Error('capacity must be a power of 2.');
15
+ }
16
+ if (this.remainderBits <= 0 || this.remainderBits > 31) {
17
+ throw new Error('remainderBits must be in [1, 31].');
18
+ }
19
+
20
+ this.addressBits = Math.log2(this.capacity);
21
+ this.remainderMask = (2 ** this.remainderBits) - 1;
22
+ this.remainders = new Uint32Array(this.capacity);
23
+ this.remainders.fill(EMPTY);
24
+ this.quotients = new Uint32Array(this.capacity);
25
+ this.quotients.fill(EMPTY);
26
+ this.occupied = new PackedBitset(this.capacity);
27
+ this.shifted = new PackedBitset(this.capacity);
28
+ this.continuation = new PackedBitset(this.capacity);
29
+ this.runends = new PackedBitset(this.capacity);
30
+ this.blockOffsets = new Uint8Array(Math.ceil(this.capacity / 64));
31
+ this.count = 0;
32
+ }
33
+
34
+ _hashKey(key) {
35
+ return this.hashFunction(String(key), 0) >>> 0;
36
+ }
37
+
38
+ _quotientAndRemainder(key) {
39
+ const hash = this._hashKey(key);
40
+ const quotient = hash & (this.capacity - 1);
41
+ const remainder = Math.floor(hash / this.capacity) & this.remainderMask;
42
+ return { quotient, remainder };
43
+ }
44
+
45
+ _isEmpty(index) {
46
+ return this.remainders[index] === EMPTY;
47
+ }
48
+
49
+ _isClusterStart(index) {
50
+ return !this._isEmpty(index) && this.shifted.get(index) === 0;
51
+ }
52
+
53
+ _findClusterStart(index) {
54
+ let current = index;
55
+ for (let i = 0; i < this.capacity; i++) {
56
+ if (this._isEmpty(current) || this.shifted.get(current) === 0) {
57
+ return current;
58
+ }
59
+ current = (current - 1 + this.capacity) % this.capacity;
60
+ }
61
+ return index;
62
+ }
63
+
64
+ _runStart(quotient) {
65
+ if (this.occupied.get(quotient) === 0) return -1;
66
+
67
+ const clusterStart = this._findClusterStart(quotient);
68
+ const runRankBeforeQuotient = this.occupied.rank1(quotient) - this.occupied.get(quotient);
69
+ const runRankBeforeCluster = clusterStart === 0 ? 0 : this.occupied.rank1(clusterStart - 1);
70
+ let runsToSkip = runRankBeforeQuotient - runRankBeforeCluster;
71
+ let current = clusterStart;
72
+
73
+ while (runsToSkip > 0) {
74
+ current = (current + 1) % this.capacity;
75
+ while (this.continuation.get(current) === 1) {
76
+ current = (current + 1) % this.capacity;
77
+ }
78
+ runsToSkip--;
79
+ }
80
+
81
+ return current;
82
+ }
83
+
84
+ _runEnd(runStart) {
85
+ if (runStart === -1) return -1;
86
+ let current = runStart;
87
+ for (let i = 1; i < this.capacity; i++) {
88
+ const next = (current + 1) % this.capacity;
89
+ if (this._isEmpty(next) || this.continuation.get(next) === 0) {
90
+ return current;
91
+ }
92
+ current = next;
93
+ }
94
+ return current;
95
+ }
96
+
97
+ _findInsertionPoint(quotient, remainder) {
98
+ const runStart = this._runStart(quotient);
99
+ if (runStart === -1) {
100
+ if (this._isEmpty(quotient) || this.shifted.get(quotient) === 0) return quotient;
101
+
102
+ const clusterStart = this._findClusterStart(quotient);
103
+ let precedingRuns = this.occupied.rank1(quotient) - (clusterStart === 0 ? 0 : this.occupied.rank1(clusterStart - 1));
104
+ let current = clusterStart;
105
+
106
+ while (precedingRuns > 0) {
107
+ current = (this._runEnd(current) + 1) % this.capacity;
108
+ precedingRuns--;
109
+ }
110
+
111
+ return current;
112
+ }
113
+
114
+ let current = runStart;
115
+ for (let i = 0; i < this.capacity; i++) {
116
+ if (this.remainders[current] >= remainder) return current;
117
+
118
+ const next = (current + 1) % this.capacity;
119
+ if (this._isEmpty(next) || this.continuation.get(next) === 0) {
120
+ return next;
121
+ }
122
+ current = next;
123
+ }
124
+
125
+ return runStart;
126
+ }
127
+
128
+ _findEmptyFrom(start) {
129
+ for (let distance = 0; distance < this.capacity; distance++) {
130
+ const index = (start + distance) % this.capacity;
131
+ if (this._isEmpty(index)) return index;
132
+ }
133
+ return -1;
134
+ }
135
+
136
+ _copySlot(dest, src) {
137
+ this.remainders[dest] = this.remainders[src];
138
+ this.quotients[dest] = this.quotients[src];
139
+ this.shifted.set(dest, this.shifted.get(src));
140
+ this.continuation.set(dest, this.continuation.get(src));
141
+ }
142
+
143
+ _clearSlot(index) {
144
+ this.remainders[index] = EMPTY;
145
+ this.quotients[index] = EMPTY;
146
+ this.shifted.set(index, 0);
147
+ this.continuation.set(index, 0);
148
+ }
149
+
150
+ _rebuildMetadata() {
151
+ this.occupied.clear();
152
+ this.shifted.clear();
153
+ this.continuation.clear();
154
+ this.runends.clear();
155
+ this.blockOffsets.fill(0);
156
+
157
+ for (let index = 0; index < this.capacity; index++) {
158
+ if (this._isEmpty(index)) continue;
159
+ const quotient = this.quotients[index];
160
+ this.occupied.set(quotient, 1);
161
+ this.shifted.set(index, quotient !== index);
162
+
163
+ const previous = (index - 1 + this.capacity) % this.capacity;
164
+ if (!this._isEmpty(previous) && this.quotients[previous] === quotient) {
165
+ this.continuation.set(index, 1);
166
+ }
167
+
168
+ const next = (index + 1) % this.capacity;
169
+ if (this._isEmpty(next) || this.quotients[next] !== quotient) {
170
+ this.runends.set(index, 1);
171
+ }
172
+ }
173
+
174
+ for (let block = 0; block < this.blockOffsets.length; block++) {
175
+ const blockStart = block * 64;
176
+ let offset = 0;
177
+ while (blockStart + offset < this.capacity && offset < 255 && this.shifted.get(blockStart + offset) === 1) {
178
+ offset++;
179
+ }
180
+ this.blockOffsets[block] = offset;
181
+ }
182
+ }
183
+
184
+ _runEndByRank(quotient) {
185
+ if (this.occupied.get(quotient) === 0) return -1;
186
+ return this.runends.select1(this.occupied.rank1(quotient));
187
+ }
188
+
189
+ _shiftRight(start, empty) {
190
+ let current = empty;
191
+ while (current !== start) {
192
+ const previous = (current - 1 + this.capacity) % this.capacity;
193
+ this._copySlot(current, previous);
194
+ this.shifted.set(current, 1);
195
+ current = previous;
196
+ }
197
+ this._clearSlot(start);
198
+ }
199
+
200
+ _writeInsertedSlot(index, quotient, remainder, runStartBeforeInsert) {
201
+ this.remainders[index] = remainder >>> 0;
202
+ this.quotients[index] = quotient >>> 0;
203
+ this.shifted.set(index, index !== quotient);
204
+
205
+ if (runStartBeforeInsert === -1 || index === quotient || index === runStartBeforeInsert) {
206
+ this.continuation.set(index, 0);
207
+ } else {
208
+ this.continuation.set(index, 1);
209
+ }
210
+
211
+ const next = (index + 1) % this.capacity;
212
+ if (runStartBeforeInsert !== -1 && index === runStartBeforeInsert && !this._isEmpty(next)) {
213
+ this.continuation.set(next, 1);
214
+ }
215
+ }
216
+
217
+ insert(key) {
218
+ if ((this.count + 1) / this.capacity > this.maxLoadFactor) {
219
+ throw new Error('PackedRSQF is above max load factor; expand before inserting.');
220
+ }
221
+
222
+ const { quotient, remainder } = this._quotientAndRemainder(key);
223
+ if (this.query(key)) return false;
224
+
225
+ const runStartBeforeInsert = this._runStart(quotient);
226
+ const insertionPoint = this._findInsertionPoint(quotient, remainder);
227
+ const empty = this._findEmptyFrom(insertionPoint);
228
+ if (empty === -1) throw new Error('PackedRSQF is full.');
229
+
230
+ if (empty !== insertionPoint) {
231
+ this._shiftRight(insertionPoint, empty);
232
+ }
233
+
234
+ this.occupied.set(quotient, 1);
235
+ this._writeInsertedSlot(insertionPoint, quotient, remainder, runStartBeforeInsert);
236
+ this._rebuildMetadata();
237
+ this.count++;
238
+ return true;
239
+ }
240
+
241
+ query(key) {
242
+ const { quotient, remainder } = this._quotientAndRemainder(key);
243
+ for (let index = 0; index < this.capacity; index++) {
244
+ if (this.quotients[index] === quotient && this.remainders[index] === remainder) return true;
245
+ }
246
+ return false;
247
+ }
248
+
249
+ _slotFor(key) {
250
+ const { quotient, remainder } = this._quotientAndRemainder(key);
251
+ for (let index = 0; index < this.capacity; index++) {
252
+ if (this.quotients[index] === quotient && this.remainders[index] === remainder) return { slot: index, quotient };
253
+ }
254
+ return { slot: -1, quotient };
255
+ }
256
+
257
+ delete(key) {
258
+ const { slot, quotient } = this._slotFor(key);
259
+ if (slot === -1) return false;
260
+
261
+ const runStart = this._runStart(quotient);
262
+ const runEnd = this._runEnd(runStart);
263
+ const runLength = runEnd >= runStart ? (runEnd - runStart + 1) : (this.capacity - runStart + runEnd + 1);
264
+
265
+ this._deleteSlot(slot, quotient);
266
+ if (runLength === 1) {
267
+ this.occupied.set(quotient, 0);
268
+ }
269
+ this._rebuildMetadata();
270
+ this.count--;
271
+ return true;
272
+ }
273
+
274
+ _deleteSlot(slot, quotient) {
275
+ let current = slot;
276
+ while (true) {
277
+ const next = (current + 1) % this.capacity;
278
+ if (this._isEmpty(next) || this.quotients[next] === next) {
279
+ this._clearSlot(current);
280
+ break;
281
+ }
282
+
283
+ this._copySlot(current, next);
284
+ current = next;
285
+ }
286
+
287
+ }
288
+
289
+ metadataBitsPerSlot() {
290
+ return 2.125;
291
+ }
292
+
293
+ debugMetadata(index) {
294
+ return {
295
+ occupied: this.occupied.get(index),
296
+ shifted: this.shifted.get(index),
297
+ continuation: this.continuation.get(index),
298
+ runend: this.runends.get(index),
299
+ blockOffset: this.blockOffsets[index >>> 6],
300
+ };
301
+ }
302
+ }