@tanstack/db-ivm 0.1.9 → 0.1.10

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 (85) hide show
  1. package/dist/cjs/d2.cjs +25 -49
  2. package/dist/cjs/d2.cjs.map +1 -1
  3. package/dist/cjs/graph.cjs +8 -20
  4. package/dist/cjs/graph.cjs.map +1 -1
  5. package/dist/cjs/indexes.cjs +118 -79
  6. package/dist/cjs/indexes.cjs.map +1 -1
  7. package/dist/cjs/indexes.d.cts +23 -0
  8. package/dist/cjs/multiset.cjs +106 -115
  9. package/dist/cjs/multiset.cjs.map +1 -1
  10. package/dist/cjs/multiset.d.cts +1 -0
  11. package/dist/cjs/operators/debug.cjs +5 -15
  12. package/dist/cjs/operators/debug.cjs.map +1 -1
  13. package/dist/cjs/operators/distinct.cjs +11 -22
  14. package/dist/cjs/operators/distinct.cjs.map +1 -1
  15. package/dist/cjs/operators/filter.cjs +3 -12
  16. package/dist/cjs/operators/filter.cjs.map +1 -1
  17. package/dist/cjs/operators/filterBy.cjs.map +1 -1
  18. package/dist/cjs/operators/filterBy.d.cts +1 -2
  19. package/dist/cjs/operators/groupBy.cjs +1 -2
  20. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  21. package/dist/cjs/operators/join.cjs +104 -93
  22. package/dist/cjs/operators/join.cjs.map +1 -1
  23. package/dist/cjs/operators/join.d.cts +11 -8
  24. package/dist/cjs/operators/map.cjs +3 -12
  25. package/dist/cjs/operators/map.cjs.map +1 -1
  26. package/dist/cjs/operators/orderBy.cjs +10 -10
  27. package/dist/cjs/operators/orderBy.cjs.map +1 -1
  28. package/dist/cjs/operators/orderBy.d.cts +1 -2
  29. package/dist/cjs/operators/output.cjs +3 -12
  30. package/dist/cjs/operators/output.cjs.map +1 -1
  31. package/dist/cjs/operators/reduce.cjs +11 -22
  32. package/dist/cjs/operators/reduce.cjs.map +1 -1
  33. package/dist/cjs/operators/tap.cjs +3 -12
  34. package/dist/cjs/operators/tap.cjs.map +1 -1
  35. package/dist/cjs/operators/topK.cjs +4 -4
  36. package/dist/cjs/operators/topK.cjs.map +1 -1
  37. package/dist/cjs/operators/topK.d.cts +1 -2
  38. package/dist/cjs/operators/topKWithFractionalIndex.cjs +56 -75
  39. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  40. package/dist/esm/d2.js +25 -49
  41. package/dist/esm/d2.js.map +1 -1
  42. package/dist/esm/graph.js +8 -20
  43. package/dist/esm/graph.js.map +1 -1
  44. package/dist/esm/indexes.d.ts +23 -0
  45. package/dist/esm/indexes.js +118 -79
  46. package/dist/esm/indexes.js.map +1 -1
  47. package/dist/esm/multiset.d.ts +1 -0
  48. package/dist/esm/multiset.js +106 -115
  49. package/dist/esm/multiset.js.map +1 -1
  50. package/dist/esm/operators/debug.js +5 -15
  51. package/dist/esm/operators/debug.js.map +1 -1
  52. package/dist/esm/operators/distinct.js +11 -22
  53. package/dist/esm/operators/distinct.js.map +1 -1
  54. package/dist/esm/operators/filter.js +3 -12
  55. package/dist/esm/operators/filter.js.map +1 -1
  56. package/dist/esm/operators/filterBy.d.ts +1 -2
  57. package/dist/esm/operators/filterBy.js.map +1 -1
  58. package/dist/esm/operators/groupBy.js +1 -2
  59. package/dist/esm/operators/groupBy.js.map +1 -1
  60. package/dist/esm/operators/join.d.ts +11 -8
  61. package/dist/esm/operators/join.js +104 -93
  62. package/dist/esm/operators/join.js.map +1 -1
  63. package/dist/esm/operators/map.js +3 -12
  64. package/dist/esm/operators/map.js.map +1 -1
  65. package/dist/esm/operators/orderBy.d.ts +1 -2
  66. package/dist/esm/operators/orderBy.js +10 -10
  67. package/dist/esm/operators/orderBy.js.map +1 -1
  68. package/dist/esm/operators/output.js +3 -12
  69. package/dist/esm/operators/output.js.map +1 -1
  70. package/dist/esm/operators/reduce.js +11 -22
  71. package/dist/esm/operators/reduce.js.map +1 -1
  72. package/dist/esm/operators/tap.js +3 -12
  73. package/dist/esm/operators/tap.js.map +1 -1
  74. package/dist/esm/operators/topK.d.ts +1 -2
  75. package/dist/esm/operators/topK.js +4 -4
  76. package/dist/esm/operators/topK.js.map +1 -1
  77. package/dist/esm/operators/topKWithFractionalIndex.js +56 -75
  78. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/indexes.ts +54 -0
  81. package/src/multiset.ts +6 -0
  82. package/src/operators/filterBy.ts +1 -2
  83. package/src/operators/join.ts +249 -139
  84. package/src/operators/orderBy.ts +1 -2
  85. package/src/operators/topK.ts +1 -2
