@tanstack/db-ivm 0.1.25 → 0.2.0

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 (151) hide show
  1. package/README.md +5 -6
  2. package/dist/cjs/d2.d.cts +3 -3
  3. package/dist/cjs/graph.d.cts +2 -2
  4. package/dist/cjs/hashing/hash.cjs +58 -3
  5. package/dist/cjs/hashing/hash.cjs.map +1 -1
  6. package/dist/cjs/hashing/hash.d.cts +9 -0
  7. package/dist/cjs/hashing/index.d.cts +2 -2
  8. package/dist/cjs/hashing/murmur.cjs +26 -15
  9. package/dist/cjs/hashing/murmur.cjs.map +1 -1
  10. package/dist/cjs/hashing/murmur.d.cts +2 -0
  11. package/dist/cjs/index.cjs +25 -51
  12. package/dist/cjs/index.cjs.map +1 -1
  13. package/dist/cjs/index.d.cts +6 -6
  14. package/dist/cjs/indexes.cjs +52 -16
  15. package/dist/cjs/indexes.cjs.map +1 -1
  16. package/dist/cjs/indexes.d.cts +42 -9
  17. package/dist/cjs/multiset.d.cts +8 -2
  18. package/dist/cjs/operators/concat.d.cts +2 -2
  19. package/dist/cjs/operators/consolidate.d.cts +2 -2
  20. package/dist/cjs/operators/distinct.d.cts +2 -2
  21. package/dist/cjs/operators/filter.d.cts +3 -3
  22. package/dist/cjs/operators/groupBy.cjs +33 -36
  23. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  24. package/dist/cjs/operators/groupBy.d.cts +6 -7
  25. package/dist/cjs/operators/groupedOrderBy.d.cts +8 -2
  26. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs +3 -9
  27. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs.map +1 -1
  28. package/dist/cjs/operators/groupedTopKWithFractionalIndex.d.cts +24 -11
  29. package/dist/cjs/operators/index.d.cts +15 -21
  30. package/dist/cjs/operators/join.cjs +0 -20
  31. package/dist/cjs/operators/join.cjs.map +1 -1
  32. package/dist/cjs/operators/join.d.cts +29 -30
  33. package/dist/cjs/operators/map.d.cts +3 -3
  34. package/dist/cjs/operators/orderBy.cjs +2 -77
  35. package/dist/cjs/operators/orderBy.cjs.map +1 -1
  36. package/dist/cjs/operators/orderBy.d.cts +10 -24
  37. package/dist/cjs/operators/output.d.cts +3 -3
  38. package/dist/cjs/operators/pipe.d.cts +1 -1
  39. package/dist/cjs/operators/reduce.cjs +36 -5
  40. package/dist/cjs/operators/reduce.cjs.map +1 -1
  41. package/dist/cjs/operators/reduce.d.cts +38 -5
  42. package/dist/cjs/operators/tap.d.cts +3 -3
  43. package/dist/cjs/operators/topKArray.cjs +9 -0
  44. package/dist/cjs/operators/topKArray.cjs.map +1 -1
  45. package/dist/cjs/operators/topKArray.d.cts +17 -2
  46. package/dist/cjs/operators/topKState.cjs +3 -0
  47. package/dist/cjs/operators/topKState.cjs.map +1 -1
  48. package/dist/cjs/operators/topKState.d.cts +45 -8
  49. package/dist/cjs/operators/topKWithFractionalIndex.cjs +5 -9
  50. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  51. package/dist/cjs/operators/topKWithFractionalIndex.d.cts +37 -9
  52. package/dist/cjs/types.d.cts +6 -3
  53. package/dist/cjs/utils.cjs.map +1 -1
  54. package/dist/cjs/utils.d.cts +4 -8
  55. package/dist/esm/hashing/hash.d.ts +9 -0
  56. package/dist/esm/hashing/hash.js +60 -5
  57. package/dist/esm/hashing/hash.js.map +1 -1
  58. package/dist/esm/hashing/murmur.d.ts +2 -0
  59. package/dist/esm/hashing/murmur.js +26 -15
  60. package/dist/esm/hashing/murmur.js.map +1 -1
  61. package/dist/esm/index.js +11 -37
  62. package/dist/esm/index.js.map +1 -1
  63. package/dist/esm/indexes.d.ts +4 -1
  64. package/dist/esm/indexes.js +52 -16
  65. package/dist/esm/indexes.js.map +1 -1
  66. package/dist/esm/operators/groupBy.d.ts +1 -5
  67. package/dist/esm/operators/groupBy.js +33 -36
  68. package/dist/esm/operators/groupBy.js.map +1 -1
  69. package/dist/esm/operators/groupedTopKWithFractionalIndex.d.ts +1 -6
  70. package/dist/esm/operators/groupedTopKWithFractionalIndex.js +3 -9
  71. package/dist/esm/operators/groupedTopKWithFractionalIndex.js.map +1 -1
  72. package/dist/esm/operators/index.d.ts +0 -6
  73. package/dist/esm/operators/join.d.ts +1 -26
  74. package/dist/esm/operators/join.js +1 -21
  75. package/dist/esm/operators/join.js.map +1 -1
  76. package/dist/esm/operators/orderBy.d.ts +2 -22
  77. package/dist/esm/operators/orderBy.js +3 -78
  78. package/dist/esm/operators/orderBy.js.map +1 -1
  79. package/dist/esm/operators/reduce.d.ts +11 -2
  80. package/dist/esm/operators/reduce.js +36 -5
  81. package/dist/esm/operators/reduce.js.map +1 -1
  82. package/dist/esm/operators/topKArray.d.ts +6 -0
  83. package/dist/esm/operators/topKArray.js +10 -1
  84. package/dist/esm/operators/topKArray.js.map +1 -1
  85. package/dist/esm/operators/topKState.d.ts +1 -0
  86. package/dist/esm/operators/topKState.js +3 -0
  87. package/dist/esm/operators/topKState.js.map +1 -1
  88. package/dist/esm/operators/topKWithFractionalIndex.d.ts +7 -3
  89. package/dist/esm/operators/topKWithFractionalIndex.js +6 -10
  90. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  91. package/dist/esm/utils.d.ts +0 -7
  92. package/dist/esm/utils.js.map +1 -1
  93. package/package.json +3 -5
  94. package/src/hashing/hash.ts +87 -5
  95. package/src/hashing/murmur.ts +31 -17
  96. package/src/indexes.ts +65 -16
  97. package/src/operators/groupBy.ts +48 -59
  98. package/src/operators/groupedTopKWithFractionalIndex.ts +4 -15
  99. package/src/operators/index.ts +0 -6
  100. package/src/operators/join.ts +1 -91
  101. package/src/operators/orderBy.ts +6 -141
  102. package/src/operators/reduce.ts +62 -5
  103. package/src/operators/topKArray.ts +15 -0
  104. package/src/operators/topKState.ts +7 -0
  105. package/src/operators/topKWithFractionalIndex.ts +15 -16
  106. package/src/utils.ts +0 -30
  107. package/dist/cjs/operators/count.cjs +0 -35
  108. package/dist/cjs/operators/count.cjs.map +0 -1
  109. package/dist/cjs/operators/count.d.cts +0 -13
  110. package/dist/cjs/operators/debug.cjs +0 -39
  111. package/dist/cjs/operators/debug.cjs.map +0 -1
  112. package/dist/cjs/operators/debug.d.cts +0 -16
  113. package/dist/cjs/operators/filterBy.cjs +0 -19
  114. package/dist/cjs/operators/filterBy.cjs.map +0 -1
  115. package/dist/cjs/operators/filterBy.d.cts +0 -9
  116. package/dist/cjs/operators/keying.cjs +0 -16
  117. package/dist/cjs/operators/keying.cjs.map +0 -1
  118. package/dist/cjs/operators/keying.d.cts +0 -19
  119. package/dist/cjs/operators/negate.cjs +0 -27
  120. package/dist/cjs/operators/negate.cjs.map +0 -1
  121. package/dist/cjs/operators/negate.d.cts +0 -13
  122. package/dist/cjs/operators/topK.cjs +0 -39
  123. package/dist/cjs/operators/topK.cjs.map +0 -1
  124. package/dist/cjs/operators/topK.d.cts +0 -31
  125. package/dist/cjs/operators/topKWithFractionalIndexBTree.d.cts +0 -28
  126. package/dist/esm/operators/count.d.ts +0 -13
  127. package/dist/esm/operators/count.js +0 -35
  128. package/dist/esm/operators/count.js.map +0 -1
  129. package/dist/esm/operators/debug.d.ts +0 -16
  130. package/dist/esm/operators/debug.js +0 -39
  131. package/dist/esm/operators/debug.js.map +0 -1
  132. package/dist/esm/operators/filterBy.d.ts +0 -9
  133. package/dist/esm/operators/filterBy.js +0 -19
  134. package/dist/esm/operators/filterBy.js.map +0 -1
  135. package/dist/esm/operators/keying.d.ts +0 -19
  136. package/dist/esm/operators/keying.js +0 -16
  137. package/dist/esm/operators/keying.js.map +0 -1
  138. package/dist/esm/operators/negate.d.ts +0 -13
  139. package/dist/esm/operators/negate.js +0 -27
  140. package/dist/esm/operators/negate.js.map +0 -1
  141. package/dist/esm/operators/topK.d.ts +0 -31
  142. package/dist/esm/operators/topK.js +0 -39
  143. package/dist/esm/operators/topK.js.map +0 -1
  144. package/dist/esm/operators/topKWithFractionalIndexBTree.d.ts +0 -28
  145. package/src/operators/count.ts +0 -51
  146. package/src/operators/debug.ts +0 -57
  147. package/src/operators/filterBy.ts +0 -30
  148. package/src/operators/keying.ts +0 -34
  149. package/src/operators/negate.ts +0 -32
  150. package/src/operators/topK.ts +0 -91
  151. package/src/operators/topKWithFractionalIndexBTree.ts +0 -297
