@tanstack/db-ivm 0.1.0 → 0.1.2

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 (34) hide show
  1. package/dist/cjs/index.cjs +3 -0
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/operators/index.d.cts +1 -0
  4. package/dist/cjs/operators/orderBy.cjs +8 -19
  5. package/dist/cjs/operators/orderBy.cjs.map +1 -1
  6. package/dist/cjs/operators/orderBy.d.cts +6 -2
  7. package/dist/cjs/operators/orderByBTree.d.cts +1 -1
  8. package/dist/cjs/operators/tap.cjs +46 -0
  9. package/dist/cjs/operators/tap.cjs.map +1 -0
  10. package/dist/cjs/operators/tap.d.cts +18 -0
  11. package/dist/cjs/operators/topKWithFractionalIndex.cjs +49 -22
  12. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  13. package/dist/cjs/operators/topKWithFractionalIndex.d.cts +13 -14
  14. package/dist/cjs/operators/topKWithFractionalIndexBTree.d.cts +5 -5
  15. package/dist/esm/index.js +3 -0
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/operators/index.d.ts +1 -0
  18. package/dist/esm/operators/orderBy.d.ts +6 -2
  19. package/dist/esm/operators/orderBy.js +8 -19
  20. package/dist/esm/operators/orderBy.js.map +1 -1
  21. package/dist/esm/operators/orderByBTree.d.ts +1 -1
  22. package/dist/esm/operators/tap.d.ts +18 -0
  23. package/dist/esm/operators/tap.js +46 -0
  24. package/dist/esm/operators/tap.js.map +1 -0
  25. package/dist/esm/operators/topKWithFractionalIndex.d.ts +13 -14
  26. package/dist/esm/operators/topKWithFractionalIndex.js +49 -22
  27. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  28. package/dist/esm/operators/topKWithFractionalIndexBTree.d.ts +5 -5
  29. package/package.json +1 -1
  30. package/src/operators/index.ts +1 -0
  31. package/src/operators/orderBy.ts +19 -31
  32. package/src/operators/tap.ts +53 -0
  33. package/src/operators/topKWithFractionalIndex.ts +83 -59
  34. package/src/operators/topKWithFractionalIndexBTree.ts +21 -19
