UNPKG

@electric-sql/d2ts

Version:

D2TS is a TypeScript implementation of Differential Dataflow.

249 lines 8.73 kB
import { DefaultMap, chunkedArrayPush, hash } from './utils.js'; /** * A multiset of data. */ export class MultiSet { #inner; constructor(data = []) { this.#inner = data; } toString(indent = false) { return `MultiSet(${JSON.stringify(this.#inner, null, indent ? 2 : undefined)})`; } toJSON() { return JSON.stringify(Array.from(this.getInner())); } static fromJSON(json) { return new MultiSet(JSON.parse(json)); } /** * Apply a function to all records in the collection. */ map(f) { return new MultiSet(this.#inner.map(([data, multiplicity]) => [f(data), multiplicity])); } /** * Filter out records for which a function f(record) evaluates to False. */ filter(f) { return new MultiSet(this.#inner.filter(([data, _]) => f(data))); } /** * Negate all multiplicities in the collection. */ negate() { return new MultiSet(this.#inner.map(([data, multiplicity]) => [data, -multiplicity])); } /** * Concatenate two collections together. */ concat(other) { const out = []; chunkedArrayPush(out, this.#inner); chunkedArrayPush(out, other.getInner()); return new MultiSet(out); } /** * Produce as output a collection that is logically equivalent to the input * but which combines identical instances of the same record into one * (record, multiplicity) pair. */ consolidate() { const consolidated = new DefaultMap(() => 0); const values = new Map(); let hasString = false; let hasNumber = false; let hasOther = false; for (const [data, _] of this.#inner) { if (typeof data === 'string') { hasString = true; } else if (typeof data === 'number') { hasNumber = true; } else { hasOther = true; break; } } const requireJson = hasOther || (hasString && hasNumber); for (const [data, multiplicity] of this.#inner) { const key = requireJson ? hash(data) : data; if (requireJson && !values.has(key)) { values.set(key, data); } consolidated.update(key, (count) => count + multiplicity); } const result = []; for (const [key, multiplicity] of consolidated.entries()) { if (multiplicity !== 0) { const parsedKey = requireJson ? values.get(key) : key; result.push([parsedKey, multiplicity]); } } return new MultiSet(result); } /** * Match pairs (k, v1) and (k, v2) from the two input collections and produce (k, (v1, v2)). */ join(other) { const out = []; for (const [[k1, v1], d1] of this.#inner) { for (const [[k2, v2], d2] of other.getInner()) { if (k1 === k2) { out.push([[k1, [v1, v2]], d1 * d2]); } } } return new MultiSet(out); } /** * Apply a reduction function to all record values, grouped by key. * This method only works on KeyedData types and returns KeyedData results. */ reduce(f) { const keys = new DefaultMap(() => []); const out = []; for (const [[key, val], multiplicity] of this.#inner) { keys.update(key, (existing) => { existing.push([val, multiplicity]); return existing; }); } for (const [key, vals] of keys.entries()) { const results = f(vals); for (const [val, multiplicity] of results) { out.push([[key, val], multiplicity]); } } return new MultiSet(out); } /** * Count the number of times each key occurs in the collection. */ count() { return this.reduce((vals) => { let out = 0; for (const [_, multiplicity] of vals) { out += multiplicity; } return [[out, 1]]; }); } /** * Produce the sum of all the values paired with a key, for all keys in the collection. */ sum() { return this.reduce((vals) => { let out = 0; for (const [val, multiplicity] of vals) { out += val * multiplicity; } return [[out, 1]]; }); } /** * Produce the minimum value associated with each key in the collection. * * Note that no record may have negative multiplicity when computing the min, * as it is unclear what exactly the minimum record is in that case. */ min() { return this.reduce((vals) => { const consolidated = new Map(); for (const [val, multiplicity] of vals) { const current = consolidated.get(val)?.[1] || 0; consolidated.set(val, [val, current + multiplicity]); } const validVals = Array.from(consolidated.values()).filter(([_, multiplicity]) => multiplicity !== 0); if (validVals.length === 0) return []; let minEntry = validVals[0]; for (const entry of validVals) { if (entry[1] <= 0) { throw new Error('Negative multiplicities not allowed in min operation'); } if (entry[0] < minEntry[0]) { minEntry = entry; } } return [[minEntry[0], 1]]; }); } /** * Produce the maximum value associated with each key in the collection. * * Note that no record may have negative multiplicity when computing the max, * as it is unclear what exactly the maximum record is in that case. */ max() { return this.reduce((vals) => { const consolidated = new Map(); for (const [val, multiplicity] of vals) { const current = consolidated.get(val)?.[1] || 0; consolidated.set(val, [val, current + multiplicity]); } const validVals = Array.from(consolidated.values()).filter(([_, multiplicity]) => multiplicity !== 0); if (validVals.length === 0) return []; let maxEntry = validVals[0]; for (const entry of validVals) { if (entry[1] <= 0) { throw new Error('Negative multiplicities not allowed in max operation'); } if (entry[0] > maxEntry[0]) { maxEntry = entry; } } return [[maxEntry[0], 1]]; }); } /** * Reduce the collection to a set of elements (from a multiset). * * Note that no record may have negative multiplicity when producing this set, * as elements of sets may only have multiplicity one, and it is unclear that is * an appropriate output for elements with negative multiplicity. */ distinct() { return this.reduce((vals) => { const consolidated = new Map(); for (const [val, multiplicity] of vals) { const key = hash(val); const current = consolidated.get(key)?.[1] || 0; consolidated.set(key, [val, current + multiplicity]); } const validVals = Array.from(consolidated.values()).filter(([_, multiplicity]) => multiplicity !== 0); for (const [_, multiplicity] of validVals) { if (multiplicity <= 0) { throw new Error('Negative multiplicities not allowed in distinct operation'); } } return validVals.map(([val, _]) => [val, 1]); }); } /** * Repeatedly invoke a function f on a collection, and return the result * of applying the function an infinite number of times (fixedpoint). * * Note that if the function does not converge to a fixedpoint this implementation * will run forever. */ iterate(f) { let curr = new MultiSet(this.#inner); let result = f(curr); while (JSON.stringify(result.#inner) !== JSON.stringify(curr.#inner)) { curr = result; result = f(curr); } return curr; } extend(other) { const otherArray = other instanceof MultiSet ? other.getInner() : other; chunkedArrayPush(this.#inner, otherArray); } getInner() { return this.#inner; } } //# sourceMappingURL=multiset.js.map