@tanstack/db-ivm 0.1.14 → 0.1.16

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 (65) hide show
  1. package/dist/cjs/index.cjs +6 -3
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +1 -1
  4. package/dist/cjs/indexes.cjs +1 -1
  5. package/dist/cjs/indexes.cjs.map +1 -1
  6. package/dist/cjs/operators/groupBy.cjs +2 -1
  7. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  8. package/dist/cjs/operators/groupedOrderBy.cjs +33 -0
  9. package/dist/cjs/operators/groupedOrderBy.cjs.map +1 -0
  10. package/dist/cjs/operators/groupedOrderBy.d.cts +31 -0
  11. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs +116 -0
  12. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs.map +1 -0
  13. package/dist/cjs/operators/groupedTopKWithFractionalIndex.d.cts +50 -0
  14. package/dist/cjs/operators/index.d.cts +2 -0
  15. package/dist/cjs/operators/topKArray.cjs +137 -0
  16. package/dist/cjs/operators/topKArray.cjs.map +1 -0
  17. package/dist/cjs/operators/topKArray.d.cts +59 -0
  18. package/dist/cjs/operators/topKState.cjs +73 -0
  19. package/dist/cjs/operators/topKState.cjs.map +1 -0
  20. package/dist/cjs/operators/topKState.d.cts +35 -0
  21. package/dist/cjs/operators/topKWithFractionalIndex.cjs +14 -190
  22. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  23. package/dist/cjs/operators/topKWithFractionalIndex.d.cts +1 -32
  24. package/dist/cjs/operators/topKWithFractionalIndexBTree.d.cts +2 -1
  25. package/dist/cjs/utils.cjs +12 -0
  26. package/dist/cjs/utils.cjs.map +1 -1
  27. package/dist/cjs/utils.d.cts +6 -0
  28. package/dist/esm/index.d.ts +1 -1
  29. package/dist/esm/index.js +8 -5
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/indexes.js +1 -1
  32. package/dist/esm/indexes.js.map +1 -1
  33. package/dist/esm/operators/groupBy.js +2 -1
  34. package/dist/esm/operators/groupBy.js.map +1 -1
  35. package/dist/esm/operators/groupedOrderBy.d.ts +31 -0
  36. package/dist/esm/operators/groupedOrderBy.js +33 -0
  37. package/dist/esm/operators/groupedOrderBy.js.map +1 -0
  38. package/dist/esm/operators/groupedTopKWithFractionalIndex.d.ts +50 -0
  39. package/dist/esm/operators/groupedTopKWithFractionalIndex.js +116 -0
  40. package/dist/esm/operators/groupedTopKWithFractionalIndex.js.map +1 -0
  41. package/dist/esm/operators/index.d.ts +2 -0
  42. package/dist/esm/operators/topKArray.d.ts +59 -0
  43. package/dist/esm/operators/topKArray.js +137 -0
  44. package/dist/esm/operators/topKArray.js.map +1 -0
  45. package/dist/esm/operators/topKState.d.ts +35 -0
  46. package/dist/esm/operators/topKState.js +73 -0
  47. package/dist/esm/operators/topKState.js.map +1 -0
  48. package/dist/esm/operators/topKWithFractionalIndex.d.ts +1 -32
  49. package/dist/esm/operators/topKWithFractionalIndex.js +12 -188
  50. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  51. package/dist/esm/operators/topKWithFractionalIndexBTree.d.ts +2 -1
  52. package/dist/esm/utils.d.ts +6 -0
  53. package/dist/esm/utils.js +13 -1
  54. package/dist/esm/utils.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/index.ts +1 -1
  57. package/src/operators/groupBy.ts +2 -1
  58. package/src/operators/groupedOrderBy.ts +95 -0
  59. package/src/operators/groupedTopKWithFractionalIndex.ts +192 -0
  60. package/src/operators/index.ts +2 -0
  61. package/src/operators/topKArray.ts +255 -0
  62. package/src/operators/topKState.ts +111 -0
  63. package/src/operators/topKWithFractionalIndex.ts +13 -337
  64. package/src/operators/topKWithFractionalIndexBTree.ts +4 -12
  65. package/src/utils.ts +17 -0
@@ -1,9 +1,9 @@
1
- import { generateKeyBetween } from 'fractional-indexing'
2
1
  import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'
3
2
  import { StreamBuilder } from '../d2.js'
