UNPKG

@electric-sql/d2mini

Version:

D2Mini is a minimal implementation of Differential Dataflow for performing in-memory incremental view maintenance.

128 lines 4.42 kB
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 { #inner; #changedKeys; constructor() { this.#inner = new DefaultMap(() => []); this.#changedKeys = new Set(); } toString(indent = false) { return `Index(${JSON.stringify([...this.#inner], undefined, indent ? ' ' : undefined)})`; } get(key) { return this.#inner.get(key); } entries() { return this.#inner.entries(); } keys() { return this.#inner.keys(); } has(key) { return this.#inner.has(key) && this.#inner.get(key).length > 0; } get size() { let count = 0; for (const [, values] of this.#inner.entries()) { if (values.length > 0) { count++; } } return count; } addValue(key, value) { const values = this.#inner.get(key); values.push(value); this.#changedKeys.add(key); } append(other) { 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 = []) { // 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); } } } consolidateValues(values) { const consolidated = new Map(); 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(other) { const result = []; // 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); } } //# sourceMappingURL=indexes.js.map