@tanstack/db-ivm
Version:
Incremental View Maintenance for TanStack DB based on Differential Dataflow
124 lines (111 loc) • 4.21 kB
text/typescript
import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
import { StreamBuilder } from "../d2.js"
import { MultiSet } from "../multiset.js"
import { Index } from "../indexes.js"
import type { DifferenceStreamReader } from "../graph.js"
import type { IStreamBuilder, KeyValue } from "../types.js"
/**
* Base operator for reduction operations (version-free)
*/
export class ReduceOperator<K, V1, V2> extends UnaryOperator<[K, V1], [K, V2]> {
#index = new Index<K, V1>()
#indexOut = new Index<K, V2>()
#f: (values: Array<[V1, number]>) => Array<[V2, number]>
constructor(
id: number,
inputA: DifferenceStreamReader<[K, V1]>,
output: DifferenceStreamWriter<[K, V2]>,
f: (values: Array<[V1, number]>) => Array<[V2, number]>
) {
super(id, inputA, output)
this.#f = f
}
run(): void {
// Collect all input messages and update the index
const keysTodo = new Set<K>()
for (const message of this.inputMessages()) {
for (const [item, multiplicity] of message.getInner()) {
const [key, value] = item
this.#index.addValue(key, [value, multiplicity])
keysTodo.add(key)
}
}
// For each key, compute the reduction and delta
const result: Array<[[K, V2], number]> = []
for (const key of keysTodo) {
const curr = this.#index.get(key)
const currOut = this.#indexOut.get(key)
const out = this.#f(curr)
// Create maps for current and previous outputs using values directly as keys
const newOutputMap = new Map<V2, number>()
const oldOutputMap = new Map<V2, number>()
// Process new output
for (const [value, multiplicity] of out) {
const existing = newOutputMap.get(value) ?? 0
newOutputMap.set(value, existing + multiplicity)
}
// Process previous output
for (const [value, multiplicity] of currOut) {
const existing = oldOutputMap.get(value) ?? 0
oldOutputMap.set(value, existing + multiplicity)
}
// First, emit removals for old values that are no longer present
for (const [value, multiplicity] of oldOutputMap) {
if (!newOutputMap.has(value)) {
// Remove the old value entirely
result.push([[key, value], -multiplicity])
this.#indexOut.addValue(key, [value, -multiplicity])
}
}
// Then, emit additions for new values that are not present in old
for (const [value, multiplicity] of newOutputMap) {
if (!oldOutputMap.has(value)) {
// Add the new value only if it has non-zero multiplicity
if (multiplicity !== 0) {
result.push([[key, value], multiplicity])
this.#indexOut.addValue(key, [value, multiplicity])
}
}
}
// Finally, emit multiplicity changes for values that were present and are still present
for (const [value, newMultiplicity] of newOutputMap) {
const oldMultiplicity = oldOutputMap.get(value)
if (oldMultiplicity !== undefined) {
const delta = newMultiplicity - oldMultiplicity
// Only emit actual changes, i.e. non-zero deltas
if (delta !== 0) {
result.push([[key, value], delta])
this.#indexOut.addValue(key, [value, delta])
}
}
}
}
if (result.length > 0) {
this.output.sendData(new MultiSet(result))
}
}
}
/**
* Reduces the elements in the stream by key (version-free)
*/
export function reduce<
KType extends T extends KeyValue<infer K, infer _V> ? K : never,
V1Type extends T extends KeyValue<KType, infer V> ? V : never,
R,
T,
>(f: (values: Array<[V1Type, number]>) => Array<[R, number]>) {
return (stream: IStreamBuilder<T>): IStreamBuilder<KeyValue<KType, R>> => {
const output = new StreamBuilder<KeyValue<KType, R>>(
stream.graph,
new DifferenceStreamWriter<KeyValue<KType, R>>()
)
const operator = new ReduceOperator<KType, V1Type, R>(
stream.graph.getNextOperatorId(),
stream.connectReader() as DifferenceStreamReader<KeyValue<KType, V1Type>>,
output.writer,
f
)
stream.graph.addOperator(operator)
return output
}
}