@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,136 +1,21 @@
1
- import { generateKeyBetween } from "fractional-indexing";
2
1
  import { UnaryOperator, DifferenceStreamWriter } from "../graph.js";
3
2
  import { StreamBuilder } from "../d2.js";
4
3
  import { MultiSet } from "../multiset.js";
5
- import { compareKeys, diffHalfOpen, binarySearch } from "../utils.js";
6
- class TopKArray {
7
- #sortedValues = [];
8
- #comparator;
9
- #topKStart;
10
- #topKEnd;
11
- constructor(offset, limit, comparator) {
12
- this.#topKStart = offset;
13
- this.#topKEnd = offset + limit;
14
- this.#comparator = comparator;
15
- }
16
- get size() {
17
- const offset = this.#topKStart;
18
- const limit = this.#topKEnd - this.#topKStart;
19
- const available = this.#sortedValues.length - offset;
20
- return Math.max(0, Math.min(limit, available));
21
- }
22
- /**
23
- * Moves the topK window
24
- */
25
- move({
26
- offset,
27
- limit
28
- }) {
29
- const oldOffset = this.#topKStart;
30
- const oldLimit = this.#topKEnd - this.#topKStart;
31
- const oldRange = [
32
- this.#topKStart,
33
- this.#topKEnd === Infinity ? this.#topKStart + this.size : this.#topKEnd
34
- ];
35
- this.#topKStart = offset ?? oldOffset;
36
- this.#topKEnd = this.#topKStart + (limit ?? oldLimit);
37
- const newRange = [
38
- this.#topKStart,
39
- this.#topKEnd === Infinity ? Math.max(this.#topKStart + this.size, oldRange[1]) : this.#topKEnd
40
- ];
41
- const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange);
42
- const moveIns = [];
43
- onlyInB.forEach((index) => {
44
- const value = this.#sortedValues[index];
45
- if (value) {
46
- moveIns.push(value);
47
- }
48
- });
49
- const moveOuts = [];
50
- onlyInA.forEach((index) => {
51
- const value = this.#sortedValues[index];
52
- if (value) {
53
- moveOuts.push(value);
54
- }
55
- });
56
- return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 };
57
- }
58
- insert(value) {
59
- const result = { moveIn: null, moveOut: null };
60
- const index = this.#findIndex(value);
61
- const indexBefore = index === 0 ? null : getIndex(this.#sortedValues[index - 1]);
62
- const indexAfter = index === this.#sortedValues.length ? null : getIndex(this.#sortedValues[index]);
63
- const fractionalIndex = generateKeyBetween(indexBefore, indexAfter);
64
- const val = indexedValue(value, fractionalIndex);
65
- this.#sortedValues.splice(index, 0, val);
66
- if (index < this.#topKEnd) {
67
- const moveInIndex = Math.max(index, this.#topKStart);
68
- if (moveInIndex < this.#sortedValues.length) {
69
- result.moveIn = this.#sortedValues[moveInIndex];
70
- if (this.#topKEnd < this.#sortedValues.length) {
71
- result.moveOut = this.#sortedValues[this.#topKEnd];
72
- }
73
- }
74
- }
75
- return result;
76
- }
77
- /**
78
- * Deletes a value that may or may not be in the topK.
79
- * IMPORTANT: this assumes that the value is present in the collection
80
- * if it's not the case it will remove the element
81
- * that is on the position where the provided `value` would be.
82
- */
83
- delete(value) {
84
- const result = { moveIn: null, moveOut: null };
85
- const index = this.#findIndex(value);
86
- const [removedElem] = this.#sortedValues.splice(index, 1);
87
- if (index < this.#topKEnd) {
88
- result.moveOut = removedElem;
89
- if (index < this.#topKStart) {
90
- const moveOutIndex = this.#topKStart - 1;
91
- if (moveOutIndex < this.#sortedValues.length) {
92
- result.moveOut = this.#sortedValues[moveOutIndex];
93
- } else {
94
- result.moveOut = null;
95
- }
96
- }
97
- const moveInIndex = this.#topKEnd - 1;
98
- if (moveInIndex < this.#sortedValues.length) {
99
- result.moveIn = this.#sortedValues[moveInIndex];
100
- }
101
- }
102
- return result;
103
- }
104
- // TODO: see if there is a way to refactor the code for insert and delete in the topK above
105
- // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right
106
- // so i have the feeling there is a common pattern here and we can implement both cases using that pattern
107
- #findIndex(value) {
108
- return binarySearch(
109
- this.#sortedValues,
110
- indexedValue(value, ``),
111
- (a, b) => this.#comparator(getValue(a), getValue(b))
112
- );
113
- }
114
- }
4
+ import { TopKState, handleMoveIn, handleMoveOut } from "./topKState.js";
5
+ import { createKeyedComparator, TopKArray } from "./topKArray.js";
115
6
  class TopKWithFractionalIndexOperator extends UnaryOperator {
116
- #index = /* @__PURE__ */ new Map();
117
- // maps keys to their multiplicity
118
- /**
119
- * topK data structure that supports insertions and deletions
120
- * and returns changes to the topK.
121
- * Elements are stored as [key, value] tuples for stable tie-breaking.
122
- */
123
- #topK;
7
+ #state;
124
8
  constructor(id, inputA, output, comparator, options) {
125
9
  super(id, inputA, output);
126
10
  const limit = options.limit ?? Infinity;
127
11
  const offset = options.offset ?? 0;
128
- this.#topK = this.createTopK(
12
+ const topK = this.createTopK(
129
13
  offset,
130
14
  limit,
131
15
  createKeyedComparator(comparator)
132
16
  );
133
- options.setSizeCallback?.(() => this.#topK.size);
17
+ this.#state = new TopKState(topK);
18
+ options.setSizeCallback?.(() => this.#state.size);
134
19
  options.setWindowFn?.(this.moveTopK.bind(this));
135
20
  }
136
21
  createTopK(offset, limit, comparator) {
@@ -141,15 +26,10 @@ class TopKWithFractionalIndexOperator extends UnaryOperator {
141
26
  * Any changes to the topK are sent to the output.
142
27
  */
143
28
  moveTopK({ offset, limit }) {
144
- if (!(this.#topK instanceof TopKArray)) {
145
- throw new Error(
146
- `Cannot move B+-tree implementation of TopK with fractional index`
147
- );
148
- }
149
29
  const result = [];
150
- const diff = this.#topK.move({ offset, limit });
151
- diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result));
152
- diff.moveOuts.forEach((moveOut) => this.handleMoveOut(moveOut, result));
30
+ const diff = this.#state.move({ offset, limit });
31
+ diff.moveIns.forEach((moveIn) => handleMoveIn(moveIn, result));
32
+ diff.moveOuts.forEach((moveOut) => handleMoveOut(moveOut, result));
153
33
  if (diff.changes) {
154
34
  this.output.sendData(new MultiSet(result));
155
35
  }
@@ -167,44 +47,9 @@ class TopKWithFractionalIndexOperator extends UnaryOperator {
167
47
  }
168
48
  }
169
49
  processElement(key, value, multiplicity, result) {
170
- const { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity);
171
- let res = {
172
- moveIn: null,
173
- moveOut: null
174
- };
175
- if (oldMultiplicity <= 0 && newMultiplicity > 0) {
176
- res = this.#topK.insert([key, value]);
177
- } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {
178
- res = this.#topK.delete([key, value]);
179
- } else ;
180
- this.handleMoveIn(res.moveIn, result);
181
- this.handleMoveOut(res.moveOut, result);
182
- return;
183
- }
184
- handleMoveIn(moveIn, result) {
185
- if (moveIn) {
186
- const [[key, value], index] = moveIn;
187
- result.push([[key, [value, index]], 1]);
188
- }
189
- }
190
- handleMoveOut(moveOut, result) {
191
- if (moveOut) {
192
- const [[key, value], index] = moveOut;
193
- result.push([[key, [value, index]], -1]);
194
- }
195
- }
196
- getMultiplicity(key) {
197
- return this.#index.get(key) ?? 0;
198
- }
199
- addKey(key, multiplicity) {
200
- const oldMultiplicity = this.getMultiplicity(key);
201
- const newMultiplicity = oldMultiplicity + multiplicity;
202
- if (newMultiplicity === 0) {
203
- this.#index.delete(key);
204
- } else {
205
- this.#index.set(key, newMultiplicity);
206
- }
207
- return { oldMultiplicity, newMultiplicity };
50
+ const changes = this.#state.processElement(key, value, multiplicity);
51
+ handleMoveIn(changes.moveIn, result);
52
+ handleMoveOut(changes.moveOut, result);
208
53
  }
209
54
  }
210
55
  function topKWithFractionalIndex(comparator, options) {
@@ -225,29 +70,8 @@ function topKWithFractionalIndex(comparator, options) {
225
70
  return output;
226
71
  };
227
72
  }
228
- function indexedValue(value, index) {
229
- return [value, index];
230
- }
231
- function getValue(indexedVal) {
232
- return indexedVal[0];
233
- }
234
- function getIndex(indexedVal) {
235
- return indexedVal[1];
236
- }
237
- function createKeyedComparator(comparator) {
238
- return ([aKey, aVal], [bKey, bVal]) => {
239
- const valueComparison = comparator(aVal, bVal);
240
- if (valueComparison !== 0) {
241
- return valueComparison;
242
- }
243
- return compareKeys(aKey, bKey);
244
- };
245
- }
246
73
  export {
247
74
  TopKWithFractionalIndexOperator,
248
- getIndex,
249
- getValue,
250
- indexedValue,
251
75
  topKWithFractionalIndex
252
76
  };
253
77
  //# sourceMappingURL=topKWithFractionalIndex.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"topKWithFractionalIndex.js","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { generateKeyBetween } from 'fractional-indexing'\nimport { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport { binarySearch, compareKeys, diffHalfOpen } from '../utils.js'\nimport type { HRange } from '../utils.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, PipedOperator } from '../types.js'\n\nexport interface TopKWithFractionalIndexOptions {\n limit?: number\n offset?: number\n setSizeCallback?: (getSize: () => number) => void\n setWindowFn?: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => void\n}\n\nexport type TopKChanges<V> = {\n /** Indicates which element moves into the topK (if any) */\n moveIn: IndexedValue<V> | null\n /** Indicates which element moves out of the topK (if any) */\n moveOut: IndexedValue<V> | null\n}\n\nexport type TopKMoveChanges<V> = {\n /** Flag that marks whether there were any changes to the topK */\n changes: boolean\n /** Indicates which elements move into the topK (if any) */\n moveIns: Array<IndexedValue<V>>\n /** Indicates which elements move out of the topK (if any) */\n moveOuts: Array<IndexedValue<V>>\n}\n\n/**\n * A topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\nexport interface TopK<V> {\n size: number\n insert: (value: V) => TopKChanges<V>\n delete: (value: V) => TopKChanges<V>\n}\n\n/**\n * Implementation of a topK data structure.\n * Uses a sorted array internally to store the values and keeps a topK window over that array.\n * Inserts and deletes are O(n) operations because worst case an element is inserted/deleted\n * at the start of the array which causes all the elements to shift to the right/left.\n */\nclass TopKArray<V> implements TopK<V> {\n #sortedValues: Array<IndexedValue<V>> = []\n #comparator: (a: V, b: V) => number\n #topKStart: number\n #topKEnd: number\n\n constructor(\n offset: number,\n limit: number,\n comparator: (a: V, b: V) => number,\n ) {\n this.#topKStart = offset\n this.#topKEnd = offset + limit\n this.#comparator = comparator\n }\n\n get size(): number {\n const offset = this.#topKStart\n const limit = this.#topKEnd - this.#topKStart\n const available = this.#sortedValues.length - offset\n return Math.max(0, Math.min(limit, available))\n }\n\n /**\n * Moves the topK window\n */\n move({\n offset,\n limit,\n }: {\n offset?: number\n limit?: number\n }): TopKMoveChanges<V> {\n const oldOffset = this.#topKStart\n const oldLimit = this.#topKEnd - this.#topKStart\n\n // `this.#topKEnd` can be `Infinity` if it has no limit\n // but `diffHalfOpen` expects a finite range\n // so we restrict it to the size of the topK if topKEnd is infinite\n const oldRange: HRange = [\n this.#topKStart,\n this.#topKEnd === Infinity ? this.#topKStart + this.size : this.#topKEnd,\n ]\n\n this.#topKStart = offset ?? oldOffset\n this.#topKEnd = this.#topKStart + (limit ?? oldLimit) // can be `Infinity` if limit is `Infinity`\n\n // Also handle `Infinity` in the newRange\n const newRange: HRange = [\n this.#topKStart,\n this.#topKEnd === Infinity\n ? 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)\n : this.#topKEnd,\n ]\n const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange)\n\n const moveIns: Array<IndexedValue<V>> = []\n onlyInB.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveIns.push(value)\n }\n })\n\n const moveOuts: Array<IndexedValue<V>> = []\n onlyInA.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveOuts.push(value)\n }\n })\n\n // It could be that there are changes (i.e. moveIns or moveOuts)\n // but that the collection is lazy so we don't have the data yet that needs to move in/out\n // so `moveIns` and `moveOuts` will be empty but `changes` will be true\n // this will tell the caller that it needs to run the graph to load more data\n return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 }\n }\n\n insert(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup insert position\n const index = this.#findIndex(value)\n // Generate fractional index based on the fractional indices of the elements before and after it\n const indexBefore =\n index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)\n const indexAfter =\n index === this.#sortedValues.length\n ? null\n : getIndex(this.#sortedValues[index]!)\n const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)\n\n // Insert the value at the correct position\n const val = indexedValue(value, fractionalIndex)\n // Splice is O(n) where n = all elements in the collection (i.e. n >= k) !\n this.#sortedValues.splice(index, 0, val)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The inserted element is either before the top K or within the top K\n // If it is before the top K then it moves the element that was right before the topK into the topK\n // If it is within the top K then the inserted element moves into the top K\n // In both cases the last element of the old top K now moves out of the top K\n const moveInIndex = Math.max(index, this.#topKStart)\n if (moveInIndex < this.#sortedValues.length) {\n // We actually have a topK\n // because in some cases there may not be enough elements in the array to reach the start of the topK\n // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK\n result.moveIn = this.#sortedValues[moveInIndex]!\n\n // We need to remove the element that falls out of the top K\n // The element that falls out of the top K has shifted one to the right\n // because of the element we inserted, so we find it at index topKEnd\n if (this.#topKEnd < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[this.#topKEnd]!\n }\n }\n }\n\n return result\n }\n\n /**\n * Deletes a value that may or may not be in the topK.\n * IMPORTANT: this assumes that the value is present in the collection\n * if it's not the case it will remove the element\n * that is on the position where the provided `value` would be.\n */\n delete(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup delete position\n const index = this.#findIndex(value)\n // Remove the value at that position\n const [removedElem] = this.#sortedValues.splice(index, 1)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The removed element is either before the top K or within the top K\n // If it is before the top K then the first element of the topK moves out of the topK\n // If it is within the top K then the removed element moves out of the topK\n result.moveOut = removedElem!\n if (index < this.#topKStart) {\n // The removed element is before the topK\n // so actually, the first element of the topK moves out of the topK\n // and not the element that we removed\n // The first element of the topK is now at index topKStart - 1\n // since we removed an element before the topK\n const moveOutIndex = this.#topKStart - 1\n if (moveOutIndex < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[moveOutIndex]!\n } else {\n // No value is moving out of the topK\n // because there are no elements in the topK\n result.moveOut = null\n }\n }\n\n // Since we removed an element that was before or in the topK\n // the first element after the topK moved one position to the left\n // and thus falls into the topK now\n const moveInIndex = this.#topKEnd - 1\n if (moveInIndex < this.#sortedValues.length) {\n result.moveIn = this.#sortedValues[moveInIndex]!\n }\n }\n\n return result\n }\n\n // TODO: see if there is a way to refactor the code for insert and delete in the topK above\n // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right\n // so i have the feeling there is a common pattern here and we can implement both cases using that pattern\n\n #findIndex(value: V): number {\n return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>\n this.#comparator(getValue(a), getValue(b)),\n )\n }\n}\n\n/**\n * Operator for fractional indexed topK operations\n * This operator maintains fractional indices for sorted elements\n * and only updates indices when elements move position\n */\nexport class TopKWithFractionalIndexOperator<\n K extends string | number,\n T,\n> extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {\n #index: Map<K, number> = new Map() // maps keys to their multiplicity\n\n /**\n * topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n * Elements are stored as [key, value] tuples for stable tie-breaking.\n */\n #topK: TopK<[K, T]>\n\n constructor(\n id: number,\n inputA: DifferenceStreamReader<[K, T]>,\n output: DifferenceStreamWriter<[K, IndexedValue<T>]>,\n comparator: (a: T, b: T) => number,\n options: TopKWithFractionalIndexOptions,\n ) {\n super(id, inputA, output)\n const limit = options.limit ?? Infinity\n const offset = options.offset ?? 0\n this.#topK = this.createTopK(\n offset,\n limit,\n createKeyedComparator(comparator),\n )\n options.setSizeCallback?.(() => this.#topK.size)\n options.setWindowFn?.(this.moveTopK.bind(this))\n }\n\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: [K, T], b: [K, T]) => number,\n ): TopK<[K, T]> {\n return new TopKArray(offset, limit, comparator)\n }\n\n /**\n * Moves the topK window based on the provided offset and limit.\n * Any changes to the topK are sent to the output.\n */\n moveTopK({ offset, limit }: { offset?: number; limit?: number }) {\n if (!(this.#topK instanceof TopKArray)) {\n throw new Error(\n `Cannot move B+-tree implementation of TopK with fractional index`,\n )\n }\n\n const result: Array<[[K, IndexedValue<T>], number]> = []\n\n const diff = this.#topK.move({ offset, limit })\n\n diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => this.handleMoveOut(moveOut, result))\n\n if (diff.changes) {\n // There are changes to the topK\n // it could be that moveIns and moveOuts are empty\n // because the collection is lazy, so we will run the graph again to load the data\n this.output.sendData(new MultiSet(result))\n }\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const message of this.inputMessages()) {\n for (const [item, multiplicity] of message.getInner()) {\n const [key, value] = item\n this.processElement(key, value, multiplicity, result)\n }\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n processElement(\n key: K,\n value: T,\n multiplicity: number,\n result: Array<[[K, IndexedValue<T>], number]>,\n ): void {\n const { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)\n\n let res: TopKChanges<[K, T]> = {\n moveIn: null,\n moveOut: null,\n }\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n // Need to insert it into the array of sorted values\n res = this.#topK.insert([key, value])\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n // Need to remove it from the array of sorted values\n res = this.#topK.delete([key, value])\n } else {\n // The value was invisible and it remains invisible\n // or it was visible and remains visible\n // so it doesn't affect the topK\n }\n\n this.handleMoveIn(res.moveIn, result)\n this.handleMoveOut(res.moveOut, result)\n\n return\n }\n\n private handleMoveIn(\n moveIn: IndexedValue<[K, T]> | null,\n result: Array<[[K, IndexedValue<T>], number]>,\n ) {\n if (moveIn) {\n const [[key, value], index] = moveIn\n result.push([[key, [value, index]], 1])\n }\n }\n\n private handleMoveOut(\n moveOut: IndexedValue<[K, T]> | null,\n result: Array<[[K, IndexedValue<T>], number]>,\n ) {\n if (moveOut) {\n const [[key, value], index] = moveOut\n result.push([[key, [value, index]], -1])\n }\n }\n\n private getMultiplicity(key: K): number {\n return this.#index.get(key) ?? 0\n }\n\n private addKey(\n key: K,\n multiplicity: number,\n ): { oldMultiplicity: number; newMultiplicity: number } {\n const oldMultiplicity = this.getMultiplicity(key)\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#index.delete(key)\n } else {\n this.#index.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\n }\n}\n\n/**\n * Limits the number of results based on a comparator, with optional offset.\n * Uses fractional indexing to minimize the number of changes when elements move positions.\n * Each element is assigned a fractional index that is lexicographically sortable.\n * When elements move, only the indices of the moved elements are updated, not all elements.\n *\n * @param comparator - A function that compares two elements\n * @param options - An optional object containing limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function topKWithFractionalIndex<KType extends string | number, T>(\n comparator: (a: T, b: T) => number,\n options?: TopKWithFractionalIndexOptions,\n): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {\n const opts = options || {}\n\n return (\n stream: IStreamBuilder<[KType, T]>,\n ): IStreamBuilder<[KType, IndexedValue<T>]> => {\n const output = new StreamBuilder<[KType, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[KType, IndexedValue<T>]>(),\n )\n const operator = new TopKWithFractionalIndexOperator<KType, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n opts,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n\n// Abstraction for fractionally indexed values\nexport type FractionalIndex = string\nexport type IndexedValue<V> = [V, FractionalIndex]\n\nexport function indexedValue<V>(\n value: V,\n index: FractionalIndex,\n): IndexedValue<V> {\n return [value, index]\n}\n\nexport function getValue<V>(indexedVal: IndexedValue<V>): V {\n return indexedVal[0]\n}\n\nexport function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {\n return indexedVal[1]\n}\n\n/**\n * Creates a comparator for [key, value] tuples that first compares values,\n * then uses the row key as a stable tie-breaker.\n */\nfunction createKeyedComparator<K extends string | number, T>(\n comparator: (a: T, b: T) => number,\n): (a: [K, T], b: [K, T]) => number {\n return ([aKey, aVal], [bKey, bVal]) => {\n // First compare on the value\n const valueComparison = comparator(aVal, bVal)\n if (valueComparison !== 0) {\n return valueComparison\n }\n // If the values are equal, use the row key as tie-breaker\n // This provides stable, deterministic ordering since keys are string | number\n return compareKeys(aKey, bKey)\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"topKWithFractionalIndex.js","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport { TopKState, handleMoveIn, handleMoveOut } from './topKState.js'\nimport { TopKArray, createKeyedComparator } from './topKArray.js'\nimport type { IndexedValue, TopK } from './topKArray.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, PipedOperator } from '../types.js'\n\nexport interface TopKWithFractionalIndexOptions {\n limit?: number\n offset?: number\n setSizeCallback?: (getSize: () => number) => void\n setWindowFn?: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => void\n}\n\n/**\n * Operator for fractional indexed topK operations\n * This operator maintains fractional indices for sorted elements\n * and only updates indices when elements move position\n */\nexport class TopKWithFractionalIndexOperator<\n K extends string | number,\n T,\n> extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {\n #state: TopKState<K, T>\n\n constructor(\n id: number,\n inputA: DifferenceStreamReader<[K, T]>,\n output: DifferenceStreamWriter<[K, IndexedValue<T>]>,\n comparator: (a: T, b: T) => number,\n options: TopKWithFractionalIndexOptions,\n ) {\n super(id, inputA, output)\n const limit = options.limit ?? Infinity\n const offset = options.offset ?? 0\n const topK = this.createTopK(\n offset,\n limit,\n createKeyedComparator(comparator),\n )\n this.#state = new TopKState(topK)\n options.setSizeCallback?.(() => this.#state.size)\n options.setWindowFn?.(this.moveTopK.bind(this))\n }\n\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: [K, T], b: [K, T]) => number,\n ): TopK<[K, T]> {\n return new TopKArray(offset, limit, comparator)\n }\n\n /**\n * Moves the topK window based on the provided offset and limit.\n * Any changes to the topK are sent to the output.\n */\n moveTopK({ offset, limit }: { offset?: number; limit?: number }) {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n const diff = this.#state.move({ offset, limit })\n\n diff.moveIns.forEach((moveIn) => handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => handleMoveOut(moveOut, result))\n\n if (diff.changes) {\n // There are changes to the topK\n // it could be that moveIns and moveOuts are empty\n // because the collection is lazy, so we will run the graph again to load the data\n this.output.sendData(new MultiSet(result))\n }\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const message of this.inputMessages()) {\n for (const [item, multiplicity] of message.getInner()) {\n const [key, value] = item\n this.processElement(key, value, multiplicity, result)\n }\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n processElement(\n key: K,\n value: T,\n multiplicity: number,\n result: Array<[[K, IndexedValue<T>], number]>,\n ): void {\n const changes = this.#state.processElement(key, value, multiplicity)\n handleMoveIn(changes.moveIn, result)\n handleMoveOut(changes.moveOut, result)\n }\n}\n\n/**\n * Limits the number of results based on a comparator, with optional offset.\n * Uses fractional indexing to minimize the number of changes when elements move positions.\n * Each element is assigned a fractional index that is lexicographically sortable.\n * When elements move, only the indices of the moved elements are updated, not all elements.\n *\n * @param comparator - A function that compares two elements\n * @param options - An optional object containing limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function topKWithFractionalIndex<KType extends string | number, T>(\n comparator: (a: T, b: T) => number,\n options?: TopKWithFractionalIndexOptions,\n): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {\n const opts = options || {}\n\n return (\n stream: IStreamBuilder<[KType, T]>,\n ): IStreamBuilder<[KType, IndexedValue<T>]> => {\n const output = new StreamBuilder<[KType, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[KType, IndexedValue<T>]>(),\n )\n const operator = new TopKWithFractionalIndexOperator<KType, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n opts,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n"],"names":[],"mappings":";;;;;AAuBO,MAAM,wCAGH,cAA4C;AAAA,EACpD;AAAA,EAEA,YACE,IACA,QACA,QACA,YACA,SACA;AACA,UAAM,IAAI,QAAQ,MAAM;AACxB,UAAM,QAAQ,QAAQ,SAAS;AAC/B,UAAM,SAAS,QAAQ,UAAU;AACjC,UAAM,OAAO,KAAK;AAAA,MAChB;AAAA,MACA;AAAA,MACA,sBAAsB,UAAU;AAAA,IAAA;AAElC,SAAK,SAAS,IAAI,UAAU,IAAI;AAChC,YAAQ,kBAAkB,MAAM,KAAK,OAAO,IAAI;AAChD,YAAQ,cAAc,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,EAChD;AAAA,EAEU,WACR,QACA,OACA,YACc;AACd,WAAO,IAAI,UAAU,QAAQ,OAAO,UAAU;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,EAAE,QAAQ,SAA8C;AAC/D,UAAM,SAAgD,CAAA;AACtD,UAAM,OAAO,KAAK,OAAO,KAAK,EAAE,QAAQ,OAAO;AAE/C,SAAK,QAAQ,QAAQ,CAAC,WAAW,aAAa,QAAQ,MAAM,CAAC;AAC7D,SAAK,SAAS,QAAQ,CAAC,YAAY,cAAc,SAAS,MAAM,CAAC;AAEjE,QAAI,KAAK,SAAS;AAIhB,WAAK,OAAO,SAAS,IAAI,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAY;AACV,UAAM,SAAgD,CAAA;AACtD,eAAW,WAAW,KAAK,iBAAiB;AAC1C,iBAAW,CAAC,MAAM,YAAY,KAAK,QAAQ,YAAY;AACrD,cAAM,CAAC,KAAK,KAAK,IAAI;AACrB,aAAK,eAAe,KAAK,OAAO,cAAc,MAAM;AAAA,MACtD;AAAA,IACF;AAEA,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO,SAAS,IAAI,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,eACE,KACA,OACA,cACA,QACM;AACN,UAAM,UAAU,KAAK,OAAO,eAAe,KAAK,OAAO,YAAY;AACnE,iBAAa,QAAQ,QAAQ,MAAM;AACnC,kBAAc,QAAQ,SAAS,MAAM;AAAA,EACvC;AACF;AAYO,SAAS,wBACd,YACA,SACqD;AACrD,QAAM,OAAO,WAAW,CAAA;AAExB,SAAO,CACL,WAC6C;AAC7C,UAAM,SAAS,IAAI;AAAA,MACjB,OAAO;AAAA,MACP,IAAI,uBAAA;AAAA,IAAiD;AAEvD,UAAM,WAAW,IAAI;AAAA,MACnB,OAAO,MAAM,kBAAA;AAAA,MACb,OAAO,cAAA;AAAA,MACP,OAAO;AAAA,MACP;AAAA,MACA;AAAA,IAAA;AAEF,WAAO,MAAM,YAAY,QAAQ;AACjC,WAAO;AAAA,EACT;AACF;"}
@@ -1,4 +1,5 @@
1
- import { TopKWithFractionalIndexOperator, IndexedValue, TopK, TopKWithFractionalIndexOptions } from './topKWithFractionalIndex.js';
1
+ import { TopKWithFractionalIndexOperator, TopKWithFractionalIndexOptions } from './topKWithFractionalIndex.js';
2
+ import { IndexedValue, TopK } from './topKArray.js';
2
3
  import { PipedOperator } from '../types.js';
3
4
  export declare function loadBTree(): Promise<void>;
4
5
  /**
@@ -57,3 +57,9 @@ export declare function diffHalfOpen(a: HRange, b: HRange): {
57
57
  * Handles mixed types by ordering strings before numbers.
58
58
  */
59
59
  export declare function compareKeys(a: string | number, b: string | number): number;
60
+ /**
61
+ * Serializes a value for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.
62
+ * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.
63
+ * This is used for creating string keys in groupBy operations.
64
+ */
65
+ export declare function serializeValue(value: unknown): string;
package/dist/esm/utils.js CHANGED
@@ -111,6 +111,17 @@ function compareKeys(a, b) {
111
111
  }
112
112
  return typeof a === `string` ? -1 : 1;
113
113
  }
114
+ function serializeValue(value) {
115
+ return JSON.stringify(value, (_, val) => {
116
+ if (typeof val === "bigint") {
117
+ return val.toString();
118
+ }
119
+ if (val instanceof Date) {
120
+ return val.toISOString();
121
+ }
122
+ return val;
123
+ });
124
+ }
114
125
  export {
115
126
  DefaultMap,
116
127
  ObjectIdGenerator,
@@ -118,6 +129,7 @@ export {
118
129
  chunkedArrayPush,
119
130
  compareKeys,
120
131
  diffHalfOpen,
121
- globalObjectIdGenerator
132
+ globalObjectIdGenerator,
133
+ serializeValue
122
134
  };
123
135
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string,\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>,\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number,\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string,\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>,\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number,\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\n/**\n * Serializes a value for use as a key, handling BigInt and Date values that JSON.stringify cannot handle.\n * Uses JSON.stringify with a replacer function to convert BigInt values to strings and Date values to ISO strings.\n * This is used for creating string keys in groupBy operations.\n */\nexport function serializeValue(value: unknown): string {\n return JSON.stringify(value, (_, val) => {\n if (typeof val === 'bigint') {\n return val.toString()\n }\n if (val instanceof Date) {\n return val.toISOString()\n }\n return val\n })\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/db-ivm",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Incremental View Maintenance for TanStack DB based on Differential Dataflow",
5
5
  "author": "Sam Willis",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -2,4 +2,4 @@ export * from './d2.js'
2
2
  export * from './multiset.js'
3
3
  export * from './operators/index.js'
4
4
  export * from './types.js'
5
- export { compareKeys } from './utils.js'
5
+ export { compareKeys, serializeValue } from './utils.js'
@@ -1,3 +1,4 @@
1
+ import { serializeValue } from '../utils.js'
1
2
  import { map } from './map.js'
2
3
  import { reduce } from './reduce.js'
3
4
  import type { IStreamBuilder, KeyValue } from '../types.js'
@@ -67,7 +68,7 @@ export function groupBy<
67
68
  const withKeysAndValues = stream.pipe(
68
69
  map((data) => {
69
70
  const key = keyExtractor(data)
70
- const keyString = JSON.stringify(key)
71
+ const keyString = serializeValue(key)
71
72
 
72
73
  // Create values object with pre-aggregated values
73
74
  const values: Record<string, unknown> = {}
@@ -0,0 +1,95 @@
1
+ import { groupedTopKWithFractionalIndex } from './groupedTopKWithFractionalIndex.js'
2
+ import { consolidate } from './consolidate.js'
3
+ import type { IStreamBuilder, KeyValue } from '../types.js'
4
+
5
+ export interface GroupedOrderByOptions<Ve> {
6
+ comparator?: (a: Ve, b: Ve) => number
7
+ limit?: number
8
+ offset?: number
9
+ }
10
+
11
+ export interface GroupedOrderByWithFractionalIndexOptions<
12
+ Ve,
13
+ KeyType = unknown,
14
+ ValueType = unknown,
15
+ > extends GroupedOrderByOptions<Ve> {
16
+ setSizeCallback?: (getSize: () => number) => void
17
+ setWindowFn?: (
18
+ windowFn: (options: { offset?: number; limit?: number }) => void,
19
+ ) => void
20
+ /**
21
+ * Function to extract a group key from the element's key and value.
22
+ * Elements with the same group key will be sorted and limited together.
23
+ */
24
+ groupKeyFn: (key: KeyType, value: ValueType) => unknown
25
+ }
26
+
27
+ /**
28
+ * Orders the elements per group and limits the number of results per group, with optional offset and
29
+ * annotates the value with a fractional index.
30
+ * This requires a keyed stream, and uses the `groupedTopKWithFractionalIndex` operator to order elements within each group.
31
+ *
32
+ * Elements are grouped by the provided groupKeyFn, and each group maintains its own sorted collection
33
+ * with independent limit/offset.
34
+ *
35
+ * @param valueExtractor - A function that extracts the value to order by from the element
36
+ * @param options - Configuration including groupKeyFn, comparator, limit, and offset
37
+ * @returns A piped operator that orders the elements per group and limits the number of results per group
38
+ */
39
+ export function groupedOrderByWithFractionalIndex<
40
+ T extends KeyValue<unknown, unknown>,
41
+ Ve = unknown,
42
+ >(
43
+ valueExtractor: (
44
+ value: T extends KeyValue<unknown, infer V> ? V : never,
45
+ ) => Ve,
46
+ options: GroupedOrderByWithFractionalIndexOptions<
47
+ Ve,
48
+ T extends KeyValue<infer K, unknown> ? K : never,
49
+ T extends KeyValue<unknown, infer V> ? V : never
50
+ >,
51
+ ) {
52
+ type KeyType = T extends KeyValue<infer K, unknown> ? K : never
53
+ type ValueType = T extends KeyValue<unknown, infer V> ? V : never
54
+
55
+ const limit = options.limit ?? Infinity
56
+ const offset = options.offset ?? 0
57
+ const setSizeCallback = options.setSizeCallback
58
+ const setWindowFn = options.setWindowFn
59
+ const groupKeyFn = options.groupKeyFn
60
+ const comparator =
61
+ options.comparator ??
62
+ ((a, b) => {
63
+ // Default to JS like ordering
64
+ if (a === b) return 0
65
+ if (a < b) return -1
66
+ return 1
67
+ })
68
+
69
+ return (
70
+ stream: IStreamBuilder<T>,
71
+ ): IStreamBuilder<[KeyType, [ValueType, string]]> => {
72
+ // Cast to the expected key type for groupedTopKWithFractionalIndex
73
+ type StreamKey = KeyType extends string | number ? KeyType : string | number
74
+
75
+ return stream.pipe(
76
+ groupedTopKWithFractionalIndex<StreamKey, ValueType>(
77
+ (a: ValueType, b: ValueType) =>
78
+ comparator(valueExtractor(a), valueExtractor(b)),
79
+ {
80
+ limit,
81
+ offset,
82
+ setSizeCallback,
83
+ setWindowFn,
84
+ groupKeyFn: groupKeyFn as (
85
+ key: StreamKey,
86
+ value: ValueType,
87
+ ) => unknown,
88
+ },
89
+ ) as (
90
+ stream: IStreamBuilder<T>,
91
+ ) => IStreamBuilder<[KeyType, [ValueType, string]]>,
92
+ consolidate(),
93
+ )
94
+ }
95
+ }