UNPKG

@electric-sql/d2ts

Version:

D2TS is a TypeScript implementation of Differential Dataflow.

180 lines 6.59 kB
import { MessageType, } from '../types.js'; import { DifferenceStreamWriter, BinaryOperator, } from '../graph.js'; import { StreamBuilder } from '../d2.js'; import { MultiSet } from '../multiset.js'; import { Index } from '../version-index.js'; import { negate } from './negate.js'; import { map } from './map.js'; import { concat } from './concat.js'; /** * Operator that joins two input streams */ export class JoinOperator extends BinaryOperator { #indexA = new Index(); #indexB = new Index(); constructor(id, inputA, inputB, output, initialFrontier) { super(id, inputA, inputB, output, initialFrontier); } run() { const deltaA = new Index(); const deltaB = new Index(); // Process input A for (const message of this.inputAMessages()) { if (message.type === MessageType.DATA) { const { version, collection } = message.data; for (const [item, multiplicity] of collection.getInner()) { const [key, value] = item; deltaA.addValue(key, version, [value, multiplicity]); } } else if (message.type === MessageType.FRONTIER) { const frontier = message.data; if (!this.inputAFrontier().lessEqual(frontier)) { throw new Error('Invalid frontier update'); } this.setInputAFrontier(frontier); } } // Process input B for (const message of this.inputBMessages()) { if (message.type === MessageType.DATA) { const { version, collection } = message.data; for (const [item, multiplicity] of collection.getInner()) { const [key, value] = item; deltaB.addValue(key, version, [value, multiplicity]); } } else if (message.type === MessageType.FRONTIER) { const frontier = message.data; if (!this.inputBFrontier().lessEqual(frontier)) { throw new Error('Invalid frontier update'); } this.setInputBFrontier(frontier); } } // Process results const results = new Map(); // Join deltaA with existing indexB for (const [version, collection] of deltaA.join(this.#indexB)) { const existing = results.get(version) || new MultiSet(); existing.extend(collection); results.set(version, existing); } // Append deltaA to indexA this.#indexA.append(deltaA); // Join existing indexA with deltaB for (const [version, collection] of this.#indexA.join(deltaB)) { const existing = results.get(version) || new MultiSet(); existing.extend(collection); results.set(version, existing); } // Send results for (const [version, collection] of results) { this.output.sendData(version, collection); } // Append deltaB to indexB this.#indexB.append(deltaB); // Update frontiers const inputFrontier = this.inputAFrontier().meet(this.inputBFrontier()); if (!this.outputFrontier.lessEqual(inputFrontier)) { throw new Error('Invalid frontier state'); } if (this.outputFrontier.lessThan(inputFrontier)) { this.outputFrontier = inputFrontier; this.output.sendFrontier(this.outputFrontier); this.#indexA.compact(this.outputFrontier); this.#indexB.compact(this.outputFrontier); } } } /** * Joins two input streams * @param other - The other stream to join with * @param type - The type of join to perform */ export function join(other, type = 'inner') { switch (type) { case 'inner': return innerJoin(other); case 'anti': return antiJoin(other); case 'left': return leftJoin(other); case 'right': return rightJoin(other); case 'full': return fullJoin(other); default: throw new Error(`Join type ${type} is invalid`); } } /** * Joins two input streams * @param other - The other stream to join with */ export function innerJoin(other) { return (stream) => { if (stream.graph !== other.graph) { throw new Error('Cannot join streams from different graphs'); } const output = new StreamBuilder(stream.graph, new DifferenceStreamWriter()); const operator = new JoinOperator(stream.graph.getNextOperatorId(), stream.connectReader(), other.connectReader(), output.writer, stream.graph.frontier()); 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(other) { return (stream) => { 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; }; } /** * Joins two input streams * @param other - The other stream to join with */ export function leftJoin(other) { return (stream) => { const left = stream; const right = other; const inner = left.pipe(innerJoin(right)); const anti = left.pipe(antiJoin(right)); return inner.pipe(concat(anti)); }; } /** * Joins two input streams * @param other - The other stream to join with */ export function rightJoin(other) { return (stream) => { const left = stream; 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)); }; } /** * Joins two input streams * @param other - The other stream to join with */ export function fullJoin(other) { return (stream) => { const left = stream; 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)); }; } //# sourceMappingURL=join.js.map