@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.
Files changed (67) hide show
  1. package/dist/cjs/d2.cjs +12 -2
  2. package/dist/cjs/d2.cjs.map +1 -1
  3. package/dist/cjs/hashing/hash.cjs +92 -104
  4. package/dist/cjs/hashing/hash.cjs.map +1 -1
  5. package/dist/cjs/hashing/hash.d.cts +5 -0
  6. package/dist/cjs/hashing/temporal.cjs +18 -0
  7. package/dist/cjs/hashing/temporal.cjs.map +1 -0
  8. package/dist/cjs/hashing/temporal.d.cts +5 -0
  9. package/dist/cjs/multiset.cjs +47 -92
  10. package/dist/cjs/multiset.cjs.map +1 -1
  11. package/dist/cjs/multiset.d.cts +16 -0
  12. package/dist/cjs/operators/distinct.cjs.map +1 -1
  13. package/dist/cjs/operators/distinct.d.cts +1 -1
  14. package/dist/cjs/operators/groupBy.cjs +2 -2
  15. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  16. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs +4 -5
  17. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs.map +1 -1
  18. package/dist/cjs/operators/topKArray.cjs +2 -0
  19. package/dist/cjs/operators/topKArray.cjs.map +1 -1
  20. package/dist/cjs/operators/topKState.cjs +30 -0
  21. package/dist/cjs/operators/topKState.cjs.map +1 -1
  22. package/dist/cjs/operators/topKState.d.cts +9 -0
  23. package/dist/cjs/operators/topKWithFractionalIndex.cjs +4 -5
  24. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  25. package/dist/cjs/utils.cjs +15 -61
  26. package/dist/cjs/utils.cjs.map +1 -1
  27. package/dist/cjs/utils.d.cts +3 -32
  28. package/dist/esm/d2.js +12 -2
  29. package/dist/esm/d2.js.map +1 -1
  30. package/dist/esm/hashing/hash.d.ts +5 -0
  31. package/dist/esm/hashing/hash.js +92 -104
  32. package/dist/esm/hashing/hash.js.map +1 -1
  33. package/dist/esm/hashing/temporal.d.ts +5 -0
  34. package/dist/esm/hashing/temporal.js +18 -0
  35. package/dist/esm/hashing/temporal.js.map +1 -0
  36. package/dist/esm/multiset.d.ts +16 -0
  37. package/dist/esm/multiset.js +48 -93
  38. package/dist/esm/multiset.js.map +1 -1
  39. package/dist/esm/operators/distinct.d.ts +1 -1
  40. package/dist/esm/operators/distinct.js.map +1 -1
  41. package/dist/esm/operators/groupBy.js +2 -2
  42. package/dist/esm/operators/groupBy.js.map +1 -1
  43. package/dist/esm/operators/groupedTopKWithFractionalIndex.js +5 -6
  44. package/dist/esm/operators/groupedTopKWithFractionalIndex.js.map +1 -1
  45. package/dist/esm/operators/topKArray.js +2 -0
  46. package/dist/esm/operators/topKArray.js.map +1 -1
  47. package/dist/esm/operators/topKState.d.ts +9 -0
  48. package/dist/esm/operators/topKState.js +31 -1
  49. package/dist/esm/operators/topKState.js.map +1 -1
  50. package/dist/esm/operators/topKWithFractionalIndex.js +5 -6
  51. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  52. package/dist/esm/utils.d.ts +3 -32
  53. package/dist/esm/utils.js +15 -61
  54. package/dist/esm/utils.js.map +1 -1
  55. package/package.json +3 -1
  56. package/src/d2.ts +15 -2
  57. package/src/hashing/hash.ts +147 -135
  58. package/src/hashing/temporal.ts +25 -0
  59. package/src/multiset.ts +73 -132
  60. package/src/operators/distinct.ts +5 -3
  61. package/src/operators/groupBy.ts +8 -2
  62. package/src/operators/groupedTopKWithFractionalIndex.ts +10 -6
  63. package/src/operators/topKArray.ts +4 -0
  64. package/src/operators/topKState.ts +42 -0
  65. package/src/operators/topKWithFractionalIndex.ts +10 -6
  66. package/src/operators/topKWithFractionalIndexBTree.ts +7 -8
  67. 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 { TopKState, handleMoveIn, handleMoveOut } from './topKState.js'
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 message of this.inputMessages()) {
129
- for (const [item, multiplicity] of message.getInner()) {
130
- const [key, value] = item
131
- this.#processElement(key, value, multiplicity, result)
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 { TopKState, handleMoveIn, handleMoveOut } from './topKState.js'
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 message of this.inputMessages()) {
80
- for (const [item, multiplicity] of message.getInner()) {
81
- const [key, value] = item
82
- this.processElement(key, value, multiplicity, result)
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 (this.#tree.size - 1 < this.#topKStart) {
110
- // We don't have a topK yet
111
- // so we don't need to do anything
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)) < 0) {
182
- // We deleted an element that was before the topK
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
- * Utility for generating unique IDs for objects and values.
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
- * Global instance for cases where a shared object ID space is needed.
57
+ * A string identity for composite keys: primitives by value, objects by
58
+ * reference.
128
59
  */
129
- export const globalObjectIdGenerator = new ObjectIdGenerator()
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
  }