@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
|
@@ -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":"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"}
|
|
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;;;"}
|
package/dist/cjs/utils.cjs
CHANGED
|
@@ -107,6 +107,14 @@ function range(start, end) {
|
|
|
107
107
|
}
|
|
108
108
|
function compareKeys(a, b) {
|
|
109
109
|
if (typeof a === typeof b) {
|
|
110
|
+
if (typeof a === `number` && typeof b === `number`) {
|
|
111
|
+
const aIsNaN = Number.isNaN(a);
|
|
112
|
+
const bIsNaN = Number.isNaN(b);
|
|
113
|
+
if (aIsNaN || bIsNaN) {
|
|
114
|
+
if (aIsNaN && bIsNaN) return 0;
|
|
115
|
+
return aIsNaN ? 1 : -1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
110
118
|
if (a < b) return -1;
|
|
111
119
|
if (a > b) return 1;
|
|
112
120
|
return 0;
|
package/dist/cjs/utils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.cjs","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.cjs","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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"}
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/** @internal Register a mutable handle before it enters a structural value. */
|
|
2
|
+
export declare function registerOpaqueHash(value: object): void;
|
|
1
3
|
export declare function hash(input: any): number;
|
|
4
|
+
/** @internal Compare immutable structural values without computing a digest.
|
|
5
|
+
* Pair memoization also permits cyclic values that structural hashing rejects.
|
|
6
|
+
* Reference-valued leaves remain opaque, including registered mutable handles.
|
|
7
|
+
*/
|
|
8
|
+
export declare function equalHashValues(left: unknown, right: unknown): boolean;
|
package/dist/esm/hashing/hash.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MurmurHashStream, randomHash } from "./murmur.js";
|
|
1
|
+
import { MurmurHashStream, randomHash, getSymbolIdentity } from "./murmur.js";
|
|
2
2
|
const TRUE = randomHash();
|
|
3
3
|
const FALSE = randomHash();
|
|
4
4
|
const NULL = randomHash();
|
|
@@ -6,12 +6,16 @@ const UNDEFINED = randomHash();
|
|
|
6
6
|
const KEY = randomHash();
|
|
7
7
|
const FUNCTIONS = randomHash();
|
|
8
8
|
const DATE_MARKER = randomHash();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const STRUCTURAL_MARKERS = {
|
|
10
|
+
object: randomHash(),
|
|
11
|
+
array: randomHash(),
|
|
12
|
+
map: randomHash(),
|
|
13
|
+
set: randomHash()
|
|
14
|
+
};
|
|
13
15
|
const UINT8ARRAY_MARKER = randomHash();
|
|
14
16
|
const TEMPORAL_MARKER = randomHash();
|
|
17
|
+
const MAX_STRUCTURAL_HASH_WORK = 1e6;
|
|
18
|
+
const MAX_STRUCTURAL_HASH_DEPTH = 768;
|
|
15
19
|
const temporalTypes = /* @__PURE__ */ new Set([
|
|
16
20
|
`Temporal.Duration`,
|
|
17
21
|
`Temporal.Instant`,
|
|
@@ -28,49 +32,42 @@ function isTemporal(input) {
|
|
|
28
32
|
}
|
|
29
33
|
const UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128;
|
|
30
34
|
const hashCache = /* @__PURE__ */ new WeakMap();
|
|
35
|
+
const referenceValues = /* @__PURE__ */ new WeakSet();
|
|
36
|
+
function registerOpaqueHash(value) {
|
|
37
|
+
cachedReferenceHash(value);
|
|
38
|
+
}
|
|
31
39
|
function hash(input) {
|
|
32
40
|
const hasher = new MurmurHashStream();
|
|
33
41
|
updateHasher(hasher, input);
|
|
34
42
|
return hasher.digest();
|
|
35
43
|
}
|
|
36
|
-
function hashObject(input) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
function hashObject(input, context) {
|
|
45
|
+
if (context.activeObjects.size >= MAX_STRUCTURAL_HASH_DEPTH) {
|
|
46
|
+
throw new RangeError(
|
|
47
|
+
`Value is too complex to hash safely: structural depth`
|
|
48
|
+
);
|
|
40
49
|
}
|
|
50
|
+
context.activeObjects.add(input);
|
|
41
51
|
let valueHash;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
typeof Buffer !== `undefined` && input instanceof Buffer || input instanceof Uint8Array
|
|
47
|
-
) {
|
|
48
|
-
if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {
|
|
52
|
+
try {
|
|
53
|
+
if (input instanceof Date) {
|
|
54
|
+
valueHash = hashDate(input);
|
|
55
|
+
} else if (isBinaryValue(input)) {
|
|
49
56
|
valueHash = hashUint8Array(input);
|
|
57
|
+
} else if (isTemporal(input)) {
|
|
58
|
+
valueHash = hashTemporal(input);
|
|
50
59
|
} else {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} else {
|
|
58
|
-
let plainObjectInput = input;
|
|
59
|
-
let marker = OBJECT_MARKER;
|
|
60
|
-
if (input instanceof Array) {
|
|
61
|
-
marker = ARRAY_MARKER;
|
|
62
|
-
}
|
|
63
|
-
if (input instanceof Map) {
|
|
64
|
-
marker = MAP_MARKER;
|
|
65
|
-
plainObjectInput = [...input.entries()];
|
|
66
|
-
}
|
|
67
|
-
if (input instanceof Set) {
|
|
68
|
-
marker = SET_MARKER;
|
|
69
|
-
plainObjectInput = [...input.entries()];
|
|
60
|
+
const [kind, plainObjectInput] = structuralShape(input);
|
|
61
|
+
valueHash = hashPlainObject(
|
|
62
|
+
plainObjectInput,
|
|
63
|
+
STRUCTURAL_MARKERS[kind],
|
|
64
|
+
context
|
|
65
|
+
);
|
|
70
66
|
}
|
|
71
|
-
|
|
67
|
+
} finally {
|
|
68
|
+
context.activeObjects.delete(input);
|
|
72
69
|
}
|
|
73
|
-
|
|
70
|
+
context.pendingHashes.set(input, valueHash);
|
|
74
71
|
return valueHash;
|
|
75
72
|
}
|
|
76
73
|
function hashDate(input) {
|
|
@@ -95,7 +92,7 @@ function hashTemporal(input) {
|
|
|
95
92
|
hasher.update(input.toString());
|
|
96
93
|
return hasher.digest();
|
|
97
94
|
}
|
|
98
|
-
function hashPlainObject(input, marker) {
|
|
95
|
+
function hashPlainObject(input, marker, context) {
|
|
99
96
|
const hasher = new MurmurHashStream();
|
|
100
97
|
hasher.update(marker);
|
|
101
98
|
const keys = Object.keys(input);
|
|
@@ -103,11 +100,20 @@ function hashPlainObject(input, marker) {
|
|
|
103
100
|
for (const key of keys) {
|
|
104
101
|
hasher.update(KEY);
|
|
105
102
|
hasher.update(key);
|
|
106
|
-
updateHasher(hasher, input[key]);
|
|
103
|
+
updateHasher(hasher, input[key], context);
|
|
104
|
+
}
|
|
105
|
+
const symbolKeys = Object.getOwnPropertySymbols(input).filter((key) => Object.prototype.propertyIsEnumerable.call(input, key)).sort((left, right) => getSymbolIdentity(left) - getSymbolIdentity(right));
|
|
106
|
+
for (const key of symbolKeys) {
|
|
107
|
+
hasher.update(KEY);
|
|
108
|
+
hasher.update(key);
|
|
109
|
+
updateHasher(hasher, input[key], context);
|
|
107
110
|
}
|
|
108
111
|
return hasher.digest();
|
|
109
112
|
}
|
|
110
|
-
function updateHasher(hasher, input) {
|
|
113
|
+
function updateHasher(hasher, input, context) {
|
|
114
|
+
if (context && ++context.work > MAX_STRUCTURAL_HASH_WORK) {
|
|
115
|
+
throw new RangeError(`Value is too complex to hash safely: structural work`);
|
|
116
|
+
}
|
|
111
117
|
if (input === null) {
|
|
112
118
|
hasher.update(NULL);
|
|
113
119
|
return;
|
|
@@ -128,7 +134,7 @@ function updateHasher(hasher, input) {
|
|
|
128
134
|
hasher.update(input);
|
|
129
135
|
return;
|
|
130
136
|
case `object`:
|
|
131
|
-
hasher.update(getCachedHash(input));
|
|
137
|
+
hasher.update(getCachedHash(input, context));
|
|
132
138
|
return;
|
|
133
139
|
case `function`:
|
|
134
140
|
hasher.update(cachedReferenceHash(input));
|
|
@@ -139,12 +145,72 @@ function updateHasher(hasher, input) {
|
|
|
139
145
|
);
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
|
-
function getCachedHash(input) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
function getCachedHash(input, context) {
|
|
149
|
+
if (!context) {
|
|
150
|
+
const cached = hashCache.get(input);
|
|
151
|
+
if (cached !== void 0) return cached;
|
|
152
|
+
if (isReferenceHashedObject(input)) return cachedReferenceHash(input);
|
|
153
|
+
context = {
|
|
154
|
+
activeObjects: /* @__PURE__ */ new Set(),
|
|
155
|
+
work: 0,
|
|
156
|
+
pendingHashes: /* @__PURE__ */ new Map()
|
|
157
|
+
};
|
|
158
|
+
const result = hashObject(input, context);
|
|
159
|
+
for (const [object, valueHash2] of context.pendingHashes) {
|
|
160
|
+
hashCache.set(object, valueHash2);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
146
163
|
}
|
|
147
|
-
|
|
164
|
+
if (context.activeObjects.has(input)) {
|
|
165
|
+
throw new TypeError(`Cannot hash cyclic structural values`);
|
|
166
|
+
}
|
|
167
|
+
if (isReferenceHashedObject(input)) return cachedReferenceHash(input);
|
|
168
|
+
const valueHash = hashCache.get(input) ?? context.pendingHashes.get(input);
|
|
169
|
+
if (valueHash !== void 0) return valueHash;
|
|
170
|
+
return hashObject(input, context);
|
|
171
|
+
}
|
|
172
|
+
function isReferenceHashedObject(input) {
|
|
173
|
+
return input instanceof File || isBinaryValue(input) && input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD;
|
|
174
|
+
}
|
|
175
|
+
function isBinaryValue(input) {
|
|
176
|
+
return typeof Buffer !== `undefined` && input instanceof Buffer || input instanceof Uint8Array;
|
|
177
|
+
}
|
|
178
|
+
function structuralShape(input) {
|
|
179
|
+
if (input instanceof Map) return [`map`, [...input.entries()]];
|
|
180
|
+
if (input instanceof Set) return [`set`, [...input.values()]];
|
|
181
|
+
return [input instanceof Array ? `array` : `object`, input];
|
|
182
|
+
}
|
|
183
|
+
function equalHashValues(left, right) {
|
|
184
|
+
const compared = /* @__PURE__ */ new Map();
|
|
185
|
+
function equal(a, b) {
|
|
186
|
+
if (a === b || Number.isNaN(a) && Number.isNaN(b)) return true;
|
|
187
|
+
if (a === null || b === null || typeof a !== `object` || typeof b !== `object`)
|
|
188
|
+
return false;
|
|
189
|
+
if (referenceValues.has(a) || referenceValues.has(b) || isReferenceHashedObject(a) || isReferenceHashedObject(b))
|
|
190
|
+
return false;
|
|
191
|
+
if (a instanceof Date || b instanceof Date)
|
|
192
|
+
return a instanceof Date && b instanceof Date && equal(a.getTime(), b.getTime());
|
|
193
|
+
if (isBinaryValue(a) || isBinaryValue(b))
|
|
194
|
+
return isBinaryValue(a) && isBinaryValue(b) && a.byteLength === b.byteLength && a.every((value, index) => value === b[index]);
|
|
195
|
+
if (isTemporal(a) || isTemporal(b))
|
|
196
|
+
return isTemporal(a) && isTemporal(b) && a[Symbol.toStringTag] === b[Symbol.toStringTag] && a.toString() === b.toString();
|
|
197
|
+
const peers = compared.get(a);
|
|
198
|
+
if (peers?.has(b)) return true;
|
|
199
|
+
if (peers) peers.add(b);
|
|
200
|
+
else compared.set(a, /* @__PURE__ */ new Set([b]));
|
|
201
|
+
const [aKind, aShape] = structuralShape(a);
|
|
202
|
+
const [bKind, bShape] = structuralShape(b);
|
|
203
|
+
if (aKind !== bKind) return false;
|
|
204
|
+
const keys = (value) => Reflect.ownKeys(value).filter(
|
|
205
|
+
(key) => Object.prototype.propertyIsEnumerable.call(value, key)
|
|
206
|
+
);
|
|
207
|
+
const aKeys = keys(aShape);
|
|
208
|
+
if (aKeys.length !== keys(bShape).length) return false;
|
|
209
|
+
return aKeys.every(
|
|
210
|
+
(key) => Object.prototype.propertyIsEnumerable.call(bShape, key) && equal(aShape[key], bShape[key])
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return equal(left, right);
|
|
148
214
|
}
|
|
149
215
|
let nextRefId = 1;
|
|
150
216
|
function cachedReferenceHash(fn) {
|
|
@@ -153,6 +219,7 @@ function cachedReferenceHash(fn) {
|
|
|
153
219
|
valueHash = nextRefId ^ FUNCTIONS;
|
|
154
220
|
nextRefId++;
|
|
155
221
|
hashCache.set(fn, valueHash);
|
|
222
|
+
referenceValues.add(fn);
|
|
156
223
|
}
|
|
157
224
|
return valueHash;
|
|
158
225
|
}
|
|
@@ -160,6 +227,8 @@ function keySort(a, b) {
|
|
|
160
227
|
return a.localeCompare(b);
|
|
161
228
|
}
|
|
162
229
|
export {
|
|
163
|
-
|
|
230
|
+
equalHashValues,
|
|
231
|
+
hash,
|
|
232
|
+
registerOpaqueHash
|
|
164
233
|
};
|
|
165
234
|
//# sourceMappingURL=hash.js.map
|