4
3
  import { MultiSet } from '../multiset.js'
5
- import { binarySearch, compareKeys, diffHalfOpen } from '../utils.js'
6
- import type { HRange } from '../utils.js'
4
+ import { TopKState, handleMoveIn, handleMoveOut } from './topKState.js'
5
+ import { TopKArray, createKeyedComparator } from './topKArray.js'
6
+ import type { IndexedValue, TopK } from './topKArray.js'
7
7
  import type { DifferenceStreamReader } from '../graph.js'
8
8
  import type { IStreamBuilder, PipedOperator } from '../types.js'
9
9
 
@@ -16,220 +16,6 @@ export interface TopKWithFractionalIndexOptions {
16
16
  ) => void
17
17
  }
18
18
 
19
- export type TopKChanges<V> = {
20
- /** Indicates which element moves into the topK (if any) */
21
- moveIn: IndexedValue<V> | null
22
- /** Indicates which element moves out of the topK (if any) */
23
- moveOut: IndexedValue<V> | null
24
- }
25
-
26
- export type TopKMoveChanges<V> = {
27
- /** Flag that marks whether there were any changes to the topK */
28
- changes: boolean
29
- /** Indicates which elements move into the topK (if any) */
30
- moveIns: Array<IndexedValue<V>>
31
- /** Indicates which elements move out of the topK (if any) */
32
- moveOuts: Array<IndexedValue<V>>
33
- }
34
-
35
- /**
36
- * A topK data structure that supports insertions and deletions
37
- * and returns changes to the topK.
38
- */
39
- export interface TopK<V> {
40
- size: number
41
- insert: (value: V) => TopKChanges<V>
42
- delete: (value: V) => TopKChanges<V>
43
- }
44
-
45
- /**
46
- * Implementation of a topK data structure.
47
- * Uses a sorted array internally to store the values and keeps a topK window over that array.
48
- * Inserts and deletes are O(n) operations because worst case an element is inserted/deleted
49
- * at the start of the array which causes all the elements to shift to the right/left.
50
- */
51
- class TopKArray<V> implements TopK<V> {
52
- #sortedValues: Array<IndexedValue<V>> = []
53
- #comparator: (a: V, b: V) => number
54
- #topKStart: number
55
- #topKEnd: number
56
-
57
- constructor(
58
- offset: number,
59
- limit: number,
60
- comparator: (a: V, b: V) => number,
61
- ) {
62
- this.#topKStart = offset
63
- this.#topKEnd = offset + limit
64
- this.#comparator = comparator
65
- }
66
-
67
- get size(): number {
68
- const offset = this.#topKStart
69
- const limit = this.#topKEnd - this.#topKStart
70
- const available = this.#sortedValues.length - offset
71
- return Math.max(0, Math.min(limit, available))
72
- }
73
-
74
- /**
75
- * Moves the topK window
76
- */
77
- move({
78
- offset,
79
- limit,
80
- }: {
81
- offset?: number
82
- limit?: number
83
- }): TopKMoveChanges<V> {
84
- const oldOffset = this.#topKStart
85
- const oldLimit = this.#topKEnd - this.#topKStart
86
-
87
- // `this.#topKEnd` can be `Infinity` if it has no limit
88
- // but `diffHalfOpen` expects a finite range
89
- // so we restrict it to the size of the topK if topKEnd is infinite
90
- const oldRange: HRange = [
91
- this.#topKStart,
92
- this.#topKEnd === Infinity ? this.#topKStart + this.size : this.#topKEnd,
93
- ]
94
-
95
- this.#topKStart = offset ?? oldOffset
96
- this.#topKEnd = this.#topKStart + (limit ?? oldLimit) // can be `Infinity` if limit is `Infinity`
97
-
98
- // Also handle `Infinity` in the newRange
99
- const newRange: HRange = [
100
- this.#topKStart,
101
- this.#topKEnd === Infinity
102
- ? Math.max(this.#topKStart + this.size, oldRange[1]) // since the new limit is Infinity we need to take everything (so we need to take the biggest (finite) topKEnd)
103
- : this.#topKEnd,
104
- ]
105
- const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange)
106
-
107
- const moveIns: Array<IndexedValue<V>> = []
108
- onlyInB.forEach((index) => {
109
- const value = this.#sortedValues[index]
110
- if (value) {
111
- moveIns.push(value)
112
- }
113
- })
114
-
115
- const moveOuts: Array<IndexedValue<V>> = []
116
- onlyInA.forEach((index) => {
117
- const value = this.#sortedValues[index]
118
- if (value) {
119
- moveOuts.push(value)
120
- }
121
- })
122
-
123
- // It could be that there are changes (i.e. moveIns or moveOuts)
124
- // but that the collection is lazy so we don't have the data yet that needs to move in/out
125
- // so `moveIns` and `moveOuts` will be empty but `changes` will be true
126
- // this will tell the caller that it needs to run the graph to load more data
127
- return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 }
128
- }
129
-
130
- insert(value: V): TopKChanges<V> {
131
- const result: TopKChanges<V> = { moveIn: null, moveOut: null }
132
-
133
- // Lookup insert position
134
- const index = this.#findIndex(value)
135
- // Generate fractional index based on the fractional indices of the elements before and after it
136
- const indexBefore =
137
- index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)
138
- const indexAfter =
139
- index === this.#sortedValues.length
140
- ? null
141
- : getIndex(this.#sortedValues[index]!)
142
- const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)
143
-
144
- // Insert the value at the correct position
145
- const val = indexedValue(value, fractionalIndex)
146
- // Splice is O(n) where n = all elements in the collection (i.e. n >= k) !
147
- this.#sortedValues.splice(index, 0, val)
148
-
149
- // Check if the topK changed
150
- if (index < this.#topKEnd) {
151
- // The inserted element is either before the top K or within the top K
152
- // If it is before the top K then it moves the element that was right before the topK into the topK
153
- // If it is within the top K then the inserted element moves into the top K
154
- // In both cases the last element of the old top K now moves out of the top K
155
- const moveInIndex = Math.max(index, this.#topKStart)
156
- if (moveInIndex < this.#sortedValues.length) {
157
- // We actually have a topK
158
- // because in some cases there may not be enough elements in the array to reach the start of the topK
159
- // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK
160
- result.moveIn = this.#sortedValues[moveInIndex]!
161
-
162
- // We need to remove the element that falls out of the top K
163
- // The element that falls out of the top K has shifted one to the right
164
- // because of the element we inserted, so we find it at index topKEnd
165
- if (this.#topKEnd < this.#sortedValues.length) {
166
- result.moveOut = this.#sortedValues[this.#topKEnd]!
167
- }
168
- }
169
- }
170
-
171
- return result
172
- }
173
-
174
- /**
175
- * Deletes a value that may or may not be in the topK.
176
- * IMPORTANT: this assumes that the value is present in the collection
177
- * if it's not the case it will remove the element
178
- * that is on the position where the provided `value` would be.
179
- */
180
- delete(value: V): TopKChanges<V> {
181
- const result: TopKChanges<V> = { moveIn: null, moveOut: null }
182
-
183
- // Lookup delete position
184
- const index = this.#findIndex(value)
185
- // Remove the value at that position
186
- const [removedElem] = this.#sortedValues.splice(index, 1)
187
-
188
- // Check if the topK changed
189
- if (index < this.#topKEnd) {
190
- // The removed element is either before the top K or within the top K
191
- // If it is before the top K then the first element of the topK moves out of the topK
192
- // If it is within the top K then the removed element moves out of the topK
193
- result.moveOut = removedElem!
194
- if (index < this.#topKStart) {
195
- // The removed element is before the topK
196
- // so actually, the first element of the topK moves out of the topK
197
- // and not the element that we removed
198
- // The first element of the topK is now at index topKStart - 1
199
- // since we removed an element before the topK
200
- const moveOutIndex = this.#topKStart - 1
201
- if (moveOutIndex < this.#sortedValues.length) {
202
- result.moveOut = this.#sortedValues[moveOutIndex]!
203
- } else {
204
- // No value is moving out of the topK
205
- // because there are no elements in the topK
206
- result.moveOut = null
207
- }
208
- }
209
-
210
- // Since we removed an element that was before or in the topK
211
- // the first element after the topK moved one position to the left
212
- // and thus falls into the topK now
213
- const moveInIndex = this.#topKEnd - 1
214
- if (moveInIndex < this.#sortedValues.length) {
215
- result.moveIn = this.#sortedValues[moveInIndex]!
216
- }
217
- }
218
-
219
- return result
220
- }
221
-
222
- // TODO: see if there is a way to refactor the code for insert and delete in the topK above
223
- // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right
224
- // so i have the feeling there is a common pattern here and we can implement both cases using that pattern
225
-
226
- #findIndex(value: V): number {
227
- return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>
228
- this.#comparator(getValue(a), getValue(b)),
229
- )
230
- }
231
- }
232
-
233
19
  /**
234
20
  * Operator for fractional indexed topK operations
235
21
  * This operator maintains fractional indices for sorted elements
@@ -239,14 +25,7 @@ export class TopKWithFractionalIndexOperator<
239
25
  K extends string | number,
240
26
  T,
241
27
  > extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {
242
- #index: Map<K, number> = new Map() // maps keys to their multiplicity
243
-
244
- /**
245
- * topK data structure that supports insertions and deletions
246
- * and returns changes to the topK.
247
- * Elements are stored as [key, value] tuples for stable tie-breaking.
248
- */
249
- #topK: TopK<[K, T]>
28
+ #state: TopKState<K, T>
250
29
 
