UNPKG

@tanstack/db-ivm

Version:

Incremental View Maintenance for TanStack DB based on Differential Dataflow

266 lines (247 loc) 7.29 kB
import { BinaryOperator, DifferenceStreamWriter } from "../graph.js" import { StreamBuilder } from "../d2.js" import { MultiSet } from "../multiset.js" import { Index } from "../indexes.js" import { negate } from "./negate.js" import { map } from "./map.js" import { concat } from "./concat.js" import type { DifferenceStreamReader } from "../graph.js" import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js" /** * Type of join to perform */ export type JoinType = `inner` | `left` | `right` | `full` | `anti` /** * Operator that joins two input streams */ export class JoinOperator<K, V1, V2> extends BinaryOperator< [K, V1] | [K, V2] | [K, [V1, V2]] > { #indexA = new Index<K, V1>() #indexB = new Index<K, V2>() constructor( id: number, inputA: DifferenceStreamReader<[K, V1]>, inputB: DifferenceStreamReader<[K, V2]>, output: DifferenceStreamWriter<[K, [V1, V2]]> ) { super(id, inputA, inputB, output) } run(): void { const deltaA = new Index<K, V1>() const deltaB = new Index<K, V2>() // Process input A - process ALL messages, not just the first one const messagesA = this.inputAMessages() for (const message of messagesA) { const multiSetMessage = message as unknown as MultiSet<[K, V1]> for (const [item, multiplicity] of multiSetMessage.getInner()) { const [key, value] = item deltaA.addValue(key, [value, multiplicity]) } } // Process input B - process ALL messages, not just the first one const messagesB = this.inputBMessages() for (const message of messagesB) { const multiSetMessage = message as unknown as MultiSet<[K, V2]> for (const [item, multiplicity] of multiSetMessage.getInner()) { const [key, value] = item deltaB.addValue(key, [value, multiplicity]) } } // Process results const results = new MultiSet<[K, [V1, V2]]>() // Join deltaA with existing indexB results.extend(deltaA.join(this.#indexB)) // Append deltaA to indexA this.#indexA.append(deltaA) // Join existing indexA with deltaB results.extend(this.#indexA.join(deltaB)) // Send results if (results.getInner().length > 0) { this.output.sendData(results) } // Append deltaB to indexB this.#indexB.append(deltaB) } } /** * Joins two input streams * @param other - The other stream to join with * @param type - The type of join to perform */ export function join< K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T, >( other: IStreamBuilder<KeyValue<K, V2>>, type: JoinType = `inner` ): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> { switch (type) { case `inner`: return innerJoin(other) as unknown as PipedOperator< T, KeyValue<K, [V1, V2]> > case `anti`: return antiJoin(other) as unknown as PipedOperator< T, KeyValue<K, [V1, null]> > case `left`: return leftJoin(other) as unknown as PipedOperator< T, KeyValue<K, [V1, V2 | null]> > case `right`: return rightJoin(other) as unknown as PipedOperator< T, KeyValue<K, [V1 | null, V2]> > case `full`: return fullJoin(other) as unknown as PipedOperator< T, KeyValue<K, [V1 | null, V2 | null]> > default: throw new Error(`Join type ${type} is invalid`) } } /** * Joins two input streams * @param other - The other stream to join with */ export function innerJoin< K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T, >( other: IStreamBuilder<KeyValue<K, V2>> ): PipedOperator<T, KeyValue<K, [V1, V2]>> { return (stream: IStreamBuilder<T>): IStreamBuilder<KeyValue<K, [V1, V2]>> => { if (stream.graph !== other.graph) { throw new Error(`Cannot join streams from different graphs`) } const output = new StreamBuilder<KeyValue<K, [V1, V2]>>( stream.graph, new DifferenceStreamWriter<KeyValue<K, [V1, V2]>>() ) const operator = new JoinOperator<K, V1, V2>( stream.graph.getNextOperatorId(), stream.connectReader() as DifferenceStreamReader<KeyValue<K, V1>>, other.connectReader(), output.writer ) stream.graph.addOperator(operator) stream.graph.addStream(output.connectReader()) return output } } /** * Joins two input streams * @param other - The other stream to join with */ export function antiJoin< K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T, >( other: IStreamBuilder<KeyValue<K, V2>> ): PipedOperator<T, KeyValue<K, [V1, null]>> { return ( stream: IStreamBuilder<T> ): IStreamBuilder<KeyValue<K, [V1, null]>> => { const matchedLeft = stream.pipe( innerJoin(other), map(([key, [valueLeft, _valueRight]]) => [key, valueLeft]) ) const anti = stream.pipe( concat(matchedLeft.pipe(negate())), // @ts-ignore TODO: fix this map(([key, value]) => [key, [value, null]]) ) return anti as IStreamBuilder<KeyValue<K, [V1, null]>> } } /** * Joins two input streams * @param other - The other stream to join with */ export function leftJoin< K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T, >( other: IStreamBuilder<KeyValue<K, V2>> ): PipedOperator<T, KeyValue<K, [V1, V2 | null]>> { return ( stream: IStreamBuilder<T> ): IStreamBuilder<KeyValue<K, [V1, V2 | null]>> => { const left = stream const right = other const inner = left.pipe(innerJoin(right)) const anti = left.pipe(antiJoin(right)) return inner.pipe(concat(anti)) as IStreamBuilder< KeyValue<K, [V1, V2 | null]> > } } /** * Joins two input streams * @param other - The other stream to join with */ export function rightJoin< K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T, >( other: IStreamBuilder<KeyValue<K, V2>> ): PipedOperator<T, KeyValue<K, [V1 | null, V2]>> { return ( stream: IStreamBuilder<T> ): IStreamBuilder<KeyValue<K, [V1 | null, V2]>> => { const left = stream as IStreamBuilder<KeyValue<K, V1>> const right = other const inner = left.pipe(innerJoin(right)) const anti = right.pipe( antiJoin(left), map(([key, [a, b]]) => [key, [b, a]]) ) return inner.pipe(concat(anti)) as IStreamBuilder< KeyValue<K, [V1 | null, V2]> > } } /** * Joins two input streams * @param other - The other stream to join with */ export function fullJoin< K, V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never, V2, T, >( other: IStreamBuilder<KeyValue<K, V2>> ): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> { return ( stream: IStreamBuilder<T> ): IStreamBuilder<KeyValue<K, [V1 | null, V2 | null]>> => { const left = stream as IStreamBuilder<KeyValue<K, V1>> const right = other const inner = left.pipe(innerJoin(right)) const antiLeft = left.pipe(antiJoin(right)) const antiRight = right.pipe( antiJoin(left), map(([key, [a, b]]) => [key, [b, a]]) ) return inner.pipe(concat(antiLeft), concat(antiRight)) as IStreamBuilder< KeyValue<K, [V1 | null, V2 | null]> > } }