@@ -1,10 +1,57 @@
1
+ /**
2
+ * # Direct Join Algorithms for Incremental View Maintenance
3
+ *
4
+ * High-performance join operations implementing all join types (inner, left, right, full, anti)
5
+ * with minimal state and optimized performance.
6
+ *
7
+ * ## Algorithm
8
+ *
9
+ * For each tick, the algorithm processes incoming changes (deltas) and emits join results:
10
+ *
11
+ * 1. **Build deltas**: Create delta indexes from input messages using `Index.fromMultiSet()`
12
+ * 2. **Inner results**: Emit `ΔA⋈B_old + A_old⋈ΔB + ΔA⋈ΔB` (matched pairs)
13
+ * 3. **Outer results**: For unmatched rows, emit null-extended tuples:
14
+ * - New unmatched rows from deltas (when opposite side empty)
15
+ * - Presence transitions: when key goes `0→>0` (retract nulls) or `>0→0` (emit nulls)
16
+ * 4. **Update state**: Append deltas to indexes (consolidated multiplicity tracking automatic)
17
+ *
18
+ * **Consolidated multiplicity tracking** enables O(1) presence checks instead of scanning index buckets.
19
+ *
20
+ * ## State
21
+ *
22
+ * **Indexes** store the actual data:
23
+ * - `indexA: Index<K, V1>` - all left-side rows accumulated over time
24
+ * - `indexB: Index<K, V2>` - all right-side rows accumulated over time
25
+ *
26
+ * **Consolidated multiplicity tracking** (built into Index):
27
+ * - Each Index maintains sum of multiplicities per key internally
28
+ * - Provides O(1) presence checks: `index.hasPresence(key)` and `index.getConsolidatedMultiplicity(key)`
29
+ * - Avoids scanning entire index buckets just to check if key has any rows
30
+ *
31
+ * ## Join Types
32
+ *
33
+ * - **Inner**: Standard delta terms only
34
+ * - **Outer**: Inner results + null-extended unmatched rows with transition handling
35
+ * - **Anti**: Unmatched rows only (no inner results)
36
+ *
37
+ * ## Key Optimizations
38
+ *
39
+ * - **No temp copying**: Uses `(A⊎ΔA)⋈ΔB = A⋈ΔB ⊎ ΔA⋈ΔB` distributive property
40
+ * - **Early-out checks**: Skip phases when no deltas present
41
+ * - **Zero-entry pruning**: Keep maps compact, O(distinct keys) memory
42
+ * - **Final presence logic**: Avoid emit→retract churn within same tick
43
+ *
44
+ * ## Correctness
45
+ *
46
+ * - **Ordering**: Pre-append snapshots for emissions, post-emit state updates
47
+ * - **Presence**: Key matched iff mass ≠ 0, transitions trigger null handling
48
+ * - **Bag semantics**: Proper multiplicity handling including negatives
49
+ */
50
+
1
51
  import { BinaryOperator, DifferenceStreamWriter } from "../graph.js"
