@electric-sql/d2mini
Version:
D2Mini is a minimal implementation of Differential Dataflow for performing in-memory incremental view maintenance.
147 lines (125 loc) • 4.04 kB
text/typescript
import { MultiSet } from './multiset.js'
import { DefaultMap, hash } from './utils.js'
/**
* A map from a difference collection trace's keys -> (value, multiplicities) that changed.
* Used in operations like join and reduce where the operation needs to
* exploit the key-value structure of the data to run efficiently.
*/
export class Index<K, V> {
#inner: DefaultMap<K, [V, number][]>
#changedKeys: Set<K>
constructor() {
this.#inner = new DefaultMap<K, [V, number][]>(() => [])
this.#changedKeys = new Set<K>()
}
toString(indent = false): string {
return `Index(${JSON.stringify(
[...this.#inner],
undefined,
indent ? ' ' : undefined,
)})`
}
get(key: K): [V, number][] {
return this.#inner.get(key)
}
entries() {
return this.#inner.entries()
}
keys() {
return this.#inner.keys()
}
has(key: K): boolean {
return this.#inner.has(key) && this.#inner.get(key).length > 0
}
get size(): number {
let count = 0
for (const [, values] of this.#inner.entries()) {
if (values.length > 0) {
count++
}
}
return count
}
addValue(key: K, value: [V, number]): void {
const values = this.#inner.get(key)
values.push(value)
this.#changedKeys.add(key)
}
append(other: Index<K, V>): void {
for (const [key, otherValues] of other.entries()) {
const thisValues = this.#inner.get(key)
for (const value of otherValues) {
thisValues.push(value)
}
this.#changedKeys.add(key)
}
}
compact(keys: K[] = []): void {
// If no keys specified, use the changed keys
const keysToProcess = keys.length === 0 ? [...this.#changedKeys] : keys
for (const key of keysToProcess) {
if (!this.#inner.has(key)) continue
const values = this.#inner.get(key)
const consolidated = this.consolidateValues(values)
// Remove the key entirely and re-add only if there are non-zero values
this.#inner.delete(key)
if (consolidated.length > 0) {
this.#inner.get(key).push(...consolidated)
}
}
// Clear the changed keys after compaction
if (keys.length === 0) {
this.#changedKeys.clear()
} else {
// Only remove the keys that were explicitly compacted
for (const key of keys) {
this.#changedKeys.delete(key)
}
}
}
private consolidateValues(values: [V, number][]): [V, number][] {
const consolidated = new Map<string, { value: V; multiplicity: number }>()
for (const [value, multiplicity] of values) {
const valueHash = hash(value)
if (consolidated.has(valueHash)) {
consolidated.get(valueHash)!.multiplicity += multiplicity
} else {
consolidated.set(valueHash, { value, multiplicity })
}
}
return [...consolidated.values()]
.filter(({ multiplicity }) => multiplicity !== 0)
.map(({ value, multiplicity }) => [value, multiplicity])
}
join<V2>(other: Index<K, V2>): MultiSet<[K, [V, V2]]> {
const result: [[K, [V, V2]], number][] = []
// We want to iterate over the smaller of the two indexes to reduce the
// number of operations we need to do.
if (this.size <= other.size) {
for (const [key, values1] of this.entries()) {
if (!other.has(key)) continue
const values2 = other.get(key)
for (const [val1, mul1] of values1) {
for (const [val2, mul2] of values2) {
if (mul1 !== 0 && mul2 !== 0) {
result.push([[key, [val1, val2]], mul1 * mul2])
}
}
}
}
} else {
for (const [key, values2] of other.entries()) {
if (!this.has(key)) continue
const values1 = this.get(key)
for (const [val2, mul2] of values2) {
for (const [val1, mul1] of values1) {
if (mul1 !== 0 && mul2 !== 0) {
result.push([[key, [val1, val2]], mul1 * mul2])
}
}
}
}
}
return new MultiSet(result)
}
}