@tanstack/db-ivm 0.1.20 → 0.1.22
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/dist/cjs/hashing/hash.cjs +52 -18
- package/dist/cjs/hashing/hash.cjs.map +1 -1
- package/dist/cjs/hashing/hash.d.cts +5 -0
- package/dist/cjs/operators/distinct.cjs.map +1 -1
- package/dist/cjs/operators/distinct.d.cts +1 -1
- package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs +4 -5
- package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs.map +1 -1
- package/dist/cjs/operators/topKArray.cjs +2 -0
- package/dist/cjs/operators/topKArray.cjs.map +1 -1
- package/dist/cjs/operators/topKState.cjs +30 -0
- package/dist/cjs/operators/topKState.cjs.map +1 -1
- package/dist/cjs/operators/topKState.d.cts +9 -0
- package/dist/cjs/operators/topKWithFractionalIndex.cjs +4 -5
- package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
- package/dist/esm/hashing/hash.d.ts +5 -0
- package/dist/esm/hashing/hash.js +52 -18
- package/dist/esm/hashing/hash.js.map +1 -1
- package/dist/esm/operators/distinct.d.ts +1 -1
- package/dist/esm/operators/distinct.js.map +1 -1
- package/dist/esm/operators/groupedTopKWithFractionalIndex.js +5 -6
- package/dist/esm/operators/groupedTopKWithFractionalIndex.js.map +1 -1
- package/dist/esm/operators/topKArray.js +2 -0
- package/dist/esm/operators/topKArray.js.map +1 -1
- package/dist/esm/operators/topKState.d.ts +9 -0
- package/dist/esm/operators/topKState.js +31 -1
- package/dist/esm/operators/topKState.js.map +1 -1
- package/dist/esm/operators/topKWithFractionalIndex.js +5 -6
- package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
- package/package.json +3 -1
- package/src/hashing/hash.ts +88 -22
- package/src/operators/distinct.ts +5 -3
- package/src/operators/groupedTopKWithFractionalIndex.ts +10 -6
- package/src/operators/topKArray.ts +4 -0
- package/src/operators/topKState.ts +42 -0
- package/src/operators/topKWithFractionalIndex.ts +10 -6
- package/src/operators/topKWithFractionalIndexBTree.ts +7 -8
package/src/hashing/hash.ts
CHANGED
|
@@ -13,10 +13,12 @@ const UNDEFINED = randomHash()
|
|
|
13
13
|
const KEY = randomHash()
|
|
14
14
|
const FUNCTIONS = randomHash()
|
|
15
15
|
const DATE_MARKER = randomHash()
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const STRUCTURAL_MARKERS = {
|
|
17
|
+
object: randomHash(),
|
|
18
|
+
array: randomHash(),
|
|
19
|
+
map: randomHash(),
|
|
20
|
+
set: randomHash(),
|
|
21
|
+
}
|
|
20
22
|
const UINT8ARRAY_MARKER = randomHash()
|
|
21
23
|
const TEMPORAL_MARKER = randomHash()
|
|
22
24
|
// Bound structural recursion and value visits. Shared acyclic subtrees are
|
|
@@ -51,6 +53,7 @@ function isTemporal(input: object): input is TemporalLike {
|
|
|
51
53
|
const UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128
|
|
52
54
|
|
|
53
55
|
const hashCache = new WeakMap<object, number>()
|
|
56
|
+
const referenceValues = new WeakSet<object>()
|
|
54
57
|
|
|
55
58
|
/** @internal Register a mutable handle before it enters a structural value. */
|
|
56
59
|
export function registerOpaqueHash(value: object): void {
|
|
@@ -87,24 +90,12 @@ function hashObject(input: object, context: HashContext): number {
|
|
|
87
90
|
} else if (isTemporal(input)) {
|
|
88
91
|
valueHash = hashTemporal(input)
|
|
89
92
|
} else {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (input instanceof Map) {
|
|
98
|
-
marker = MAP_MARKER
|
|
99
|
-
plainObjectInput = [...input.entries()]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (input instanceof Set) {
|
|
103
|
-
marker = SET_MARKER
|
|
104
|
-
plainObjectInput = [...input.entries()]
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
valueHash = hashPlainObject(plainObjectInput, marker, context)
|
|
93
|
+
const [kind, plainObjectInput] = structuralShape(input)
|
|
94
|
+
valueHash = hashPlainObject(
|
|
95
|
+
plainObjectInput,
|
|
96
|
+
STRUCTURAL_MARKERS[kind],
|
|
97
|
+
context,
|
|
98
|
+
)
|
|
108
99
|
}
|
|
109
100
|
} finally {
|
|
110
101
|
context.activeObjects.delete(input)
|
|
@@ -261,6 +252,80 @@ function isBinaryValue(input: object): input is Uint8Array {
|
|
|
261
252
|
)
|
|
262
253
|
}
|
|
263
254
|
|
|
255
|
+
function structuralShape(
|
|
256
|
+
input: object,
|
|
257
|
+
): [keyof typeof STRUCTURAL_MARKERS, object] {
|
|
258
|
+
if (input instanceof Map) return [`map`, [...input.entries()]]
|
|
259
|
+
if (input instanceof Set) return [`set`, [...input.values()]]
|
|
260
|
+
return [input instanceof Array ? `array` : `object`, input]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** @internal Compare immutable structural values without computing a digest.
|
|
264
|
+
* Pair memoization also permits cyclic values that structural hashing rejects.
|
|
265
|
+
* Reference-valued leaves remain opaque, including registered mutable handles.
|
|
266
|
+
*/
|
|
267
|
+
export function equalHashValues(left: unknown, right: unknown): boolean {
|
|
268
|
+
const compared = new Map<object, Set<object>>()
|
|
269
|
+
function equal(a: unknown, b: unknown): boolean {
|
|
270
|
+
if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true
|
|
271
|
+
if (
|
|
272
|
+
a === null ||
|
|
273
|
+
b === null ||
|
|
274
|
+
typeof a !== `object` ||
|
|
275
|
+
typeof b !== `object`
|
|
276
|
+
)
|
|
277
|
+
return false
|
|
278
|
+
if (
|
|
279
|
+
referenceValues.has(a) ||
|
|
280
|
+
referenceValues.has(b) ||
|
|
281
|
+
isReferenceHashedObject(a) ||
|
|
282
|
+
isReferenceHashedObject(b)
|
|
283
|
+
)
|
|
284
|
+
return false
|
|
285
|
+
if (a instanceof Date || b instanceof Date)
|
|
286
|
+
return (
|
|
287
|
+
a instanceof Date &&
|
|
288
|
+
b instanceof Date &&
|
|
289
|
+
equal(a.getTime(), b.getTime())
|
|
290
|
+
)
|
|
291
|
+
if (isBinaryValue(a) || isBinaryValue(b))
|
|
292
|
+
return (
|
|
293
|
+
isBinaryValue(a) &&
|
|
294
|
+
isBinaryValue(b) &&
|
|
295
|
+
a.byteLength === b.byteLength &&
|
|
296
|
+
a.every((value, index) => value === b[index])
|
|
297
|
+
)
|
|
298
|
+
if (isTemporal(a) || isTemporal(b))
|
|
299
|
+
return (
|
|
300
|
+
isTemporal(a) &&
|
|
301
|
+
isTemporal(b) &&
|
|
302
|
+
a[Symbol.toStringTag] === b[Symbol.toStringTag] &&
|
|
303
|
+
a.toString() === b.toString()
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
// Revisited pairs close cycles and avoid expanding shared subtrees.
|
|
307
|
+
const peers = compared.get(a)
|
|
308
|
+
if (peers?.has(b)) return true
|
|
309
|
+
if (peers) peers.add(b)
|
|
310
|
+
else compared.set(a, new Set([b]))
|
|
311
|
+
const [aKind, aShape] = structuralShape(a)
|
|
312
|
+
const [bKind, bShape] = structuralShape(b)
|
|
313
|
+
if (aKind !== bKind) return false
|
|
314
|
+
const keys = (value: object) =>
|
|
315
|
+
Reflect.ownKeys(value).filter((key) =>
|
|
316
|
+
Object.prototype.propertyIsEnumerable.call(value, key),
|
|
317
|
+
)
|
|
318
|
+
const aKeys = keys(aShape)
|
|
319
|
+
if (aKeys.length !== keys(bShape).length) return false
|
|
320
|
+
return aKeys.every(
|
|
321
|
+
(key) =>
|
|
322
|
+
Object.prototype.propertyIsEnumerable.call(bShape, key) &&
|
|
323
|
+
equal(aShape[key as keyof object], bShape[key as keyof object]),
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
return equal(left, right)
|
|
327
|
+
}
|
|
328
|
+
|
|
264
329
|
let nextRefId = 1
|
|
265
330
|
function cachedReferenceHash(fn: object): number {
|
|
266
331
|
let valueHash = hashCache.get(fn)
|
|
@@ -268,6 +333,7 @@ function cachedReferenceHash(fn: object): number {
|
|
|
268
333
|
valueHash = nextRefId ^ FUNCTIONS
|
|
269
334
|
nextRefId++
|
|
270
335
|
hashCache.set(fn, valueHash)
|
|
336
|
+
referenceValues.add(fn)
|
|
271
337
|
}
|
|
272
338
|
return valueHash
|
|
273
339
|
}
|
|
@@ -85,10 +85,12 @@ export class DistinctOperator<
|
|
|
85
85
|
export function distinct<T extends KeyValue<any, any>>(
|
|
86
86
|
by: (value: T) => any = (value: T) => value,
|
|
87
87
|
) {
|
|
88
|
-
return (
|
|
89
|
-
|
|
88
|
+
return (
|
|
89
|
+
stream: IStreamBuilder<T>,
|
|
90
|
+
): IStreamBuilder<KeyValue<number, GetValue<T>>> => {
|
|
91
|
+
const output = new StreamBuilder<KeyValue<number, GetValue<T>>>(
|
|
90
92
|
stream.graph,
|
|
91
|
-
new DifferenceStreamWriter<T
|
|
93
|
+
new DifferenceStreamWriter<KeyValue<number, GetValue<T>>>(),
|
|
92
94
|
)
|
|
93
95
|
const operator = new DistinctOperator<T>(
|
|
94
96
|
stream.graph.getNextOperatorId(),
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'
|
|
2
2
|
import { StreamBuilder } from '../d2.js'
|
|
3
3
|
import { MultiSet } from '../multiset.js'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
TopKState,
|
|
6
|
+
handleMoveIn,
|
|
7
|
+
handleMoveOut,
|
|
8
|
+
topKBatch,
|
|
9
|
+
} from './topKState.js'
|
|
5
10
|
import { TopKArray, createKeyedComparator } from './topKArray.js'
|
|
6
11
|
import type { IndexedValue, TopK } from './topKArray.js'
|
|
7
12
|
import type { DifferenceStreamReader } from '../graph.js'
|
|
@@ -125,11 +130,10 @@ export class GroupedTopKWithFractionalIndexOperator<
|
|
|
125
130
|
|
|
126
131
|
run(): void {
|
|
127
132
|
const result: Array<[[K, IndexedValue<T>], number]> = []
|
|
128
|
-
for (const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
+
for (const [[key, value], multiplicity] of topKBatch(
|
|
134
|
+
this.inputMessages(),
|
|
135
|
+
)) {
|
|
136
|
+
this.#processElement(key, value, multiplicity, result)
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
if (result.length > 0) {
|
|
@@ -170,6 +170,8 @@ export class TopKArray<V> implements TopK<V> {
|
|
|
170
170
|
// Splice is O(n) where n = all elements in the collection (i.e. n >= k) !
|
|
171
171
|
this.#sortedValues.splice(index, 0, val)
|
|
172
172
|
|
|
173
|
+
if (this.#topKStart === this.#topKEnd) return result
|
|
174
|
+
|
|
173
175
|
// Check if the topK changed
|
|
174
176
|
if (index < this.#topKEnd) {
|
|
175
177
|
// The inserted element is either before the top K or within the top K
|
|
@@ -209,6 +211,8 @@ export class TopKArray<V> implements TopK<V> {
|
|
|
209
211
|
// Remove the value at that position
|
|
210
212
|
const [removedElem] = this.#sortedValues.splice(index, 1)
|
|
211
213
|
|
|
214
|
+
if (this.#topKStart === this.#topKEnd) return result
|
|
215
|
+
|
|
212
216
|
// Check if the topK changed
|
|
213
217
|
if (index < this.#topKEnd) {
|
|
214
218
|
// The removed element is either before the top K or within the top K
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { equalHashValues } from '../hashing/hash.js'
|
|
1
2
|
import { TopKArray } from './topKArray.js'
|
|
3
|
+
import type { MultiSet } from '../multiset.js'
|
|
2
4
|
import type {
|
|
3
5
|
IndexedValue,
|
|
4
6
|
TopK,
|
|
@@ -6,6 +8,46 @@ import type {
|
|
|
6
8
|
TopKMoveChanges,
|
|
7
9
|
} from './topKArray.js'
|
|
8
10
|
|
|
11
|
+
/** Apply a keyed batch's retractions before additions. An outer join can emit
|
|
12
|
+
* a replacement in the opposite order; key multiplicity alone would hide it.
|
|
13
|
+
* Consolidate first so a transient value added and removed in this same turn
|
|
14
|
+
* cannot be mistaken for the final replacement. Use structural relation
|
|
15
|
+
* identity: map can allocate a fresh value for each side of a cancelling pair.
|
|
16
|
+
* No state survives the run.
|
|
17
|
+
*/
|
|
18
|
+
export function* topKBatch<K, T>(messages: Array<MultiSet<[K, T]>>) {
|
|
19
|
+
const keyed = new Map<K, Array<[[K, T], number]>>()
|
|
20
|
+
for (const message of messages) {
|
|
21
|
+
for (const [value, weight] of message.getInner()) {
|
|
22
|
+
const entries = keyed.get(value[0])
|
|
23
|
+
if (entries) entries.push([value, weight])
|
|
24
|
+
else keyed.set(value[0], [[value, weight]])
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const batch: Array<[[K, T], number]> = []
|
|
28
|
+
for (const entries of keyed.values()) {
|
|
29
|
+
// Ordering distinct keys needs only the caller's comparator. In particular,
|
|
30
|
+
// do not traverse irrelevant payloads or merge keys on a hash collision.
|
|
31
|
+
if (entries.length === 1) {
|
|
32
|
+
batch.push(entries[0]!)
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
// A per-key scan keeps normal two-version replacements cheap and permits
|
|
36
|
+
// cycles, at the cost of quadratic comparisons for long same-key histories.
|
|
37
|
+
const consolidated: Array<[[K, T], number]> = []
|
|
38
|
+
for (const [value, weight] of entries) {
|
|
39
|
+
const previous = consolidated.find(([candidate]) =>
|
|
40
|
+
equalHashValues(candidate[1], value[1]),
|
|
41
|
+
)
|
|
42
|
+
if (previous) previous[1] += weight
|
|
43
|
+
else consolidated.push([value, weight])
|
|
44
|
+
}
|
|
45
|
+
batch.push(...consolidated)
|
|
46
|
+
}
|
|
47
|
+
for (const entry of batch) if (entry[1] < 0) yield entry
|
|
48
|
+
for (const entry of batch) if (entry[1] > 0) yield entry
|
|
49
|
+
}
|
|
50
|
+
|
|
9
51
|
/**
|
|
10
52
|
* Helper class that manages the state for a single topK window.
|
|
11
53
|
* Encapsulates the multiplicity tracking and topK data structure,
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'
|
|
2
2
|
import { StreamBuilder } from '../d2.js'
|
|
3
3
|
import { MultiSet } from '../multiset.js'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
TopKState,
|
|
6
|
+
handleMoveIn,
|
|
7
|
+
handleMoveOut,
|
|
8
|
+
topKBatch,
|
|
9
|
+
} from './topKState.js'
|
|
5
10
|
import { TopKArray, createKeyedComparator } from './topKArray.js'
|
|
6
11
|
import type { IndexedValue, TopK } from './topKArray.js'
|
|
7
12
|
import type { DifferenceStreamReader } from '../graph.js'
|
|
@@ -76,11 +81,10 @@ export class TopKWithFractionalIndexOperator<
|
|
|
76
81
|
|
|
77
82
|
run(): void {
|
|
78
83
|
const result: Array<[[K, IndexedValue<T>], number]> = []
|
|
79
|
-
for (const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
+
for (const [[key, value], multiplicity] of topKBatch(
|
|
85
|
+
this.inputMessages(),
|
|
86
|
+
)) {
|
|
87
|
+
this.processElement(key, value, multiplicity, result)
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
if (result.length > 0) {
|
|
@@ -106,9 +106,11 @@ class TopKTree<V> implements TopK<V> {
|
|
|
106
106
|
return result
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
if (
|
|
110
|
+
this.#topKStart === this.#topKEnd ||
|
|
111
|
+
this.#tree.size - 1 < this.#topKStart
|
|
112
|
+
) {
|
|
113
|
+
// The window is empty, or there aren't enough rows to reach its offset.
|
|
112
114
|
return result
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -178,11 +180,8 @@ class TopKTree<V> implements TopK<V> {
|
|
|
178
180
|
return result
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
if (this.#comparator(value, getValue(this.#topKFirstElem))
|
|
182
|
-
//
|
|
183
|
-
// so the topK has shifted one position to the left
|
|
184
|
-
|
|
185
|
-
// the old first element moves out of the topK
|
|
183
|
+
if (this.#comparator(value, getValue(this.#topKFirstElem)) <= 0) {
|
|
184
|
+
// Deleting at or before the first selected value advances that boundary.
|
|
186
185
|
result.moveOut = this.#topKFirstElem
|
|
187
186
|
// the element that was right after the first element of the topK
|
|
188
187
|
// is now the new first element of the topK
|