251
30
  constructor(
252
31
  id: number,
@@ -258,12 +37,13 @@ export class TopKWithFractionalIndexOperator<
258
37
  super(id, inputA, output)
259
38
  const limit = options.limit ?? Infinity
260
39
  const offset = options.offset ?? 0
261
- this.#topK = this.createTopK(
40
+ const topK = this.createTopK(
262
41
  offset,
263
42
  limit,
264
43
  createKeyedComparator(comparator),
265
44
  )
266
- options.setSizeCallback?.(() => this.#topK.size)
45
+ this.#state = new TopKState(topK)
46
+ options.setSizeCallback?.(() => this.#state.size)
267
47
  options.setWindowFn?.(this.moveTopK.bind(this))
268
48
  }
269
49
 
@@ -280,18 +60,11 @@ export class TopKWithFractionalIndexOperator<
280
60
  * Any changes to the topK are sent to the output.
281
61
  */
282
62
  moveTopK({ offset, limit }: { offset?: number; limit?: number }) {
283
- if (!(this.#topK instanceof TopKArray)) {
284
- throw new Error(
285
- `Cannot move B+-tree implementation of TopK with fractional index`,
286
- )
287
- }
288
-
289
63
  const result: Array<[[K, IndexedValue<T>], number]> = []
64
+ const diff = this.#state.move({ offset, limit })
290
65
 
291
- const diff = this.#topK.move({ offset, limit })
292
-
293
- diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result))
294
- diff.moveOuts.forEach((moveOut) => this.handleMoveOut(moveOut, result))
66
+ diff.moveIns.forEach((moveIn) => handleMoveIn(moveIn, result))
67
+ diff.moveOuts.forEach((moveOut) => handleMoveOut(moveOut, result))
295
68
 
296
69
  if (diff.changes) {
297
70
  // There are changes to the topK
@@ -321,68 +94,9 @@ export class TopKWithFractionalIndexOperator<
321
94
  multiplicity: number,
322
95
  result: Array<[[K, IndexedValue<T>], number]>,
323
96
  ): void {
324
- const { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)
325
-
326
- let res: TopKChanges<[K, T]> = {
327
- moveIn: null,
328
- moveOut: null,
329
- }
330
- if (oldMultiplicity <= 0 && newMultiplicity > 0) {
331
- // The value was invisible but should now be visible
332
- // Need to insert it into the array of sorted values
333
- res = this.#topK.insert([key, value])
334
- } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {
335
- // The value was visible but should now be invisible
336
- // Need to remove it from the array of sorted values
337
- res = this.#topK.delete([key, value])
338
- } else {
339
- // The value was invisible and it remains invisible
340
- // or it was visible and remains visible
341
- // so it doesn't affect the topK
342
- }
343
-
344
- this.handleMoveIn(res.moveIn, result)
345
- this.handleMoveOut(res.moveOut, result)
346
-
347
- return
348
- }
349
-
350
- private handleMoveIn(
351
- moveIn: IndexedValue<[K, T]> | null,
352
- result: Array<[[K, IndexedValue<T>], number]>,
353
- ) {
354
- if (moveIn) {
355
- const [[key, value], index] = moveIn
356
- result.push([[key, [value, index]], 1])
357
- }
358
- }
359
-
360
- private handleMoveOut(
361
- moveOut: IndexedValue<[K, T]> | null,
362
- result: Array<[[K, IndexedValue<T>], number]>,
363
- ) {
364
- if (moveOut) {
365
- const [[key, value], index] = moveOut
366
- result.push([[key, [value, index]], -1])
367
- }
368
- }
369
-
370
- private getMultiplicity(key: K): number {
371
- return this.#index.get(key) ?? 0
372
- }
373
-
374
- private addKey(
375
- key: K,
376
- multiplicity: number,
377
- ): { oldMultiplicity: number; newMultiplicity: number } {
378
- const oldMultiplicity = this.getMultiplicity(key)
379
- const newMultiplicity = oldMultiplicity + multiplicity
380
- if (newMultiplicity === 0) {
381
- this.#index.delete(key)
382
- } else {
383
- this.#index.set(key, newMultiplicity)
384
- }
385
- return { oldMultiplicity, newMultiplicity }
97
+ const changes = this.#state.processElement(key, value, multiplicity)
98
+ handleMoveIn(changes.moveIn, result)
99
+ handleMoveOut(changes.moveOut, result)
386
100
  }
387
101
  }
388
102
 
@@ -420,41 +134,3 @@ export function topKWithFractionalIndex<KType extends string | number, T>(
420
134
  return output
421
135
  }
422
136
  }