2
52
  import { StreamBuilder } from "../d2.js"
3
53
  import { MultiSet } from "../multiset.js"
4
54
  import { Index } from "../indexes.js"
5
- import { negate } from "./negate.js"
6
- import { map } from "./map.js"
7
- import { concat } from "./concat.js"
8
55
  import type { DifferenceStreamReader } from "../graph.js"
9
56
  import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js"
10
57
 
@@ -14,66 +61,189 @@ import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js"
14
61
  export type JoinType = `inner` | `left` | `right` | `full` | `anti`
15
62
 
16
63
  /**
17
- * Operator that joins two input streams
64
+ * Operator that joins two input streams using direct join algorithms
18
65
  */
19
66
  export class JoinOperator<K, V1, V2> extends BinaryOperator<
20
- [K, V1] | [K, V2] | [K, [V1, V2]]
67
+ [K, V1] | [K, V2] | [K, [V1, V2]] | [K, [V1 | null, V2 | null]]
21
68
  > {
22
69
  #indexA = new Index<K, V1>()
23
70
  #indexB = new Index<K, V2>()
71
+ #mode: JoinType
24
72
 
25
73
  constructor(
26
74
  id: number,
27
75
  inputA: DifferenceStreamReader<[K, V1]>,
28
76
  inputB: DifferenceStreamReader<[K, V2]>,
29
- output: DifferenceStreamWriter<[K, [V1, V2]]>
77
+ output: DifferenceStreamWriter<any>,
78
+ mode: JoinType = `inner`
30
79
  ) {
31
80
  super(id, inputA, inputB, output)
81
+ this.#mode = mode
32
82
  }
33
83
 
34
84
  run(): void {
35
- const deltaA = new Index<K, V1>()
36
- const deltaB = new Index<K, V2>()
85
+ // Build deltas from input messages
86
+ const deltaA = Index.fromMultiSets<K, V1>(
87
+ this.inputAMessages() as Array<MultiSet<[K, V1]>>
88
+ )
89
+ const deltaB = Index.fromMultiSets<K, V2>(
90
+ this.inputBMessages() as Array<MultiSet<[K, V2]>>
91
+ )
37
92
 
38
- // Process input A - process ALL messages, not just the first one
39
- const messagesA = this.inputAMessages()
40
- for (const message of messagesA) {
41
- const multiSetMessage = message as unknown as MultiSet<[K, V1]>
42
- for (const [item, multiplicity] of multiSetMessage.getInner()) {
43
- const [key, value] = item
44
- deltaA.addValue(key, [value, multiplicity])
45
- }
46
- }
93
+ // Early-out if nothing changed
94
+ if (deltaA.size === 0 && deltaB.size === 0) return
47
95
 
48
- // Process input B - process ALL messages, not just the first one
49
- const messagesB = this.inputBMessages()
50
- for (const message of messagesB) {
51
- const multiSetMessage = message as unknown as MultiSet<[K, V2]>
52
- for (const [item, multiplicity] of multiSetMessage.getInner()) {
53
- const [key, value] = item
54
- deltaB.addValue(key, [value, multiplicity])
55
- }
96
+ const results = new MultiSet<any>()
97
+
98
+ // Emit inner results (all modes except anti)
99
+ if (this.#mode !== `anti`) {
100
+ this.emitInnerResults(deltaA, deltaB, results)
56
101
  }
57
102
 
58
- // Process results
59
- const results = new MultiSet<[K, [V1, V2]]>()
103
+ // Emit left outer/anti results
104
+ if (
105
+ this.#mode === `left` ||
106
+ this.#mode === `full` ||
107
+ this.#mode === `anti`
108
+ ) {
109
+ this.emitLeftOuterResults(deltaA, deltaB, results)
110
+ }
60
111
 
61
- // Join deltaA with existing indexB
62
- results.extend(deltaA.join(this.#indexB))
112
+ // Emit right outer results
113
+ if (this.#mode === `right` || this.#mode === `full`) {
114
+ this.emitRightOuterResults(deltaA, deltaB, results)
115
+ }
63
116
 
64
- // Append deltaA to indexA
117
+ // Update state and send results
118
+ // IMPORTANT: All emissions use pre-append snapshots of indexA/indexB.
119
+ // Now append ALL deltas to indices - this happens unconditionally for every key,
120
+ // regardless of whether presence flipped. Consolidated multiplicity tracking is automatic.
65
121
  this.#indexA.append(deltaA)
66
-
67
- // Join existing indexA with deltaB
68
- results.extend(this.#indexA.join(deltaB))
122
+ this.#indexB.append(deltaB)
69
123
 
70
124
  // Send results
71
125
  if (results.getInner().length > 0) {
72
126
  this.output.sendData(results)
73
127
  }
128
+ }
74
129
 
75
- // Append deltaB to indexB
76
- this.#indexB.append(deltaB)
130
+ private emitInnerResults(
131
+ deltaA: Index<K, V1>,
132
+ deltaB: Index<K, V2>,
133
+ results: MultiSet<any>
134
+ ): void {
135
+ // Emit the three standard delta terms: ΔA⋈B_old, A_old⋈ΔB, ΔA⋈ΔB
136
+ if (deltaA.size > 0) results.extend(deltaA.join(this.#indexB))
137
+ if (deltaB.size > 0) results.extend(this.#indexA.join(deltaB))
138
+ if (deltaA.size > 0 && deltaB.size > 0) results.extend(deltaA.join(deltaB))
139
+ }
140
+
141
+ private emitLeftOuterResults(
142
+ deltaA: Index<K, V1>,
143
+ deltaB: Index<K, V2>,
144
+ results: MultiSet<any>
145
+ ): void {
146
+ // Emit unmatched left rows from deltaA
147
+ if (deltaA.size > 0) {
148
+ for (const [key, valueIterator] of deltaA.entriesIterators()) {
149
+ const currentMultiplicityB =
150
+ this.#indexB.getConsolidatedMultiplicity(key)
151
+ const deltaMultiplicityB = deltaB.getConsolidatedMultiplicity(key)
152
+ const finalMultiplicityB = currentMultiplicityB + deltaMultiplicityB
153
+
154
+ if (finalMultiplicityB === 0) {
155
+ for (const [value, multiplicity] of valueIterator) {
156
+ if (multiplicity !== 0) {
157
+ results.add([key, [value, null]], multiplicity)
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ // Handle presence transitions from right side changes
165
+ if (deltaB.size > 0) {
166
+ for (const key of deltaB.getPresenceKeys()) {
167
+ const before = this.#indexB.getConsolidatedMultiplicity(key)
168
+ const deltaMult = deltaB.getConsolidatedMultiplicity(key)
169
+ if (deltaMult === 0) continue
170
+ const after = before + deltaMult
171
+
172
+ // Skip transition handling if presence doesn't flip (both zero or both non-zero)
173
+ // Note: Index updates happen later regardless - we're only skipping null-extension emissions here
174
+ if ((before === 0) === (after === 0)) continue
175
+
176
+ // Determine the type of transition:
177
+ // - 0 → non-zero: Right becomes non-empty, left rows transition from unmatched to matched
178
+ // → RETRACT previously emitted null-extended rows (emit with negative multiplicity)
179
+ // - non-zero → 0: Right becomes empty, left rows transition from matched to unmatched
180
+ // → EMIT new null-extended rows (emit with positive multiplicity)
181
+ const transitioningToMatched = before === 0
182
+
183
+ for (const [value, multiplicity] of this.#indexA.getIterator(key)) {
184
+ if (multiplicity !== 0) {
185
+ results.add(
186
+ [key, [value, null]],
187
+ transitioningToMatched ? -multiplicity : +multiplicity
188
+ )
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ private emitRightOuterResults(
196
+ deltaA: Index<K, V1>,
197
+ deltaB: Index<K, V2>,
198
+ results: MultiSet<any>
199
+ ): void {
200
+ // Emit unmatched right rows from deltaB
201
+ if (deltaB.size > 0) {
202
+ for (const [key, valueIterator] of deltaB.entriesIterators()) {
203
+ const currentMultiplicityA =
204
+ this.#indexA.getConsolidatedMultiplicity(key)
205
+ const deltaMultiplicityA = deltaA.getConsolidatedMultiplicity(key)
206
+ const finalMultiplicityA = currentMultiplicityA + deltaMultiplicityA
207
+
208
+ if (finalMultiplicityA === 0) {
209
+ for (const [value, multiplicity] of valueIterator) {
210
+ if (multiplicity !== 0) {
211
+ results.add([key, [null, value]], multiplicity)
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ // Handle presence transitions from left side changes
219
+ if (deltaA.size > 0) {
220
+ for (const key of deltaA.getPresenceKeys()) {
221
+ const before = this.#indexA.getConsolidatedMultiplicity(key)
222
+ const deltaMult = deltaA.getConsolidatedMultiplicity(key)
223
+ if (deltaMult === 0) continue
224
+ const after = before + deltaMult
225
+
226
+ // Skip transition handling if presence doesn't flip (both zero or both non-zero)
227
+ // Note: Index updates happen later regardless - we're only skipping null-extension emissions here
228
+ if ((before === 0) === (after === 0)) continue
229
+
230
+ // Determine the type of transition:
231
+ // - 0 → non-zero: Left becomes non-empty, right rows transition from unmatched to matched
232
+ // → RETRACT previously emitted null-extended rows (emit with negative multiplicity)
233
+ // - non-zero → 0: Left becomes empty, right rows transition from matched to unmatched
234
+ // → EMIT new null-extended rows (emit with positive multiplicity)
235
+ const transitioningToMatched = before === 0
236
+
237
+ for (const [value, multiplicity] of this.#indexB.getIterator(key)) {
238
+ if (multiplicity !== 0) {
239
+ results.add(
240
+ [key, [null, value]],
241
+ transitioningToMatched ? -multiplicity : +multiplicity
242
+ )
243
+ }
244
+ }
245
+ }
246
+ }
77
247
  }
78
248
  }
79
249
 
@@ -91,39 +261,30 @@ export function join<
91
261
  other: IStreamBuilder<KeyValue<K, V2>>,
92
262
  type: JoinType = `inner`
93
263
  ): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {
94
- switch (type) {
95
- case `inner`:
96
- return innerJoin(other) as unknown as PipedOperator<
97
- T,
98
- KeyValue<K, [V1, V2]>
99
- >
100
- case `anti`:
101
- return antiJoin(other) as unknown as PipedOperator<
102
- T,
103
- KeyValue<K, [V1, null]>
104
- >
105
- case `left`:
106
- return leftJoin(other) as unknown as PipedOperator<
107
- T,
108
- KeyValue<K, [V1, V2 | null]>
109
- >
110
- case `right`:
111
- return rightJoin(other) as unknown as PipedOperator<
112
- T,
113
- KeyValue<K, [V1 | null, V2]>
114
- >
115
- case `full`:
116
- return fullJoin(other) as unknown as PipedOperator<
117
- T,
118
- KeyValue<K, [V1 | null, V2 | null]>
119
- >
120
- default:
121
- throw new Error(`Join type ${type} is invalid`)
264
+ return (
265
+ stream: IStreamBuilder<T>
266
+ ): IStreamBuilder<KeyValue<K, [V1 | null, V2 | null]>> => {
267
+ if (stream.graph !== other.graph) {
268
+ throw new Error(`Cannot join streams from different graphs`)
269
+ }
270
+ const output = new StreamBuilder<KeyValue<K, [V1 | null, V2 | null]>>(
271
+ stream.graph,
272
+ new DifferenceStreamWriter<KeyValue<K, [V1 | null, V2 | null]>>()
273
+ )
274
+ const operator = new JoinOperator<K, V1, V2>(
275
+ stream.graph.getNextOperatorId(),
276
+ stream.connectReader() as DifferenceStreamReader<KeyValue<K, V1>>,
277
+ other.connectReader(),
278
+ output.writer,
279
+ type
280
+ )
281
+ stream.graph.addOperator(operator)
282
+ return output
122
283
  }
123
284
  }
124
285
 
125
286
  /**
126
- * Joins two input streams
287
+ * Joins two input streams (inner join)
127
288
  * @param other - The other stream to join with
128
289
  */
129
290
  export function innerJoin<
@@ -134,27 +295,14 @@ export function innerJoin<
134
295
  >(
135
296
  other: IStreamBuilder<KeyValue<K, V2>>
136
297
  ): PipedOperator<T, KeyValue<K, [V1, V2]>> {
137
- return (stream: IStreamBuilder<T>): IStreamBuilder<KeyValue<K, [V1, V2]>> => {
138
- if (stream.graph !== other.graph) {
139
- throw new Error(`Cannot join streams from different graphs`)
140
- }
141
- const output = new StreamBuilder<KeyValue<K, [V1, V2]>>(
142
- stream.graph,
143
- new DifferenceStreamWriter<KeyValue<K, [V1, V2]>>()
144
- )
145
- const operator = new JoinOperator<K, V1, V2>(
146
- stream.graph.getNextOperatorId(),
147
- stream.connectReader() as DifferenceStreamReader<KeyValue<K, V1>>,
148
- other.connectReader(),
149
- output.writer
150
- )
151
- stream.graph.addOperator(operator)
152
- return output
153
- }
298
+ return join(other, `inner`) as unknown as PipedOperator<
299
+ T,
300
+ KeyValue<K, [V1, V2]>
301
+ >
154
302
  }
155
303
 
156
304
  /**
157
- * Joins two input streams
305
+ * Joins two input streams (anti join)
158
306
  * @param other - The other stream to join with
159
307
  */
160
308
  export function antiJoin<
@@ -165,24 +313,14 @@ export function antiJoin<
165
313
  >(
166
314
  other: IStreamBuilder<KeyValue<K, V2>>
167
315
  ): PipedOperator<T, KeyValue<K, [V1, null]>> {
168
- return (
169
- stream: IStreamBuilder<T>
170
- ): IStreamBuilder<KeyValue<K, [V1, null]>> => {
171
- const matchedLeft = stream.pipe(
172
- innerJoin(other),
173
- map(([key, [valueLeft, _valueRight]]) => [key, valueLeft])
174
- )
175
- const anti = stream.pipe(
176
- concat(matchedLeft.pipe(negate())),
177
- // @ts-ignore TODO: fix this
178
- map(([key, value]) => [key, [value, null]])
179
- )
180
- return anti as IStreamBuilder<KeyValue<K, [V1, null]>>
181
- }
316
+ return join(other, `anti`) as unknown as PipedOperator<
317
+ T,
318
+ KeyValue<K, [V1, null]>
319
+ >
182
320
  }
183
321
 
184
322
  /**
185
- * Joins two input streams
323
+ * Joins two input streams (left join)
186
324
  * @param other - The other stream to join with
187
325
  */
188
326
  export function leftJoin<
@@ -193,21 +331,14 @@ export function leftJoin<
193
331
  >(
194
332
  other: IStreamBuilder<KeyValue<K, V2>>
195
333
  ): PipedOperator<T, KeyValue<K, [V1, V2 | null]>> {
196
- return (
197
- stream: IStreamBuilder<T>
198
- ): IStreamBuilder<KeyValue<K, [V1, V2 | null]>> => {
199
- const left = stream
200
- const right = other
201
- const inner = left.pipe(innerJoin(right))
202
- const anti = left.pipe(antiJoin(right))
203
- return inner.pipe(concat(anti)) as IStreamBuilder<
204
- KeyValue<K, [V1, V2 | null]>
205
- >
206
- }
334
+ return join(other, `left`) as unknown as PipedOperator<
335
+ T,
336
+ KeyValue<K, [V1, V2 | null]>
337
+ >
207
338
  }
208
339
 
209
340
  /**
210
- * Joins two input streams
341
+ * Joins two input streams (right join)
211
342
  * @param other - The other stream to join with
212
343
  */
213
344
  export function rightJoin<
@@ -218,24 +349,14 @@ export function rightJoin<
218
349
  >(
219
350
  other: IStreamBuilder<KeyValue<K, V2>>
220
351
  ): PipedOperator<T, KeyValue<K, [V1 | null, V2]>> {
221
- return (
222
- stream: IStreamBuilder<T>
223
- ): IStreamBuilder<KeyValue<K, [V1 | null, V2]>> => {
224
- const left = stream as IStreamBuilder<KeyValue<K, V1>>
225
- const right = other
226
- const inner = left.pipe(innerJoin(right))
227
- const anti = right.pipe(
228
- antiJoin(left),
229
- map(([key, [a, b]]) => [key, [b, a]])
230
- )
231
- return inner.pipe(concat(anti)) as IStreamBuilder<
232
- KeyValue<K, [V1 | null, V2]>
233
- >
234
- }
352
+ return join(other, `right`) as unknown as PipedOperator<
353
+ T,
354
+ KeyValue<K, [V1 | null, V2]>
355
+ >
235
356
  }
236
357
 
237
358
  /**
238
- * Joins two input streams
359
+ * Joins two input streams (full join)
239
360
  * @param other - The other stream to join with
240
361
  */
241
362
  export function fullJoin<
@@ -246,19 +367,8 @@ export function fullJoin<
246
367
  >(
247
368
  other: IStreamBuilder<KeyValue<K, V2>>
248
369
  ): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {
249
- return (
250
- stream: IStreamBuilder<T>
251
- ): IStreamBuilder<KeyValue<K, [V1 | null, V2 | null]>> => {
252
- const left = stream as IStreamBuilder<KeyValue<K, V1>>
253
- const right = other
254
- const inner = left.pipe(innerJoin(right))
255
- const antiLeft = left.pipe(antiJoin(right))
256
- const antiRight = right.pipe(
257
- antiJoin(left),
258
- map(([key, [a, b]]) => [key, [b, a]])
259
- )
260
- return inner.pipe(concat(antiLeft), concat(antiRight)) as IStreamBuilder<
261
- KeyValue<K, [V1 | null, V2 | null]>
262
- >
263
- }
370
+ return join(other, `full`) as unknown as PipedOperator<
371
+ T,
372
+ KeyValue<K, [V1 | null, V2 | null]>
373
+ >
264
374
  }
@@ -3,8 +3,7 @@ import { topKWithFractionalIndex } from "./topKWithFractionalIndex.js"
3
3
  import { map } from "./map.js"
4
4
  import { innerJoin } from "./join.js"
5
5
  import { consolidate } from "./consolidate.js"
6
- import type { KeyValue } from "../types.js"
7
- import type { IStreamBuilder } from "../types"
6
+ import type { IStreamBuilder, KeyValue } from "../types.js"
8
7
 
9
8
  export interface OrderByOptions<Ve> {
10
9
  comparator?: (a: Ve, b: Ve) => number
@@ -1,7 +1,6 @@
1
1
  import { MultiSet } from "../multiset.js"
2
2
  import { reduce } from "./reduce.js"
3
- import type { IStreamBuilder, PipedOperator } from "../types"
4
- import type { KeyValue } from "../types.js"
3
+ import type { IStreamBuilder, KeyValue, PipedOperator } from "../types"
5
4
 
6
5
  interface TopKOptions {
7
6
  limit?: number