@@ -63,11 +63,8 @@ export interface Hasher {
63
63
  digest: () => number
64
64
  }
65
65
 
66
- // Allocate these once, outside the hot path
67
- const buf = new ArrayBuffer(8)
68
- // dv and u8 are 2 different views on the same buffer `buf`
69
- const dv = new DataView(buf)
70
- const u8 = new Uint8Array(buf)
66
+ // Allocate this once, outside the hot path
67
+ const dv = new DataView(new ArrayBuffer(8))
71
68
 
72
69
  /**
73
70
  * This implementation of Murmur hash uses a random initial seed and random markers.
@@ -106,6 +103,20 @@ export class MurmurHashStream implements Hasher {
106
103
  }
107
104
  }
108
105
 
106
+ /** Write four bytes, low byte first, as four `writeByte` calls would. */
107
+ private writeWord(word: number): void {
108
+ this.length += 4
109
+ if (this.carryBytes === 0) {
110
+ this._mix(word >>> 0)
111
+ return
112
+ }
113
+ // The carry's bytes come first. The word's low bytes complete that block,
114
+ // and its high bytes become the new carry, which keeps its byte count.
115
+ const shift = 8 * this.carryBytes
116
+ this._mix((this.carry | (word << shift)) >>> 0)
117
+ this.carry = word >>> (32 - shift)
118
+ }
119
+
109
120
  update(chunk: symbol | string | number | bigint): void {
110
121
  switch (typeof chunk) {
111
122
  case `symbol`: {
@@ -115,22 +126,25 @@ export class MurmurHashStream implements Hasher {
115
126
  }
116
127
  case `string`:
117
128
  this.update(STRING_MARKER)
118
- for (let i = 0; i < chunk.length; i++) {
119
- const code = chunk.charCodeAt(i)
120
- this.writeByte(code & 0xff)
121
- this.writeByte((code >>> 8) & 0xff)
129
+ {
130
+ // Each code unit is two bytes, so a pair of them is one word.
131
+ const pairs = chunk.length - (chunk.length % 2)
132
+ for (let i = 0; i < pairs; i += 2) {
133
+ this.writeWord(
134
+ chunk.charCodeAt(i) | (chunk.charCodeAt(i + 1) << 16),
135
+ )
136
+ }
137
+ if (pairs < chunk.length) {
138
+ const code = chunk.charCodeAt(pairs)
139
+ this.writeByte(code & 0xff)
140
+ this.writeByte((code >>> 8) & 0xff)
141
+ }
122
142
  }
123
143
  return
124
144
  case `number`:
125
145
  dv.setFloat64(0, chunk, true) // fixed little-endian
126
- this.writeByte(u8[0]!)
127
- this.writeByte(u8[1]!)
128
- this.writeByte(u8[2]!)
129
- this.writeByte(u8[3]!)
130
- this.writeByte(u8[4]!)
131
- this.writeByte(u8[5]!)
132
- this.writeByte(u8[6]!)
133
- this.writeByte(u8[7]!)
146
+ this.writeWord(dv.getUint32(0, true))
147
+ this.writeWord(dv.getUint32(4, true))
134
148
  return
135
149
  case `bigint`: {
136
150
  let value = chunk
package/src/indexes.ts CHANGED
@@ -58,8 +58,13 @@ class PrefixMap<TValue, TPrefix> extends Map<
58
58
  > {
59
59
  /**
60
60
  * Add a value to the PrefixMap. Returns true if the map becomes empty after the operation.
61
+ * With `prefixIdentity`, values that share a prefix are the same value.
61
62
  */
62
- addValue(value: TValue, multiplicity: number): boolean {
63
+ addValue(
64
+ value: TValue,
65
+ multiplicity: number,
66
+ prefixIdentity = false,
67
+ ): boolean {
63
68
  if (multiplicity === 0) return this.size === 0
64
69
 
65
70
  const prefix = getPrefix<TValue, TPrefix>(value)
@@ -73,7 +78,11 @@ class PrefixMap<TValue, TPrefix> extends Map<
73
78
  throw new Error(`Mismatching prefixes, this should never happen`)
74
79
  }
75
80
 
76
- if (currentValue === value || hash(currentValue) === hash(value)) {
81
+ if (
82
+ currentValue === value ||
83
+ (prefixIdentity && prefix !== NO_PREFIX) ||
84
+ hash(currentValue) === hash(value)
85
+ ) {
77
86
  // Same value, update multiplicity
78
87
  const newMultiplicity = currentMultiplicity + multiplicity
79
88
  if (newMultiplicity === 0) {
@@ -150,10 +159,21 @@ export class Index<TKey, TValue, TPrefix = any> {
150
159
  * hash to identify identical values, storing them in a third level value map.
151
160
  */
152
161
  #inner: IndexMap<TKey, TValue, TPrefix>
153
- #consolidatedMultiplicity: Map<TKey, number> = new Map() // sum of multiplicities per key
162
+ // Sum of multiplicities per key, for presence. Only joins read presence; a
163
+ // reduce index is built without it.
164
+ #consolidatedMultiplicity: Map<TKey, number> | undefined
165
+ /**
166
+ * When true, the producer guarantees that a value's string or number
167
+ * prefix determines the value: two values under one key with an equal
168
+ * prefix are equal, so they merge without a structural hash.
169
+ */
170
+ #prefixIdentity: boolean
154
171
 
155
- constructor() {
172
+ constructor(options?: { prefixIdentity?: boolean; trackPresence?: boolean }) {
156
173
  this.#inner = new Map()
174
+ if (options?.trackPresence ?? true)
175
+ this.#consolidatedMultiplicity = new Map()
176
+ this.#prefixIdentity = options?.prefixIdentity ?? false
157
177
  }
158
178
 
159
179
  /**
@@ -194,6 +214,12 @@ export class Index<TKey, TValue, TPrefix = any> {
194
214
  return this.#inner.size
195
215
  }
196
216
 
217
+ #presence(): Map<TKey, number> {
218
+ if (this.#consolidatedMultiplicity === undefined)
219
+ throw new Error(`This Index does not track presence`)
220
+ return this.#consolidatedMultiplicity
221
+ }
222
+
197
223
  /**
198
224
  * This method checks if the index has a given key.
199
225
  * @param key - The key to check.
@@ -209,7 +235,7 @@ export class Index<TKey, TValue, TPrefix = any> {
209
235
  * @returns True if the key has non-zero consolidated multiplicity, false otherwise.
210
236
  */
211
237
  hasPresence(key: TKey): boolean {
212
- return (this.#consolidatedMultiplicity.get(key) || 0) !== 0
238
+ return (this.#presence().get(key) || 0) !== 0
213
239
  }
214
240
 
215
241
  /**
@@ -218,7 +244,7 @@ export class Index<TKey, TValue, TPrefix = any> {
218
244
  * @returns The consolidated multiplicity for the key.
219
245
  */
220
246
  getConsolidatedMultiplicity(key: TKey): number {
221
- return this.#consolidatedMultiplicity.get(key) || 0
247
+ return this.#presence().get(key) || 0
222
248
  }
223
249
 
224
250
  /**
@@ -226,7 +252,7 @@ export class Index<TKey, TValue, TPrefix = any> {
226
252
  * @returns An iterator of keys with non-zero consolidated multiplicity.
227
253
  */
228
254
  getPresenceKeys(): Iterable<TKey> {
229
- return this.#consolidatedMultiplicity.keys()
255
+ return this.#presence().keys()
230
256
  }
231
257
 
232
258
  /**
@@ -235,7 +261,22 @@ export class Index<TKey, TValue, TPrefix = any> {
235
261
  * @returns An array of value tuples [value, multiplicity].
236
262
  */
237
263
  get(key: TKey): Array<[TValue, number]> {
238
- return [...this.getIterator(key)]
264
+ // Built directly, in getIterator's order: join and reduce call this for
265
+ // every changed key.
266
+ const mapOrSingleValue = this.#inner.get(key)
267
+ if (mapOrSingleValue === undefined) return []
268
+ if (isSingleValue(mapOrSingleValue)) return [mapOrSingleValue]
269
+ if (mapOrSingleValue instanceof ValueMap)
270
+ return [...mapOrSingleValue.values()]
271
+ const result: Array<[TValue, number]> = []
272
+ for (const singleValueOrValueMap of mapOrSingleValue.values()) {
273
+ if (isSingleValue(singleValueOrValueMap))
274
+ result.push(singleValueOrValueMap)
275
+ else
276
+ for (const valueTuple of singleValueOrValueMap.values())
277
+ result.push(valueTuple)
278
+ }
279
+ return result
239
280
  }
240
281
 
241
282
  /**
@@ -303,12 +344,15 @@ export class Index<TKey, TValue, TPrefix = any> {
303
344
  if (multiplicity === 0) return
304
345
 
305
346
  // Update consolidated multiplicity tracking
306
- const newConsolidatedMultiplicity =
307
- (this.#consolidatedMultiplicity.get(key) || 0) + multiplicity
308
- if (newConsolidatedMultiplicity === 0) {
309
- this.#consolidatedMultiplicity.delete(key)
310
- } else {
311
- this.#consolidatedMultiplicity.set(key, newConsolidatedMultiplicity)
347
+ const presence = this.#consolidatedMultiplicity
348
+ if (presence) {
349
+ const newConsolidatedMultiplicity =
350
+ (presence.get(key) || 0) + multiplicity
351
+ if (newConsolidatedMultiplicity === 0) {
352
+ presence.delete(key)
353
+ } else {
354
+ presence.set(key, newConsolidatedMultiplicity)
355
+ }
312
356
  }
313
357
 
314
358
  const mapOrSingleValue = this.#inner.get(key)
@@ -348,7 +392,11 @@ export class Index<TKey, TValue, TPrefix = any> {
348
392
  }
349
393
  } else {
350
394
  // Handle existing PrefixMap
351
- const isEmpty = mapOrSingleValue.addValue(value, multiplicity)
395
+ const isEmpty = mapOrSingleValue.addValue(
396
+ value,
397
+ multiplicity,
398
+ this.#prefixIdentity,
399
+ )
352
400
  if (isEmpty) {
353
401
  this.#inner.delete(key)
354
402
  }
@@ -384,7 +432,8 @@ export class Index<TKey, TValue, TPrefix = any> {
384
432
  // Check if they're the same value by prefix/suffix comparison
385
433
  if (
386
434
  isSamePrefix(currentPrefix, newPrefix) &&
387
- (currentValue === newValue || hash(currentValue) === hash(newValue))
435
+ ((this.#prefixIdentity && newPrefix !== NO_PREFIX) ||
436
+ hash(currentValue) === hash(newValue))
388
437
  ) {
389
438
  const newMultiplicity = currentMultiplicity + multiplicity
390
439
  if (newMultiplicity === 0) {
@@ -1,3 +1,4 @@
1
+ import { structuralIdentity } from '../hashing/hash.js'
1
2
  import { serializeValue } from '../utils.js'
2
3
  import { map } from './map.js'
3
4
  import { reduce } from './reduce.js'
@@ -5,20 +6,12 @@ import type { IStreamBuilder, KeyValue } from '../types.js'
5
6
 
6
7
  type GroupKey = Record<string, unknown>
7
8
 
8
- type BasicAggregateFunction<T, R, V = unknown> = {
9
+ type AggregateFunction<T, R, V = unknown> = {
9
10
  preMap: (data: T) => V
10
11
  reduce: (values: Array<[V, number]>) => V
11
12
  postMap?: (result: V) => R
12
13
  }
13
14
 
14
- type PipedAggregateFunction<T, R> = {
15
- pipe: (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<string, R>>
16
- }
17
-
18
- type AggregateFunction<T, R, V = unknown> =
19
- | BasicAggregateFunction<T, R, V>
20
- | PipedAggregateFunction<T, R>
21
-
22
15
  type ExtractAggregateReturnType<T, A> =
23
16
  A extends AggregateFunction<T, infer R, any> ? R : never
24
17
 
@@ -26,12 +19,6 @@ type AggregatesReturnType<T, A> = {
26
19
  [K in keyof A]: ExtractAggregateReturnType<T, A[K]>
27
20
  }
28
21
 
29
- function isPipedAggregateFunction<T, R>(
30
- aggregate: AggregateFunction<T, R>,
31
- ): aggregate is PipedAggregateFunction<T, R> {
32
- return `pipe` in aggregate
33
- }
34
-
35
22
  /**
36
23
  * Groups data by key and applies multiple aggregate operations
37
24
  * @param keyExtractor Function to extract grouping key from data
@@ -44,27 +31,19 @@ export function groupBy<
44
31
  >(keyExtractor: (data: T) => K, aggregates: A = {} as A) {
45
32
  type ResultType = K & AggregatesReturnType<T, A>
46
33
 
47
- const basicAggregates = Object.fromEntries(
48
- Object.entries(aggregates).filter(
49
- ([_, aggregate]) => !isPipedAggregateFunction(aggregate),
50
- ),
51
- ) as Record<string, BasicAggregateFunction<T, any, any>>
52
-
53
- // @ts-expect-error - TODO: we don't use this yet, but we will
54
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
55
- const pipedAggregates = Object.fromEntries(
56
- Object.entries(aggregates).filter(([_, aggregate]) =>
57
- isPipedAggregateFunction(aggregate),
58
- ),
59
- ) as Record<string, PipedAggregateFunction<T, any>>
60
-
61
34
  return (
62
35
  stream: IStreamBuilder<T>,
63
36
  ): IStreamBuilder<KeyValue<string, ResultType>> => {
64
37
  // Special key to store the original key object
65
38
  const KEY_SENTINEL = Symbol(`original_group_key`)
66
39
 
67
- // First map to extract keys and pre-aggregate values
40
+ const aggregateEntries = Object.entries(aggregates)
41
+
42
+ // First map to extract keys and pre-aggregate values. Each contribution
43
+ // leads with an identity built from its group key and pre-aggregate
44
+ // values when every one of them has one. Within one group the reduce
45
+ // index then merges equal contributions by that identity instead of
46
+ // hashing them. A null identity falls back to the structural hash.
68
47
  const withKeysAndValues = stream.pipe(
69
48
  map((data) => {
70
49
  const key = keyExtractor(data)
@@ -73,51 +52,61 @@ export function groupBy<
73
52
  // Create values object with pre-aggregated values
74
53
  const values: Record<string | symbol, unknown> = {}
75
54
 
76
- // Store the original key object
55
+ // Store the original key object. Contributions whose keys serialize
56
+ // alike but hash apart must not merge, so the key is part of the
57
+ // identity.
77
58
  values[KEY_SENTINEL] = key
78
59
 
79
60
  // Add pre-aggregated values
80
- for (const [name, aggregate] of Object.entries(basicAggregates)) {
81
- values[name] = aggregate.preMap(data)
61
+ let identity: string | null = structuralIdentity(key) ?? null
62
+ for (const [name, aggregate] of aggregateEntries) {
63
+ const value = aggregate.preMap(data)
64
+ values[name] = value
65
+ if (identity === null) continue
66
+ const part = structuralIdentity(value)
67
+ identity = part === undefined ? null : `${identity}|${part}`
82
68
  }
83
69
 
84
- return [keyString, values] as KeyValue<
70
+ return [keyString, [identity, values]] as KeyValue<
85
71
  string,
86
- Record<string | symbol, unknown>
72
+ [string | null, Record<string | symbol, unknown>]
87
73
  >
88
74
  }),
89
75
  )
90
76
 
91
77
  // Then reduce to compute aggregates
92
78
  const reduced = withKeysAndValues.pipe(
93
- reduce((values) => {
94
- // Calculate total multiplicity to check if the group should exist
95
- let totalMultiplicity = 0
96
- for (const [_, multiplicity] of values) {
97
- totalMultiplicity += multiplicity
98
- }
79
+ reduce(
80
+ (contributions) => {
81
+ // Calculate total multiplicity to check if the group should exist
82
+ let totalMultiplicity = 0
83
+ for (const [_, multiplicity] of contributions) {
84
+ totalMultiplicity += multiplicity
85
+ }
99
86
 
100
- // If total multiplicity is 0 or negative, the group should be removed completely
101
- if (totalMultiplicity <= 0) {
102
- return []
103
- }
87
+ // If total multiplicity is 0 or negative, the group should be removed completely
88
+ if (totalMultiplicity <= 0) {
89
+ return []
90
+ }
104
91
 
105
- const result: Record<PropertyKey, unknown> = {}
92
+ const result: Record<PropertyKey, unknown> = {}
106
93
 
107
- // Get the original key from first value in group
108
- const originalKey = values[0]?.[0]?.[KEY_SENTINEL]
109
- result[KEY_SENTINEL] = originalKey
94
+ // Get the original key from first value in group
95
+ const originalKey = contributions[0]?.[0][1][KEY_SENTINEL]
96
+ result[KEY_SENTINEL] = originalKey
110
97
 
111
- // Apply each aggregate function
112
- for (const [name, aggregate] of Object.entries(basicAggregates)) {
113
- const preValues = values.map(
114
- ([v, m]) => [v[name], m] as [any, number],
115
- )
116
- result[name] = aggregate.reduce(preValues)
117
- }
98
+ // Apply each aggregate function
99
+ for (const [name, aggregate] of aggregateEntries) {
100
+ const preValues = contributions.map(
101
+ ([[, v], m]) => [v[name], m] as [any, number],
102
+ )
103
+ result[name] = aggregate.reduce(preValues)
104
+ }
118
105
 
119
- return [[result, 1]]
120
- }),
106
+ return [[result, 1]]
107
+ },
108
+ { prefixIdentity: true },
109
+ ),
121
110
  )
122
111
 
123
112
  // Finally map to extract the key and include all values
@@ -133,7 +122,7 @@ export function groupBy<
133
122
  Object.assign(result, key)
134
123
 
135
124
  // Apply postMap if provided
136
- for (const [name, aggregate] of Object.entries(basicAggregates)) {
125
+ for (const [name, aggregate] of aggregateEntries) {
137
126
  if (aggregate.postMap) {
138
127
  result[name] = aggregate.postMap(values[name])
139
128
  } else {
@@ -8,7 +8,7 @@ import {
8
8
  topKBatch,
9
9
  } from './topKState.js'
10
10
  import { TopKArray, createKeyedComparator } from './topKArray.js'
11
- import type { IndexedValue, TopK } from './topKArray.js'
11
+ import type { IndexedValue } from './topKArray.js'
12
12
  import type { DifferenceStreamReader } from '../graph.js'
13
13
  import type { IStreamBuilder, PipedOperator } from '../types.js'
14
14
 
@@ -61,18 +61,6 @@ export class GroupedTopKWithFractionalIndexOperator<
61
61
  options.setWindowFn?.(this.#moveTopK.bind(this))
62
62
  }
63
63
 
64
- /**
65
- * Creates a new TopK data structure for a group.
66
- * Can be overridden in subclasses to use different implementations (e.g., B+ tree).
67
- */
68
- protected createTopK(
69
- offset: number,
70
- limit: number,
71
- comparator: (a: [K, T], b: [K, T]) => number,
72
- ): TopK<[K, T]> {
73
- return new TopKArray(offset, limit, comparator)
74
- }
75
-
76
64
  #getTotalSize(): number {
77
65
  let size = 0
78
66
  for (const state of this.#groupStates.values()) {
@@ -84,8 +72,9 @@ export class GroupedTopKWithFractionalIndexOperator<
84
72
  #getOrCreateGroupState(groupKey: unknown): TopKState<K, T> {
85
73
  let state = this.#groupStates.get(groupKey)
86
74
  if (!state) {
87
- const topK = this.createTopK(this.#offset, this.#limit, this.#comparator)
88
- state = new TopKState(topK)
75
+ state = new TopKState(
76
+ new TopKArray(this.#offset, this.#limit, this.#comparator),
77
+ )
89
78
  this.#groupStates.set(groupKey, state)
90
79
  }
91
80
  return state
@@ -2,20 +2,14 @@ export * from './pipe.js'
2
2
  export * from './map.js'
3
3
  export * from './tap.js'
4
4
  export * from './filter.js'
5
- export * from './negate.js'
6
5
  export * from './concat.js'
7
- export * from './debug.js'
8
6
  export * from './output.js'
9
7
  export * from './consolidate.js'
10
8
  export * from './join.js'
11
9
  export * from './reduce.js'
12
- export * from './count.js'
13
10
  export * from './distinct.js'
14
- export * from './keying.js'
15
- export * from './topK.js'
16
11
  export * from './topKWithFractionalIndex.js'
17
12
  export * from './groupedTopKWithFractionalIndex.js'
18
13
  export * from './orderBy.js'
19
14
  export * from './groupedOrderBy.js'
20
- export * from './filterBy.js'
21
15
  export { groupBy, groupByOperators } from './groupBy.js'
@@ -254,7 +254,7 @@ export class JoinOperator<K, V1, V2> extends BinaryOperator<
254
254
  */
255
255
  export function join<
256
256
  K,
257
- V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
257
+ V1 extends (T extends KeyValue<infer _KT, infer VT> ? VT : never),
258
258
  V2,
259
259
  T,
260
260
  >(
@@ -282,93 +282,3 @@ export function join<
282
282
  return output
283
283
  }
284
284
  }
285
-
286
- /**
287
- * Joins two input streams (inner join)
288
- * @param other - The other stream to join with
289
- */
290
- export function innerJoin<
291
- K,
292
- V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
293
- V2,
294
- T,
295
- >(
296
- other: IStreamBuilder<KeyValue<K, V2>>,
297
- ): PipedOperator<T, KeyValue<K, [V1, V2]>> {
298
- return join(other, `inner`) as unknown as PipedOperator<
299
- T,
300
- KeyValue<K, [V1, V2]>
301
- >
302
- }
303
-
304
- /**
305
- * Joins two input streams (anti join)
306
- * @param other - The other stream to join with
307
- */
308
- export function antiJoin<
309
- K,
310
- V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
311
- V2,
312
- T,
313
- >(
314
- other: IStreamBuilder<KeyValue<K, V2>>,
315
- ): PipedOperator<T, KeyValue<K, [V1, null]>> {
316
- return join(other, `anti`) as unknown as PipedOperator<
317
- T,
318
- KeyValue<K, [V1, null]>
319
- >
320
- }
321
-
322
- /**
323
- * Joins two input streams (left join)
324
- * @param other - The other stream to join with
325
- */
326
- export function leftJoin<
327
- K,
328
- V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
329
- V2,
330
- T,
331
- >(
332
- other: IStreamBuilder<KeyValue<K, V2>>,
333
- ): PipedOperator<T, KeyValue<K, [V1, V2 | null]>> {
334
- return join(other, `left`) as unknown as PipedOperator<
335
- T,
336
- KeyValue<K, [V1, V2 | null]>
337
- >
338
- }
339
-
340
- /**
341
- * Joins two input streams (right join)
342
- * @param other - The other stream to join with
343
- */
344
- export function rightJoin<
345
- K,
346
- V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
347
- V2,
348
- T,
349
- >(
350
- other: IStreamBuilder<KeyValue<K, V2>>,
351
- ): PipedOperator<T, KeyValue<K, [V1 | null, V2]>> {
352
- return join(other, `right`) as unknown as PipedOperator<
353
- T,
354
- KeyValue<K, [V1 | null, V2]>
355
- >
356
- }
357
-
358
- /**
359
- * Joins two input streams (full join)
360
- * @param other - The other stream to join with
361
- */
362
- export function fullJoin<
363
- K,
364
- V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
365
- V2,
366
- T,
367
- >(
368
- other: IStreamBuilder<KeyValue<K, V2>>,
369
- ): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {
370
- return join(other, `full`) as unknown as PipedOperator<
371
- T,
372
- KeyValue<K, [V1 | null, V2 | null]>
373
- >
374
- }