423
-
424
- // Abstraction for fractionally indexed values
425
- export type FractionalIndex = string
426
- export type IndexedValue<V> = [V, FractionalIndex]
427
-
428
- export function indexedValue<V>(
429
- value: V,
430
- index: FractionalIndex,
431
- ): IndexedValue<V> {
432
- return [value, index]
433
- }
434
-
435
- export function getValue<V>(indexedVal: IndexedValue<V>): V {
436
- return indexedVal[0]
437
- }
438
-
439
- export function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {
440
- return indexedVal[1]
441
- }
442
-
443
- /**
444
- * Creates a comparator for [key, value] tuples that first compares values,
445
- * then uses the row key as a stable tie-breaker.
446
- */
447
- function createKeyedComparator<K extends string | number, T>(
448
- comparator: (a: T, b: T) => number,
449
- ): (a: [K, T], b: [K, T]) => number {
450
- return ([aKey, aVal], [bKey, bVal]) => {
451
- // First compare on the value
452
- const valueComparison = comparator(aVal, bVal)
453
- if (valueComparison !== 0) {
454
- return valueComparison
455
- }
456
- // If the values are equal, use the row key as tie-breaker
457
- // This provides stable, deterministic ordering since keys are string | number
458
- return compareKeys(aKey, bKey)
459
- }
460
- }
@@ -1,19 +1,11 @@
1
1
  import { generateKeyBetween } from 'fractional-indexing'
