@tanstack/db-ivm 0.1.20 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/d2.cjs +12 -2
- package/dist/cjs/d2.cjs.map +1 -1
- package/dist/cjs/hashing/hash.cjs +92 -104
- package/dist/cjs/hashing/hash.cjs.map +1 -1
- package/dist/cjs/hashing/hash.d.cts +5 -0
- package/dist/cjs/hashing/temporal.cjs +18 -0
- package/dist/cjs/hashing/temporal.cjs.map +1 -0
- package/dist/cjs/hashing/temporal.d.cts +5 -0
- package/dist/cjs/multiset.cjs +47 -92
- package/dist/cjs/multiset.cjs.map +1 -1
- package/dist/cjs/multiset.d.cts +16 -0
- package/dist/cjs/operators/distinct.cjs.map +1 -1
- package/dist/cjs/operators/distinct.d.cts +1 -1
- package/dist/cjs/operators/groupBy.cjs +2 -2
- package/dist/cjs/operators/groupBy.cjs.map +1 -1
- package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs +4 -5
- package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs.map +1 -1
- package/dist/cjs/operators/topKArray.cjs +2 -0
- package/dist/cjs/operators/topKArray.cjs.map +1 -1
- package/dist/cjs/operators/topKState.cjs +30 -0
- package/dist/cjs/operators/topKState.cjs.map +1 -1
- package/dist/cjs/operators/topKState.d.cts +9 -0
- package/dist/cjs/operators/topKWithFractionalIndex.cjs +4 -5
- package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
- package/dist/cjs/utils.cjs +15 -61
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +3 -32
- package/dist/esm/d2.js +12 -2
- package/dist/esm/d2.js.map +1 -1
- package/dist/esm/hashing/hash.d.ts +5 -0
- package/dist/esm/hashing/hash.js +92 -104
- package/dist/esm/hashing/hash.js.map +1 -1
- package/dist/esm/hashing/temporal.d.ts +5 -0
- package/dist/esm/hashing/temporal.js +18 -0
- package/dist/esm/hashing/temporal.js.map +1 -0
- package/dist/esm/multiset.d.ts +16 -0
- package/dist/esm/multiset.js +48 -93
- package/dist/esm/multiset.js.map +1 -1
- package/dist/esm/operators/distinct.d.ts +1 -1
- package/dist/esm/operators/distinct.js.map +1 -1
- package/dist/esm/operators/groupBy.js +2 -2
- package/dist/esm/operators/groupBy.js.map +1 -1
- package/dist/esm/operators/groupedTopKWithFractionalIndex.js +5 -6
- package/dist/esm/operators/groupedTopKWithFractionalIndex.js.map +1 -1
- package/dist/esm/operators/topKArray.js +2 -0
- package/dist/esm/operators/topKArray.js.map +1 -1
- package/dist/esm/operators/topKState.d.ts +9 -0
- package/dist/esm/operators/topKState.js +31 -1
- package/dist/esm/operators/topKState.js.map +1 -1
- package/dist/esm/operators/topKWithFractionalIndex.js +5 -6
- package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
- package/dist/esm/utils.d.ts +3 -32
- package/dist/esm/utils.js +15 -61
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -1
- package/src/d2.ts +15 -2
- package/src/hashing/hash.ts +147 -135
- package/src/hashing/temporal.ts +25 -0
- package/src/multiset.ts +73 -132
- package/src/operators/distinct.ts +5 -3
- package/src/operators/groupBy.ts +8 -2
- package/src/operators/groupedTopKWithFractionalIndex.ts +10 -6
- package/src/operators/topKArray.ts +4 -0
- package/src/operators/topKState.ts +42 -0
- package/src/operators/topKWithFractionalIndex.ts +10 -6
- package/src/operators/topKWithFractionalIndexBTree.ts +7 -8
- package/src/utils.ts +20 -76
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multiset.cjs","sources":["../../src/multiset.ts"],"sourcesContent":["import {\n DefaultMap,\n chunkedArrayPush,\n globalObjectIdGenerator,\n} from './utils.js'\nimport { hash } from './hashing/index.js'\n\nexport type MultiSetArray<T> = Array<[T, number]>\nexport type KeyedData<T> = [key: string, value: T]\n\n/**\n * A multiset of data.\n */\nexport class MultiSet<T> {\n #inner: MultiSetArray<T>\n\n constructor(data: MultiSetArray<T> = []) {\n this.#inner = data\n }\n\n toString(indent = false): string {\n return `MultiSet(${JSON.stringify(this.#inner, null, indent ? 2 : undefined)})`\n }\n\n toJSON(): string {\n return JSON.stringify(Array.from(this.getInner()))\n }\n\n static fromJSON<U>(json: string): MultiSet<U> {\n return new MultiSet(JSON.parse(json))\n }\n\n /**\n * Apply a function to all records in the collection.\n */\n map<U>(f: (data: T) => U): MultiSet<U> {\n return new MultiSet(\n this.#inner.map(([data, multiplicity]) => [f(data), multiplicity]),\n )\n }\n\n /**\n * Filter out records for which a function f(record) evaluates to False.\n */\n filter(f: (data: T) => boolean): MultiSet<T> {\n return new MultiSet(this.#inner.filter(([data, _]) => f(data)))\n }\n\n /**\n * Negate all multiplicities in the collection.\n */\n negate(): MultiSet<T> {\n return new MultiSet(\n this.#inner.map(([data, multiplicity]) => [data, -multiplicity]),\n )\n }\n\n /**\n * Concatenate two collections together.\n */\n concat(other: MultiSet<T>): MultiSet<T> {\n const out: MultiSetArray<T> = []\n chunkedArrayPush(out, this.#inner)\n chunkedArrayPush(out, other.getInner())\n return new MultiSet(out)\n }\n\n /**\n * Produce as output a collection that is logically equivalent to the input\n * but which combines identical instances of the same record into one\n * (record, multiplicity) pair.\n */\n consolidate(): MultiSet<T> {\n // Check if this looks like a keyed multiset (first item is a tuple of length 2)\n if (this.#inner.length > 0) {\n const firstItem = this.#inner[0]?.[0]\n if (Array.isArray(firstItem) && firstItem.length === 2) {\n return this.#consolidateKeyed()\n }\n }\n\n // Fall back to original method for unkeyed data\n return this.#consolidateUnkeyed()\n }\n\n /**\n * Private method for consolidating keyed multisets where keys are strings/numbers\n * and values are compared by reference equality.\n *\n * This method provides significant performance improvements over the hash-based approach\n * by using WeakMap for object reference tracking and avoiding expensive serialization.\n *\n * Special handling for join operations: When values are tuples of length 2 (common in joins),\n * we unpack them and compare each element individually to maintain proper equality semantics.\n */\n #consolidateKeyed(): MultiSet<T> {\n const consolidated = new Map<string, number>()\n const values = new Map<string, T>()\n\n // Use global object ID generator for consistent reference equality\n\n /**\n * Special handler for tuples (arrays of length 2) commonly produced by join operations.\n * Unpacks the tuple and generates an ID based on both elements to ensure proper\n * consolidation of join results like ['A', null] and [null, 'X'].\n */\n const getTupleId = (tuple: Array<any>): string => {\n if (tuple.length !== 2) {\n throw new Error(`Expected tuple of length 2`)\n }\n const [first, second] = tuple\n return `${globalObjectIdGenerator.getStringId(first)}|${globalObjectIdGenerator.getStringId(second)}`\n }\n\n // Process each item in the multiset\n for (const [data, multiplicity] of this.#inner) {\n // Verify this is still a keyed item (should be [key, value] pair)\n if (!Array.isArray(data) || data.length !== 2) {\n // Found non-keyed item, fall back to unkeyed consolidation\n return this.#consolidateUnkeyed()\n }\n\n const [key, value] = data\n\n // Verify key is string or number as expected for keyed multisets\n if (typeof key !== `string` && typeof key !== `number`) {\n // Found non-string/number key, fall back to unkeyed consolidation\n return this.#consolidateUnkeyed()\n }\n\n // Generate value ID with special handling for join tuples\n let valueId: string\n if (Array.isArray(value) && value.length === 2) {\n // Special case: value is a tuple from join operations\n valueId = getTupleId(value)\n } else {\n // Regular case: use reference/value equality\n valueId = globalObjectIdGenerator.getStringId(value)\n }\n\n // Create composite key and consolidate\n const compositeKey = key + `|` + valueId\n consolidated.set(\n compositeKey,\n (consolidated.get(compositeKey) || 0) + multiplicity,\n )\n\n // Store the original data for the first occurrence\n if (!values.has(compositeKey)) {\n values.set(compositeKey, data as T)\n }\n }\n\n // Build result array, filtering out zero multiplicities\n const result: MultiSetArray<T> = []\n for (const [compositeKey, multiplicity] of consolidated) {\n if (multiplicity !== 0) {\n result.push([values.get(compositeKey)!, multiplicity])\n }\n }\n\n return new MultiSet(result)\n }\n\n /**\n * Private method for consolidating unkeyed multisets using the original approach.\n */\n #consolidateUnkeyed(): MultiSet<T> {\n const consolidated = new DefaultMap<string | number, number>(() => 0)\n const values = new Map<string, any>()\n\n let hasString = false\n let hasNumber = false\n let hasOther = false\n for (const [data, _] of this.#inner) {\n if (typeof data === `string`) {\n hasString = true\n } else if (typeof data === `number`) {\n hasNumber = true\n } else {\n hasOther = true\n break\n }\n }\n\n const requireJson = hasOther || (hasString && hasNumber)\n\n for (const [data, multiplicity] of this.#inner) {\n const key = requireJson ? hash(data) : (data as string | number)\n if (requireJson && !values.has(key as string)) {\n values.set(key as string, data)\n }\n consolidated.update(key, (count) => count + multiplicity)\n }\n\n const result: MultiSetArray<T> = []\n for (const [key, multiplicity] of consolidated.entries()) {\n if (multiplicity !== 0) {\n const parsedKey = requireJson ? values.get(key as string) : key\n result.push([parsedKey as T, multiplicity])\n }\n }\n\n return new MultiSet(result)\n }\n\n extend(other: MultiSet<T> | MultiSetArray<T>): void {\n const otherArray = other instanceof MultiSet ? other.getInner() : other\n chunkedArrayPush(this.#inner, otherArray)\n }\n\n add(item: T, multiplicity: number): void {\n if (multiplicity !== 0) {\n this.#inner.push([item, multiplicity])\n }\n }\n\n getInner(): MultiSetArray<T> {\n return this.#inner\n }\n}\n"],"names":["chunkedArrayPush","globalObjectIdGenerator","DefaultMap","hash"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"multiset.cjs","sources":["../../src/multiset.ts"],"sourcesContent":["import { chunkedArrayPush, getStringId } from './utils.js'\nimport { hash } from './hashing/index.js'\n\nexport type MultiSetArray<T> = Array<[T, number]>\nexport type KeyedData<T> = [key: string, value: T]\n\n/**\n * A multiset of data.\n */\nexport class MultiSet<T> {\n #inner: MultiSetArray<T>\n\n constructor(data: MultiSetArray<T> = []) {\n this.#inner = data\n }\n\n toString(indent = false): string {\n return `MultiSet(${JSON.stringify(this.#inner, null, indent ? 2 : undefined)})`\n }\n\n toJSON(): string {\n return JSON.stringify(Array.from(this.getInner()))\n }\n\n static fromJSON<U>(json: string): MultiSet<U> {\n return new MultiSet(JSON.parse(json))\n }\n\n /**\n * Apply a function to all records in the collection.\n */\n map<U>(f: (data: T) => U): MultiSet<U> {\n return new MultiSet(\n this.#inner.map(([data, multiplicity]) => [f(data), multiplicity]),\n )\n }\n\n /**\n * Filter out records for which a function f(record) evaluates to False.\n */\n filter(f: (data: T) => boolean): MultiSet<T> {\n return new MultiSet(this.#inner.filter(([data, _]) => f(data)))\n }\n\n /**\n * Negate all multiplicities in the collection.\n */\n negate(): MultiSet<T> {\n return new MultiSet(\n this.#inner.map(([data, multiplicity]) => [data, -multiplicity]),\n )\n }\n\n /**\n * Concatenate two collections together.\n */\n concat(other: MultiSet<T>): MultiSet<T> {\n const out: MultiSetArray<T> = []\n chunkedArrayPush(out, this.#inner)\n chunkedArrayPush(out, other.getInner())\n return new MultiSet(out)\n }\n\n /**\n * Produce as output a collection that is logically equivalent to the input\n * but which combines identical instances of the same record into one\n * (record, multiplicity) pair.\n *\n * Record identity depends on the shape of the whole multiset:\n *\n * - Keyed: every record is a `[key, value]` pair with a string or number\n * key. Keys and primitive values compare by value, and object values by\n * reference. A value that is an array of length 2 is a join tuple, and\n * its two elements compare by the same rule. The keyed identity text\n * has known collisions: https://github.com/TanStack/db/issues/1948\n * - Unkeyed, one primitive type: every record is a string, or every record\n * is a number. Records compare as `Map` keys do, and a `-0` record is\n * returned as `0`.\n * - Otherwise, records compare by structure (`hash`).\n *\n * Each identity keeps its first record in input order with the summed\n * multiplicity. Identities whose sum is zero are dropped. The input is not\n * changed.\n */\n consolidate(): MultiSet<T> {\n const inner = this.#inner\n // Keyed multisets hold [string | number key, value] pairs. Their values are\n // compared by reference, which avoids hashing. Join tuples (values of\n // length 2) are unpacked so ['A', null] and [null, 'X'] stay distinct.\n if (\n inner.every(\n ([data]) =>\n Array.isArray(data) &&\n data.length === 2 &&\n (typeof data[0] === `string` || typeof data[0] === `number`),\n )\n ) {\n return consolidateBy(inner, (data) => {\n const [key, value] = data as [string | number, unknown]\n const valueId =\n Array.isArray(value) && value.length === 2\n ? `${getStringId(value[0])}|${getStringId(value[1])}`\n : getStringId(value)\n return key + `|` + valueId\n })\n }\n\n // Unkeyed data of a single primitive type is its own identity, and the\n // result holds the Map key, so -0 becomes 0. Anything else is compared\n // structurally by hash.\n let primitiveType: string | undefined\n const requireHash = inner.some(([data]) => {\n const type = typeof data\n if (type !== `string` && type !== `number`) return true\n primitiveType ??= type\n return type !== primitiveType\n })\n return requireHash\n ? consolidateBy(inner, hash)\n : consolidateBy(inner, (data) => data, true)\n }\n\n extend(other: MultiSet<T> | MultiSetArray<T>): void {\n const otherArray = other instanceof MultiSet ? other.getInner() : other\n chunkedArrayPush(this.#inner, otherArray)\n }\n\n add(item: T, multiplicity: number): void {\n if (multiplicity !== 0) {\n this.#inner.push([item, multiplicity])\n }\n }\n\n getInner(): MultiSetArray<T> {\n return this.#inner\n }\n}\n\n/**\n * Sums multiplicities of records with the same identity, keeping the first\n * record seen for each identity (or its Map key, which writes -0 as 0, when\n * `keepIdentity` is set) and dropping records whose sum is zero.\n */\nfunction consolidateBy<T>(\n inner: MultiSetArray<T>,\n identityOf: (data: T) => unknown,\n keepIdentity = false,\n): MultiSet<T> {\n const consolidated = new Map<unknown, [T, number]>()\n for (const [data, multiplicity] of inner) {\n const identity = identityOf(data)\n const entry = consolidated.get(identity)\n if (entry) entry[1] += multiplicity\n else consolidated.set(identity, [data, multiplicity])\n }\n const result: MultiSetArray<T> = []\n for (const [identity, [data, multiplicity]] of consolidated) {\n if (multiplicity !== 0)\n result.push([keepIdentity ? (identity as T) : data, multiplicity])\n }\n return new MultiSet(result)\n}\n"],"names":["chunkedArrayPush","getStringId","hash"],"mappings":"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"}
|
package/dist/cjs/multiset.d.cts
CHANGED
|
@@ -29,6 +29,22 @@ export declare class MultiSet<T> {
|
|
|
29
29
|
* Produce as output a collection that is logically equivalent to the input
|
|
30
30
|
* but which combines identical instances of the same record into one
|
|
31
31
|
* (record, multiplicity) pair.
|
|
32
|
+
*
|
|
33
|
+
* Record identity depends on the shape of the whole multiset:
|
|
34
|
+
*
|
|
35
|
+
* - Keyed: every record is a `[key, value]` pair with a string or number
|
|
36
|
+
* key. Keys and primitive values compare by value, and object values by
|
|
37
|
+
* reference. A value that is an array of length 2 is a join tuple, and
|
|
38
|
+
* its two elements compare by the same rule. The keyed identity text
|
|
39
|
+
* has known collisions: https://github.com/TanStack/db/issues/1948
|
|
40
|
+
* - Unkeyed, one primitive type: every record is a string, or every record
|
|
41
|
+
* is a number. Records compare as `Map` keys do, and a `-0` record is
|
|
42
|
+
* returned as `0`.
|
|
43
|
+
* - Otherwise, records compare by structure (`hash`).
|
|
44
|
+
*
|
|
45
|
+
* Each identity keeps its first record in input order with the summed
|
|
46
|
+
* multiplicity. Identities whose sum is zero are dropped. The input is not
|
|
47
|
+
* changed.
|
|
32
48
|
*/
|
|
33
49
|
consolidate(): MultiSet<T>;
|
|
34
50
|
extend(other: MultiSet<T> | MultiSetArray<T>): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"distinct.cjs","sources":["../../../src/operators/distinct.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { hash } from '../hashing/index.js'\nimport { MultiSet } from '../multiset.js'\nimport type { Hash } from '../hashing/index.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, KeyValue } from '../types.js'\n\ntype Multiplicity = number\n\ntype GetValue<T> = T extends KeyValue<any, infer V> ? V : never\n\n/**\n * Operator that removes duplicates\n */\nexport class DistinctOperator<\n T extends KeyValue<any, any>,\n> extends UnaryOperator<T, KeyValue<number, GetValue<T>>> {\n #by: (value: T) => any\n #values: Map<Hash, Multiplicity> // keeps track of the number of times each value has been seen\n\n constructor(\n id: number,\n input: DifferenceStreamReader<T>,\n output: DifferenceStreamWriter<KeyValue<number, GetValue<T>>>,\n by: (value: T) => any = (value: T) => value,\n ) {\n super(id, input, output)\n this.#by = by\n this.#values = new Map()\n }\n\n run(): void {\n const updatedValues = new Map<Hash, [Multiplicity, T]>()\n\n // Compute the new multiplicity for each value\n for (const message of this.inputMessages()) {\n for (const [value, diff] of message.getInner()) {\n const hashedValue = hash(this.#by(value))\n\n const oldMultiplicity =\n updatedValues.get(hashedValue)?.[0] ??\n this.#values.get(hashedValue) ??\n 0\n const newMultiplicity = oldMultiplicity + diff\n updatedValues.set(hashedValue, [newMultiplicity, value])\n }\n }\n\n const result: Array<[KeyValue<number, GetValue<T>>, number]> = []\n\n // Check which values became visible or disappeared\n for (const [\n hashedValue,\n [newMultiplicity, value],\n ] of updatedValues.entries()) {\n const oldMultiplicity = this.#values.get(hashedValue) ?? 0\n\n if (newMultiplicity === 0) {\n this.#values.delete(hashedValue)\n } else {\n this.#values.set(hashedValue, newMultiplicity)\n }\n\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value wasn't present in the stream\n // but with this change it is now present in the stream\n result.push([[hash(this.#by(value)), value[1]], 1])\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was present in the stream\n // but with this change it is no longer present in the stream\n result.push([[hash(this.#by(value)), value[1]], -1])\n }\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n}\n\n/**\n * Removes duplicate values\n */\nexport function distinct<T extends KeyValue<any, any>>(\n by: (value: T) => any = (value: T) => value,\n) {\n return (stream: IStreamBuilder<T
|
|
1
|
+
{"version":3,"file":"distinct.cjs","sources":["../../../src/operators/distinct.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { hash } from '../hashing/index.js'\nimport { MultiSet } from '../multiset.js'\nimport type { Hash } from '../hashing/index.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, KeyValue } from '../types.js'\n\ntype Multiplicity = number\n\ntype GetValue<T> = T extends KeyValue<any, infer V> ? V : never\n\n/**\n * Operator that removes duplicates\n */\nexport class DistinctOperator<\n T extends KeyValue<any, any>,\n> extends UnaryOperator<T, KeyValue<number, GetValue<T>>> {\n #by: (value: T) => any\n #values: Map<Hash, Multiplicity> // keeps track of the number of times each value has been seen\n\n constructor(\n id: number,\n input: DifferenceStreamReader<T>,\n output: DifferenceStreamWriter<KeyValue<number, GetValue<T>>>,\n by: (value: T) => any = (value: T) => value,\n ) {\n super(id, input, output)\n this.#by = by\n this.#values = new Map()\n }\n\n run(): void {\n const updatedValues = new Map<Hash, [Multiplicity, T]>()\n\n // Compute the new multiplicity for each value\n for (const message of this.inputMessages()) {\n for (const [value, diff] of message.getInner()) {\n const hashedValue = hash(this.#by(value))\n\n const oldMultiplicity =\n updatedValues.get(hashedValue)?.[0] ??\n this.#values.get(hashedValue) ??\n 0\n const newMultiplicity = oldMultiplicity + diff\n updatedValues.set(hashedValue, [newMultiplicity, value])\n }\n }\n\n const result: Array<[KeyValue<number, GetValue<T>>, number]> = []\n\n // Check which values became visible or disappeared\n for (const [\n hashedValue,\n [newMultiplicity, value],\n ] of updatedValues.entries()) {\n const oldMultiplicity = this.#values.get(hashedValue) ?? 0\n\n if (newMultiplicity === 0) {\n this.#values.delete(hashedValue)\n } else {\n this.#values.set(hashedValue, newMultiplicity)\n }\n\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value wasn't present in the stream\n // but with this change it is now present in the stream\n result.push([[hash(this.#by(value)), value[1]], 1])\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was present in the stream\n // but with this change it is no longer present in the stream\n result.push([[hash(this.#by(value)), value[1]], -1])\n }\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n}\n\n/**\n * Removes duplicate values\n */\nexport function distinct<T extends KeyValue<any, any>>(\n by: (value: T) => any = (value: T) => value,\n) {\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<KeyValue<number, GetValue<T>>> => {\n const output = new StreamBuilder<KeyValue<number, GetValue<T>>>(\n stream.graph,\n new DifferenceStreamWriter<KeyValue<number, GetValue<T>>>(),\n )\n const operator = new DistinctOperator<T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n by,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n"],"names":["UnaryOperator","hash","MultiSet","StreamBuilder","DifferenceStreamWriter"],"mappings":";;;;;;AAeO,MAAM,yBAEHA,MAAAA,cAAgD;AAAA,EACxD;AAAA,EACA;AAAA;AAAA,EAEA,YACE,IACA,OACA,QACA,KAAwB,CAAC,UAAa,OACtC;AACA,UAAM,IAAI,OAAO,MAAM;AACvB,SAAK,MAAM;AACX,SAAK,8BAAc,IAAA;AAAA,EACrB;AAAA,EAEA,MAAY;AACV,UAAM,oCAAoB,IAAA;AAG1B,eAAW,WAAW,KAAK,iBAAiB;AAC1C,iBAAW,CAAC,OAAO,IAAI,KAAK,QAAQ,YAAY;AAC9C,cAAM,cAAcC,KAAAA,KAAK,KAAK,IAAI,KAAK,CAAC;AAExC,cAAM,kBACJ,cAAc,IAAI,WAAW,IAAI,CAAC,KAClC,KAAK,QAAQ,IAAI,WAAW,KAC5B;AACF,cAAM,kBAAkB,kBAAkB;AAC1C,sBAAc,IAAI,aAAa,CAAC,iBAAiB,KAAK,CAAC;AAAA,MACzD;AAAA,IACF;AAEA,UAAM,SAAyD,CAAA;AAG/D,eAAW;AAAA,MACT;AAAA,MACA,CAAC,iBAAiB,KAAK;AAAA,IAAA,KACpB,cAAc,WAAW;AAC5B,YAAM,kBAAkB,KAAK,QAAQ,IAAI,WAAW,KAAK;AAEzD,UAAI,oBAAoB,GAAG;AACzB,aAAK,QAAQ,OAAO,WAAW;AAAA,MACjC,OAAO;AACL,aAAK,QAAQ,IAAI,aAAa,eAAe;AAAA,MAC/C;AAEA,UAAI,mBAAmB,KAAK,kBAAkB,GAAG;AAG/C,eAAO,KAAK,CAAC,CAACA,KAAAA,KAAK,KAAK,IAAI,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC;AAAA,MACpD,WAAW,kBAAkB,KAAK,mBAAmB,GAAG;AAGtD,eAAO,KAAK,CAAC,CAACA,KAAAA,KAAK,KAAK,IAAI,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC;AAAA,MACrD;AAAA,IACF;AAEA,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO,SAAS,IAAIC,SAAAA,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AACF;AAKO,SAAS,SACd,KAAwB,CAAC,UAAa,OACtC;AACA,SAAO,CACL,WACkD;AAClD,UAAM,SAAS,IAAIC,GAAAA;AAAAA,MACjB,OAAO;AAAA,MACP,IAAIC,MAAAA,uBAAA;AAAA,IAAsD;AAE5D,UAAM,WAAW,IAAI;AAAA,MACnB,OAAO,MAAM,kBAAA;AAAA,MACb,OAAO,cAAA;AAAA,MACP,OAAO;AAAA,MACP;AAAA,IAAA;AAEF,WAAO,MAAM,YAAY,QAAQ;AACjC,WAAO;AAAA,EACT;AACF;;;"}
|
|
@@ -12,5 +12,5 @@ export declare class DistinctOperator<T extends KeyValue<any, any>> extends Unar
|
|
|
12
12
|
/**
|
|
13
13
|
* Removes duplicate values
|
|
14
14
|
*/
|
|
15
|
-
export declare function distinct<T extends KeyValue<any, any>>(by?: (value: T) => any): (stream: IStreamBuilder<T>) => IStreamBuilder<T
|
|
15
|
+
export declare function distinct<T extends KeyValue<any, any>>(by?: (value: T) => any): (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<number, GetValue<T>>>;
|
|
16
16
|
export {};
|
|
@@ -124,7 +124,7 @@ function min(valueExtractor) {
|
|
|
124
124
|
reduce: (values) => {
|
|
125
125
|
let minValue;
|
|
126
126
|
for (const [value, _multiplicity] of values) {
|
|
127
|
-
if (
|
|
127
|
+
if (value !== void 0 && (minValue === void 0 || value < minValue)) {
|
|
128
128
|
minValue = value;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -139,7 +139,7 @@ function max(valueExtractor) {
|
|
|
139
139
|
reduce: (values) => {
|
|
140
140
|
let maxValue;
|
|
141
141
|
for (const [value, _multiplicity] of values) {
|
|
142
|
-
if (
|
|
142
|
+
if (value !== void 0 && (maxValue === void 0 || value > maxValue)) {
|
|
143
143
|
maxValue = value;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"groupBy.cjs","sources":["../../../src/operators/groupBy.ts"],"sourcesContent":["import { serializeValue } from '../utils.js'\nimport { map } from './map.js'\nimport { reduce } from './reduce.js'\nimport type { IStreamBuilder, KeyValue } from '../types.js'\n\ntype GroupKey = Record<string, unknown>\n\ntype BasicAggregateFunction<T, R, V = unknown> = {\n preMap: (data: T) => V\n reduce: (values: Array<[V, number]>) => V\n postMap?: (result: V) => R\n}\n\ntype PipedAggregateFunction<T, R> = {\n pipe: (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<string, R>>\n}\n\ntype AggregateFunction<T, R, V = unknown> =\n | BasicAggregateFunction<T, R, V>\n | PipedAggregateFunction<T, R>\n\ntype ExtractAggregateReturnType<T, A> =\n A extends AggregateFunction<T, infer R, any> ? R : never\n\ntype AggregatesReturnType<T, A> = {\n [K in keyof A]: ExtractAggregateReturnType<T, A[K]>\n}\n\nfunction isPipedAggregateFunction<T, R>(\n aggregate: AggregateFunction<T, R>,\n): aggregate is PipedAggregateFunction<T, R> {\n return `pipe` in aggregate\n}\n\n/**\n * Groups data by key and applies multiple aggregate operations\n * @param keyExtractor Function to extract grouping key from data\n * @param aggregates Object mapping aggregate names to aggregate functions\n */\nexport function groupBy<\n T,\n K extends GroupKey,\n A extends Record<string, AggregateFunction<T, any, any>>,\n>(keyExtractor: (data: T) => K, aggregates: A = {} as A) {\n type ResultType = K & AggregatesReturnType<T, A>\n\n const basicAggregates = Object.fromEntries(\n Object.entries(aggregates).filter(\n ([_, aggregate]) => !isPipedAggregateFunction(aggregate),\n ),\n ) as Record<string, BasicAggregateFunction<T, any, any>>\n\n // @ts-expect-error - TODO: we don't use this yet, but we will\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const pipedAggregates = Object.fromEntries(\n Object.entries(aggregates).filter(([_, aggregate]) =>\n isPipedAggregateFunction(aggregate),\n ),\n ) as Record<string, PipedAggregateFunction<T, any>>\n\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<KeyValue<string, ResultType>> => {\n // Special key to store the original key object\n const KEY_SENTINEL = Symbol(`original_group_key`)\n\n // First map to extract keys and pre-aggregate values\n const withKeysAndValues = stream.pipe(\n map((data) => {\n const key = keyExtractor(data)\n const keyString = serializeValue(key)\n\n // Create values object with pre-aggregated values\n const values: Record<string | symbol, unknown> = {}\n\n // Store the original key object\n values[KEY_SENTINEL] = key\n\n // Add pre-aggregated values\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n values[name] = aggregate.preMap(data)\n }\n\n return [keyString, values] as KeyValue<\n string,\n Record<string | symbol, unknown>\n >\n }),\n )\n\n // Then reduce to compute aggregates\n const reduced = withKeysAndValues.pipe(\n reduce((values) => {\n // Calculate total multiplicity to check if the group should exist\n let totalMultiplicity = 0\n for (const [_, multiplicity] of values) {\n totalMultiplicity += multiplicity\n }\n\n // If total multiplicity is 0 or negative, the group should be removed completely\n if (totalMultiplicity <= 0) {\n return []\n }\n\n const result: Record<PropertyKey, unknown> = {}\n\n // Get the original key from first value in group\n const originalKey = values[0]?.[0]?.[KEY_SENTINEL]\n result[KEY_SENTINEL] = originalKey\n\n // Apply each aggregate function\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n const preValues = values.map(\n ([v, m]) => [v[name], m] as [any, number],\n )\n result[name] = aggregate.reduce(preValues)\n }\n\n return [[result, 1]]\n }),\n )\n\n // Finally map to extract the key and include all values\n return reduced.pipe(\n map(([keyString, values]) => {\n // Extract the original key\n const key = values[KEY_SENTINEL] as K\n\n // Create intermediate result with key values and aggregate results\n const result: Record<string, unknown> = {}\n\n // Add key properties to result\n Object.assign(result, key)\n\n // Apply postMap if provided\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n if (aggregate.postMap) {\n result[name] = aggregate.postMap(values[name])\n } else {\n result[name] = values[name]\n }\n }\n\n // Return with the string key instead of the object\n return [keyString, result] as KeyValue<string, ResultType>\n }),\n )\n }\n}\n\n/**\n * Creates a sum aggregate function\n */\nexport function sum<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, number> {\n return {\n preMap: (data: T) => valueExtractor(data),\n reduce: (values: Array<[number, number]>) => {\n let total = 0\n for (const [value, multiplicity] of values) {\n total += value * multiplicity\n }\n return total\n },\n }\n}\n\n/**\n * Creates a count aggregate function\n */\nexport function count<T>(\n valueExtractor: (value: T) => any = (v) => v,\n): AggregateFunction<T, number, number> {\n return {\n // Count only not-null values (the `== null` comparison gives true for both null and undefined)\n preMap: (data: T) => (valueExtractor(data) == null ? 0 : 1),\n reduce: (values: Array<[number, number]>) => {\n let totalCount = 0\n for (const [nullMultiplier, multiplicity] of values) {\n totalCount += nullMultiplier * multiplicity\n }\n return totalCount\n },\n }\n}\n\n/**\n * Creates an average aggregate function\n */\nexport function avg<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, { sum: number; count: number }> {\n return {\n preMap: (data: T) => ({\n sum: valueExtractor(data),\n count: 0,\n }),\n reduce: (values: Array<[{ sum: number; count: number }, number]>) => {\n let totalSum = 0\n let totalCount = 0\n for (const [value, multiplicity] of values) {\n totalSum += value.sum * multiplicity\n totalCount += multiplicity\n }\n return {\n sum: totalSum,\n count: totalCount,\n }\n },\n postMap: (result: { sum: number; count: number }) => {\n return result.sum / result.count\n },\n }\n}\n\ntype CanMinMax = number | Date | bigint | string\n\n/**\n * Creates a min aggregate function that computes the minimum value in a group\n * @param valueExtractor Function to extract a comparable value from each data entry\n */\nexport function min<T extends CanMinMax>(): AggregateFunction<\n T,\n T | undefined,\n T | undefined\n>\nexport function min<T, V extends CanMinMax>(\n valueExtractor: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined>\nexport function min<T, V extends CanMinMax>(\n valueExtractor?: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined> {\n const extractor = valueExtractor ?? ((v: T) => v as unknown as V)\n return {\n preMap: (data: T) => extractor(data),\n reduce: (values) => {\n let minValue: V | undefined\n for (const [value, _multiplicity] of values) {\n if (!minValue || (value && value < minValue)) {\n minValue = value\n }\n }\n return minValue\n },\n }\n}\n\n/**\n * Creates a max aggregate function that computes the maximum value in a group\n * @param valueExtractor Function to extract a comparable value from each data entry\n */\nexport function max<T extends CanMinMax>(): AggregateFunction<\n T,\n T | undefined,\n T | undefined\n>\nexport function max<T, V extends CanMinMax>(\n valueExtractor: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined>\nexport function max<T, V extends CanMinMax>(\n valueExtractor?: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined> {\n const extractor = valueExtractor ?? ((v: T) => v as unknown as V)\n return {\n preMap: (data: T) => extractor(data),\n reduce: (values) => {\n let maxValue: V | undefined\n for (const [value, _multiplicity] of values) {\n if (!maxValue || (value && value > maxValue)) {\n maxValue = value\n }\n }\n return maxValue\n },\n }\n}\n\n/**\n * Creates a median aggregate function that computes the middle value in a sorted group\n * If there's an even number of values, returns the average of the two middle values\n * @param valueExtractor Function to extract a numeric value from each data entry\n */\nexport function median<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, Array<number>> {\n return {\n preMap: (data: T) => [valueExtractor(data)],\n reduce: (values: Array<[Array<number>, number]>) => {\n // Flatten all values, taking multiplicity into account\n const allValues: Array<number> = []\n for (const [valueArray, multiplicity] of values) {\n for (const value of valueArray) {\n // Add each value multiple times based on multiplicity\n for (let i = 0; i < multiplicity; i++) {\n allValues.push(value)\n }\n }\n }\n\n // Return empty array if no values\n if (allValues.length === 0) {\n return []\n }\n\n // Sort values\n allValues.sort((a, b) => a - b)\n\n return allValues\n },\n postMap: (result: Array<number>) => {\n if (result.length === 0) return 0\n\n const mid = Math.floor(result.length / 2)\n\n // If even number of values, average the two middle values\n if (result.length % 2 === 0) {\n return (result[mid - 1]! + result[mid]!) / 2\n }\n\n // If odd number of values, return the middle value\n return result[mid]!\n },\n }\n}\n\n/**\n * Creates a mode aggregate function that computes the most frequent value in a group\n * If multiple values have the same highest frequency, returns the first one encountered\n * @param valueExtractor Function to extract a value from each data entry\n */\nexport function mode<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, Map<number, number>> {\n return {\n preMap: (data: T) => {\n const value = valueExtractor(data)\n const frequencyMap = new Map<number, number>()\n frequencyMap.set(value, 1)\n return frequencyMap\n },\n reduce: (values: Array<[Map<number, number>, number]>) => {\n // Combine all frequency maps\n const combinedMap = new Map<number, number>()\n\n for (const [frequencyMap, multiplicity] of values) {\n for (const [value, frequencyCount] of frequencyMap.entries()) {\n const currentCount = combinedMap.get(value) || 0\n combinedMap.set(value, currentCount + frequencyCount * multiplicity)\n }\n }\n\n return combinedMap\n },\n postMap: (result: Map<number, number>) => {\n if (result.size === 0) return 0\n\n let modeValue = 0\n let maxFrequency = 0\n\n for (const [value, frequency] of result.entries()) {\n if (frequency > maxFrequency) {\n maxFrequency = frequency\n modeValue = value\n }\n }\n\n return modeValue\n },\n }\n}\n\nexport const groupByOperators = {\n sum,\n count,\n avg,\n min,\n max,\n median,\n mode,\n}\n"],"names":["map","serializeValue","reduce"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"groupBy.cjs","sources":["../../../src/operators/groupBy.ts"],"sourcesContent":["import { serializeValue } from '../utils.js'\nimport { map } from './map.js'\nimport { reduce } from './reduce.js'\nimport type { IStreamBuilder, KeyValue } from '../types.js'\n\ntype GroupKey = Record<string, unknown>\n\ntype BasicAggregateFunction<T, R, V = unknown> = {\n preMap: (data: T) => V\n reduce: (values: Array<[V, number]>) => V\n postMap?: (result: V) => R\n}\n\ntype PipedAggregateFunction<T, R> = {\n pipe: (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<string, R>>\n}\n\ntype AggregateFunction<T, R, V = unknown> =\n | BasicAggregateFunction<T, R, V>\n | PipedAggregateFunction<T, R>\n\ntype ExtractAggregateReturnType<T, A> =\n A extends AggregateFunction<T, infer R, any> ? R : never\n\ntype AggregatesReturnType<T, A> = {\n [K in keyof A]: ExtractAggregateReturnType<T, A[K]>\n}\n\nfunction isPipedAggregateFunction<T, R>(\n aggregate: AggregateFunction<T, R>,\n): aggregate is PipedAggregateFunction<T, R> {\n return `pipe` in aggregate\n}\n\n/**\n * Groups data by key and applies multiple aggregate operations\n * @param keyExtractor Function to extract grouping key from data\n * @param aggregates Object mapping aggregate names to aggregate functions\n */\nexport function groupBy<\n T,\n K extends GroupKey,\n A extends Record<string, AggregateFunction<T, any, any>>,\n>(keyExtractor: (data: T) => K, aggregates: A = {} as A) {\n type ResultType = K & AggregatesReturnType<T, A>\n\n const basicAggregates = Object.fromEntries(\n Object.entries(aggregates).filter(\n ([_, aggregate]) => !isPipedAggregateFunction(aggregate),\n ),\n ) as Record<string, BasicAggregateFunction<T, any, any>>\n\n // @ts-expect-error - TODO: we don't use this yet, but we will\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const pipedAggregates = Object.fromEntries(\n Object.entries(aggregates).filter(([_, aggregate]) =>\n isPipedAggregateFunction(aggregate),\n ),\n ) as Record<string, PipedAggregateFunction<T, any>>\n\n return (\n stream: IStreamBuilder<T>,\n ): IStreamBuilder<KeyValue<string, ResultType>> => {\n // Special key to store the original key object\n const KEY_SENTINEL = Symbol(`original_group_key`)\n\n // First map to extract keys and pre-aggregate values\n const withKeysAndValues = stream.pipe(\n map((data) => {\n const key = keyExtractor(data)\n const keyString = serializeValue(key)\n\n // Create values object with pre-aggregated values\n const values: Record<string | symbol, unknown> = {}\n\n // Store the original key object\n values[KEY_SENTINEL] = key\n\n // Add pre-aggregated values\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n values[name] = aggregate.preMap(data)\n }\n\n return [keyString, values] as KeyValue<\n string,\n Record<string | symbol, unknown>\n >\n }),\n )\n\n // Then reduce to compute aggregates\n const reduced = withKeysAndValues.pipe(\n reduce((values) => {\n // Calculate total multiplicity to check if the group should exist\n let totalMultiplicity = 0\n for (const [_, multiplicity] of values) {\n totalMultiplicity += multiplicity\n }\n\n // If total multiplicity is 0 or negative, the group should be removed completely\n if (totalMultiplicity <= 0) {\n return []\n }\n\n const result: Record<PropertyKey, unknown> = {}\n\n // Get the original key from first value in group\n const originalKey = values[0]?.[0]?.[KEY_SENTINEL]\n result[KEY_SENTINEL] = originalKey\n\n // Apply each aggregate function\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n const preValues = values.map(\n ([v, m]) => [v[name], m] as [any, number],\n )\n result[name] = aggregate.reduce(preValues)\n }\n\n return [[result, 1]]\n }),\n )\n\n // Finally map to extract the key and include all values\n return reduced.pipe(\n map(([keyString, values]) => {\n // Extract the original key\n const key = values[KEY_SENTINEL] as K\n\n // Create intermediate result with key values and aggregate results\n const result: Record<string, unknown> = {}\n\n // Add key properties to result\n Object.assign(result, key)\n\n // Apply postMap if provided\n for (const [name, aggregate] of Object.entries(basicAggregates)) {\n if (aggregate.postMap) {\n result[name] = aggregate.postMap(values[name])\n } else {\n result[name] = values[name]\n }\n }\n\n // Return with the string key instead of the object\n return [keyString, result] as KeyValue<string, ResultType>\n }),\n )\n }\n}\n\n/**\n * Creates a sum aggregate function\n */\nexport function sum<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, number> {\n return {\n preMap: (data: T) => valueExtractor(data),\n reduce: (values: Array<[number, number]>) => {\n let total = 0\n for (const [value, multiplicity] of values) {\n total += value * multiplicity\n }\n return total\n },\n }\n}\n\n/**\n * Creates a count aggregate function\n */\nexport function count<T>(\n valueExtractor: (value: T) => any = (v) => v,\n): AggregateFunction<T, number, number> {\n return {\n // Count only not-null values (the `== null` comparison gives true for both null and undefined)\n preMap: (data: T) => (valueExtractor(data) == null ? 0 : 1),\n reduce: (values: Array<[number, number]>) => {\n let totalCount = 0\n for (const [nullMultiplier, multiplicity] of values) {\n totalCount += nullMultiplier * multiplicity\n }\n return totalCount\n },\n }\n}\n\n/**\n * Creates an average aggregate function\n */\nexport function avg<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, { sum: number; count: number }> {\n return {\n preMap: (data: T) => ({\n sum: valueExtractor(data),\n count: 0,\n }),\n reduce: (values: Array<[{ sum: number; count: number }, number]>) => {\n let totalSum = 0\n let totalCount = 0\n for (const [value, multiplicity] of values) {\n totalSum += value.sum * multiplicity\n totalCount += multiplicity\n }\n return {\n sum: totalSum,\n count: totalCount,\n }\n },\n postMap: (result: { sum: number; count: number }) => {\n return result.sum / result.count\n },\n }\n}\n\ntype CanMinMax = number | Date | bigint | string\n\n/**\n * Creates a min aggregate function that computes the minimum value in a group\n * @param valueExtractor Function to extract a comparable value from each data entry\n */\nexport function min<T extends CanMinMax>(): AggregateFunction<\n T,\n T | undefined,\n T | undefined\n>\nexport function min<T, V extends CanMinMax>(\n valueExtractor: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined>\nexport function min<T, V extends CanMinMax>(\n valueExtractor?: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined> {\n const extractor = valueExtractor ?? ((v: T) => v as unknown as V)\n return {\n preMap: (data: T) => extractor(data),\n reduce: (values) => {\n let minValue: V | undefined\n for (const [value, _multiplicity] of values) {\n if (\n value !== undefined &&\n (minValue === undefined || value < minValue)\n ) {\n minValue = value\n }\n }\n return minValue\n },\n }\n}\n\n/**\n * Creates a max aggregate function that computes the maximum value in a group\n * @param valueExtractor Function to extract a comparable value from each data entry\n */\nexport function max<T extends CanMinMax>(): AggregateFunction<\n T,\n T | undefined,\n T | undefined\n>\nexport function max<T, V extends CanMinMax>(\n valueExtractor: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined>\nexport function max<T, V extends CanMinMax>(\n valueExtractor?: (value: T) => V,\n): AggregateFunction<T, V | undefined, V | undefined> {\n const extractor = valueExtractor ?? ((v: T) => v as unknown as V)\n return {\n preMap: (data: T) => extractor(data),\n reduce: (values) => {\n let maxValue: V | undefined\n for (const [value, _multiplicity] of values) {\n if (\n value !== undefined &&\n (maxValue === undefined || value > maxValue)\n ) {\n maxValue = value\n }\n }\n return maxValue\n },\n }\n}\n\n/**\n * Creates a median aggregate function that computes the middle value in a sorted group\n * If there's an even number of values, returns the average of the two middle values\n * @param valueExtractor Function to extract a numeric value from each data entry\n */\nexport function median<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, Array<number>> {\n return {\n preMap: (data: T) => [valueExtractor(data)],\n reduce: (values: Array<[Array<number>, number]>) => {\n // Flatten all values, taking multiplicity into account\n const allValues: Array<number> = []\n for (const [valueArray, multiplicity] of values) {\n for (const value of valueArray) {\n // Add each value multiple times based on multiplicity\n for (let i = 0; i < multiplicity; i++) {\n allValues.push(value)\n }\n }\n }\n\n // Return empty array if no values\n if (allValues.length === 0) {\n return []\n }\n\n // Sort values\n allValues.sort((a, b) => a - b)\n\n return allValues\n },\n postMap: (result: Array<number>) => {\n if (result.length === 0) return 0\n\n const mid = Math.floor(result.length / 2)\n\n // If even number of values, average the two middle values\n if (result.length % 2 === 0) {\n return (result[mid - 1]! + result[mid]!) / 2\n }\n\n // If odd number of values, return the middle value\n return result[mid]!\n },\n }\n}\n\n/**\n * Creates a mode aggregate function that computes the most frequent value in a group\n * If multiple values have the same highest frequency, returns the first one encountered\n * @param valueExtractor Function to extract a value from each data entry\n */\nexport function mode<T>(\n valueExtractor: (value: T) => number = (v) => v as unknown as number,\n): AggregateFunction<T, number, Map<number, number>> {\n return {\n preMap: (data: T) => {\n const value = valueExtractor(data)\n const frequencyMap = new Map<number, number>()\n frequencyMap.set(value, 1)\n return frequencyMap\n },\n reduce: (values: Array<[Map<number, number>, number]>) => {\n // Combine all frequency maps\n const combinedMap = new Map<number, number>()\n\n for (const [frequencyMap, multiplicity] of values) {\n for (const [value, frequencyCount] of frequencyMap.entries()) {\n const currentCount = combinedMap.get(value) || 0\n combinedMap.set(value, currentCount + frequencyCount * multiplicity)\n }\n }\n\n return combinedMap\n },\n postMap: (result: Map<number, number>) => {\n if (result.size === 0) return 0\n\n let modeValue = 0\n let maxFrequency = 0\n\n for (const [value, frequency] of result.entries()) {\n if (frequency > maxFrequency) {\n maxFrequency = frequency\n modeValue = value\n }\n }\n\n return modeValue\n },\n }\n}\n\nexport const groupByOperators = {\n sum,\n count,\n avg,\n min,\n max,\n median,\n mode,\n}\n"],"names":["map","serializeValue","reduce"],"mappings":"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"}
|
|
@@ -75,11 +75,10 @@ class GroupedTopKWithFractionalIndexOperator extends graph.UnaryOperator {
|
|
|
75
75
|
}
|
|
76
76
|
run() {
|
|
77
77
|
const result = [];
|
|
78
|
-
for (const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
78
|
+
for (const [[key, value], multiplicity] of topKState.topKBatch(
|
|
79
|
+
this.inputMessages()
|
|
80
|
+
)) {
|
|
81
|
+
this.#processElement(key, value, multiplicity, result);
|
|
83
82
|
}
|
|
84
83
|
if (result.length > 0) {
|
|
85
84
|
this.output.sendData(new multiset.MultiSet(result));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"groupedTopKWithFractionalIndex.cjs","sources":["../../../src/operators/groupedTopKWithFractionalIndex.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport {
|
|
1
|
+
{"version":3,"file":"groupedTopKWithFractionalIndex.cjs","sources":["../../../src/operators/groupedTopKWithFractionalIndex.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport {\n TopKState,\n handleMoveIn,\n handleMoveOut,\n topKBatch,\n} from './topKState.js'\nimport { TopKArray, createKeyedComparator } from './topKArray.js'\nimport type { IndexedValue, TopK } from './topKArray.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, PipedOperator } from '../types.js'\n\nexport interface GroupedTopKWithFractionalIndexOptions<K, T> {\n limit?: number\n offset?: number\n setSizeCallback?: (getSize: () => number) => void\n setWindowFn?: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => void\n /**\n * Function to extract a group key from the element's key and value.\n * Elements with the same group key will be sorted and limited together.\n */\n groupKeyFn: (key: K, value: T) => unknown\n}\n\n/**\n * Operator for grouped fractional indexed topK operations.\n * This operator maintains separate topK windows for each group,\n * allowing per-group limits and ordering.\n *\n * The input is a keyed stream [K, T] and outputs [K, IndexedValue<T>].\n * Elements are grouped by the groupKeyFn, and each group maintains\n * its own sorted collection with independent limit/offset.\n */\nexport class GroupedTopKWithFractionalIndexOperator<\n K extends string | number,\n T,\n> extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {\n #groupStates: Map<unknown, TopKState<K, T>> = new Map()\n #groupKeyFn: (key: K, value: T) => unknown\n #comparator: (a: [K, T], b: [K, T]) => number\n #offset: number\n #limit: number\n\n constructor(\n id: number,\n inputA: DifferenceStreamReader<[K, T]>,\n output: DifferenceStreamWriter<[K, IndexedValue<T>]>,\n comparator: (a: T, b: T) => number,\n options: GroupedTopKWithFractionalIndexOptions<K, T>,\n ) {\n super(id, inputA, output)\n this.#groupKeyFn = options.groupKeyFn\n this.#limit = options.limit ?? Infinity\n this.#offset = options.offset ?? 0\n this.#comparator = createKeyedComparator(comparator)\n options.setSizeCallback?.(() => this.#getTotalSize())\n options.setWindowFn?.(this.#moveTopK.bind(this))\n }\n\n /**\n * Creates a new TopK data structure for a group.\n * Can be overridden in subclasses to use different implementations (e.g., B+ tree).\n */\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: [K, T], b: [K, T]) => number,\n ): TopK<[K, T]> {\n return new TopKArray(offset, limit, comparator)\n }\n\n #getTotalSize(): number {\n let size = 0\n for (const state of this.#groupStates.values()) {\n size += state.size\n }\n return size\n }\n\n #getOrCreateGroupState(groupKey: unknown): TopKState<K, T> {\n let state = this.#groupStates.get(groupKey)\n if (!state) {\n const topK = this.createTopK(this.#offset, this.#limit, this.#comparator)\n state = new TopKState(topK)\n this.#groupStates.set(groupKey, state)\n }\n return state\n }\n\n #cleanupGroupIfEmpty(groupKey: unknown, state: TopKState<K, T>): void {\n if (state.isEmpty) {\n this.#groupStates.delete(groupKey)\n }\n }\n\n /**\n * Moves the topK window for all groups based on the provided offset and limit.\n * Any changes to the topK are sent to the output.\n */\n #moveTopK({ offset, limit }: { offset?: number; limit?: number }): void {\n if (offset !== undefined) {\n this.#offset = offset\n }\n if (limit !== undefined) {\n this.#limit = limit\n }\n\n const result: Array<[[K, IndexedValue<T>], number]> = []\n let hasChanges = false\n\n for (const state of this.#groupStates.values()) {\n const diff = state.move({ offset: this.#offset, limit: this.#limit }) // TODO: think we should just pass offset and limit\n\n diff.moveIns.forEach((moveIn) => handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => handleMoveOut(moveOut, result))\n\n if (diff.changes) {\n hasChanges = true\n }\n }\n\n if (hasChanges) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const [[key, value], multiplicity] of topKBatch(\n this.inputMessages(),\n )) {\n this.#processElement(key, value, multiplicity, result)\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n #processElement(\n key: K,\n value: T,\n multiplicity: number,\n result: Array<[[K, IndexedValue<T>], number]>,\n ): void {\n const groupKey = this.#groupKeyFn(key, value)\n const state = this.#getOrCreateGroupState(groupKey)\n\n const changes = state.processElement(key, value, multiplicity)\n handleMoveIn(changes.moveIn, result)\n handleMoveOut(changes.moveOut, result)\n\n // Cleanup empty groups to prevent memory leaks\n this.#cleanupGroupIfEmpty(groupKey, state)\n }\n}\n\n/**\n * Limits the number of results per group based on a comparator, with optional offset.\n * Uses fractional indexing to minimize the number of changes when elements move positions.\n * Each element is assigned a fractional index that is lexicographically sortable.\n * When elements move, only the indices of the moved elements are updated, not all elements.\n *\n * This operator groups elements by the provided groupKeyFn and applies the limit/offset\n * independently to each group.\n *\n * @param comparator - A function that compares two elements for ordering\n * @param options - Configuration including groupKeyFn, limit, and offset\n * @returns A piped operator that orders elements per group and limits results per group\n */\nexport function groupedTopKWithFractionalIndex<K extends string | number, T>(\n comparator: (a: T, b: T) => number,\n options: GroupedTopKWithFractionalIndexOptions<K, T>,\n): PipedOperator<[K, T], [K, IndexedValue<T>]> {\n return (\n stream: IStreamBuilder<[K, T]>,\n ): IStreamBuilder<[K, IndexedValue<T>]> => {\n const output = new StreamBuilder<[K, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[K, IndexedValue<T>]>(),\n )\n const operator = new GroupedTopKWithFractionalIndexOperator<K, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n options,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n"],"names":["UnaryOperator","createKeyedComparator","TopKArray","TopKState","handleMoveIn","handleMoveOut","MultiSet","topKBatch","StreamBuilder","DifferenceStreamWriter"],"mappings":"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"}
|
|
@@ -80,6 +80,7 @@ class TopKArray {
|
|
|
80
80
|
const fractionalIndex = fractionalIndexing.generateKeyBetween(indexBefore, indexAfter);
|
|
81
81
|
const val = indexedValue(value, fractionalIndex);
|
|
82
82
|
this.#sortedValues.splice(index, 0, val);
|
|
83
|
+
if (this.#topKStart === this.#topKEnd) return result;
|
|
83
84
|
if (index < this.#topKEnd) {
|
|
84
85
|
const moveInIndex = Math.max(index, this.#topKStart);
|
|
85
86
|
if (moveInIndex < this.#sortedValues.length) {
|
|
@@ -101,6 +102,7 @@ class TopKArray {
|
|
|
101
102
|
const result = { moveIn: null, moveOut: null };
|
|
102
103
|
const index = this.#findIndex(value);
|
|
103
104
|
const [removedElem] = this.#sortedValues.splice(index, 1);
|
|
105
|
+
if (this.#topKStart === this.#topKEnd) return result;
|
|
104
106
|
if (index < this.#topKEnd) {
|
|
105
107
|
result.moveOut = removedElem;
|
|
106
108
|
if (index < this.#topKStart) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topKArray.cjs","sources":["../../../src/operators/topKArray.ts"],"sourcesContent":["import { generateKeyBetween } from 'fractional-indexing'\nimport { binarySearch, compareKeys, diffHalfOpen } from '../utils.js'\nimport type { HRange } from '../utils.js'\n\n// Abstraction for fractionally indexed values\nexport type FractionalIndex = string\nexport type IndexedValue<V> = [V, FractionalIndex]\n\nexport function indexedValue<V>(\n value: V,\n index: FractionalIndex,\n): IndexedValue<V> {\n return [value, index]\n}\n\nexport function getValue<V>(indexedVal: IndexedValue<V>): V {\n return indexedVal[0]\n}\n\nexport function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {\n return indexedVal[1]\n}\n\n/**\n * Creates a comparator for [key, value] tuples that first compares values,\n * then uses the row key as a stable tie-breaker.\n */\nexport function createKeyedComparator<K extends string | number, T>(\n comparator: (a: T, b: T) => number,\n): (a: [K, T], b: [K, T]) => number {\n return ([aKey, aVal], [bKey, bVal]) => {\n // First compare on the value\n const valueComparison = comparator(aVal, bVal)\n if (valueComparison !== 0) {\n return valueComparison\n }\n // If the values are equal, use the row key as tie-breaker\n // This provides stable, deterministic ordering since keys are string | number\n return compareKeys(aKey, bKey)\n }\n}\n\nexport type TopKChanges<V> = {\n /** Indicates which element moves into the topK (if any) */\n moveIn: IndexedValue<V> | null\n /** Indicates which element moves out of the topK (if any) */\n moveOut: IndexedValue<V> | null\n}\n\nexport type TopKMoveChanges<V> = {\n /** Flag that marks whether there were any changes to the topK */\n changes: boolean\n /** Indicates which elements move into the topK (if any) */\n moveIns: Array<IndexedValue<V>>\n /** Indicates which elements move out of the topK (if any) */\n moveOuts: Array<IndexedValue<V>>\n}\n\n/**\n * A topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\nexport interface TopK<V> {\n size: number\n insert: (value: V) => TopKChanges<V>\n delete: (value: V) => TopKChanges<V>\n}\n\n/**\n * Implementation of a topK data structure.\n * Uses a sorted array internally to store the values and keeps a topK window over that array.\n * Inserts and deletes are O(n) operations because worst case an element is inserted/deleted\n * at the start of the array which causes all the elements to shift to the right/left.\n */\nexport class TopKArray<V> implements TopK<V> {\n #sortedValues: Array<IndexedValue<V>> = []\n #comparator: (a: V, b: V) => number\n #topKStart: number\n #topKEnd: number\n\n constructor(\n offset: number,\n limit: number,\n comparator: (a: V, b: V) => number,\n ) {\n this.#topKStart = offset\n this.#topKEnd = offset + limit\n this.#comparator = comparator\n }\n\n get size(): number {\n const offset = this.#topKStart\n const limit = this.#topKEnd - this.#topKStart\n const available = this.#sortedValues.length - offset\n return Math.max(0, Math.min(limit, available))\n }\n\n /**\n * Moves the topK window\n */\n move({\n offset,\n limit,\n }: {\n offset?: number\n limit?: number\n }): TopKMoveChanges<V> {\n const oldOffset = this.#topKStart\n const oldLimit = this.#topKEnd - this.#topKStart\n\n // `this.#topKEnd` can be `Infinity` if it has no limit\n // but `diffHalfOpen` expects a finite range\n // so we restrict it to the size of the topK if topKEnd is infinite\n const oldRange: HRange = [\n this.#topKStart,\n this.#topKEnd === Infinity ? this.#topKStart + this.size : this.#topKEnd,\n ]\n\n this.#topKStart = offset ?? oldOffset\n this.#topKEnd = this.#topKStart + (limit ?? oldLimit) // can be `Infinity` if limit is `Infinity`\n\n // Also handle `Infinity` in the newRange\n const newRange: HRange = [\n this.#topKStart,\n this.#topKEnd === Infinity\n ? Math.max(this.#topKStart + this.size, oldRange[1]) // since the new limit is Infinity we need to take everything (so we need to take the biggest (finite) topKEnd)\n : this.#topKEnd,\n ]\n const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange)\n\n const moveIns: Array<IndexedValue<V>> = []\n onlyInB.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveIns.push(value)\n }\n })\n\n const moveOuts: Array<IndexedValue<V>> = []\n onlyInA.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveOuts.push(value)\n }\n })\n\n // It could be that there are changes (i.e. moveIns or moveOuts)\n // but that the collection is lazy so we don't have the data yet that needs to move in/out\n // so `moveIns` and `moveOuts` will be empty but `changes` will be true\n // this will tell the caller that it needs to run the graph to load more data\n return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 }\n }\n\n insert(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup insert position\n const index = this.#findIndex(value)\n // Generate fractional index based on the fractional indices of the elements before and after it\n const indexBefore =\n index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)\n const indexAfter =\n index === this.#sortedValues.length\n ? null\n : getIndex(this.#sortedValues[index]!)\n const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)\n\n // Insert the value at the correct position\n const val = indexedValue(value, fractionalIndex)\n // Splice is O(n) where n = all elements in the collection (i.e. n >= k) !\n this.#sortedValues.splice(index, 0, val)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The inserted element is either before the top K or within the top K\n // If it is before the top K then it moves the element that was right before the topK into the topK\n // If it is within the top K then the inserted element moves into the top K\n // In both cases the last element of the old top K now moves out of the top K\n const moveInIndex = Math.max(index, this.#topKStart)\n if (moveInIndex < this.#sortedValues.length) {\n // We actually have a topK\n // because in some cases there may not be enough elements in the array to reach the start of the topK\n // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK\n result.moveIn = this.#sortedValues[moveInIndex]!\n\n // We need to remove the element that falls out of the top K\n // The element that falls out of the top K has shifted one to the right\n // because of the element we inserted, so we find it at index topKEnd\n if (this.#topKEnd < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[this.#topKEnd]!\n }\n }\n }\n\n return result\n }\n\n /**\n * Deletes a value that may or may not be in the topK.\n * IMPORTANT: this assumes that the value is present in the collection\n * if it's not the case it will remove the element\n * that is on the position where the provided `value` would be.\n */\n delete(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup delete position\n const index = this.#findIndex(value)\n // Remove the value at that position\n const [removedElem] = this.#sortedValues.splice(index, 1)\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The removed element is either before the top K or within the top K\n // If it is before the top K then the first element of the topK moves out of the topK\n // If it is within the top K then the removed element moves out of the topK\n result.moveOut = removedElem!\n if (index < this.#topKStart) {\n // The removed element is before the topK\n // so actually, the first element of the topK moves out of the topK\n // and not the element that we removed\n // The first element of the topK is now at index topKStart - 1\n // since we removed an element before the topK\n const moveOutIndex = this.#topKStart - 1\n if (moveOutIndex < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[moveOutIndex]!\n } else {\n // No value is moving out of the topK\n // because there are no elements in the topK\n result.moveOut = null\n }\n }\n\n // Since we removed an element that was before or in the topK\n // the first element after the topK moved one position to the left\n // and thus falls into the topK now\n const moveInIndex = this.#topKEnd - 1\n if (moveInIndex < this.#sortedValues.length) {\n result.moveIn = this.#sortedValues[moveInIndex]!\n }\n }\n\n return result\n }\n\n // TODO: see if there is a way to refactor the code for insert and delete in the topK above\n // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right\n // so i have the feeling there is a common pattern here and we can implement both cases using that pattern\n\n #findIndex(value: V): number {\n return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>\n this.#comparator(getValue(a), getValue(b)),\n )\n }\n}\n"],"names":["compareKeys","diffHalfOpen","generateKeyBetween","binarySearch"],"mappings":";;;;AAQO,SAAS,aACd,OACA,OACiB;AACjB,SAAO,CAAC,OAAO,KAAK;AACtB;AAEO,SAAS,SAAY,YAAgC;AAC1D,SAAO,WAAW,CAAC;AACrB;AAEO,SAAS,SAAY,YAA8C;AACxE,SAAO,WAAW,CAAC;AACrB;AAMO,SAAS,sBACd,YACkC;AAClC,SAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,MAAM;AAErC,UAAM,kBAAkB,WAAW,MAAM,IAAI;AAC7C,QAAI,oBAAoB,GAAG;AACzB,aAAO;AAAA,IACT;AAGA,WAAOA,MAAAA,YAAY,MAAM,IAAI;AAAA,EAC/B;AACF;AAkCO,MAAM,UAAgC;AAAA,EAC3C,gBAAwC,CAAA;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YACE,QACA,OACA,YACA;AACA,SAAK,aAAa;AAClB,SAAK,WAAW,SAAS;AACzB,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,IAAI,OAAe;AACjB,UAAM,SAAS,KAAK;AACpB,UAAM,QAAQ,KAAK,WAAW,KAAK;AACnC,UAAM,YAAY,KAAK,cAAc,SAAS;AAC9C,WAAO,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,SAAS,CAAC;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK;AAAA,IACH;AAAA,IACA;AAAA,EAAA,GAIqB;AACrB,UAAM,YAAY,KAAK;AACvB,UAAM,WAAW,KAAK,WAAW,KAAK;AAKtC,UAAM,WAAmB;AAAA,MACvB,KAAK;AAAA,MACL,KAAK,aAAa,WAAW,KAAK,aAAa,KAAK,OAAO,KAAK;AAAA,IAAA;AAGlE,SAAK,aAAa,UAAU;AAC5B,SAAK,WAAW,KAAK,cAAc,SAAS;AAG5C,UAAM,WAAmB;AAAA,MACvB,KAAK;AAAA,MACL,KAAK,aAAa,WACd,KAAK,IAAI,KAAK,aAAa,KAAK,MAAM,SAAS,CAAC,CAAC,IACjD,KAAK;AAAA,IAAA;AAEX,UAAM,EAAE,SAAS,QAAA,IAAYC,MAAAA,aAAa,UAAU,QAAQ;AAE5D,UAAM,UAAkC,CAAA;AACxC,YAAQ,QAAQ,CAAC,UAAU;AACzB,YAAM,QAAQ,KAAK,cAAc,KAAK;AACtC,UAAI,OAAO;AACT,gBAAQ,KAAK,KAAK;AAAA,MACpB;AAAA,IACF,CAAC;AAED,UAAM,WAAmC,CAAA;AACzC,YAAQ,QAAQ,CAAC,UAAU;AACzB,YAAM,QAAQ,KAAK,cAAc,KAAK;AACtC,UAAI,OAAO;AACT,iBAAS,KAAK,KAAK;AAAA,MACrB;AAAA,IACF,CAAC;AAMD,WAAO,EAAE,SAAS,UAAU,SAAS,QAAQ,SAAS,QAAQ,SAAS,EAAA;AAAA,EACzE;AAAA,EAEA,OAAO,OAA0B;AAC/B,UAAM,SAAyB,EAAE,QAAQ,MAAM,SAAS,KAAA;AAGxD,UAAM,QAAQ,KAAK,WAAW,KAAK;AAEnC,UAAM,cACJ,UAAU,IAAI,OAAO,SAAS,KAAK,cAAc,QAAQ,CAAC,CAAE;AAC9D,UAAM,aACJ,UAAU,KAAK,cAAc,SACzB,OACA,SAAS,KAAK,cAAc,KAAK,CAAE;AACzC,UAAM,kBAAkBC,mBAAAA,mBAAmB,aAAa,UAAU;AAGlE,UAAM,MAAM,aAAa,OAAO,eAAe;AAE/C,SAAK,cAAc,OAAO,OAAO,GAAG,GAAG;AAGvC,QAAI,QAAQ,KAAK,UAAU;AAKzB,YAAM,cAAc,KAAK,IAAI,OAAO,KAAK,UAAU;AACnD,UAAI,cAAc,KAAK,cAAc,QAAQ;AAI3C,eAAO,SAAS,KAAK,cAAc,WAAW;AAK9C,YAAI,KAAK,WAAW,KAAK,cAAc,QAAQ;AAC7C,iBAAO,UAAU,KAAK,cAAc,KAAK,QAAQ;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,OAA0B;AAC/B,UAAM,SAAyB,EAAE,QAAQ,MAAM,SAAS,KAAA;AAGxD,UAAM,QAAQ,KAAK,WAAW,KAAK;AAEnC,UAAM,CAAC,WAAW,IAAI,KAAK,cAAc,OAAO,OAAO,CAAC;AAGxD,QAAI,QAAQ,KAAK,UAAU;AAIzB,aAAO,UAAU;AACjB,UAAI,QAAQ,KAAK,YAAY;AAM3B,cAAM,eAAe,KAAK,aAAa;AACvC,YAAI,eAAe,KAAK,cAAc,QAAQ;AAC5C,iBAAO,UAAU,KAAK,cAAc,YAAY;AAAA,QAClD,OAAO;AAGL,iBAAO,UAAU;AAAA,QACnB;AAAA,MACF;AAKA,YAAM,cAAc,KAAK,WAAW;AACpC,UAAI,cAAc,KAAK,cAAc,QAAQ;AAC3C,eAAO,SAAS,KAAK,cAAc,WAAW;AAAA,MAChD;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,WAAW,OAAkB;AAC3B,WAAOC,MAAAA;AAAAA,MAAa,KAAK;AAAA,MAAe,aAAa,OAAO,EAAE;AAAA,MAAG,CAAC,GAAG,MACnE,KAAK,YAAY,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC;AAAA,IAAA;AAAA,EAE7C;AACF;;;;;;"}
|
|
1
|
+
{"version":3,"file":"topKArray.cjs","sources":["../../../src/operators/topKArray.ts"],"sourcesContent":["import { generateKeyBetween } from 'fractional-indexing'\nimport { binarySearch, compareKeys, diffHalfOpen } from '../utils.js'\nimport type { HRange } from '../utils.js'\n\n// Abstraction for fractionally indexed values\nexport type FractionalIndex = string\nexport type IndexedValue<V> = [V, FractionalIndex]\n\nexport function indexedValue<V>(\n value: V,\n index: FractionalIndex,\n): IndexedValue<V> {\n return [value, index]\n}\n\nexport function getValue<V>(indexedVal: IndexedValue<V>): V {\n return indexedVal[0]\n}\n\nexport function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {\n return indexedVal[1]\n}\n\n/**\n * Creates a comparator for [key, value] tuples that first compares values,\n * then uses the row key as a stable tie-breaker.\n */\nexport function createKeyedComparator<K extends string | number, T>(\n comparator: (a: T, b: T) => number,\n): (a: [K, T], b: [K, T]) => number {\n return ([aKey, aVal], [bKey, bVal]) => {\n // First compare on the value\n const valueComparison = comparator(aVal, bVal)\n if (valueComparison !== 0) {\n return valueComparison\n }\n // If the values are equal, use the row key as tie-breaker\n // This provides stable, deterministic ordering since keys are string | number\n return compareKeys(aKey, bKey)\n }\n}\n\nexport type TopKChanges<V> = {\n /** Indicates which element moves into the topK (if any) */\n moveIn: IndexedValue<V> | null\n /** Indicates which element moves out of the topK (if any) */\n moveOut: IndexedValue<V> | null\n}\n\nexport type TopKMoveChanges<V> = {\n /** Flag that marks whether there were any changes to the topK */\n changes: boolean\n /** Indicates which elements move into the topK (if any) */\n moveIns: Array<IndexedValue<V>>\n /** Indicates which elements move out of the topK (if any) */\n moveOuts: Array<IndexedValue<V>>\n}\n\n/**\n * A topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\nexport interface TopK<V> {\n size: number\n insert: (value: V) => TopKChanges<V>\n delete: (value: V) => TopKChanges<V>\n}\n\n/**\n * Implementation of a topK data structure.\n * Uses a sorted array internally to store the values and keeps a topK window over that array.\n * Inserts and deletes are O(n) operations because worst case an element is inserted/deleted\n * at the start of the array which causes all the elements to shift to the right/left.\n */\nexport class TopKArray<V> implements TopK<V> {\n #sortedValues: Array<IndexedValue<V>> = []\n #comparator: (a: V, b: V) => number\n #topKStart: number\n #topKEnd: number\n\n constructor(\n offset: number,\n limit: number,\n comparator: (a: V, b: V) => number,\n ) {\n this.#topKStart = offset\n this.#topKEnd = offset + limit\n this.#comparator = comparator\n }\n\n get size(): number {\n const offset = this.#topKStart\n const limit = this.#topKEnd - this.#topKStart\n const available = this.#sortedValues.length - offset\n return Math.max(0, Math.min(limit, available))\n }\n\n /**\n * Moves the topK window\n */\n move({\n offset,\n limit,\n }: {\n offset?: number\n limit?: number\n }): TopKMoveChanges<V> {\n const oldOffset = this.#topKStart\n const oldLimit = this.#topKEnd - this.#topKStart\n\n // `this.#topKEnd` can be `Infinity` if it has no limit\n // but `diffHalfOpen` expects a finite range\n // so we restrict it to the size of the topK if topKEnd is infinite\n const oldRange: HRange = [\n this.#topKStart,\n this.#topKEnd === Infinity ? this.#topKStart + this.size : this.#topKEnd,\n ]\n\n this.#topKStart = offset ?? oldOffset\n this.#topKEnd = this.#topKStart + (limit ?? oldLimit) // can be `Infinity` if limit is `Infinity`\n\n // Also handle `Infinity` in the newRange\n const newRange: HRange = [\n this.#topKStart,\n this.#topKEnd === Infinity\n ? Math.max(this.#topKStart + this.size, oldRange[1]) // since the new limit is Infinity we need to take everything (so we need to take the biggest (finite) topKEnd)\n : this.#topKEnd,\n ]\n const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange)\n\n const moveIns: Array<IndexedValue<V>> = []\n onlyInB.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveIns.push(value)\n }\n })\n\n const moveOuts: Array<IndexedValue<V>> = []\n onlyInA.forEach((index) => {\n const value = this.#sortedValues[index]\n if (value) {\n moveOuts.push(value)\n }\n })\n\n // It could be that there are changes (i.e. moveIns or moveOuts)\n // but that the collection is lazy so we don't have the data yet that needs to move in/out\n // so `moveIns` and `moveOuts` will be empty but `changes` will be true\n // this will tell the caller that it needs to run the graph to load more data\n return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 }\n }\n\n insert(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup insert position\n const index = this.#findIndex(value)\n // Generate fractional index based on the fractional indices of the elements before and after it\n const indexBefore =\n index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)\n const indexAfter =\n index === this.#sortedValues.length\n ? null\n : getIndex(this.#sortedValues[index]!)\n const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)\n\n // Insert the value at the correct position\n const val = indexedValue(value, fractionalIndex)\n // Splice is O(n) where n = all elements in the collection (i.e. n >= k) !\n this.#sortedValues.splice(index, 0, val)\n\n if (this.#topKStart === this.#topKEnd) return result\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The inserted element is either before the top K or within the top K\n // If it is before the top K then it moves the element that was right before the topK into the topK\n // If it is within the top K then the inserted element moves into the top K\n // In both cases the last element of the old top K now moves out of the top K\n const moveInIndex = Math.max(index, this.#topKStart)\n if (moveInIndex < this.#sortedValues.length) {\n // We actually have a topK\n // because in some cases there may not be enough elements in the array to reach the start of the topK\n // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK\n result.moveIn = this.#sortedValues[moveInIndex]!\n\n // We need to remove the element that falls out of the top K\n // The element that falls out of the top K has shifted one to the right\n // because of the element we inserted, so we find it at index topKEnd\n if (this.#topKEnd < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[this.#topKEnd]!\n }\n }\n }\n\n return result\n }\n\n /**\n * Deletes a value that may or may not be in the topK.\n * IMPORTANT: this assumes that the value is present in the collection\n * if it's not the case it will remove the element\n * that is on the position where the provided `value` would be.\n */\n delete(value: V): TopKChanges<V> {\n const result: TopKChanges<V> = { moveIn: null, moveOut: null }\n\n // Lookup delete position\n const index = this.#findIndex(value)\n // Remove the value at that position\n const [removedElem] = this.#sortedValues.splice(index, 1)\n\n if (this.#topKStart === this.#topKEnd) return result\n\n // Check if the topK changed\n if (index < this.#topKEnd) {\n // The removed element is either before the top K or within the top K\n // If it is before the top K then the first element of the topK moves out of the topK\n // If it is within the top K then the removed element moves out of the topK\n result.moveOut = removedElem!\n if (index < this.#topKStart) {\n // The removed element is before the topK\n // so actually, the first element of the topK moves out of the topK\n // and not the element that we removed\n // The first element of the topK is now at index topKStart - 1\n // since we removed an element before the topK\n const moveOutIndex = this.#topKStart - 1\n if (moveOutIndex < this.#sortedValues.length) {\n result.moveOut = this.#sortedValues[moveOutIndex]!\n } else {\n // No value is moving out of the topK\n // because there are no elements in the topK\n result.moveOut = null\n }\n }\n\n // Since we removed an element that was before or in the topK\n // the first element after the topK moved one position to the left\n // and thus falls into the topK now\n const moveInIndex = this.#topKEnd - 1\n if (moveInIndex < this.#sortedValues.length) {\n result.moveIn = this.#sortedValues[moveInIndex]!\n }\n }\n\n return result\n }\n\n // TODO: see if there is a way to refactor the code for insert and delete in the topK above\n // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right\n // so i have the feeling there is a common pattern here and we can implement both cases using that pattern\n\n #findIndex(value: V): number {\n return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>\n this.#comparator(getValue(a), getValue(b)),\n )\n }\n}\n"],"names":["compareKeys","diffHalfOpen","generateKeyBetween","binarySearch"],"mappings":"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"}
|
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const hash = require("../hashing/hash.cjs");
|
|
3
4
|
const topKArray = require("./topKArray.cjs");
|
|
5
|
+
function* topKBatch(messages) {
|
|
6
|
+
const keyed = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const message of messages) {
|
|
8
|
+
for (const [value, weight] of message.getInner()) {
|
|
9
|
+
const entries = keyed.get(value[0]);
|
|
10
|
+
if (entries) entries.push([value, weight]);
|
|
11
|
+
else keyed.set(value[0], [[value, weight]]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const batch = [];
|
|
15
|
+
for (const entries of keyed.values()) {
|
|
16
|
+
if (entries.length === 1) {
|
|
17
|
+
batch.push(entries[0]);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const consolidated = [];
|
|
21
|
+
for (const [value, weight] of entries) {
|
|
22
|
+
const previous = consolidated.find(
|
|
23
|
+
([candidate]) => hash.equalHashValues(candidate[1], value[1])
|
|
24
|
+
);
|
|
25
|
+
if (previous) previous[1] += weight;
|
|
26
|
+
else consolidated.push([value, weight]);
|
|
27
|
+
}
|
|
28
|
+
batch.push(...consolidated);
|
|
29
|
+
}
|
|
30
|
+
for (const entry of batch) if (entry[1] < 0) yield entry;
|
|
31
|
+
for (const entry of batch) if (entry[1] > 0) yield entry;
|
|
32
|
+
}
|
|
4
33
|
class TopKState {
|
|
5
34
|
#multiplicities = /* @__PURE__ */ new Map();
|
|
6
35
|
#topK;
|
|
@@ -70,4 +99,5 @@ function handleMoveOut(moveOut, result) {
|
|
|
70
99
|
exports.TopKState = TopKState;
|
|
71
100
|
exports.handleMoveIn = handleMoveIn;
|
|
72
101
|
exports.handleMoveOut = handleMoveOut;
|
|
102
|
+
exports.topKBatch = topKBatch;
|
|
73
103
|
//# sourceMappingURL=topKState.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topKState.cjs","sources":["../../../src/operators/topKState.ts"],"sourcesContent":["import { TopKArray } from './topKArray.js'\nimport type {\n IndexedValue,\n TopK,\n TopKChanges,\n TopKMoveChanges,\n} from './topKArray.js'\n\n/**\n * Helper class that manages the state for a single topK window.\n * Encapsulates the multiplicity tracking and topK data structure,\n * providing a clean interface for processing elements and moving the window.\n *\n * This class is used by both TopKWithFractionalIndexOperator (single instance)\n * and GroupedTopKWithFractionalIndexOperator (one instance per group).\n */\nexport class TopKState<K extends string | number, T> {\n #multiplicities: Map<K, number> = new Map()\n #topK: TopK<[K, T]>\n\n constructor(topK: TopK<[K, T]>) {\n this.#topK = topK\n }\n\n get size(): number {\n return this.#topK.size\n }\n\n get isEmpty(): boolean {\n return this.#multiplicities.size === 0 && this.#topK.size === 0\n }\n\n /**\n * Process an element update (insert or delete based on multiplicity change).\n * Returns the changes to the topK window.\n */\n processElement(key: K, value: T, multiplicity: number): TopKChanges<[K, T]> {\n const { oldMultiplicity, newMultiplicity } = this.#updateMultiplicity(\n key,\n multiplicity,\n )\n\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n return this.#topK.insert([key, value])\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n return this.#topK.delete([key, value])\n }\n // The value was invisible and remains invisible,\n // or was visible and remains visible - no topK change\n return { moveIn: null, moveOut: null }\n }\n\n /**\n * Move the topK window. Only works with TopKArray implementation.\n */\n move(options: { offset?: number; limit?: number }): TopKMoveChanges<[K, T]> {\n if (!(this.#topK instanceof TopKArray)) {\n throw new Error(\n `Cannot move B+-tree implementation of TopK with fractional index`,\n )\n }\n return this.#topK.move(options)\n }\n\n #updateMultiplicity(\n key: K,\n multiplicity: number,\n ): { oldMultiplicity: number; newMultiplicity: number } {\n if (multiplicity === 0) {\n const current = this.#multiplicities.get(key) ?? 0\n return { oldMultiplicity: current, newMultiplicity: current }\n }\n\n const oldMultiplicity = this.#multiplicities.get(key) ?? 0\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#multiplicities.delete(key)\n } else {\n this.#multiplicities.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\n }\n}\n\n/**\n * Handles a moveIn change by adding it to the result array.\n */\nexport function handleMoveIn<K extends string | number, T>(\n moveIn: IndexedValue<[K, T]> | null,\n result: Array<[[K, IndexedValue<T>], number]>,\n): void {\n if (moveIn) {\n const [[key, value], index] = moveIn\n result.push([[key, [value, index]], 1])\n }\n}\n\n/**\n * Handles a moveOut change by adding it to the result array.\n */\nexport function handleMoveOut<K extends string | number, T>(\n moveOut: IndexedValue<[K, T]> | null,\n result: Array<[[K, IndexedValue<T>], number]>,\n): void {\n if (moveOut) {\n const [[key, value], index] = moveOut\n result.push([[key, [value, index]], -1])\n }\n}\n"],"names":["TopKArray"],"mappings":"
|
|
1
|
+
{"version":3,"file":"topKState.cjs","sources":["../../../src/operators/topKState.ts"],"sourcesContent":["import { equalHashValues } from '../hashing/hash.js'\nimport { TopKArray } from './topKArray.js'\nimport type { MultiSet } from '../multiset.js'\nimport type {\n IndexedValue,\n TopK,\n TopKChanges,\n TopKMoveChanges,\n} from './topKArray.js'\n\n/** Apply a keyed batch's retractions before additions. An outer join can emit\n * a replacement in the opposite order; key multiplicity alone would hide it.\n * Consolidate first so a transient value added and removed in this same turn\n * cannot be mistaken for the final replacement. Use structural relation\n * identity: map can allocate a fresh value for each side of a cancelling pair.\n * No state survives the run.\n */\nexport function* topKBatch<K, T>(messages: Array<MultiSet<[K, T]>>) {\n const keyed = new Map<K, Array<[[K, T], number]>>()\n for (const message of messages) {\n for (const [value, weight] of message.getInner()) {\n const entries = keyed.get(value[0])\n if (entries) entries.push([value, weight])\n else keyed.set(value[0], [[value, weight]])\n }\n }\n const batch: Array<[[K, T], number]> = []\n for (const entries of keyed.values()) {\n // Ordering distinct keys needs only the caller's comparator. In particular,\n // do not traverse irrelevant payloads or merge keys on a hash collision.\n if (entries.length === 1) {\n batch.push(entries[0]!)\n continue\n }\n // A per-key scan keeps normal two-version replacements cheap and permits\n // cycles, at the cost of quadratic comparisons for long same-key histories.\n const consolidated: Array<[[K, T], number]> = []\n for (const [value, weight] of entries) {\n const previous = consolidated.find(([candidate]) =>\n equalHashValues(candidate[1], value[1]),\n )\n if (previous) previous[1] += weight\n else consolidated.push([value, weight])\n }\n batch.push(...consolidated)\n }\n for (const entry of batch) if (entry[1] < 0) yield entry\n for (const entry of batch) if (entry[1] > 0) yield entry\n}\n\n/**\n * Helper class that manages the state for a single topK window.\n * Encapsulates the multiplicity tracking and topK data structure,\n * providing a clean interface for processing elements and moving the window.\n *\n * This class is used by both TopKWithFractionalIndexOperator (single instance)\n * and GroupedTopKWithFractionalIndexOperator (one instance per group).\n */\nexport class TopKState<K extends string | number, T> {\n #multiplicities: Map<K, number> = new Map()\n #topK: TopK<[K, T]>\n\n constructor(topK: TopK<[K, T]>) {\n this.#topK = topK\n }\n\n get size(): number {\n return this.#topK.size\n }\n\n get isEmpty(): boolean {\n return this.#multiplicities.size === 0 && this.#topK.size === 0\n }\n\n /**\n * Process an element update (insert or delete based on multiplicity change).\n * Returns the changes to the topK window.\n */\n processElement(key: K, value: T, multiplicity: number): TopKChanges<[K, T]> {\n const { oldMultiplicity, newMultiplicity } = this.#updateMultiplicity(\n key,\n multiplicity,\n )\n\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n return this.#topK.insert([key, value])\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n return this.#topK.delete([key, value])\n }\n // The value was invisible and remains invisible,\n // or was visible and remains visible - no topK change\n return { moveIn: null, moveOut: null }\n }\n\n /**\n * Move the topK window. Only works with TopKArray implementation.\n */\n move(options: { offset?: number; limit?: number }): TopKMoveChanges<[K, T]> {\n if (!(this.#topK instanceof TopKArray)) {\n throw new Error(\n `Cannot move B+-tree implementation of TopK with fractional index`,\n )\n }\n return this.#topK.move(options)\n }\n\n #updateMultiplicity(\n key: K,\n multiplicity: number,\n ): { oldMultiplicity: number; newMultiplicity: number } {\n if (multiplicity === 0) {\n const current = this.#multiplicities.get(key) ?? 0\n return { oldMultiplicity: current, newMultiplicity: current }\n }\n\n const oldMultiplicity = this.#multiplicities.get(key) ?? 0\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#multiplicities.delete(key)\n } else {\n this.#multiplicities.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\n }\n}\n\n/**\n * Handles a moveIn change by adding it to the result array.\n */\nexport function handleMoveIn<K extends string | number, T>(\n moveIn: IndexedValue<[K, T]> | null,\n result: Array<[[K, IndexedValue<T>], number]>,\n): void {\n if (moveIn) {\n const [[key, value], index] = moveIn\n result.push([[key, [value, index]], 1])\n }\n}\n\n/**\n * Handles a moveOut change by adding it to the result array.\n */\nexport function handleMoveOut<K extends string | number, T>(\n moveOut: IndexedValue<[K, T]> | null,\n result: Array<[[K, IndexedValue<T>], number]>,\n): void {\n if (moveOut) {\n const [[key, value], index] = moveOut\n result.push([[key, [value, index]], -1])\n }\n}\n"],"names":["equalHashValues","TopKArray"],"mappings":"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"}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
import { MultiSet } from '../multiset.js';
|
|
1
2
|
import { IndexedValue, TopK, TopKChanges, TopKMoveChanges } from './topKArray.js';
|
|
3
|
+
/** Apply a keyed batch's retractions before additions. An outer join can emit
|
|
4
|
+
* a replacement in the opposite order; key multiplicity alone would hide it.
|
|
5
|
+
* Consolidate first so a transient value added and removed in this same turn
|
|
6
|
+
* cannot be mistaken for the final replacement. Use structural relation
|
|
7
|
+
* identity: map can allocate a fresh value for each side of a cancelling pair.
|
|
8
|
+
* No state survives the run.
|
|
9
|
+
*/
|
|
10
|
+
export declare function topKBatch<K, T>(messages: Array<MultiSet<[K, T]>>): Generator<[[K, T], number], void, unknown>;
|
|
2
11
|
/**
|
|
3
12
|
* Helper class that manages the state for a single topK window.
|
|
4
13
|
* Encapsulates the multiplicity tracking and topK data structure,
|
|
@@ -38,11 +38,10 @@ class TopKWithFractionalIndexOperator extends graph.UnaryOperator {
|
|
|
38
38
|
}
|
|
39
39
|
run() {
|
|
40
40
|
const result = [];
|
|
41
|
-
for (const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
41
|
+
for (const [[key, value], multiplicity] of topKState.topKBatch(
|
|
42
|
+
this.inputMessages()
|
|
43
|
+
)) {
|
|
44
|
+
this.processElement(key, value, multiplicity, result);
|
|
46
45
|
}
|
|
47
46
|
if (result.length > 0) {
|
|
48
47
|
this.output.sendData(new multiset.MultiSet(result));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topKWithFractionalIndex.cjs","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport {
|
|
1
|
+
{"version":3,"file":"topKWithFractionalIndex.cjs","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport {\n TopKState,\n handleMoveIn,\n handleMoveOut,\n topKBatch,\n} from './topKState.js'\nimport { TopKArray, createKeyedComparator } from './topKArray.js'\nimport type { IndexedValue, TopK } from './topKArray.js'\nimport type { DifferenceStreamReader } from '../graph.js'\nimport type { IStreamBuilder, PipedOperator } from '../types.js'\n\nexport interface TopKWithFractionalIndexOptions {\n limit?: number\n offset?: number\n setSizeCallback?: (getSize: () => number) => void\n setWindowFn?: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => void\n}\n\n/**\n * Operator for fractional indexed topK operations\n * This operator maintains fractional indices for sorted elements\n * and only updates indices when elements move position\n */\nexport class TopKWithFractionalIndexOperator<\n K extends string | number,\n T,\n> extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {\n #state: TopKState<K, T>\n\n constructor(\n id: number,\n inputA: DifferenceStreamReader<[K, T]>,\n output: DifferenceStreamWriter<[K, IndexedValue<T>]>,\n comparator: (a: T, b: T) => number,\n options: TopKWithFractionalIndexOptions,\n ) {\n super(id, inputA, output)\n const limit = options.limit ?? Infinity\n const offset = options.offset ?? 0\n const topK = this.createTopK(\n offset,\n limit,\n createKeyedComparator(comparator),\n )\n this.#state = new TopKState(topK)\n options.setSizeCallback?.(() => this.#state.size)\n options.setWindowFn?.(this.moveTopK.bind(this))\n }\n\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: [K, T], b: [K, T]) => number,\n ): TopK<[K, T]> {\n return new TopKArray(offset, limit, comparator)\n }\n\n /**\n * Moves the topK window based on the provided offset and limit.\n * Any changes to the topK are sent to the output.\n */\n moveTopK({ offset, limit }: { offset?: number; limit?: number }) {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n const diff = this.#state.move({ offset, limit })\n\n diff.moveIns.forEach((moveIn) => handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => handleMoveOut(moveOut, result))\n\n if (diff.changes) {\n // There are changes to the topK\n // it could be that moveIns and moveOuts are empty\n // because the collection is lazy, so we will run the graph again to load the data\n this.output.sendData(new MultiSet(result))\n }\n }\n\n run(): void {\n const result: Array<[[K, IndexedValue<T>], number]> = []\n for (const [[key, value], multiplicity] of topKBatch(\n this.inputMessages(),\n )) {\n this.processElement(key, value, multiplicity, result)\n }\n\n if (result.length > 0) {\n this.output.sendData(new MultiSet(result))\n }\n }\n\n processElement(\n key: K,\n value: T,\n multiplicity: number,\n result: Array<[[K, IndexedValue<T>], number]>,\n ): void {\n const changes = this.#state.processElement(key, value, multiplicity)\n handleMoveIn(changes.moveIn, result)\n handleMoveOut(changes.moveOut, result)\n }\n}\n\n/**\n * Limits the number of results based on a comparator, with optional offset.\n * Uses fractional indexing to minimize the number of changes when elements move positions.\n * Each element is assigned a fractional index that is lexicographically sortable.\n * When elements move, only the indices of the moved elements are updated, not all elements.\n *\n * @param comparator - A function that compares two elements\n * @param options - An optional object containing limit and offset properties\n * @returns A piped operator that orders the elements and limits the number of results\n */\nexport function topKWithFractionalIndex<KType extends string | number, T>(\n comparator: (a: T, b: T) => number,\n options?: TopKWithFractionalIndexOptions,\n): PipedOperator<[KType, T], [KType, IndexedValue<T>]> {\n const opts = options || {}\n\n return (\n stream: IStreamBuilder<[KType, T]>,\n ): IStreamBuilder<[KType, IndexedValue<T>]> => {\n const output = new StreamBuilder<[KType, IndexedValue<T>]>(\n stream.graph,\n new DifferenceStreamWriter<[KType, IndexedValue<T>]>(),\n )\n const operator = new TopKWithFractionalIndexOperator<KType, T>(\n stream.graph.getNextOperatorId(),\n stream.connectReader(),\n output.writer,\n comparator,\n opts,\n )\n stream.graph.addOperator(operator)\n return output\n }\n}\n"],"names":["UnaryOperator","createKeyedComparator","TopKState","TopKArray","handleMoveIn","handleMoveOut","MultiSet","topKBatch","StreamBuilder","DifferenceStreamWriter"],"mappings":";;;;;;;AA4BO,MAAM,wCAGHA,MAAAA,cAA4C;AAAA,EACpD;AAAA,EAEA,YACE,IACA,QACA,QACA,YACA,SACA;AACA,UAAM,IAAI,QAAQ,MAAM;AACxB,UAAM,QAAQ,QAAQ,SAAS;AAC/B,UAAM,SAAS,QAAQ,UAAU;AACjC,UAAM,OAAO,KAAK;AAAA,MAChB;AAAA,MACA;AAAA,MACAC,UAAAA,sBAAsB,UAAU;AAAA,IAAA;AAElC,SAAK,SAAS,IAAIC,UAAAA,UAAU,IAAI;AAChC,YAAQ,kBAAkB,MAAM,KAAK,OAAO,IAAI;AAChD,YAAQ,cAAc,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,EAChD;AAAA,EAEU,WACR,QACA,OACA,YACc;AACd,WAAO,IAAIC,UAAAA,UAAU,QAAQ,OAAO,UAAU;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,EAAE,QAAQ,SAA8C;AAC/D,UAAM,SAAgD,CAAA;AACtD,UAAM,OAAO,KAAK,OAAO,KAAK,EAAE,QAAQ,OAAO;AAE/C,SAAK,QAAQ,QAAQ,CAAC,WAAWC,uBAAa,QAAQ,MAAM,CAAC;AAC7D,SAAK,SAAS,QAAQ,CAAC,YAAYC,wBAAc,SAAS,MAAM,CAAC;AAEjE,QAAI,KAAK,SAAS;AAIhB,WAAK,OAAO,SAAS,IAAIC,SAAAA,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAY;AACV,UAAM,SAAgD,CAAA;AACtD,eAAW,CAAC,CAAC,KAAK,KAAK,GAAG,YAAY,KAAKC,UAAAA;AAAAA,MACzC,KAAK,cAAA;AAAA,IAAc,GAClB;AACD,WAAK,eAAe,KAAK,OAAO,cAAc,MAAM;AAAA,IACtD;AAEA,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO,SAAS,IAAID,SAAAA,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,eACE,KACA,OACA,cACA,QACM;AACN,UAAM,UAAU,KAAK,OAAO,eAAe,KAAK,OAAO,YAAY;AACnEF,2BAAa,QAAQ,QAAQ,MAAM;AACnCC,4BAAc,QAAQ,SAAS,MAAM;AAAA,EACvC;AACF;AAYO,SAAS,wBACd,YACA,SACqD;AACrD,QAAM,OAAO,WAAW,CAAA;AAExB,SAAO,CACL,WAC6C;AAC7C,UAAM,SAAS,IAAIG,GAAAA;AAAAA,MACjB,OAAO;AAAA,MACP,IAAIC,MAAAA,uBAAA;AAAA,IAAiD;AAEvD,UAAM,WAAW,IAAI;AAAA,MACnB,OAAO,MAAM,kBAAA;AAAA,MACb,OAAO,cAAA;AAAA,MACP,OAAO;AAAA,MACP;AAAA,MACA;AAAA,IAAA;AAEF,WAAO,MAAM,YAAY,QAAQ;AACjC,WAAO;AAAA,EACT;AACF;;;"}
|