@tanstack/db-ivm 0.1.19 → 0.1.22
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/hashing/hash.cjs +114 -45
- package/dist/cjs/hashing/hash.cjs.map +1 -1
- package/dist/cjs/hashing/hash.d.cts +7 -0
- package/dist/cjs/hashing/murmur.cjs +32 -9
- package/dist/cjs/hashing/murmur.cjs.map +1 -1
- package/dist/cjs/hashing/murmur.d.cts +1 -0
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -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 +1 -1
- 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 +8 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/hashing/hash.d.ts +7 -0
- package/dist/esm/hashing/hash.js +116 -47
- package/dist/esm/hashing/hash.js.map +1 -1
- package/dist/esm/hashing/murmur.d.ts +1 -0
- package/dist/esm/hashing/murmur.js +32 -9
- package/dist/esm/hashing/murmur.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.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 +1 -1
- 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.js +8 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -2
- package/src/hashing/hash.ts +189 -57
- package/src/hashing/murmur.ts +44 -10
- package/src/index.ts +1 -0
- package/src/operators/distinct.ts +5 -3
- package/src/operators/groupBy.ts +7 -4
- 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 +8 -0
- package/dist/cjs/operators/orderByBTree.d.cts +0 -3
- package/dist/esm/operators/orderByBTree.d.ts +0 -3
- package/src/operators/orderByBTree.ts +0 -20
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topKWithFractionalIndex.js","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.js","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":[],"mappings":";;;;;AA4BO,MAAM,wCAGH,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,MACA,sBAAsB,UAAU;AAAA,IAAA;AAElC,SAAK,SAAS,IAAI,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,IAAI,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,WAAW,aAAa,QAAQ,MAAM,CAAC;AAC7D,SAAK,SAAS,QAAQ,CAAC,YAAY,cAAc,SAAS,MAAM,CAAC;AAEjE,QAAI,KAAK,SAAS;AAIhB,WAAK,OAAO,SAAS,IAAI,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAY;AACV,UAAM,SAAgD,CAAA;AACtD,eAAW,CAAC,CAAC,KAAK,KAAK,GAAG,YAAY,KAAK;AAAA,MACzC,KAAK,cAAA;AAAA,IAAc,GAClB;AACD,WAAK,eAAe,KAAK,OAAO,cAAc,MAAM;AAAA,IACtD;AAEA,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,OAAO,SAAS,IAAI,SAAS,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,eACE,KACA,OACA,cACA,QACM;AACN,UAAM,UAAU,KAAK,OAAO,eAAe,KAAK,OAAO,YAAY;AACnE,iBAAa,QAAQ,QAAQ,MAAM;AACnC,kBAAc,QAAQ,SAAS,MAAM;AAAA,EACvC;AACF;AAYO,SAAS,wBACd,YACA,SACqD;AACrD,QAAM,OAAO,WAAW,CAAA;AAExB,SAAO,CACL,WAC6C;AAC7C,UAAM,SAAS,IAAI;AAAA,MACjB,OAAO;AAAA,MACP,IAAI,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;"}
|
package/dist/esm/utils.js
CHANGED
|
@@ -105,6 +105,14 @@ function range(start, end) {
|
|
|
105
105
|
}
|
|
106
106
|
function compareKeys(a, b) {
|
|
107
107
|
if (typeof a === typeof b) {
|
|
108
|
+
if (typeof a === `number` && typeof b === `number`) {
|
|
109
|
+
const aIsNaN = Number.isNaN(a);
|
|
110
|
+
const bIsNaN = Number.isNaN(b);
|
|
111
|
+
if (aIsNaN || bIsNaN) {
|
|
112
|
+
if (aIsNaN && bIsNaN) return 0;
|
|
113
|
+
return aIsNaN ? 1 : -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
108
116
|
if (a < b) return -1;
|
|
109
117
|
if (a > b) return 1;
|
|
110
118
|
return 0;
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string,\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>,\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number,\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\ntype CanonicalValue =\n | readonly [`undefined`]\n | readonly [`null`]\n | readonly [`boolean`, boolean]\n | readonly [`number`, number | `NaN` | `Infinity` | `-Infinity`]\n | readonly [`bigint`, string]\n | readonly [`string`, string]\n | readonly [`date`, number | `Invalid`]\n | readonly [`regexp`, string, string]\n | readonly [`bytes`, Array<number>]\n | readonly [`array`, Array<CanonicalValue>]\n | readonly [`map`, Array<readonly [CanonicalValue, CanonicalValue]>]\n | readonly [`set`, Array<CanonicalValue>]\n | readonly [`object`, Array<readonly [string, CanonicalValue]>]\n\n/**\n * Serializes a supported query value into one canonical key.\n *\n * JSON's native encoding is not suitable for relation keys: it merges BigInt\n * with strings when a replacer is used, merges NaN with null, drops undefined,\n * and depends on object insertion order. Ordinary JSON values keep their\n * established wire form. Values that need richer types use a reserved prefix\n * plus a structural, type-tagged encoding.\n */\nexport function serializeValue(value: unknown): string {\n if (isJsonSafeStructuralValue(value, new Set())) {\n return JSON.stringify(toStableJsonValue(value))\n }\n\n return `~${JSON.stringify(toCanonicalValue(value, new Set()))}`\n}\n\ntype JsonValue =\n | null\n | boolean\n | number\n | string\n | Array<JsonValue>\n | { [key: string]: JsonValue }\n\nfunction isJsonSafeStructuralValue(\n value: unknown,\n ancestors: Set<object>,\n): boolean {\n if (value === null) return true\n\n switch (typeof value) {\n case `boolean`:\n case `string`:\n return true\n case `number`:\n return Number.isFinite(value)\n case `undefined`:\n case `bigint`:\n case `symbol`:\n case `function`:\n return false\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (\n value instanceof Date ||\n value instanceof RegExp ||\n value instanceof Uint8Array ||\n value instanceof Map ||\n value instanceof Set\n ) {\n return false\n }\n\n return Array.isArray(value)\n ? value.every((item) => isJsonSafeStructuralValue(item, ancestors))\n : Object.keys(value).every((key) =>\n isJsonSafeStructuralValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n )\n })\n}\n\nfunction toStableJsonValue(value: unknown): JsonValue {\n if (\n value === null ||\n typeof value === `boolean` ||\n typeof value === `number` ||\n typeof value === `string`\n ) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map(toStableJsonValue)\n }\n\n return Object.fromEntries(\n Object.keys(value as object)\n .sort()\n .map((key) => [\n key,\n toStableJsonValue((value as Record<string, unknown>)[key]),\n ]),\n )\n}\n\nfunction toCanonicalValue(\n value: unknown,\n ancestors: Set<object>,\n): CanonicalValue {\n if (value === undefined) return [`undefined`]\n if (value === null) return [`null`]\n\n switch (typeof value) {\n case `boolean`:\n return [`boolean`, value]\n case `number`:\n if (Number.isNaN(value)) return [`number`, `NaN`]\n if (value === Infinity) return [`number`, `Infinity`]\n if (value === -Infinity) return [`number`, `-Infinity`]\n return [`number`, value === 0 ? 0 : value]\n case `bigint`:\n return [`bigint`, value.toString()]\n case `string`:\n return [`string`, value]\n case `symbol`:\n case `function`:\n throw new TypeError(\n `Cannot serialize ${typeof value} as a structural relation key`,\n )\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (value instanceof Date) {\n const timestamp = value.getTime()\n return Number.isNaN(timestamp)\n ? ([`date`, `Invalid`] as const)\n : ([`date`, timestamp] as const)\n }\n\n if (value instanceof RegExp) {\n return [`regexp`, value.source, value.flags]\n }\n\n if (value instanceof Uint8Array) {\n return [`bytes`, Array.from(value)]\n }\n\n if (Array.isArray(value)) {\n return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]\n }\n\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([key, entryValue]) =>\n [\n toCanonicalValue(key, ancestors),\n toCanonicalValue(entryValue, ancestors),\n ] as const,\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`map`, entries]\n }\n\n if (value instanceof Set) {\n const entries = [...value].map((entry) =>\n toCanonicalValue(entry, ancestors),\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`set`, entries]\n }\n\n const entries = Object.keys(value)\n .sort()\n .map(\n (key) =>\n [\n key,\n toCanonicalValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n ] as const,\n )\n return [`object`, entries]\n })\n}\n\nfunction compareSerializedValues(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0\n}\n\nfunction withAcyclicValue<T>(\n value: object,\n ancestors: Set<object>,\n encode: () => T,\n): T {\n if (ancestors.has(value)) {\n throw new TypeError(`Cannot serialize a cyclic structural relation key`)\n }\n\n ancestors.add(value)\n try {\n return encode()\n } finally {\n ancestors.delete(value)\n }\n}\n"],"names":["entries"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string,\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>,\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number,\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (typeof a === `number` && typeof b === `number`) {\n const aIsNaN = Number.isNaN(a)\n const bIsNaN = Number.isNaN(b)\n if (aIsNaN || bIsNaN) {\n if (aIsNaN && bIsNaN) return 0\n return aIsNaN ? 1 : -1\n }\n }\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\ntype CanonicalValue =\n | readonly [`undefined`]\n | readonly [`null`]\n | readonly [`boolean`, boolean]\n | readonly [`number`, number | `NaN` | `Infinity` | `-Infinity`]\n | readonly [`bigint`, string]\n | readonly [`string`, string]\n | readonly [`date`, number | `Invalid`]\n | readonly [`regexp`, string, string]\n | readonly [`bytes`, Array<number>]\n | readonly [`array`, Array<CanonicalValue>]\n | readonly [`map`, Array<readonly [CanonicalValue, CanonicalValue]>]\n | readonly [`set`, Array<CanonicalValue>]\n | readonly [`object`, Array<readonly [string, CanonicalValue]>]\n\n/**\n * Serializes a supported query value into one canonical key.\n *\n * JSON's native encoding is not suitable for relation keys: it merges BigInt\n * with strings when a replacer is used, merges NaN with null, drops undefined,\n * and depends on object insertion order. Ordinary JSON values keep their\n * established wire form. Values that need richer types use a reserved prefix\n * plus a structural, type-tagged encoding.\n */\nexport function serializeValue(value: unknown): string {\n if (isJsonSafeStructuralValue(value, new Set())) {\n return JSON.stringify(toStableJsonValue(value))\n }\n\n return `~${JSON.stringify(toCanonicalValue(value, new Set()))}`\n}\n\ntype JsonValue =\n | null\n | boolean\n | number\n | string\n | Array<JsonValue>\n | { [key: string]: JsonValue }\n\nfunction isJsonSafeStructuralValue(\n value: unknown,\n ancestors: Set<object>,\n): boolean {\n if (value === null) return true\n\n switch (typeof value) {\n case `boolean`:\n case `string`:\n return true\n case `number`:\n return Number.isFinite(value)\n case `undefined`:\n case `bigint`:\n case `symbol`:\n case `function`:\n return false\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (\n value instanceof Date ||\n value instanceof RegExp ||\n value instanceof Uint8Array ||\n value instanceof Map ||\n value instanceof Set\n ) {\n return false\n }\n\n return Array.isArray(value)\n ? value.every((item) => isJsonSafeStructuralValue(item, ancestors))\n : Object.keys(value).every((key) =>\n isJsonSafeStructuralValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n )\n })\n}\n\nfunction toStableJsonValue(value: unknown): JsonValue {\n if (\n value === null ||\n typeof value === `boolean` ||\n typeof value === `number` ||\n typeof value === `string`\n ) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map(toStableJsonValue)\n }\n\n return Object.fromEntries(\n Object.keys(value as object)\n .sort()\n .map((key) => [\n key,\n toStableJsonValue((value as Record<string, unknown>)[key]),\n ]),\n )\n}\n\nfunction toCanonicalValue(\n value: unknown,\n ancestors: Set<object>,\n): CanonicalValue {\n if (value === undefined) return [`undefined`]\n if (value === null) return [`null`]\n\n switch (typeof value) {\n case `boolean`:\n return [`boolean`, value]\n case `number`:\n if (Number.isNaN(value)) return [`number`, `NaN`]\n if (value === Infinity) return [`number`, `Infinity`]\n if (value === -Infinity) return [`number`, `-Infinity`]\n return [`number`, value === 0 ? 0 : value]\n case `bigint`:\n return [`bigint`, value.toString()]\n case `string`:\n return [`string`, value]\n case `symbol`:\n case `function`:\n throw new TypeError(\n `Cannot serialize ${typeof value} as a structural relation key`,\n )\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (value instanceof Date) {\n const timestamp = value.getTime()\n return Number.isNaN(timestamp)\n ? ([`date`, `Invalid`] as const)\n : ([`date`, timestamp] as const)\n }\n\n if (value instanceof RegExp) {\n return [`regexp`, value.source, value.flags]\n }\n\n if (value instanceof Uint8Array) {\n return [`bytes`, Array.from(value)]\n }\n\n if (Array.isArray(value)) {\n return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]\n }\n\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([key, entryValue]) =>\n [\n toCanonicalValue(key, ancestors),\n toCanonicalValue(entryValue, ancestors),\n ] as const,\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`map`, entries]\n }\n\n if (value instanceof Set) {\n const entries = [...value].map((entry) =>\n toCanonicalValue(entry, ancestors),\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`set`, entries]\n }\n\n const entries = Object.keys(value)\n .sort()\n .map(\n (key) =>\n [\n key,\n toCanonicalValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n ] as const,\n )\n return [`object`, entries]\n })\n}\n\nfunction compareSerializedValues(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0\n}\n\nfunction withAcyclicValue<T>(\n value: object,\n ancestors: Set<object>,\n encode: () => T,\n): T {\n if (ancestors.has(value)) {\n throw new TypeError(`Cannot serialize a cyclic structural relation key`)\n }\n\n ancestors.add(value)\n try {\n return encode()\n } finally {\n ancestors.delete(value)\n }\n}\n"],"names":["entries"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db-ivm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Incremental View Maintenance for TanStack DB based on Differential Dataflow",
|
|
5
5
|
"author": "Sam Willis",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
"build": "vite build",
|
|
57
57
|
"dev": "vite build --watch",
|
|
58
58
|
"lint": "eslint . --fix",
|
|
59
|
-
"test": "vitest --run"
|
|
59
|
+
"test": "vitest --run",
|
|
60
|
+
"test:incrementalization": "vitest --run tests/incrementalization-law.property.test.ts",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"bench:hash": "vitest bench --run tests/hash.bench.ts --coverage.enabled=false"
|
|
60
63
|
}
|
|
61
64
|
}
|
package/src/hashing/hash.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MurmurHashStream, randomHash } from './murmur.js'
|
|
1
|
+
import { MurmurHashStream, getSymbolIdentity, randomHash } from './murmur.js'
|
|
2
2
|
import type { Hasher } from './murmur.js'
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -13,12 +13,18 @@ const UNDEFINED = randomHash()
|
|
|
13
13
|
const KEY = randomHash()
|
|
14
14
|
const FUNCTIONS = randomHash()
|
|
15
15
|
const DATE_MARKER = randomHash()
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const STRUCTURAL_MARKERS = {
|
|
17
|
+
object: randomHash(),
|
|
18
|
+
array: randomHash(),
|
|
19
|
+
map: randomHash(),
|
|
20
|
+
set: randomHash(),
|
|
21
|
+
}
|
|
20
22
|
const UINT8ARRAY_MARKER = randomHash()
|
|
21
23
|
const TEMPORAL_MARKER = randomHash()
|
|
24
|
+
// Bound structural recursion and value visits. Shared acyclic subtrees are
|
|
25
|
+
// cached; cycles are rejected rather than given context-dependent hashes.
|
|
26
|
+
const MAX_STRUCTURAL_HASH_WORK = 1_000_000
|
|
27
|
+
const MAX_STRUCTURAL_HASH_DEPTH = 768
|
|
22
28
|
|
|
23
29
|
const temporalTypes = new Set([
|
|
24
30
|
`Temporal.Duration`,
|
|
@@ -47,6 +53,18 @@ function isTemporal(input: object): input is TemporalLike {
|
|
|
47
53
|
const UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128
|
|
48
54
|
|
|
49
55
|
const hashCache = new WeakMap<object, number>()
|
|
56
|
+
const referenceValues = new WeakSet<object>()
|
|
57
|
+
|
|
58
|
+
/** @internal Register a mutable handle before it enters a structural value. */
|
|
59
|
+
export function registerOpaqueHash(value: object): void {
|
|
60
|
+
cachedReferenceHash(value)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type HashContext = {
|
|
64
|
+
activeObjects: Set<object>
|
|
65
|
+
work: number
|
|
66
|
+
pendingHashes: Map<object, number>
|
|
67
|
+
}
|
|
50
68
|
|
|
51
69
|
export function hash(input: any): number {
|
|
52
70
|
const hasher = new MurmurHashStream()
|
|
@@ -54,57 +72,36 @@ export function hash(input: any): number {
|
|
|
54
72
|
return hasher.digest()
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
function hashObject(input: object): number {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
function hashObject(input: object, context: HashContext): number {
|
|
76
|
+
if (context.activeObjects.size >= MAX_STRUCTURAL_HASH_DEPTH) {
|
|
77
|
+
throw new RangeError(
|
|
78
|
+
`Value is too complex to hash safely: structural depth`,
|
|
79
|
+
)
|
|
61
80
|
}
|
|
62
81
|
|
|
82
|
+
context.activeObjects.add(input)
|
|
83
|
+
|
|
63
84
|
let valueHash: number | undefined
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(typeof Buffer !== `undefined` && input instanceof Buffer) ||
|
|
69
|
-
input instanceof Uint8Array
|
|
70
|
-
) {
|
|
71
|
-
// For small Uint8Arrays/Buffers (e.g., ULIDs, UUIDs), hash by content
|
|
72
|
-
// to enable proper equality comparisons. For large arrays, hash by reference
|
|
73
|
-
// to avoid performance costs.
|
|
74
|
-
if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {
|
|
85
|
+
try {
|
|
86
|
+
if (input instanceof Date) {
|
|
87
|
+
valueHash = hashDate(input)
|
|
88
|
+
} else if (isBinaryValue(input)) {
|
|
75
89
|
valueHash = hashUint8Array(input)
|
|
90
|
+
} else if (isTemporal(input)) {
|
|
91
|
+
valueHash = hashTemporal(input)
|
|
76
92
|
} else {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return cachedReferenceHash(input)
|
|
84
|
-
} else if (isTemporal(input)) {
|
|
85
|
-
valueHash = hashTemporal(input)
|
|
86
|
-
} else {
|
|
87
|
-
let plainObjectInput = input
|
|
88
|
-
let marker = OBJECT_MARKER
|
|
89
|
-
|
|
90
|
-
if (input instanceof Array) {
|
|
91
|
-
marker = ARRAY_MARKER
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (input instanceof Map) {
|
|
95
|
-
marker = MAP_MARKER
|
|
96
|
-
plainObjectInput = [...input.entries()]
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (input instanceof Set) {
|
|
100
|
-
marker = SET_MARKER
|
|
101
|
-
plainObjectInput = [...input.entries()]
|
|
93
|
+
const [kind, plainObjectInput] = structuralShape(input)
|
|
94
|
+
valueHash = hashPlainObject(
|
|
95
|
+
plainObjectInput,
|
|
96
|
+
STRUCTURAL_MARKERS[kind],
|
|
97
|
+
context,
|
|
98
|
+
)
|
|
102
99
|
}
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
} finally {
|
|
101
|
+
context.activeObjects.delete(input)
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
|
|
104
|
+
context.pendingHashes.set(input, valueHash)
|
|
108
105
|
return valueHash
|
|
109
106
|
}
|
|
110
107
|
|
|
@@ -135,7 +132,11 @@ function hashTemporal(input: TemporalLike): number {
|
|
|
135
132
|
return hasher.digest()
|
|
136
133
|
}
|
|
137
134
|
|
|
138
|
-
function hashPlainObject(
|
|
135
|
+
function hashPlainObject(
|
|
136
|
+
input: object,
|
|
137
|
+
marker: number,
|
|
138
|
+
context: HashContext,
|
|
139
|
+
): number {
|
|
139
140
|
const hasher = new MurmurHashStream()
|
|
140
141
|
|
|
141
142
|
// Mark the type of the input
|
|
@@ -145,13 +146,28 @@ function hashPlainObject(input: object, marker: number): number {
|
|
|
145
146
|
for (const key of keys) {
|
|
146
147
|
hasher.update(KEY)
|
|
147
148
|
hasher.update(key)
|
|
148
|
-
updateHasher(hasher, input[key as keyof typeof input])
|
|
149
|
+
updateHasher(hasher, input[key as keyof typeof input], context)
|
|
150
|
+
}
|
|
151
|
+
const symbolKeys = Object.getOwnPropertySymbols(input)
|
|
152
|
+
.filter((key) => Object.prototype.propertyIsEnumerable.call(input, key))
|
|
153
|
+
.sort((left, right) => getSymbolIdentity(left) - getSymbolIdentity(right))
|
|
154
|
+
for (const key of symbolKeys) {
|
|
155
|
+
hasher.update(KEY)
|
|
156
|
+
hasher.update(key)
|
|
157
|
+
updateHasher(hasher, input[key as keyof typeof input], context)
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
return hasher.digest()
|
|
152
161
|
}
|
|
153
162
|
|
|
154
|
-
function updateHasher(
|
|
163
|
+
function updateHasher(
|
|
164
|
+
hasher: Hasher,
|
|
165
|
+
input: unknown,
|
|
166
|
+
context?: HashContext,
|
|
167
|
+
): void {
|
|
168
|
+
if (context && ++context.work > MAX_STRUCTURAL_HASH_WORK) {
|
|
169
|
+
throw new RangeError(`Value is too complex to hash safely: structural work`)
|
|
170
|
+
}
|
|
155
171
|
if (input === null) {
|
|
156
172
|
hasher.update(NULL)
|
|
157
173
|
return
|
|
@@ -173,7 +189,7 @@ function updateHasher(hasher: Hasher, input: unknown): void {
|
|
|
173
189
|
hasher.update(input)
|
|
174
190
|
return
|
|
175
191
|
case `object`:
|
|
176
|
-
hasher.update(getCachedHash(input))
|
|
192
|
+
hasher.update(getCachedHash(input, context))
|
|
177
193
|
return
|
|
178
194
|
case `function`:
|
|
179
195
|
// Functions are assigned a globally unique ID
|
|
@@ -187,12 +203,127 @@ function updateHasher(hasher: Hasher, input: unknown): void {
|
|
|
187
203
|
}
|
|
188
204
|
}
|
|
189
205
|
|
|
190
|
-
function getCachedHash(input: object): number {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
206
|
+
function getCachedHash(input: object, context?: HashContext): number {
|
|
207
|
+
if (!context) {
|
|
208
|
+
const cached = hashCache.get(input)
|
|
209
|
+
if (cached !== undefined) return cached
|
|
210
|
+
if (isReferenceHashedObject(input)) return cachedReferenceHash(input)
|
|
211
|
+
|
|
212
|
+
// Only an uncached structural root needs graph traversal state. Commit its
|
|
213
|
+
// cache entries after success so a failed traversal cannot poison retries.
|
|
214
|
+
context = {
|
|
215
|
+
activeObjects: new Set(),
|
|
216
|
+
work: 0,
|
|
217
|
+
pendingHashes: new Map(),
|
|
218
|
+
}
|
|
219
|
+
const result = hashObject(input, context)
|
|
220
|
+
for (const [object, valueHash] of context.pendingHashes) {
|
|
221
|
+
hashCache.set(object, valueHash)
|
|
222
|
+
}
|
|
223
|
+
return result
|
|
194
224
|
}
|
|
195
|
-
|
|
225
|
+
|
|
226
|
+
if (context.activeObjects.has(input)) {
|
|
227
|
+
throw new TypeError(`Cannot hash cyclic structural values`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Opaque leaves cannot contain structural back-references. Resolve them
|
|
231
|
+
// before entering structural recursion, even when they have user properties.
|
|
232
|
+
if (isReferenceHashedObject(input)) return cachedReferenceHash(input)
|
|
233
|
+
|
|
234
|
+
const valueHash = hashCache.get(input) ?? context.pendingHashes.get(input)
|
|
235
|
+
if (valueHash !== undefined) return valueHash
|
|
236
|
+
|
|
237
|
+
return hashObject(input, context)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isReferenceHashedObject(input: object): boolean {
|
|
241
|
+
return (
|
|
242
|
+
input instanceof File ||
|
|
243
|
+
(isBinaryValue(input) &&
|
|
244
|
+
input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD)
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isBinaryValue(input: object): input is Uint8Array {
|
|
249
|
+
return (
|
|
250
|
+
(typeof Buffer !== `undefined` && input instanceof Buffer) ||
|
|
251
|
+
input instanceof Uint8Array
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function structuralShape(
|
|
256
|
+
input: object,
|
|
257
|
+
): [keyof typeof STRUCTURAL_MARKERS, object] {
|
|
258
|
+
if (input instanceof Map) return [`map`, [...input.entries()]]
|
|
259
|
+
if (input instanceof Set) return [`set`, [...input.values()]]
|
|
260
|
+
return [input instanceof Array ? `array` : `object`, input]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** @internal Compare immutable structural values without computing a digest.
|
|
264
|
+
* Pair memoization also permits cyclic values that structural hashing rejects.
|
|
265
|
+
* Reference-valued leaves remain opaque, including registered mutable handles.
|
|
266
|
+
*/
|
|
267
|
+
export function equalHashValues(left: unknown, right: unknown): boolean {
|
|
268
|
+
const compared = new Map<object, Set<object>>()
|
|
269
|
+
function equal(a: unknown, b: unknown): boolean {
|
|
270
|
+
if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true
|
|
271
|
+
if (
|
|
272
|
+
a === null ||
|
|
273
|
+
b === null ||
|
|
274
|
+
typeof a !== `object` ||
|
|
275
|
+
typeof b !== `object`
|
|
276
|
+
)
|
|
277
|
+
return false
|
|
278
|
+
if (
|
|
279
|
+
referenceValues.has(a) ||
|
|
280
|
+
referenceValues.has(b) ||
|
|
281
|
+
isReferenceHashedObject(a) ||
|
|
282
|
+
isReferenceHashedObject(b)
|
|
283
|
+
)
|
|
284
|
+
return false
|
|
285
|
+
if (a instanceof Date || b instanceof Date)
|
|
286
|
+
return (
|
|
287
|
+
a instanceof Date &&
|
|
288
|
+
b instanceof Date &&
|
|
289
|
+
equal(a.getTime(), b.getTime())
|
|
290
|
+
)
|
|
291
|
+
if (isBinaryValue(a) || isBinaryValue(b))
|
|
292
|
+
return (
|
|
293
|
+
isBinaryValue(a) &&
|
|
294
|
+
isBinaryValue(b) &&
|
|
295
|
+
a.byteLength === b.byteLength &&
|
|
296
|
+
a.every((value, index) => value === b[index])
|
|
297
|
+
)
|
|
298
|
+
if (isTemporal(a) || isTemporal(b))
|
|
299
|
+
return (
|
|
300
|
+
isTemporal(a) &&
|
|
301
|
+
isTemporal(b) &&
|
|
302
|
+
a[Symbol.toStringTag] === b[Symbol.toStringTag] &&
|
|
303
|
+
a.toString() === b.toString()
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
// Revisited pairs close cycles and avoid expanding shared subtrees.
|
|
307
|
+
const peers = compared.get(a)
|
|
308
|
+
if (peers?.has(b)) return true
|
|
309
|
+
if (peers) peers.add(b)
|
|
310
|
+
else compared.set(a, new Set([b]))
|
|
311
|
+
const [aKind, aShape] = structuralShape(a)
|
|
312
|
+
const [bKind, bShape] = structuralShape(b)
|
|
313
|
+
if (aKind !== bKind) return false
|
|
314
|
+
const keys = (value: object) =>
|
|
315
|
+
Reflect.ownKeys(value).filter((key) =>
|
|
316
|
+
Object.prototype.propertyIsEnumerable.call(value, key),
|
|
317
|
+
)
|
|
318
|
+
const aKeys = keys(aShape)
|
|
319
|
+
if (aKeys.length !== keys(bShape).length) return false
|
|
320
|
+
return aKeys.every(
|
|
321
|
+
(key) =>
|
|
322
|
+
Object.prototype.propertyIsEnumerable.call(bShape, key) &&
|
|
323
|
+
equal(aShape[key as keyof object], bShape[key as keyof object]),
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
return equal(left, right)
|
|
196
327
|
}
|
|
197
328
|
|
|
198
329
|
let nextRefId = 1
|
|
@@ -202,6 +333,7 @@ function cachedReferenceHash(fn: object): number {
|
|
|
202
333
|
valueHash = nextRefId ^ FUNCTIONS
|
|
203
334
|
nextRefId++
|
|
204
335
|
hashCache.set(fn, valueHash)
|
|
336
|
+
referenceValues.add(fn)
|
|
205
337
|
}
|
|
206
338
|
return valueHash
|
|
207
339
|
}
|
package/src/hashing/murmur.ts
CHANGED
|
@@ -9,6 +9,49 @@ const BIG_INT_MARKER = randomHash()
|
|
|
9
9
|
const NEG_BIG_INT_MARKER = randomHash()
|
|
10
10
|
const SYMBOL_MARKER = randomHash()
|
|
11
11
|
|
|
12
|
+
type SymbolIdStore = {
|
|
13
|
+
get: (key: symbol) => number | undefined
|
|
14
|
+
set: (key: symbol, value: number) => unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const symbolIds = createSymbolIdStore()
|
|
18
|
+
const registeredSymbolIds = new Map<string, number>()
|
|
19
|
+
let nextSymbolId = 0
|
|
20
|
+
|
|
21
|
+
export function getSymbolIdentity(symbol: symbol): number {
|
|
22
|
+
const registeredKey = Symbol.keyFor(symbol)
|
|
23
|
+
if (registeredKey !== undefined) {
|
|
24
|
+
let id = registeredSymbolIds.get(registeredKey)
|
|
25
|
+
if (id === undefined) {
|
|
26
|
+
id = ++nextSymbolId
|
|
27
|
+
registeredSymbolIds.set(registeredKey, id)
|
|
28
|
+
}
|
|
29
|
+
return id
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let id = symbolIds.get(symbol)
|
|
33
|
+
if (id === undefined) {
|
|
34
|
+
id = ++nextSymbolId
|
|
35
|
+
symbolIds.set(symbol, id)
|
|
36
|
+
}
|
|
37
|
+
return id
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createSymbolIdStore(): SymbolIdStore {
|
|
41
|
+
const weakIds = new WeakMap<object, number>() as unknown as SymbolIdStore
|
|
42
|
+
const probe = Symbol()
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
weakIds.set(probe, 0)
|
|
46
|
+
if (weakIds.get(probe) === 0) return weakIds
|
|
47
|
+
} catch {
|
|
48
|
+
// Older runtimes reject symbols as weak keys. Retain them rather than
|
|
49
|
+
// merge distinct symbols and corrupt differential state.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Map<symbol, number>()
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
export type Hash = number
|
|
13
56
|
|
|
14
57
|
export function randomHash() {
|
|
@@ -67,16 +110,7 @@ export class MurmurHashStream implements Hasher {
|
|
|
67
110
|
switch (typeof chunk) {
|
|
68
111
|
case `symbol`: {
|
|
69
112
|
this.update(SYMBOL_MARKER)
|
|
70
|
-
|
|
71
|
-
if (!description) {
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (let i = 0; i < description.length; i++) {
|
|
76
|
-
const code = description.charCodeAt(i)
|
|
77
|
-
this.writeByte(code & 0xff)
|
|
78
|
-
this.writeByte((code >>> 8) & 0xff)
|
|
79
|
-
}
|
|
113
|
+
this.update(getSymbolIdentity(chunk))
|
|
80
114
|
return
|
|
81
115
|
}
|
|
82
116
|
case `string`:
|
package/src/index.ts
CHANGED
|
@@ -85,10 +85,12 @@ export class DistinctOperator<
|
|
|
85
85
|
export function distinct<T extends KeyValue<any, any>>(
|
|
86
86
|
by: (value: T) => any = (value: T) => value,
|
|
87
87
|
) {
|
|
88
|
-
return (
|
|
89
|
-
|
|
88
|
+
return (
|
|
89
|
+
stream: IStreamBuilder<T>,
|
|
90
|
+
): IStreamBuilder<KeyValue<number, GetValue<T>>> => {
|
|
91
|
+
const output = new StreamBuilder<KeyValue<number, GetValue<T>>>(
|
|
90
92
|
stream.graph,
|
|
91
|
-
new DifferenceStreamWriter<T
|
|
93
|
+
new DifferenceStreamWriter<KeyValue<number, GetValue<T>>>(),
|
|
92
94
|
)
|
|
93
95
|
const operator = new DistinctOperator<T>(
|
|
94
96
|
stream.graph.getNextOperatorId(),
|
package/src/operators/groupBy.ts
CHANGED
|
@@ -62,7 +62,7 @@ export function groupBy<
|
|
|
62
62
|
stream: IStreamBuilder<T>,
|
|
63
63
|
): IStreamBuilder<KeyValue<string, ResultType>> => {
|
|
64
64
|
// Special key to store the original key object
|
|
65
|
-
const KEY_SENTINEL = `
|
|
65
|
+
const KEY_SENTINEL = Symbol(`original_group_key`)
|
|
66
66
|
|
|
67
67
|
// First map to extract keys and pre-aggregate values
|
|
68
68
|
const withKeysAndValues = stream.pipe(
|
|
@@ -71,7 +71,7 @@ export function groupBy<
|
|
|
71
71
|
const keyString = serializeValue(key)
|
|
72
72
|
|
|
73
73
|
// Create values object with pre-aggregated values
|
|
74
|
-
const values: Record<string, unknown> = {}
|
|
74
|
+
const values: Record<string | symbol, unknown> = {}
|
|
75
75
|
|
|
76
76
|
// Store the original key object
|
|
77
77
|
values[KEY_SENTINEL] = key
|
|
@@ -81,7 +81,10 @@ export function groupBy<
|
|
|
81
81
|
values[name] = aggregate.preMap(data)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
return [keyString, values] as KeyValue<
|
|
84
|
+
return [keyString, values] as KeyValue<
|
|
85
|
+
string,
|
|
86
|
+
Record<string | symbol, unknown>
|
|
87
|
+
>
|
|
85
88
|
}),
|
|
86
89
|
)
|
|
87
90
|
|
|
@@ -99,7 +102,7 @@ export function groupBy<
|
|
|
99
102
|
return []
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
const result: Record<
|
|
105
|
+
const result: Record<PropertyKey, unknown> = {}
|
|
103
106
|
|
|
104
107
|
// Get the original key from first value in group
|
|
105
108
|
const originalKey = values[0]?.[0]?.[KEY_SENTINEL]
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { DifferenceStreamWriter, UnaryOperator } from '../graph.js'
|
|
2
2
|
import { StreamBuilder } from '../d2.js'
|
|
3
3
|
import { MultiSet } from '../multiset.js'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
TopKState,
|
|
6
|
+
handleMoveIn,
|
|
7
|
+
handleMoveOut,
|
|
8
|
+
topKBatch,
|
|
9
|
+
} from './topKState.js'
|
|
5
10
|
import { TopKArray, createKeyedComparator } from './topKArray.js'
|
|
6
11
|
import type { IndexedValue, TopK } from './topKArray.js'
|
|
7
12
|
import type { DifferenceStreamReader } from '../graph.js'
|
|
@@ -125,11 +130,10 @@ export class GroupedTopKWithFractionalIndexOperator<
|
|
|
125
130
|
|
|
126
131
|
run(): void {
|
|
127
132
|
const result: Array<[[K, IndexedValue<T>], number]> = []
|
|
128
|
-
for (const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
+
for (const [[key, value], multiplicity] of topKBatch(
|
|
134
|
+
this.inputMessages(),
|
|
135
|
+
)) {
|
|
136
|
+
this.#processElement(key, value, multiplicity, result)
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
if (result.length > 0) {
|
|
@@ -170,6 +170,8 @@ export class TopKArray<V> implements TopK<V> {
|
|
|
170
170
|
// Splice is O(n) where n = all elements in the collection (i.e. n >= k) !
|
|
171
171
|
this.#sortedValues.splice(index, 0, val)
|
|
172
172
|
|
|
173
|
+
if (this.#topKStart === this.#topKEnd) return result
|
|
174
|
+
|
|
173
175
|
// Check if the topK changed
|
|
174
176
|
if (index < this.#topKEnd) {
|
|
175
177
|
// The inserted element is either before the top K or within the top K
|
|
@@ -209,6 +211,8 @@ export class TopKArray<V> implements TopK<V> {
|
|
|
209
211
|
// Remove the value at that position
|
|
210
212
|
const [removedElem] = this.#sortedValues.splice(index, 1)
|
|
211
213
|
|
|
214
|
+
if (this.#topKStart === this.#topKEnd) return result
|
|
215
|
+
|
|
212
216
|
// Check if the topK changed
|
|
213
217
|
if (index < this.#topKEnd) {
|
|
214
218
|
// The removed element is either before the top K or within the top K
|