2
2
  import { DifferenceStreamWriter } from '../graph.js'
3
3
  import { StreamBuilder } from '../d2.js'
4
- import {
5
- TopKWithFractionalIndexOperator,
6
- getIndex,
7
- getValue,
8
- indexedValue,
9
- } from './topKWithFractionalIndex.js'
4
+ import { TopKWithFractionalIndexOperator } from './topKWithFractionalIndex.js'
5
+ import { getIndex, getValue, indexedValue } from './topKArray.js'
6
+ import type { IndexedValue, TopK, TopKChanges } from './topKArray.js'
10
7
  import type { IStreamBuilder, PipedOperator } from '../types.js'
11
- import type {
12
- IndexedValue,
13
- TopK,
14
- TopKChanges,
15
- TopKWithFractionalIndexOptions,
16
- } from './topKWithFractionalIndex.js'
8
+ import type { TopKWithFractionalIndexOptions } from './topKWithFractionalIndex.js'
17
9
 
18
10
  interface BTree<Key, Value> {
19
11
  nextLowerPair: (key: Key) => [Key, Value] | undefined
package/src/utils.ts CHANGED
@@ -192,3 +192,20 @@ export function compareKeys(a: string | number, b: string | number): number {
192
192
  // Different types: strings come before numbers
193
193
  return typeof a === `string` ? -1 : 1
194
194
  }
195
+
196
+ /**
197
+ * Serializes a value for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.
198
+ * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.
199
+ * This is used for creating string keys in groupBy operations.
200
+ */
201
+ export function serializeValue(value: unknown): string {
202
+ return JSON.stringify(value, (_, val) => {
203
+ if (typeof val === 'bigint') {
204
+ return val.toString()
205
+ }
206
+ if (val instanceof Date) {
207
+ return val.toISOString()
208
+ }
209
+ return val
210
+ })
211
+ }