@electric-sql/d2ts
Version:
D2TS is a TypeScript implementation of Differential Dataflow.
206 lines (175 loc) • 5.24 kB
text/typescript
import { MultiSet, MultiSetArray } from './multiset.js'
import { Version, Antichain, v } from './order.js'
import {
Message,
MessageType,
IOperator,
DataMessage,
FrontierMessage,
IDifferenceStreamReader,
IDifferenceStreamWriter,
} from './types.js'
/**
* A read handle to a dataflow edge that receives data and frontier updates from a writer.
*
* The data received over this edge are pairs of (version, MultiSet) and the frontier
* updates are either integers (in the one dimensional case) or Antichains (in the general
* case).
*/
export class DifferenceStreamReader<T> implements IDifferenceStreamReader<T> {
#queue: Message<T>[]
constructor(queue: Message<T>[]) {
this.#queue = queue
}
drain(): Message<T>[] {
const out = [...this.#queue].reverse()
this.#queue.length = 0
return out
}
isEmpty(): boolean {
return this.#queue.length === 0
}
probeFrontierLessThan(frontier: Antichain): boolean {
for (const { type, data } of this.#queue) {
if (type === MessageType.FRONTIER) {
const receivedFrontier = data as FrontierMessage
if (frontier.lessEqual(receivedFrontier as Antichain)) {
return false
}
}
}
return true
}
}
/**
* A write handle to a dataflow edge that is allowed to publish data and send
* frontier updates.
*/
export class DifferenceStreamWriter<T> implements IDifferenceStreamWriter<T> {
#queues: Message<T>[][] = []
frontier: Antichain | null = null
sendData(
version: Version | number | number[],
collection: MultiSet<T> | MultiSetArray<T>,
): void {
if (typeof version === 'number') {
version = v(version)
} else if (Array.isArray(version)) {
version = v(version)
}
if (!(collection instanceof MultiSet)) {
collection = new MultiSet(collection)
}
if (this.frontier) {
if (!this.frontier.lessEqualVersion(version)) {
throw new Error('Invalid version')
}
}
const dataMessage: DataMessage<T> = { version, collection }
for (const q of this.#queues) {
q.unshift({
type: MessageType.DATA,
data: dataMessage,
})
}
}
sendFrontier(frontier: Antichain | Version | number | number[]): void {
frontier = Antichain.create(frontier)
if (this.frontier && !this.frontier.lessEqual(frontier)) {
throw new Error('Invalid frontier')
}
this.frontier = frontier
for (const q of this.#queues) {
q.unshift({ type: MessageType.FRONTIER, data: frontier })
}
}
newReader(): DifferenceStreamReader<T> {
const q: Message<T>[] = []
this.#queues.push(q)
return new DifferenceStreamReader(q)
}
}
/**
* A generic implementation of a dataflow operator (node) that has multiple incoming edges (read handles) and
* one outgoing edge (write handle).
*/
export abstract class Operator<T> implements IOperator<T> {
protected inputs: DifferenceStreamReader<T>[]
protected output: DifferenceStreamWriter<T>
protected inputFrontiers: Antichain[]
protected outputFrontier: Antichain
constructor(
public id: number,
inputs: DifferenceStreamReader<T>[],
output: DifferenceStreamWriter<T>,
initialFrontier: Antichain,
) {
this.inputs = inputs
this.output = output
this.inputFrontiers = inputs.map(() => initialFrontier)
this.outputFrontier = initialFrontier
}
abstract run(): void
hasPendingWork(): boolean {
return this.inputs.some((input) => !input.isEmpty())
}
frontiers(): [Antichain[], Antichain] {
return [this.inputFrontiers, this.outputFrontier]
}
}
/**
* A convenience implementation of a dataflow operator that has a handle to one
* incoming stream of data, and one handle to an outgoing stream of data.
*/
export abstract class UnaryOperator<T> extends Operator<T> {
constructor(
public id: number,
inputA: DifferenceStreamReader<T>,
output: DifferenceStreamWriter<T>,
initialFrontier: Antichain,
) {
super(id, [inputA], output, initialFrontier)
}
inputMessages(): Message<T>[] {
return this.inputs[0].drain()
}
inputFrontier(): Antichain {
return this.inputFrontiers[0]
}
setInputFrontier(frontier: Antichain): void {
this.inputFrontiers[0] = frontier
}
}
/**
* A convenience implementation of a dataflow operator that has a handle to two
* incoming streams of data, and one handle to an outgoing stream of data.
*/
export abstract class BinaryOperator<T> extends Operator<T> {
constructor(
public id: number,
inputA: DifferenceStreamReader<T>,
inputB: DifferenceStreamReader<T>,
output: DifferenceStreamWriter<T>,
initialFrontier: Antichain,
) {
super(id, [inputA, inputB], output, initialFrontier)
}
inputAMessages(): Message<T>[] {
return this.inputs[0].drain()
}
inputAFrontier(): Antichain {
return this.inputFrontiers[0]
}
setInputAFrontier(frontier: Antichain): void {
this.inputFrontiers[0] = frontier
}
inputBMessages(): Message<T>[] {
return this.inputs[1].drain()
}
inputBFrontier(): Antichain {
return this.inputFrontiers[1]
}
setInputBFrontier(frontier: Antichain): void {
this.inputFrontiers[1] = frontier
}
}