@@ -0,0 +1,53 @@
1
+ import { DifferenceStreamWriter, LinearUnaryOperator } from "../graph.js"
2
+ import { StreamBuilder } from "../d2.js"
3
+ import type { IStreamBuilder, PipedOperator } from "../types.js"
4
+ import type { DifferenceStreamReader } from "../graph.js"
5
+ import type { MultiSet } from "../multiset.js"
6
+
7
+ /**
8
+ * Operator that applies a function to each element in the input stream
9
+ */
10
+ export class TapOperator<T> extends LinearUnaryOperator<T, T> {
11
+ #f: (data: T) => void
12
+
13
+ constructor(
14
+ id: number,
15
+ inputA: DifferenceStreamReader<T>,
16
+ output: DifferenceStreamWriter<T>,
17
+ f: (data: T) => void
18
+ ) {
19
+ super(id, inputA, output)
20
+ this.#f = f
21
+ }
22
+
23
+ inner(collection: MultiSet<T>): MultiSet<T> {
24
+ return collection.map((data) => {
25
+ this.#f(data)
26
+ return data
27
+ })
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Invokes a function for each element in the input stream.
33
+ * This operator doesn't modify the stream and is used to perform side effects.
34
+ * @param f - The function to invoke on each element
35
+ * @returns The input stream
36
+ */
37
+ export function tap<T>(f: (data: T) => void): PipedOperator<T, T> {
38
+ return (stream: IStreamBuilder<T>): IStreamBuilder<T> => {
39
+ const output = new StreamBuilder<T>(
40
+ stream.graph,
41
+ new DifferenceStreamWriter<T>()
42
+ )
43
+ const operator = new TapOperator<T>(
44
+ stream.graph.getNextOperatorId(),
45
+ stream.connectReader(),
46
+ output.writer,
47
+ f
48
+ )
49
+ stream.graph.addOperator(operator)
50
+ stream.graph.addStream(output.connectReader())
51
+ return output
52
+ }
53
+ }
@@ -2,14 +2,14 @@ import { generateKeyBetween } from "fractional-indexing"
2
2
  import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
3
3
  import { StreamBuilder } from "../d2.js"
4
4
  import { MultiSet } from "../multiset.js"
5
- import { Index } from "../indexes.js"
6
5
  import { binarySearch, globalObjectIdGenerator } from "../utils.js"
7
6
  import type { DifferenceStreamReader } from "../graph.js"
8
- import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js"
7
+ import type { IStreamBuilder, PipedOperator } from "../types.js"
9
8
 
10
9
  export interface TopKWithFractionalIndexOptions {
11
10
  limit?: number
12
11
  offset?: number
12
+ setSizeCallback?: (getSize: () => number) => void
13
13
  }
14
14
 
15
15
  export type TopKChanges<V> = {
@@ -24,6 +24,7 @@ export type TopKChanges<V> = {
24
24
  * and returns changes to the topK.
25
25
  */
26
26
  export interface TopK<V> {
27
+ size: number
27
28
  insert: (value: V) => TopKChanges<V>
28
29
  delete: (value: V) => TopKChanges<V>
29
30
  }
@@ -50,6 +51,13 @@ class TopKArray<V> implements TopK<V> {
50
51
  this.#comparator = comparator
51
52
  }
52
53
 
54
+ get size(): number {
55
+ const offset = this.#topKStart
56
+ const limit = this.#topKEnd - this.#topKStart
57
+ const available = this.#sortedValues.length - offset
58
+ return Math.max(0, Math.min(limit, available))
59
+ }
60
+
53
61
  insert(value: V): TopKChanges<V> {
54
62
  const result: TopKChanges<V> = { moveIn: null, moveOut: null }
55
63
 
@@ -158,31 +166,36 @@ class TopKArray<V> implements TopK<V> {
158
166
  * This operator maintains fractional indices for sorted elements
159
167
  * and only updates indices when elements move position
160
168
  */
161
- export class TopKWithFractionalIndexOperator<K, V1> extends UnaryOperator<
162
- [K, V1],
163
- [K, IndexedValue<V1>]
169
+ export class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<
170
+ [K, T],
171
+ [K, IndexedValue<T>]
164
172
  > {
165
- #index = new Index<K, V1>()
173
+ #index: Map<K, number> = new Map() // maps keys to their multiplicity
166
174
 
167
175
  /**
168
176
  * topK data structure that supports insertions and deletions
169
177
  * and returns changes to the topK.
170
178
  */
171
- #topK: TopK<TaggedValue<V1>>
179
+ #topK: TopK<TaggedValue<K, T>>
180
+
181
+ #limit: number
172
182
 
173
183
  constructor(
174
184
  id: number,
175
- inputA: DifferenceStreamReader<[K, V1]>,
176
- output: DifferenceStreamWriter<[K, [V1, string]]>,
177
- comparator: (a: V1, b: V1) => number,
185
+ inputA: DifferenceStreamReader<[K, T]>,
186
+ output: DifferenceStreamWriter<[K, IndexedValue<T>]>,
187
+ comparator: (a: T, b: T) => number,
178
188
  options: TopKWithFractionalIndexOptions
179
189
  ) {
180
190
  super(id, inputA, output)
181
- const limit = options.limit ?? Infinity
191
+ this.#limit = options.limit ?? Infinity
182
192
  const offset = options.offset ?? 0
183
- const compareTaggedValues = (a: TaggedValue<V1>, b: TaggedValue<V1>) => {
193
+ const compareTaggedValues = (
194
+ a: TaggedValue<K, T>,
195
+ b: TaggedValue<K, T>
196
+ ) => {
184
197
  // First compare on the value
185
- const valueComparison = comparator(untagValue(a), untagValue(b))
198
+ const valueComparison = comparator(getVal(a), getVal(b))
186
199
  if (valueComparison !== 0) {
187
200
  return valueComparison
188
201
  }
@@ -191,19 +204,20 @@ export class TopKWithFractionalIndexOperator<K, V1> extends UnaryOperator<
191
204
  const tieBreakerB = getTag(b)
192
205
  return tieBreakerA - tieBreakerB
193
206
  }
194
- this.#topK = this.createTopK(offset, limit, compareTaggedValues)
207
+ this.#topK = this.createTopK(offset, this.#limit, compareTaggedValues)
208
+ options.setSizeCallback?.(() => this.#topK.size)
195
209
  }
196
210
 
197
211
  protected createTopK(
198
212
  offset: number,
199
213
  limit: number,
200
- comparator: (a: TaggedValue<V1>, b: TaggedValue<V1>) => number
201
- ): TopK<TaggedValue<V1>> {
214
+ comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number
215
+ ): TopK<TaggedValue<K, T>> {
202
216
  return new TopKArray(offset, limit, comparator)
203
217
  }
204
218
 
205
219
  run(): void {
206
- const result: Array<[[K, [V1, string]], number]> = []
220
+ const result: Array<[[K, IndexedValue<T>], number]> = []
207
221
  for (const message of this.inputMessages()) {
208
222
  for (const [item, multiplicity] of message.getInner()) {
209
223
  const [key, value] = item
@@ -218,27 +232,25 @@ export class TopKWithFractionalIndexOperator<K, V1> extends UnaryOperator<
218
232
 
219
233
  processElement(
220
234
  key: K,
221
- value: V1,
235
+ value: T,
222
236
  multiplicity: number,
223
- result: Array<[[K, [V1, string]], number]>
237
+ result: Array<[[K, IndexedValue<T>], number]>
224
238
  ): void {
225
- const oldMultiplicity = this.#index.getMultiplicity(key, value)
226
- this.#index.addValue(key, [value, multiplicity])
227
- const newMultiplicity = this.#index.getMultiplicity(key, value)
239
+ const { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)
228
240
 
229
- let res: TopKChanges<TaggedValue<V1>> = {
241
+ let res: TopKChanges<TaggedValue<K, T>> = {
230
242
  moveIn: null,
231
243
  moveOut: null,
232
244
  }
233
245
  if (oldMultiplicity <= 0 && newMultiplicity > 0) {
234
246
  // The value was invisible but should now be visible
235
247
  // Need to insert it into the array of sorted values
236
- const taggedValue = tagValue(value)
248
+ const taggedValue = tagValue(key, value)
237
249
  res = this.#topK.insert(taggedValue)
238
250
  } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {
239
251
  // The value was visible but should now be invisible
240
252
  // Need to remove it from the array of sorted values
241
- const taggedValue = tagValue(value)
253
+ const taggedValue = tagValue(key, value)
242
254
  res = this.#topK.delete(taggedValue)
243
255
  } else {
244
256
  // The value was invisible and it remains invisible
@@ -247,26 +259,45 @@ export class TopKWithFractionalIndexOperator<K, V1> extends UnaryOperator<
247
259
  }
248
260
 
249
261
  if (res.moveIn) {
250
- const valueWithoutTieBreaker = mapValue(res.moveIn, untagValue)
251
- result.push([[key, valueWithoutTieBreaker], 1])
262
+ const index = getIndex(res.moveIn)
263
+ const taggedValue = getValue(res.moveIn)
264
+ const k = getKey(taggedValue)
265
+ const val = getVal(taggedValue)
266
+ result.push([[k, [val, index]], 1])
252
267
  }
253
268
 
254
269
  if (res.moveOut) {
255
- const valueWithoutTieBreaker = mapValue(res.moveOut, untagValue)
256
- result.push([[key, valueWithoutTieBreaker], -1])
270
+ const index = getIndex(res.moveOut)
271
+ const taggedValue = getValue(res.moveOut)
272
+ const k = getKey(taggedValue)
273
+ const val = getVal(taggedValue)
274
+ result.push([[k, [val, index]], -1])
257
275
  }
258
276
 
259
277
  return
260
278
  }
279
+
280
+ private getMultiplicity(key: K): number {
281
+ return this.#index.get(key) ?? 0
282
+ }
283
+
284
+ private addKey(
285
+ key: K,
286
+ multiplicity: number
287
+ ): { oldMultiplicity: number; newMultiplicity: number } {
288
+ const oldMultiplicity = this.getMultiplicity(key)
289
+ const newMultiplicity = oldMultiplicity + multiplicity
290
+ if (newMultiplicity === 0) {
291
+ this.#index.delete(key)
292
+ } else {
293
+ this.#index.set(key, newMultiplicity)
294
+ }
295
+ return { oldMultiplicity, newMultiplicity }
296
+ }
261
297
  }
262
298
 
263
299
  /**
264
300
  * Limits the number of results based on a comparator, with optional offset.
265
- * This works on a keyed stream, where the key is the first element of the tuple.
266
- * The ordering is within a key group, i.e. elements are sorted within a key group
267
- * and the limit + offset is applied to that sorted group.
268
- * To order the entire stream, key by the same value for all elements such as null.
269
- *
270
301
  * Uses fractional indexing to minimize the number of changes when elements move positions.
271
302
  * Each element is assigned a fractional index that is lexicographically sortable.
272
303
  * When elements move, only the indices of the moved elements are updated, not all elements.
@@ -275,26 +306,22 @@ export class TopKWithFractionalIndexOperator<K, V1> extends UnaryOperator<
275
306
  * @param options - An optional object containing limit and offset properties
276
307
  * @returns A piped operator that orders the elements and limits the number of results
277
308
  */
278
- export function topKWithFractionalIndex<
279
- KType extends T extends KeyValue<infer K, infer _V> ? K : never,
280
- V1Type extends T extends KeyValue<KType, infer V> ? V : never,
281
- T,
282
- >(
283
- comparator: (a: V1Type, b: V1Type) => number,
309
+ export function topKWithFractionalIndex<KType, T>(
310
+ comparator: (a: T, b: T) => number,
284
311
  options?: TopKWithFractionalIndexOptions
285
- ): PipedOperator<T, KeyValue<KType, [V1Type, string]>> {
312
+ ): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {
286
313
  const opts = options || {}
287
314
 
288
315
  return (
289
- stream: IStreamBuilder<T>
290
- ): IStreamBuilder<KeyValue<KType, [V1Type, string]>> => {
291
- const output = new StreamBuilder<KeyValue<KType, [V1Type, string]>>(
316
+ stream: IStreamBuilder<[KType, T]>
317
+ ): IStreamBuilder<[KType, IndexedValue<T>]> => {
318
+ const output = new StreamBuilder<[KType, IndexedValue<T>]>(
292
319
  stream.graph,
293
- new DifferenceStreamWriter<KeyValue<KType, [V1Type, string]>>()
320
+ new DifferenceStreamWriter<[KType, IndexedValue<T>]>()
294
321
  )
295
- const operator = new TopKWithFractionalIndexOperator<KType, V1Type>(
322
+ const operator = new TopKWithFractionalIndexOperator<KType, T>(
296
323
  stream.graph.getNextOperatorId(),
297
- stream.connectReader() as DifferenceStreamReader<KeyValue<KType, V1Type>>,
324
+ stream.connectReader(),
298
325
  output.writer,
299
326
  comparator,
300
327
  opts
@@ -324,24 +351,21 @@ export function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {
324
351
  return indexedVal[1]
325
352
  }
326
353
 
327
- function mapValue<V, W>(
328
- indexedVal: IndexedValue<V>,
329
- f: (value: V) => W
330
- ): IndexedValue<W> {
331
- return [f(getValue(indexedVal)), getIndex(indexedVal)]
332
- }
333
-
334
354
  export type Tag = number
335
- export type TaggedValue<V> = [V, Tag]
355
+ export type TaggedValue<K, V> = [K, V, Tag]
336
356
 
337
- function tagValue<V>(value: V): TaggedValue<V> {
338
- return [value, globalObjectIdGenerator.getId(value)]
357
+ function tagValue<K, V>(key: K, value: V): TaggedValue<K, V> {
358
+ return [key, value, globalObjectIdGenerator.getId(key)]
339
359
  }
340
360
 
341
- function untagValue<V>(tieBreakerTaggedValue: TaggedValue<V>): V {
361
+ function getKey<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): K {
342
362
  return tieBreakerTaggedValue[0]
343
363
  }
344
364
 
345
- function getTag<V>(tieBreakerTaggedValue: TaggedValue<V>): Tag {
365
+ function getVal<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): V {
346
366
  return tieBreakerTaggedValue[1]
347
367
  }
368
+
369
+ function getTag<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): Tag {
370
+ return tieBreakerTaggedValue[2]
371
+ }
@@ -7,8 +7,7 @@ import {
7
7
  getValue,
8
8
  indexedValue,
9
9
  } from "./topKWithFractionalIndex.js"
10
- import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js"
11
- import type { DifferenceStreamReader } from "../graph.js"
10
+ import type { IStreamBuilder, PipedOperator } from "../types.js"
12
11
  import type {
13
12
  IndexedValue,
14
13
  TaggedValue,
@@ -76,6 +75,13 @@ class TopKTree<V> implements TopK<V> {
76
75
  this.#tree = new BTree(undefined, comparator)
77
76
  }
78
77
 
78
+ get size(): number {
79
+ const offset = this.#topKStart
80
+ const limit = this.#topKEnd - this.#topKStart
81
+ const available = this.#tree.size - offset
82
+ return Math.max(0, Math.min(limit, available))
83
+ }
84
+
79
85
  /**
80
86
  * Insert a *new* value.
81
87
  * Ignores the value if it is already present.
@@ -238,13 +244,13 @@ class TopKTree<V> implements TopK<V> {
238
244
  */
239
245
  export class TopKWithFractionalIndexBTreeOperator<
240
246
  K,
241
- V1,
242
- > extends TopKWithFractionalIndexOperator<K, V1> {
247
+ T,
248
+ > extends TopKWithFractionalIndexOperator<K, T> {
243
249
  protected override createTopK(
244
250
  offset: number,
245
251
  limit: number,
246
- comparator: (a: TaggedValue<V1>, b: TaggedValue<V1>) => number
247
- ): TopK<TaggedValue<V1>> {
252
+ comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number
253
+ ): TopK<TaggedValue<K, T>> {
248
254
  if (BTree === undefined) {
249
255
  throw new Error(
250
256
  `B+ tree not loaded. You need to call loadBTree() before using TopKWithFractionalIndexBTreeOperator.`
@@ -269,14 +275,10 @@ export class TopKWithFractionalIndexBTreeOperator<
269
275
  * @param options - An optional object containing limit and offset properties
270
276
  * @returns A piped operator that orders the elements and limits the number of results
271
277
  */
272
- export function topKWithFractionalIndexBTree<
273
- KType extends T extends KeyValue<infer K, infer _V> ? K : never,
274
- V1Type extends T extends KeyValue<KType, infer V> ? V : never,
275
- T,
276
- >(
277
- comparator: (a: V1Type, b: V1Type) => number,
278
+ export function topKWithFractionalIndexBTree<KType, T>(
279
+ comparator: (a: T, b: T) => number,
278
280
  options?: TopKWithFractionalIndexOptions
279
- ): PipedOperator<T, KeyValue<KType, [V1Type, string]>> {
281
+ ): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {
280
282
  const opts = options || {}
281
283
 
282
284
  if (BTree === undefined) {
@@ -286,15 +288,15 @@ export function topKWithFractionalIndexBTree<
286
288
  }
287
289
 
288
290
  return (
289
- stream: IStreamBuilder<T>
290
- ): IStreamBuilder<KeyValue<KType, [V1Type, string]>> => {
291
- const output = new StreamBuilder<KeyValue<KType, [V1Type, string]>>(
291
+ stream: IStreamBuilder<[KType, T]>
292
+ ): IStreamBuilder<[KType, IndexedValue<T>]> => {
293
+ const output = new StreamBuilder<[KType, IndexedValue<T>]>(
292
294
  stream.graph,
293
- new DifferenceStreamWriter<KeyValue<KType, [V1Type, string]>>()
295
+ new DifferenceStreamWriter<[KType, IndexedValue<T>]>()
294
296
  )
295
- const operator = new TopKWithFractionalIndexOperator<KType, V1Type>(
297
+ const operator = new TopKWithFractionalIndexBTreeOperator<KType, T>(
296
298
  stream.graph.getNextOperatorId(),
297
- stream.connectReader() as DifferenceStreamReader<KeyValue<KType, V1Type>>,
299
+ stream.connectReader(),
298
300
  output.writer,
299
301
  comparator,
300
302
  opts