@tanstack/db-ivm 0.1.20 → 0.1.24
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/d2.cjs +12 -2
- package/dist/cjs/d2.cjs.map +1 -1
- package/dist/cjs/hashing/hash.cjs +92 -104
- package/dist/cjs/hashing/hash.cjs.map +1 -1
- package/dist/cjs/hashing/hash.d.cts +5 -0
- package/dist/cjs/hashing/temporal.cjs +18 -0
- package/dist/cjs/hashing/temporal.cjs.map +1 -0
- package/dist/cjs/hashing/temporal.d.cts +5 -0
- package/dist/cjs/multiset.cjs +47 -92
- package/dist/cjs/multiset.cjs.map +1 -1
- package/dist/cjs/multiset.d.cts +16 -0
- package/dist/cjs/operators/distinct.cjs.map +1 -1
- package/dist/cjs/operators/distinct.d.cts +1 -1
- package/dist/cjs/operators/groupBy.cjs +2 -2
- package/dist/cjs/operators/groupBy.cjs.map +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/cjs/utils.cjs +15 -61
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +3 -32
- package/dist/esm/d2.js +12 -2
- package/dist/esm/d2.js.map +1 -1
- package/dist/esm/hashing/hash.d.ts +5 -0
- package/dist/esm/hashing/hash.js +92 -104
- package/dist/esm/hashing/hash.js.map +1 -1
- package/dist/esm/hashing/temporal.d.ts +5 -0
- package/dist/esm/hashing/temporal.js +18 -0
- package/dist/esm/hashing/temporal.js.map +1 -0
- package/dist/esm/multiset.d.ts +16 -0
- package/dist/esm/multiset.js +48 -93
- package/dist/esm/multiset.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/groupBy.js +2 -2
- package/dist/esm/operators/groupBy.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/dist/esm/utils.d.ts +3 -32
- package/dist/esm/utils.js +15 -61
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -1
- package/src/d2.ts +15 -2
- package/src/hashing/hash.ts +147 -135
- package/src/hashing/temporal.ts +25 -0
- package/src/multiset.ts +73 -132
- package/src/operators/distinct.ts +5 -3
- package/src/operators/groupBy.ts +8 -2
- 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/utils.ts +20 -76
|
@@ -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
|
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isTemporal } from './hashing/temporal.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Simple assertion function for runtime checks.
|
|
3
5
|
* Throws an error if the condition is false.
|
|
@@ -11,36 +13,6 @@ export function assert(
|
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
/**
|
|
15
|
-
* A map that returns a default value for keys that are not present.
|
|
16
|
-
*/
|
|
17
|
-
export class DefaultMap<K, V> extends Map<K, V> {
|
|
18
|
-
constructor(
|
|
19
|
-
private defaultValue: () => V,
|
|
20
|
-
entries?: Iterable<[K, V]>,
|
|
21
|
-
) {
|
|
22
|
-
super(entries)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
get(key: K): V {
|
|
26
|
-
if (!this.has(key)) {
|
|
27
|
-
// this.set(key, this.defaultValue())
|
|
28
|
-
return this.defaultValue()
|
|
29
|
-
}
|
|
30
|
-
return super.get(key)!
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Update the value for a key using a function.
|
|
35
|
-
*/
|
|
36
|
-
update(key: K, updater: (value: V) => V): V {
|
|
37
|
-
const value = this.get(key)
|
|
38
|
-
const newValue = updater(value)
|
|
39
|
-
this.set(key, newValue)
|
|
40
|
-
return newValue
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
16
|
// JS engines have various limits on how many args can be passed to a function
|
|
45
17
|
// with a spread operator, so we need to split the operation into chunks
|
|
46
18
|
// 32767 is the max for Chrome 14, all others are higher
|
|
@@ -78,55 +50,21 @@ export function binarySearch<T>(
|
|
|
78
50
|
return low
|
|
79
51
|
}
|
|
80
52
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* Uses WeakMap for object reference tracking and consistent hashing for primitives.
|
|
84
|
-
*/
|
|
85
|
-
export class ObjectIdGenerator {
|
|
86
|
-
private objectIds = new WeakMap<object, number>()
|
|
87
|
-
private nextId = 0
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get a unique identifier for any value.
|
|
91
|
-
* - Objects: Uses WeakMap for reference-based identity
|
|
92
|
-
* - Primitives: Uses consistent string-based hashing
|
|
93
|
-
*/
|
|
94
|
-
getId(value: any): number {
|
|
95
|
-
// For primitives, use a simple hash of their string representation
|
|
96
|
-
if (typeof value !== `object` || value === null) {
|
|
97
|
-
const str = String(value)
|
|
98
|
-
let hashValue = 0
|
|
99
|
-
for (let i = 0; i < str.length; i++) {
|
|
100
|
-
const char = str.charCodeAt(i)
|
|
101
|
-
hashValue = (hashValue << 5) - hashValue + char
|
|
102
|
-
hashValue = hashValue & hashValue // Convert to 32-bit integer
|
|
103
|
-
}
|
|
104
|
-
return hashValue
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// For objects, use WeakMap to assign unique IDs
|
|
108
|
-
if (!this.objectIds.has(value)) {
|
|
109
|
-
this.objectIds.set(value, this.nextId++)
|
|
110
|
-
}
|
|
111
|
-
return this.objectIds.get(value)!
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get a string representation of the ID for use in composite keys.
|
|
116
|
-
*/
|
|
117
|
-
getStringId(value: any): string {
|
|
118
|
-
if (value === null) return `null`
|
|
119
|
-
if (value === undefined) return `undefined`
|
|
120
|
-
if (typeof value !== `object`) return `str_${String(value)}`
|
|
121
|
-
|
|
122
|
-
return `obj_${this.getId(value)}`
|
|
123
|
-
}
|
|
124
|
-
}
|
|
53
|
+
const objectIds = new WeakMap<object, number>()
|
|
54
|
+
let nextObjectId = 0
|
|
125
55
|
|
|
126
56
|
/**
|
|
127
|
-
*
|
|
57
|
+
* A string identity for composite keys: primitives by value, objects by
|
|
58
|
+
* reference.
|
|
128
59
|
*/
|
|
129
|
-
export
|
|
60
|
+
export function getStringId(value: unknown): string {
|
|
61
|
+
if (value === null) return `null`
|
|
62
|
+
if (value === undefined) return `undefined`
|
|
63
|
+
if (typeof value !== `object`) return `str_${String(value)}`
|
|
64
|
+
let id = objectIds.get(value)
|
|
65
|
+
if (id === undefined) objectIds.set(value, (id = nextObjectId++))
|
|
66
|
+
return `obj_${id}`
|
|
67
|
+
}
|
|
130
68
|
|
|
131
69
|
export function* concatIterable<T>(
|
|
132
70
|
...iterables: Array<Iterable<T>>
|
|
@@ -209,6 +147,7 @@ type CanonicalValue =
|
|
|
209
147
|
| readonly [`bigint`, string]
|
|
210
148
|
| readonly [`string`, string]
|
|
211
149
|
| readonly [`date`, number | `Invalid`]
|
|
150
|
+
| readonly [`temporal`, string, string]
|
|
212
151
|
| readonly [`regexp`, string, string]
|
|
213
152
|
| readonly [`bytes`, Array<number>]
|
|
214
153
|
| readonly [`array`, Array<CanonicalValue>]
|
|
@@ -265,6 +204,7 @@ function isJsonSafeStructuralValue(
|
|
|
265
204
|
value instanceof Date ||
|
|
266
205
|
value instanceof RegExp ||
|
|
267
206
|
value instanceof Uint8Array ||
|
|
207
|
+
isTemporal(value) ||
|
|
268
208
|
value instanceof Map ||
|
|
269
209
|
value instanceof Set
|
|
270
210
|
) {
|
|
@@ -348,6 +288,10 @@ function toCanonicalValue(
|
|
|
348
288
|
return [`bytes`, Array.from(value)]
|
|
349
289
|
}
|
|
350
290
|
|
|
291
|
+
if (isTemporal(value)) {
|
|
292
|
+
return [`temporal`, value[Symbol.toStringTag], value.toString()]
|
|
293
|
+
}
|
|
294
|
+
|
|
351
295
|
if (Array.isArray(value)) {
|
|
352
296
|
return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]
|
|
353
297
|
}
|