@tanstack/db-ivm 0.1.12 → 0.1.14

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.
Files changed (108) hide show
  1. package/README.md +3 -3
  2. package/dist/cjs/d2.cjs.map +1 -1
  3. package/dist/cjs/graph.cjs.map +1 -1
  4. package/dist/cjs/hashing/hash.cjs +22 -3
  5. package/dist/cjs/hashing/hash.cjs.map +1 -1
  6. package/dist/cjs/hashing/murmur.cjs +15 -15
  7. package/dist/cjs/hashing/murmur.cjs.map +1 -1
  8. package/dist/cjs/hashing/murmur.d.cts +1 -1
  9. package/dist/cjs/index.cjs +2 -0
  10. package/dist/cjs/index.cjs.map +1 -1
  11. package/dist/cjs/index.d.cts +1 -0
  12. package/dist/cjs/indexes.cjs.map +1 -1
  13. package/dist/cjs/multiset.cjs.map +1 -1
  14. package/dist/cjs/operators/concat.cjs.map +1 -1
  15. package/dist/cjs/operators/consolidate.cjs.map +1 -1
  16. package/dist/cjs/operators/count.cjs.map +1 -1
  17. package/dist/cjs/operators/debug.cjs.map +1 -1
  18. package/dist/cjs/operators/distinct.cjs.map +1 -1
  19. package/dist/cjs/operators/filter.cjs.map +1 -1
  20. package/dist/cjs/operators/filterBy.cjs.map +1 -1
  21. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  22. package/dist/cjs/operators/join.cjs.map +1 -1
  23. package/dist/cjs/operators/keying.cjs.map +1 -1
  24. package/dist/cjs/operators/map.cjs.map +1 -1
  25. package/dist/cjs/operators/negate.cjs.map +1 -1
  26. package/dist/cjs/operators/orderBy.cjs.map +1 -1
  27. package/dist/cjs/operators/output.cjs.map +1 -1
  28. package/dist/cjs/operators/pipe.cjs.map +1 -1
  29. package/dist/cjs/operators/reduce.cjs.map +1 -1
  30. package/dist/cjs/operators/tap.cjs.map +1 -1
  31. package/dist/cjs/operators/topK.cjs.map +1 -1
  32. package/dist/cjs/operators/topKWithFractionalIndex.cjs +20 -35
  33. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  34. package/dist/cjs/operators/topKWithFractionalIndex.d.cts +3 -11
  35. package/dist/cjs/operators/topKWithFractionalIndexBTree.d.cts +4 -4
  36. package/dist/cjs/utils.cjs +9 -0
  37. package/dist/cjs/utils.cjs.map +1 -1
  38. package/dist/cjs/utils.d.cts +5 -0
  39. package/dist/esm/d2.js.map +1 -1
  40. package/dist/esm/graph.js.map +1 -1
  41. package/dist/esm/hashing/hash.js +22 -3
  42. package/dist/esm/hashing/hash.js.map +1 -1
  43. package/dist/esm/hashing/murmur.d.ts +1 -1
  44. package/dist/esm/hashing/murmur.js +15 -15
  45. package/dist/esm/hashing/murmur.js.map +1 -1
  46. package/dist/esm/index.d.ts +1 -0
  47. package/dist/esm/index.js +2 -0
  48. package/dist/esm/index.js.map +1 -1
  49. package/dist/esm/indexes.js.map +1 -1
  50. package/dist/esm/multiset.js.map +1 -1
  51. package/dist/esm/operators/concat.js.map +1 -1
  52. package/dist/esm/operators/consolidate.js.map +1 -1
  53. package/dist/esm/operators/count.js.map +1 -1
  54. package/dist/esm/operators/debug.js.map +1 -1
  55. package/dist/esm/operators/distinct.js.map +1 -1
  56. package/dist/esm/operators/filter.js.map +1 -1
  57. package/dist/esm/operators/filterBy.js.map +1 -1
  58. package/dist/esm/operators/groupBy.js.map +1 -1
  59. package/dist/esm/operators/join.js.map +1 -1
  60. package/dist/esm/operators/keying.js.map +1 -1
  61. package/dist/esm/operators/map.js.map +1 -1
  62. package/dist/esm/operators/negate.js.map +1 -1
  63. package/dist/esm/operators/orderBy.js.map +1 -1
  64. package/dist/esm/operators/output.js.map +1 -1
  65. package/dist/esm/operators/pipe.js.map +1 -1
  66. package/dist/esm/operators/reduce.js.map +1 -1
  67. package/dist/esm/operators/tap.js.map +1 -1
  68. package/dist/esm/operators/topK.js.map +1 -1
  69. package/dist/esm/operators/topKWithFractionalIndex.d.ts +3 -11
  70. package/dist/esm/operators/topKWithFractionalIndex.js +21 -36
  71. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  72. package/dist/esm/operators/topKWithFractionalIndexBTree.d.ts +4 -4
  73. package/dist/esm/utils.d.ts +5 -0
  74. package/dist/esm/utils.js +9 -0
  75. package/dist/esm/utils.js.map +1 -1
  76. package/package.json +29 -29
  77. package/src/d2.ts +4 -4
  78. package/src/graph.ts +6 -6
  79. package/src/hashing/hash.ts +39 -14
  80. package/src/hashing/index.ts +2 -2
  81. package/src/hashing/murmur.ts +15 -15
  82. package/src/index.ts +5 -4
  83. package/src/indexes.ts +8 -8
  84. package/src/multiset.ts +5 -5
  85. package/src/operators/concat.ts +6 -6
  86. package/src/operators/consolidate.ts +6 -6
  87. package/src/operators/count.ts +9 -9
  88. package/src/operators/debug.ts +8 -8
  89. package/src/operators/distinct.ts +11 -11
  90. package/src/operators/filter.ts +8 -8
  91. package/src/operators/filterBy.ts +7 -7
  92. package/src/operators/groupBy.ts +22 -22
  93. package/src/operators/index.ts +19 -19
  94. package/src/operators/join.ts +23 -23
  95. package/src/operators/keying.ts +4 -4
  96. package/src/operators/map.ts +8 -8
  97. package/src/operators/negate.ts +6 -6
  98. package/src/operators/orderBy.ts +26 -26
  99. package/src/operators/orderByBTree.ts +7 -7
  100. package/src/operators/output.ts +9 -9
  101. package/src/operators/pipe.ts +1 -1
  102. package/src/operators/reduce.ts +9 -9
  103. package/src/operators/tap.ts +8 -8
  104. package/src/operators/topK.ts +8 -8
  105. package/src/operators/topKWithFractionalIndex.ts +62 -83
  106. package/src/operators/topKWithFractionalIndexBTree.ts +19 -20
  107. package/src/types.ts +22 -22
  108. package/src/utils.ts +19 -4
@@ -1 +1 @@
1
- {"version":3,"file":"topKWithFractionalIndex.cjs","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { generateKeyBetween } from \"fractional-indexing\"\nimport { DifferenceStreamWriter, UnaryOperator } from \"../graph.js\"\nimport { StreamBuilder } from \"../d2.js\"\nimport { MultiSet } from \"../multiset.js\"\nimport {\n binarySearch,\n diffHalfOpen,\n globalObjectIdGenerator,\n} from \"../utils.js\"\nimport type { HRange } from \"../utils.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\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 */\nclass 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\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<K, T> extends UnaryOperator<\n [K, T],\n [K, IndexedValue<T>]\n> {\n #index: Map<K, number> = new Map() // maps keys to their multiplicity\n\n /**\n * topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n */\n #topK: TopK<TaggedValue<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 compareTaggedValues = (\n a: TaggedValue<K, T>,\n b: TaggedValue<K, T>\n ) => {\n // First compare on the value\n const valueComparison = comparator(getVal(a), getVal(b))\n if (valueComparison !== 0) {\n return valueComparison\n }\n // If the values are equal, compare on the tag (object identity)\n const tieBreakerA = getTag(a)\n const tieBreakerB = getTag(b)\n return tieBreakerA - tieBreakerB\n }\n this.#topK = this.createTopK(offset, limit, compareTaggedValues)\n options.setSizeCallback?.(() => this.#topK.size)\n options.setWindowFn?.(this.moveTopK.bind(this))\n }\n\n protected createTopK(\n offset: number,\n limit: number,\n comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number\n ): TopK<TaggedValue<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 if (!(this.#topK instanceof TopKArray)) {\n throw new Error(\n `Cannot move B+-tree implementation of TopK with fractional index`\n )\n }\n\n const result: Array<[[K, IndexedValue<T>], number]> = []\n\n const diff = this.#topK.move({ offset, limit })\n\n diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => this.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 message of this.inputMessages()) {\n for (const [item, multiplicity] of message.getInner()) {\n const [key, value] = item\n this.processElement(key, value, multiplicity, result)\n }\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 { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)\n\n let res: TopKChanges<TaggedValue<K, T>> = {\n moveIn: null,\n moveOut: null,\n }\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n // Need to insert it into the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.insert(taggedValue)\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n // Need to remove it from the array of sorted values\n const taggedValue = tagValue(key, value)\n res = this.#topK.delete(taggedValue)\n } else {\n // The value was invisible and it remains invisible\n // or it was visible and remains visible\n // so it doesn't affect the topK\n }\n\n this.handleMoveIn(res.moveIn, result)\n this.handleMoveOut(res.moveOut, result)\n\n return\n }\n\n private handleMoveIn(\n moveIn: IndexedValue<TaggedValue<K, T>> | null,\n result: Array<[[K, IndexedValue<T>], number]>\n ) {\n if (moveIn) {\n const index = getIndex(moveIn)\n const taggedValue = getValue(moveIn)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], 1])\n }\n }\n\n private handleMoveOut(\n moveOut: IndexedValue<TaggedValue<K, T>> | null,\n result: Array<[[K, IndexedValue<T>], number]>\n ) {\n if (moveOut) {\n const index = getIndex(moveOut)\n const taggedValue = getValue(moveOut)\n const k = getKey(taggedValue)\n const val = getVal(taggedValue)\n result.push([[k, [val, index]], -1])\n }\n }\n\n private getMultiplicity(key: K): number {\n return this.#index.get(key) ?? 0\n }\n\n private addKey(\n key: K,\n multiplicity: number\n ): { oldMultiplicity: number; newMultiplicity: number } {\n const oldMultiplicity = this.getMultiplicity(key)\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#index.delete(key)\n } else {\n this.#index.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\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, 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\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\nexport type Tag = number\nexport type TaggedValue<K, V> = [K, V, Tag]\n\nfunction tagValue<K, V>(key: K, value: V): TaggedValue<K, V> {\n return [key, value, globalObjectIdGenerator.getId(key)]\n}\n\nfunction getKey<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): K {\n return tieBreakerTaggedValue[0]\n}\n\nfunction getVal<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): V {\n return tieBreakerTaggedValue[1]\n}\n\nfunction getTag<K, V>(tieBreakerTaggedValue: TaggedValue<K, V>): Tag {\n return tieBreakerTaggedValue[2]\n}\n"],"names":["diffHalfOpen","generateKeyBetween","binarySearch","UnaryOperator","MultiSet","StreamBuilder","DifferenceStreamWriter","globalObjectIdGenerator"],"mappings":"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"}
1
+ {"version":3,"file":"topKWithFractionalIndex.cjs","sources":["../../../src/operators/topKWithFractionalIndex.ts"],"sourcesContent":["import { generateKeyBetween } from 'fractional-indexing'\nimport { DifferenceStreamWriter, UnaryOperator } from '../graph.js'\nimport { StreamBuilder } from '../d2.js'\nimport { MultiSet } from '../multiset.js'\nimport { binarySearch, compareKeys, diffHalfOpen } from '../utils.js'\nimport type { HRange } from '../utils.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\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 */\nclass 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\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 #index: Map<K, number> = new Map() // maps keys to their multiplicity\n\n /**\n * topK data structure that supports insertions and deletions\n * and returns changes to the topK.\n * Elements are stored as [key, value] tuples for stable tie-breaking.\n */\n #topK: TopK<[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 this.#topK = this.createTopK(\n offset,\n limit,\n createKeyedComparator(comparator),\n )\n options.setSizeCallback?.(() => this.#topK.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 if (!(this.#topK instanceof TopKArray)) {\n throw new Error(\n `Cannot move B+-tree implementation of TopK with fractional index`,\n )\n }\n\n const result: Array<[[K, IndexedValue<T>], number]> = []\n\n const diff = this.#topK.move({ offset, limit })\n\n diff.moveIns.forEach((moveIn) => this.handleMoveIn(moveIn, result))\n diff.moveOuts.forEach((moveOut) => this.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 message of this.inputMessages()) {\n for (const [item, multiplicity] of message.getInner()) {\n const [key, value] = item\n this.processElement(key, value, multiplicity, result)\n }\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 { oldMultiplicity, newMultiplicity } = this.addKey(key, multiplicity)\n\n let res: TopKChanges<[K, T]> = {\n moveIn: null,\n moveOut: null,\n }\n if (oldMultiplicity <= 0 && newMultiplicity > 0) {\n // The value was invisible but should now be visible\n // Need to insert it into the array of sorted values\n res = this.#topK.insert([key, value])\n } else if (oldMultiplicity > 0 && newMultiplicity <= 0) {\n // The value was visible but should now be invisible\n // Need to remove it from the array of sorted values\n res = this.#topK.delete([key, value])\n } else {\n // The value was invisible and it remains invisible\n // or it was visible and remains visible\n // so it doesn't affect the topK\n }\n\n this.handleMoveIn(res.moveIn, result)\n this.handleMoveOut(res.moveOut, result)\n\n return\n }\n\n private handleMoveIn(\n moveIn: IndexedValue<[K, T]> | null,\n result: Array<[[K, IndexedValue<T>], number]>,\n ) {\n if (moveIn) {\n const [[key, value], index] = moveIn\n result.push([[key, [value, index]], 1])\n }\n }\n\n private handleMoveOut(\n moveOut: IndexedValue<[K, T]> | null,\n result: Array<[[K, IndexedValue<T>], number]>,\n ) {\n if (moveOut) {\n const [[key, value], index] = moveOut\n result.push([[key, [value, index]], -1])\n }\n }\n\n private getMultiplicity(key: K): number {\n return this.#index.get(key) ?? 0\n }\n\n private addKey(\n key: K,\n multiplicity: number,\n ): { oldMultiplicity: number; newMultiplicity: number } {\n const oldMultiplicity = this.getMultiplicity(key)\n const newMultiplicity = oldMultiplicity + multiplicity\n if (newMultiplicity === 0) {\n this.#index.delete(key)\n } else {\n this.#index.set(key, newMultiplicity)\n }\n return { oldMultiplicity, newMultiplicity }\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\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 */\nfunction 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"],"names":["diffHalfOpen","generateKeyBetween","binarySearch","UnaryOperator","MultiSet","StreamBuilder","DifferenceStreamWriter","compareKeys"],"mappings":"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"}
@@ -37,16 +37,10 @@ export interface TopK<V> {
37
37
  * This operator maintains fractional indices for sorted elements
38
38
  * and only updates indices when elements move position
39
39
  */
40
- export declare class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator<[
41
- K,
42
- T
43
- ], [
44
- K,
45
- IndexedValue<T>
46
- ]> {
40
+ export declare class TopKWithFractionalIndexOperator<K extends string | number, T> extends UnaryOperator<[K, T], [K, IndexedValue<T>]> {
47
41
  #private;
48
42
  constructor(id: number, inputA: DifferenceStreamReader<[K, T]>, output: DifferenceStreamWriter<[K, IndexedValue<T>]>, comparator: (a: T, b: T) => number, options: TopKWithFractionalIndexOptions);
49
- protected createTopK(offset: number, limit: number, comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number): TopK<TaggedValue<K, T>>;
43
+ protected createTopK(offset: number, limit: number, comparator: (a: [K, T], b: [K, T]) => number): TopK<[K, T]>;
50
44
  /**
51
45
  * Moves the topK window based on the provided offset and limit.
52
46
  * Any changes to the topK are sent to the output.
@@ -72,11 +66,9 @@ export declare class TopKWithFractionalIndexOperator<K, T> extends UnaryOperator
72
66
  * @param options - An optional object containing limit and offset properties
73
67
  * @returns A piped operator that orders the elements and limits the number of results
74
68
  */
75
- export declare function topKWithFractionalIndex<KType, T>(comparator: (a: T, b: T) => number, options?: TopKWithFractionalIndexOptions): PipedOperator<[KType, T], [KType, IndexedValue<T>]>;
69
+ export declare function topKWithFractionalIndex<KType extends string | number, T>(comparator: (a: T, b: T) => number, options?: TopKWithFractionalIndexOptions): PipedOperator<[KType, T], [KType, IndexedValue<T>]>;
76
70
  export type FractionalIndex = string;
77
71
  export type IndexedValue<V> = [V, FractionalIndex];
78
72
  export declare function indexedValue<V>(value: V, index: FractionalIndex): IndexedValue<V>;
79
73
  export declare function getValue<V>(indexedVal: IndexedValue<V>): V;
80
74
  export declare function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex;
81
- export type Tag = number;
82
- export type TaggedValue<K, V> = [K, V, Tag];
@@ -1,4 +1,4 @@
1
- import { TopKWithFractionalIndexOperator, IndexedValue, TaggedValue, TopK, TopKWithFractionalIndexOptions } from './topKWithFractionalIndex.js';
1
+ import { TopKWithFractionalIndexOperator, IndexedValue, TopK, TopKWithFractionalIndexOptions } from './topKWithFractionalIndex.js';
2
2
  import { PipedOperator } from '../types.js';
3
3
  export declare function loadBTree(): Promise<void>;
4
4
  /**
@@ -6,8 +6,8 @@ export declare function loadBTree(): Promise<void>;
6
6
  * This operator maintains fractional indices for sorted elements
7
7
  * and only updates indices when elements move position
8
8
  */
9
- export declare class TopKWithFractionalIndexBTreeOperator<K, T> extends TopKWithFractionalIndexOperator<K, T> {
10
- protected createTopK(offset: number, limit: number, comparator: (a: TaggedValue<K, T>, b: TaggedValue<K, T>) => number): TopK<TaggedValue<K, T>>;
9
+ export declare class TopKWithFractionalIndexBTreeOperator<K extends string | number, T> extends TopKWithFractionalIndexOperator<K, T> {
10
+ protected createTopK(offset: number, limit: number, comparator: (a: [K, T], b: [K, T]) => number): TopK<[K, T]>;
11
11
  }
12
12
  /**
13
13
  * Limits the number of results based on a comparator, with optional offset.
@@ -24,4 +24,4 @@ export declare class TopKWithFractionalIndexBTreeOperator<K, T> extends TopKWith
24
24
  * @param options - An optional object containing limit and offset properties
25
25
  * @returns A piped operator that orders the elements and limits the number of results
26
26
  */
27
- export declare function topKWithFractionalIndexBTree<KType, T>(comparator: (a: T, b: T) => number, options?: TopKWithFractionalIndexOptions): PipedOperator<[KType, T], [KType, IndexedValue<T>]>;
27
+ export declare function topKWithFractionalIndexBTree<KType extends string | number, T>(comparator: (a: T, b: T) => number, options?: TopKWithFractionalIndexOptions): PipedOperator<[KType, T], [KType, IndexedValue<T>]>;
@@ -105,10 +105,19 @@ function range(start, end) {
105
105
  for (let i = start; i < end; i++) out.push(i);
106
106
  return out;
107
107
  }
108
+ function compareKeys(a, b) {
109
+ if (typeof a === typeof b) {
110
+ if (a < b) return -1;
111
+ if (a > b) return 1;
112
+ return 0;
113
+ }
114
+ return typeof a === `string` ? -1 : 1;
115
+ }
108
116
  exports.DefaultMap = DefaultMap;
109
117
  exports.ObjectIdGenerator = ObjectIdGenerator;
110
118
  exports.binarySearch = binarySearch;
111
119
  exports.chunkedArrayPush = chunkedArrayPush;
120
+ exports.compareKeys = compareKeys;
112
121
  exports.diffHalfOpen = diffHalfOpen;
113
122
  exports.globalObjectIdGenerator = globalObjectIdGenerator;
114
123
  //# sourceMappingURL=utils.cjs.map
@@ -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"],"names":[],"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 (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"],"names":[],"mappings":"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"}
@@ -52,3 +52,8 @@ export declare function diffHalfOpen(a: HRange, b: HRange): {
52
52
  onlyInA: number[];
53
53
  onlyInB: number[];
54
54
  };
55
+ /**
56
+ * Compares two keys (string | number) in a consistent, deterministic way.
57
+ * Handles mixed types by ordering strings before numbers.
58
+ */
59
+ export declare function compareKeys(a: string | number, b: string | number): number;
@@ -1 +1 @@
1
- {"version":3,"file":"d2.js","sources":["../../src/d2.ts"],"sourcesContent":["import { DifferenceStreamWriter } from \"./graph.js\"\nimport type {\n BinaryOperator,\n DifferenceStreamReader,\n UnaryOperator,\n} from \"./graph.js\"\nimport type { MultiSet, MultiSetArray } from \"./multiset.js\"\nimport type { ID2, IStreamBuilder, PipedOperator } from \"./types.js\"\n\nexport class D2 implements ID2 {\n #operators: Array<UnaryOperator<any> | BinaryOperator<any>> = []\n #nextOperatorId = 0\n #finalized = false\n\n constructor() {}\n\n #checkNotFinalized(): void {\n if (this.#finalized) {\n throw new Error(`Graph already finalized`)\n }\n }\n\n getNextOperatorId(): number {\n this.#checkNotFinalized()\n return this.#nextOperatorId++\n }\n\n newInput<T>(): RootStreamBuilder<T> {\n this.#checkNotFinalized()\n const writer = new DifferenceStreamWriter<T>()\n // Use the root stream builder that exposes the sendData and sendFrontier methods\n const streamBuilder = new RootStreamBuilder<T>(this, writer)\n return streamBuilder\n }\n\n addOperator(operator: UnaryOperator<any> | BinaryOperator<any>): void {\n this.#checkNotFinalized()\n this.#operators.push(operator)\n }\n\n finalize() {\n this.#checkNotFinalized()\n this.#finalized = true\n }\n\n step(): void {\n if (!this.#finalized) {\n throw new Error(`Graph not finalized`)\n }\n for (const op of this.#operators) {\n op.run()\n }\n }\n\n pendingWork(): boolean {\n return this.#operators.some((op) => op.hasPendingWork())\n }\n\n run(): void {\n while (this.pendingWork()) {\n this.step()\n }\n }\n}\n\nexport class StreamBuilder<T> implements IStreamBuilder<T> {\n #graph: ID2\n #writer: DifferenceStreamWriter<T>\n\n constructor(graph: ID2, writer: DifferenceStreamWriter<T>) {\n this.#graph = graph\n this.#writer = writer\n }\n\n connectReader(): DifferenceStreamReader<T> {\n return this.#writer.newReader()\n }\n\n get writer(): DifferenceStreamWriter<T> {\n return this.#writer\n }\n\n get graph(): ID2 {\n return this.#graph\n }\n\n // Don't judge, this is the only way to type this function.\n // rxjs has very similar code to type its pipe function\n // https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/util/pipe.ts\n // We go to 20 operators deep, because surly that's enough for anyone...\n // A user can always split the pipe into multiple pipes to get around this.\n pipe<O>(o1: PipedOperator<T, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, T17>, o17: PipedOperator<T17, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, T17>, o17: PipedOperator<T17, T18>, o18: PipedOperator<T18, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, T17>, o17: PipedOperator<T17, T18>, o18: PipedOperator<T18, T19>, o19: PipedOperator<T19, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, T17>, o17: PipedOperator<T17, T18>, o18: PipedOperator<T18, T19>, o19: PipedOperator<T19, T20>, o20: PipedOperator<T20, O>): IStreamBuilder<O>\n\n pipe(...operators: Array<PipedOperator<any, any>>): IStreamBuilder<any> {\n return operators.reduce((stream, operator) => {\n return operator(stream)\n }, this as IStreamBuilder<any>)\n }\n}\n\nexport class RootStreamBuilder<T> extends StreamBuilder<T> {\n sendData(collection: MultiSet<T> | MultiSetArray<T>): void {\n this.writer.sendData(collection)\n }\n}\n"],"names":[],"mappings":";AASO,MAAM,GAAkB;AAAA,EAC7B,aAA8D,CAAA;AAAA,EAC9D,kBAAkB;AAAA,EAClB,aAAa;AAAA,EAEb,cAAc;AAAA,EAAC;AAAA,EAEf,qBAA2B;AACzB,QAAI,KAAK,YAAY;AACnB,YAAM,IAAI,MAAM,yBAAyB;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,oBAA4B;AAC1B,SAAK,mBAAA;AACL,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAAoC;AAClC,SAAK,mBAAA;AACL,UAAM,SAAS,IAAI,uBAAA;AAEnB,UAAM,gBAAgB,IAAI,kBAAqB,MAAM,MAAM;AAC3D,WAAO;AAAA,EACT;AAAA,EAEA,YAAY,UAA0D;AACpE,SAAK,mBAAA;AACL,SAAK,WAAW,KAAK,QAAQ;AAAA,EAC/B;AAAA,EAEA,WAAW;AACT,SAAK,mBAAA;AACL,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,OAAa;AACX,QAAI,CAAC,KAAK,YAAY;AACpB,YAAM,IAAI,MAAM,qBAAqB;AAAA,IACvC;AACA,eAAW,MAAM,KAAK,YAAY;AAChC,SAAG,IAAA;AAAA,IACL;AAAA,EACF;AAAA,EAEA,cAAuB;AACrB,WAAO,KAAK,WAAW,KAAK,CAAC,OAAO,GAAG,gBAAgB;AAAA,EACzD;AAAA,EAEA,MAAY;AACV,WAAO,KAAK,eAAe;AACzB,WAAK,KAAA;AAAA,IACP;AAAA,EACF;AACF;AAEO,MAAM,cAA8C;AAAA,EACzD;AAAA,EACA;AAAA,EAEA,YAAY,OAAY,QAAmC;AACzD,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,gBAA2C;AACzC,WAAO,KAAK,QAAQ,UAAA;AAAA,EACtB;AAAA,EAEA,IAAI,SAAoC;AACtC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,QAAa;AACf,WAAO,KAAK;AAAA,EACd;AAAA,EA+CA,QAAQ,WAAgE;AACtE,WAAO,UAAU,OAAO,CAAC,QAAQ,aAAa;AAC5C,aAAO,SAAS,MAAM;AAAA,IACxB,GAAG,IAA2B;AAAA,EAChC;AACF;AAEO,MAAM,0BAA6B,cAAiB;AAAA,EACzD,SAAS,YAAkD;AACzD,SAAK,OAAO,SAAS,UAAU;AAAA,EACjC;AACF;"}
1
+ {"version":3,"file":"d2.js","sources":["../../src/d2.ts"],"sourcesContent":["import { DifferenceStreamWriter } from './graph.js'\nimport type {\n BinaryOperator,\n DifferenceStreamReader,\n UnaryOperator,\n} from './graph.js'\nimport type { MultiSet, MultiSetArray } from './multiset.js'\nimport type { ID2, IStreamBuilder, PipedOperator } from './types.js'\n\nexport class D2 implements ID2 {\n #operators: Array<UnaryOperator<any> | BinaryOperator<any>> = []\n #nextOperatorId = 0\n #finalized = false\n\n constructor() {}\n\n #checkNotFinalized(): void {\n if (this.#finalized) {\n throw new Error(`Graph already finalized`)\n }\n }\n\n getNextOperatorId(): number {\n this.#checkNotFinalized()\n return this.#nextOperatorId++\n }\n\n newInput<T>(): RootStreamBuilder<T> {\n this.#checkNotFinalized()\n const writer = new DifferenceStreamWriter<T>()\n // Use the root stream builder that exposes the sendData and sendFrontier methods\n const streamBuilder = new RootStreamBuilder<T>(this, writer)\n return streamBuilder\n }\n\n addOperator(operator: UnaryOperator<any> | BinaryOperator<any>): void {\n this.#checkNotFinalized()\n this.#operators.push(operator)\n }\n\n finalize() {\n this.#checkNotFinalized()\n this.#finalized = true\n }\n\n step(): void {\n if (!this.#finalized) {\n throw new Error(`Graph not finalized`)\n }\n for (const op of this.#operators) {\n op.run()\n }\n }\n\n pendingWork(): boolean {\n return this.#operators.some((op) => op.hasPendingWork())\n }\n\n run(): void {\n while (this.pendingWork()) {\n this.step()\n }\n }\n}\n\nexport class StreamBuilder<T> implements IStreamBuilder<T> {\n #graph: ID2\n #writer: DifferenceStreamWriter<T>\n\n constructor(graph: ID2, writer: DifferenceStreamWriter<T>) {\n this.#graph = graph\n this.#writer = writer\n }\n\n connectReader(): DifferenceStreamReader<T> {\n return this.#writer.newReader()\n }\n\n get writer(): DifferenceStreamWriter<T> {\n return this.#writer\n }\n\n get graph(): ID2 {\n return this.#graph\n }\n\n // Don't judge, this is the only way to type this function.\n // rxjs has very similar code to type its pipe function\n // https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/util/pipe.ts\n // We go to 20 operators deep, because surly that's enough for anyone...\n // A user can always split the pipe into multiple pipes to get around this.\n pipe<O>(o1: PipedOperator<T, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, T17>, o17: PipedOperator<T17, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, T17>, o17: PipedOperator<T17, T18>, o18: PipedOperator<T18, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, T17>, o17: PipedOperator<T17, T18>, o18: PipedOperator<T18, T19>, o19: PipedOperator<T19, O>): IStreamBuilder<O>\n // prettier-ignore\n pipe<T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, O>(o1: PipedOperator<T, T2>, o2: PipedOperator<T2, T3>, o3: PipedOperator<T3, T4>, o4: PipedOperator<T4, T5>, o5: PipedOperator<T5, T6>, o6: PipedOperator<T6, T7>, o7: PipedOperator<T7, T8>, o8: PipedOperator<T8, T9>, o9: PipedOperator<T9, T10>, o10: PipedOperator<T10, T11>, o11: PipedOperator<T11, T12>, o12: PipedOperator<T12, T13>, o13: PipedOperator<T13, T14>, o14: PipedOperator<T14, T15>, o15: PipedOperator<T15, T16>, o16: PipedOperator<T16, T17>, o17: PipedOperator<T17, T18>, o18: PipedOperator<T18, T19>, o19: PipedOperator<T19, T20>, o20: PipedOperator<T20, O>): IStreamBuilder<O>\n\n pipe(...operators: Array<PipedOperator<any, any>>): IStreamBuilder<any> {\n return operators.reduce((stream, operator) => {\n return operator(stream)\n }, this as IStreamBuilder<any>)\n }\n}\n\nexport class RootStreamBuilder<T> extends StreamBuilder<T> {\n sendData(collection: MultiSet<T> | MultiSetArray<T>): void {\n this.writer.sendData(collection)\n }\n}\n"],"names":[],"mappings":";AASO,MAAM,GAAkB;AAAA,EAC7B,aAA8D,CAAA;AAAA,EAC9D,kBAAkB;AAAA,EAClB,aAAa;AAAA,EAEb,cAAc;AAAA,EAAC;AAAA,EAEf,qBAA2B;AACzB,QAAI,KAAK,YAAY;AACnB,YAAM,IAAI,MAAM,yBAAyB;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,oBAA4B;AAC1B,SAAK,mBAAA;AACL,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAAoC;AAClC,SAAK,mBAAA;AACL,UAAM,SAAS,IAAI,uBAAA;AAEnB,UAAM,gBAAgB,IAAI,kBAAqB,MAAM,MAAM;AAC3D,WAAO;AAAA,EACT;AAAA,EAEA,YAAY,UAA0D;AACpE,SAAK,mBAAA;AACL,SAAK,WAAW,KAAK,QAAQ;AAAA,EAC/B;AAAA,EAEA,WAAW;AACT,SAAK,mBAAA;AACL,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,OAAa;AACX,QAAI,CAAC,KAAK,YAAY;AACpB,YAAM,IAAI,MAAM,qBAAqB;AAAA,IACvC;AACA,eAAW,MAAM,KAAK,YAAY;AAChC,SAAG,IAAA;AAAA,IACL;AAAA,EACF;AAAA,EAEA,cAAuB;AACrB,WAAO,KAAK,WAAW,KAAK,CAAC,OAAO,GAAG,gBAAgB;AAAA,EACzD;AAAA,EAEA,MAAY;AACV,WAAO,KAAK,eAAe;AACzB,WAAK,KAAA;AAAA,IACP;AAAA,EACF;AACF;AAEO,MAAM,cAA8C;AAAA,EACzD;AAAA,EACA;AAAA,EAEA,YAAY,OAAY,QAAmC;AACzD,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,gBAA2C;AACzC,WAAO,KAAK,QAAQ,UAAA;AAAA,EACtB;AAAA,EAEA,IAAI,SAAoC;AACtC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,QAAa;AACf,WAAO,KAAK;AAAA,EACd;AAAA,EA+CA,QAAQ,WAAgE;AACtE,WAAO,UAAU,OAAO,CAAC,QAAQ,aAAa;AAC5C,aAAO,SAAS,MAAM;AAAA,IACxB,GAAG,IAA2B;AAAA,EAChC;AACF;AAEO,MAAM,0BAA6B,cAAiB;AAAA,EACzD,SAAS,YAAkD;AACzD,SAAK,OAAO,SAAS,UAAU;AAAA,EACjC;AACF;"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph.js","sources":["../../src/graph.ts"],"sourcesContent":["import { MultiSet } from \"./multiset.js\"\nimport type { MultiSetArray } from \"./multiset.js\"\nimport type {\n IDifferenceStreamReader,\n IDifferenceStreamWriter,\n IOperator,\n} from \"./types.js\"\n\n/**\n * A read handle to a dataflow edge that receives data from a writer.\n */\nexport class DifferenceStreamReader<T> implements IDifferenceStreamReader<T> {\n #queue: Array<MultiSet<T>>\n\n constructor(queue: Array<MultiSet<T>>) {\n this.#queue = queue\n }\n\n drain(): Array<MultiSet<T>> {\n const out = [...this.#queue].reverse()\n this.#queue.length = 0\n return out\n }\n\n isEmpty(): boolean {\n return this.#queue.length === 0\n }\n}\n\n/**\n * A write handle to a dataflow edge that is allowed to publish data.\n */\nexport class DifferenceStreamWriter<T> implements IDifferenceStreamWriter<T> {\n #queues: Array<Array<MultiSet<T>>> = []\n\n sendData(collection: MultiSet<T> | MultiSetArray<T>): void {\n if (!(collection instanceof MultiSet)) {\n collection = new MultiSet(collection)\n }\n\n for (const q of this.#queues) {\n q.unshift(collection)\n }\n }\n\n newReader(): DifferenceStreamReader<T> {\n const q: Array<MultiSet<T>> = []\n this.#queues.push(q)\n return new DifferenceStreamReader(q)\n }\n}\n\n/**\n * A generic implementation of a dataflow operator (node) that has multiple incoming edges (read handles) and\n * one outgoing edge (write handle).\n */\nexport abstract class Operator<T> implements IOperator<T> {\n protected inputs: Array<DifferenceStreamReader<T>>\n protected output: DifferenceStreamWriter<T>\n\n constructor(\n public id: number,\n inputs: Array<DifferenceStreamReader<T>>,\n output: DifferenceStreamWriter<T>\n ) {\n this.inputs = inputs\n this.output = output\n }\n\n abstract run(): void\n\n hasPendingWork(): boolean {\n return this.inputs.some((input) => !input.isEmpty())\n }\n}\n\n/**\n * A convenience implementation of a dataflow operator that has a handle to one\n * incoming stream of data, and one handle to an outgoing stream of data.\n */\nexport abstract class UnaryOperator<Tin, Tout = Tin> extends Operator<\n Tin | Tout\n> {\n constructor(\n public id: number,\n inputA: DifferenceStreamReader<Tin>,\n output: DifferenceStreamWriter<Tout>\n ) {\n super(id, [inputA], output)\n }\n\n inputMessages(): Array<MultiSet<Tin>> {\n return this.inputs[0]!.drain() as Array<MultiSet<Tin>>\n }\n}\n\n/**\n * A convenience implementation of a dataflow operator that has a handle to two\n * incoming streams of data, and one handle to an outgoing stream of data.\n */\nexport abstract class BinaryOperator<T> extends Operator<T> {\n constructor(\n public id: number,\n inputA: DifferenceStreamReader<T>,\n inputB: DifferenceStreamReader<T>,\n output: DifferenceStreamWriter<T>\n ) {\n super(id, [inputA, inputB], output)\n }\n\n inputAMessages(): Array<MultiSet<T>> {\n return this.inputs[0]!.drain()\n }\n\n inputBMessages(): Array<MultiSet<T>> {\n return this.inputs[1]!.drain()\n }\n}\n\n/**\n * Base class for operators that process a single input stream\n */\nexport abstract class LinearUnaryOperator<T, U> extends UnaryOperator<T | U> {\n abstract inner(collection: MultiSet<T | U>): MultiSet<U>\n\n run(): void {\n for (const message of this.inputMessages()) {\n this.output.sendData(this.inner(message))\n }\n }\n}\n"],"names":[],"mappings":";AAWO,MAAM,uBAAgE;AAAA,EAC3E;AAAA,EAEA,YAAY,OAA2B;AACrC,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,QAA4B;AAC1B,UAAM,MAAM,CAAC,GAAG,KAAK,MAAM,EAAE,QAAA;AAC7B,SAAK,OAAO,SAAS;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,UAAmB;AACjB,WAAO,KAAK,OAAO,WAAW;AAAA,EAChC;AACF;AAKO,MAAM,uBAAgE;AAAA,EAC3E,UAAqC,CAAA;AAAA,EAErC,SAAS,YAAkD;AACzD,QAAI,EAAE,sBAAsB,WAAW;AACrC,mBAAa,IAAI,SAAS,UAAU;AAAA,IACtC;AAEA,eAAW,KAAK,KAAK,SAAS;AAC5B,QAAE,QAAQ,UAAU;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,YAAuC;AACrC,UAAM,IAAwB,CAAA;AAC9B,SAAK,QAAQ,KAAK,CAAC;AACnB,WAAO,IAAI,uBAAuB,CAAC;AAAA,EACrC;AACF;AAMO,MAAe,SAAoC;AAAA,EAIxD,YACS,IACP,QACA,QACA;AAHO,SAAA,KAAA;AAIP,SAAK,SAAS;AACd,SAAK,SAAS;AAAA,EAChB;AAAA,EAIA,iBAA0B;AACxB,WAAO,KAAK,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,SAAS;AAAA,EACrD;AACF;AAMO,MAAe,sBAAuC,SAE3D;AAAA,EACA,YACS,IACP,QACA,QACA;AACA,UAAM,IAAI,CAAC,MAAM,GAAG,MAAM;AAJnB,SAAA,KAAA;AAAA,EAKT;AAAA,EAEA,gBAAsC;AACpC,WAAO,KAAK,OAAO,CAAC,EAAG,MAAA;AAAA,EACzB;AACF;AAMO,MAAe,uBAA0B,SAAY;AAAA,EAC1D,YACS,IACP,QACA,QACA,QACA;AACA,UAAM,IAAI,CAAC,QAAQ,MAAM,GAAG,MAAM;AAL3B,SAAA,KAAA;AAAA,EAMT;AAAA,EAEA,iBAAqC;AACnC,WAAO,KAAK,OAAO,CAAC,EAAG,MAAA;AAAA,EACzB;AAAA,EAEA,iBAAqC;AACnC,WAAO,KAAK,OAAO,CAAC,EAAG,MAAA;AAAA,EACzB;AACF;AAKO,MAAe,4BAAkC,cAAqB;AAAA,EAG3E,MAAY;AACV,eAAW,WAAW,KAAK,iBAAiB;AAC1C,WAAK,OAAO,SAAS,KAAK,MAAM,OAAO,CAAC;AAAA,IAC1C;AAAA,EACF;AACF;"}
1
+ {"version":3,"file":"graph.js","sources":["../../src/graph.ts"],"sourcesContent":["import { MultiSet } from './multiset.js'\nimport type { MultiSetArray } from './multiset.js'\nimport type {\n IDifferenceStreamReader,\n IDifferenceStreamWriter,\n IOperator,\n} from './types.js'\n\n/**\n * A read handle to a dataflow edge that receives data from a writer.\n */\nexport class DifferenceStreamReader<T> implements IDifferenceStreamReader<T> {\n #queue: Array<MultiSet<T>>\n\n constructor(queue: Array<MultiSet<T>>) {\n this.#queue = queue\n }\n\n drain(): Array<MultiSet<T>> {\n const out = [...this.#queue].reverse()\n this.#queue.length = 0\n return out\n }\n\n isEmpty(): boolean {\n return this.#queue.length === 0\n }\n}\n\n/**\n * A write handle to a dataflow edge that is allowed to publish data.\n */\nexport class DifferenceStreamWriter<T> implements IDifferenceStreamWriter<T> {\n #queues: Array<Array<MultiSet<T>>> = []\n\n sendData(collection: MultiSet<T> | MultiSetArray<T>): void {\n if (!(collection instanceof MultiSet)) {\n collection = new MultiSet(collection)\n }\n\n for (const q of this.#queues) {\n q.unshift(collection)\n }\n }\n\n newReader(): DifferenceStreamReader<T> {\n const q: Array<MultiSet<T>> = []\n this.#queues.push(q)\n return new DifferenceStreamReader(q)\n }\n}\n\n/**\n * A generic implementation of a dataflow operator (node) that has multiple incoming edges (read handles) and\n * one outgoing edge (write handle).\n */\nexport abstract class Operator<T> implements IOperator<T> {\n protected inputs: Array<DifferenceStreamReader<T>>\n protected output: DifferenceStreamWriter<T>\n\n constructor(\n public id: number,\n inputs: Array<DifferenceStreamReader<T>>,\n output: DifferenceStreamWriter<T>,\n ) {\n this.inputs = inputs\n this.output = output\n }\n\n abstract run(): void\n\n hasPendingWork(): boolean {\n return this.inputs.some((input) => !input.isEmpty())\n }\n}\n\n/**\n * A convenience implementation of a dataflow operator that has a handle to one\n * incoming stream of data, and one handle to an outgoing stream of data.\n */\nexport abstract class UnaryOperator<Tin, Tout = Tin> extends Operator<\n Tin | Tout\n> {\n constructor(\n public id: number,\n inputA: DifferenceStreamReader<Tin>,\n output: DifferenceStreamWriter<Tout>,\n ) {\n super(id, [inputA], output)\n }\n\n inputMessages(): Array<MultiSet<Tin>> {\n return this.inputs[0]!.drain() as Array<MultiSet<Tin>>\n }\n}\n\n/**\n * A convenience implementation of a dataflow operator that has a handle to two\n * incoming streams of data, and one handle to an outgoing stream of data.\n */\nexport abstract class BinaryOperator<T> extends Operator<T> {\n constructor(\n public id: number,\n inputA: DifferenceStreamReader<T>,\n inputB: DifferenceStreamReader<T>,\n output: DifferenceStreamWriter<T>,\n ) {\n super(id, [inputA, inputB], output)\n }\n\n inputAMessages(): Array<MultiSet<T>> {\n return this.inputs[0]!.drain()\n }\n\n inputBMessages(): Array<MultiSet<T>> {\n return this.inputs[1]!.drain()\n }\n}\n\n/**\n * Base class for operators that process a single input stream\n */\nexport abstract class LinearUnaryOperator<T, U> extends UnaryOperator<T | U> {\n abstract inner(collection: MultiSet<T | U>): MultiSet<U>\n\n run(): void {\n for (const message of this.inputMessages()) {\n this.output.sendData(this.inner(message))\n }\n }\n}\n"],"names":[],"mappings":";AAWO,MAAM,uBAAgE;AAAA,EAC3E;AAAA,EAEA,YAAY,OAA2B;AACrC,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,QAA4B;AAC1B,UAAM,MAAM,CAAC,GAAG,KAAK,MAAM,EAAE,QAAA;AAC7B,SAAK,OAAO,SAAS;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,UAAmB;AACjB,WAAO,KAAK,OAAO,WAAW;AAAA,EAChC;AACF;AAKO,MAAM,uBAAgE;AAAA,EAC3E,UAAqC,CAAA;AAAA,EAErC,SAAS,YAAkD;AACzD,QAAI,EAAE,sBAAsB,WAAW;AACrC,mBAAa,IAAI,SAAS,UAAU;AAAA,IACtC;AAEA,eAAW,KAAK,KAAK,SAAS;AAC5B,QAAE,QAAQ,UAAU;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,YAAuC;AACrC,UAAM,IAAwB,CAAA;AAC9B,SAAK,QAAQ,KAAK,CAAC;AACnB,WAAO,IAAI,uBAAuB,CAAC;AAAA,EACrC;AACF;AAMO,MAAe,SAAoC;AAAA,EAIxD,YACS,IACP,QACA,QACA;AAHO,SAAA,KAAA;AAIP,SAAK,SAAS;AACd,SAAK,SAAS;AAAA,EAChB;AAAA,EAIA,iBAA0B;AACxB,WAAO,KAAK,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,SAAS;AAAA,EACrD;AACF;AAMO,MAAe,sBAAuC,SAE3D;AAAA,EACA,YACS,IACP,QACA,QACA;AACA,UAAM,IAAI,CAAC,MAAM,GAAG,MAAM;AAJnB,SAAA,KAAA;AAAA,EAKT;AAAA,EAEA,gBAAsC;AACpC,WAAO,KAAK,OAAO,CAAC,EAAG,MAAA;AAAA,EACzB;AACF;AAMO,MAAe,uBAA0B,SAAY;AAAA,EAC1D,YACS,IACP,QACA,QACA,QACA;AACA,UAAM,IAAI,CAAC,QAAQ,MAAM,GAAG,MAAM;AAL3B,SAAA,KAAA;AAAA,EAMT;AAAA,EAEA,iBAAqC;AACnC,WAAO,KAAK,OAAO,CAAC,EAAG,MAAA;AAAA,EACzB;AAAA,EAEA,iBAAqC;AACnC,WAAO,KAAK,OAAO,CAAC,EAAG,MAAA;AAAA,EACzB;AACF;AAKO,MAAe,4BAAkC,cAAqB;AAAA,EAG3E,MAAY;AACV,eAAW,WAAW,KAAK,iBAAiB;AAC1C,WAAK,OAAO,SAAS,KAAK,MAAM,OAAO,CAAC;AAAA,IAC1C;AAAA,EACF;AACF;"}
@@ -10,6 +10,8 @@ const OBJECT_MARKER = randomHash();
10
10
  const ARRAY_MARKER = randomHash();
11
11
  const MAP_MARKER = randomHash();
12
12
  const SET_MARKER = randomHash();
13
+ const UINT8ARRAY_MARKER = randomHash();
14
+ const UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128;
13
15
  const hashCache = /* @__PURE__ */ new WeakMap();
14
16
  function hash(input) {
15
17
  const hasher = new MurmurHashStream();
@@ -24,6 +26,17 @@ function hashObject(input) {
24
26
  let valueHash;
25
27
  if (input instanceof Date) {
26
28
  valueHash = hashDate(input);
29
+ } else if (
30
+ // Check if input is a Uint8Array or Buffer
31
+ typeof Buffer !== `undefined` && input instanceof Buffer || input instanceof Uint8Array
32
+ ) {
33
+ if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {
34
+ valueHash = hashUint8Array(input);
35
+ } else {
36
+ return cachedReferenceHash(input);
37
+ }
38
+ } else if (input instanceof File) {
39
+ return cachedReferenceHash(input);
27
40
  } else {
28
41
  let plainObjectInput = input;
29
42
  let marker = OBJECT_MARKER;
@@ -38,9 +51,6 @@ function hashObject(input) {
38
51
  marker = SET_MARKER;
39
52
  plainObjectInput = [...input.entries()];
40
53
  }
41
- if (typeof Buffer !== `undefined` && input instanceof Buffer || input instanceof Uint8Array || input instanceof File) {
42
- return cachedReferenceHash(input);
43
- }
44
54
  valueHash = hashPlainObject(plainObjectInput, marker);
45
55
  }
46
56
  hashCache.set(input, valueHash);
@@ -52,6 +62,15 @@ function hashDate(input) {
52
62
  hasher.update(input.getTime());
53
63
  return hasher.digest();
54
64
  }
65
+ function hashUint8Array(input) {
66
+ const hasher = new MurmurHashStream();
67
+ hasher.update(UINT8ARRAY_MARKER);
68
+ hasher.update(input.byteLength);
69
+ for (let i = 0; i < input.byteLength; i++) {
70
+ hasher.writeByte(input[i]);
71
+ }
72
+ return hasher.digest();
73
+ }
55
74
  function hashPlainObject(input, marker) {
56
75
  const hasher = new MurmurHashStream();
57
76
  hasher.update(marker);
@@ -1 +1 @@
1
- {"version":3,"file":"hash.js","sources":["../../../src/hashing/hash.ts"],"sourcesContent":["import { MurmurHashStream, randomHash } from \"./murmur.js\"\nimport type { Hasher } from \"./murmur.js\"\n\n/*\n * Implementation of structural hashing based on the Composites polyfill implementation:\n * https://github.com/tc39/proposal-composites\n */\n\nconst TRUE = randomHash()\nconst FALSE = randomHash()\nconst NULL = randomHash()\nconst UNDEFINED = randomHash()\nconst KEY = randomHash()\nconst FUNCTIONS = randomHash()\nconst DATE_MARKER = randomHash()\nconst OBJECT_MARKER = randomHash()\nconst ARRAY_MARKER = randomHash()\nconst MAP_MARKER = randomHash()\nconst SET_MARKER = randomHash()\n\nconst hashCache = new WeakMap<object, number>()\n\nexport function hash(input: any): number {\n const hasher = new MurmurHashStream()\n updateHasher(hasher, input)\n return hasher.digest()\n}\n\nfunction hashObject(input: object): number {\n const cachedHash = hashCache.get(input)\n if (cachedHash !== undefined) {\n return cachedHash\n }\n\n let valueHash: number | undefined\n if (input instanceof Date) {\n valueHash = hashDate(input)\n } else {\n let plainObjectInput = input\n let marker = OBJECT_MARKER\n\n if (input instanceof Array) {\n marker = ARRAY_MARKER\n }\n\n if (input instanceof Map) {\n marker = MAP_MARKER\n plainObjectInput = [...input.entries()]\n }\n\n if (input instanceof Set) {\n marker = SET_MARKER\n plainObjectInput = [...input.entries()]\n }\n\n if (\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array ||\n input instanceof File\n ) {\n // Deeply hashing these objects would be too costly\n // but we also don't want to ignore them\n // so we track them by reference and cache them in a weak map\n return cachedReferenceHash(input)\n }\n\n valueHash = hashPlainObject(plainObjectInput, marker)\n }\n\n hashCache.set(input, valueHash)\n return valueHash\n}\n\nfunction hashDate(input: Date): number {\n const hasher = new MurmurHashStream()\n hasher.update(DATE_MARKER)\n hasher.update(input.getTime())\n return hasher.digest()\n}\n\nfunction hashPlainObject(input: object, marker: number): number {\n const hasher = new MurmurHashStream()\n\n // Mark the type of the input\n hasher.update(marker)\n const keys = Object.keys(input)\n keys.sort(keySort)\n for (const key of keys) {\n hasher.update(KEY)\n hasher.update(key)\n updateHasher(hasher, input[key as keyof typeof input])\n }\n\n return hasher.digest()\n}\n\nfunction updateHasher(hasher: Hasher, input: unknown): void {\n if (input === null) {\n hasher.update(NULL)\n return\n }\n switch (typeof input) {\n case `undefined`:\n hasher.update(UNDEFINED)\n return\n case `boolean`:\n hasher.update(input ? TRUE : FALSE)\n return\n case `number`:\n // Normalize NaNs and -0\n hasher.update(isNaN(input) ? NaN : input === 0 ? 0 : input)\n return\n case `bigint`:\n case `string`:\n case `symbol`:\n hasher.update(input)\n return\n case `object`:\n hasher.update(getCachedHash(input))\n return\n case `function`:\n // Functions are assigned a globally unique ID\n // and that ID is cached in the weak map\n hasher.update(cachedReferenceHash(input))\n return\n default:\n console.warn(\n `Ignored input during hashing because it is of type ${typeof input} which is not supported`\n )\n }\n}\n\nfunction getCachedHash(input: object): number {\n let valueHash = hashCache.get(input)\n if (valueHash === undefined) {\n valueHash = hashObject(input)\n }\n return valueHash\n}\n\nlet nextRefId = 1\nfunction cachedReferenceHash(fn: object): number {\n let valueHash = hashCache.get(fn)\n if (valueHash === undefined) {\n valueHash = nextRefId ^ FUNCTIONS\n nextRefId++\n hashCache.set(fn, valueHash)\n }\n return valueHash\n}\n\n/**\n * Strings sorted lexicographically.\n */\nfunction keySort(a: string, b: string): number {\n return a.localeCompare(b)\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hash.js","sources":["../../../src/hashing/hash.ts"],"sourcesContent":["import { MurmurHashStream, randomHash } from './murmur.js'\nimport type { Hasher } from './murmur.js'\n\n/*\n * Implementation of structural hashing based on the Composites polyfill implementation:\n * https://github.com/tc39/proposal-composites\n */\n\nconst TRUE = randomHash()\nconst FALSE = randomHash()\nconst NULL = randomHash()\nconst UNDEFINED = randomHash()\nconst KEY = randomHash()\nconst FUNCTIONS = randomHash()\nconst DATE_MARKER = randomHash()\nconst OBJECT_MARKER = randomHash()\nconst ARRAY_MARKER = randomHash()\nconst MAP_MARKER = randomHash()\nconst SET_MARKER = randomHash()\nconst UINT8ARRAY_MARKER = randomHash()\n\n// Maximum byte length for Uint8Arrays to hash by content instead of reference\n// Arrays smaller than this will be hashed by content, allowing proper equality comparisons\n// for small arrays like ULIDs (16 bytes) while still avoiding performance costs for large arrays\nconst UINT8ARRAY_CONTENT_HASH_THRESHOLD = 128\n\nconst hashCache = new WeakMap<object, number>()\n\nexport function hash(input: any): number {\n const hasher = new MurmurHashStream()\n updateHasher(hasher, input)\n return hasher.digest()\n}\n\nfunction hashObject(input: object): number {\n const cachedHash = hashCache.get(input)\n if (cachedHash !== undefined) {\n return cachedHash\n }\n\n let valueHash: number | undefined\n if (input instanceof Date) {\n valueHash = hashDate(input)\n } else if (\n // Check if input is a Uint8Array or Buffer\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n ) {\n // For small Uint8Arrays/Buffers (e.g., ULIDs, UUIDs), hash by content\n // to enable proper equality comparisons. For large arrays, hash by reference\n // to avoid performance costs.\n if (input.byteLength <= UINT8ARRAY_CONTENT_HASH_THRESHOLD) {\n valueHash = hashUint8Array(input)\n } else {\n // Deeply hashing large arrays would be too costly\n // so we track them by reference and cache them in a weak map\n return cachedReferenceHash(input)\n }\n } else if (input instanceof File) {\n // Files are always hashed by reference due to their potentially large size\n return cachedReferenceHash(input)\n } else {\n let plainObjectInput = input\n let marker = OBJECT_MARKER\n\n if (input instanceof Array) {\n marker = ARRAY_MARKER\n }\n\n if (input instanceof Map) {\n marker = MAP_MARKER\n plainObjectInput = [...input.entries()]\n }\n\n if (input instanceof Set) {\n marker = SET_MARKER\n plainObjectInput = [...input.entries()]\n }\n\n valueHash = hashPlainObject(plainObjectInput, marker)\n }\n\n hashCache.set(input, valueHash)\n return valueHash\n}\n\nfunction hashDate(input: Date): number {\n const hasher = new MurmurHashStream()\n hasher.update(DATE_MARKER)\n hasher.update(input.getTime())\n return hasher.digest()\n}\n\nfunction hashUint8Array(input: Uint8Array): number {\n const hasher = new MurmurHashStream()\n hasher.update(UINT8ARRAY_MARKER)\n // Hash the byte length first to differentiate arrays of different sizes\n hasher.update(input.byteLength)\n // Hash each byte in the array\n for (let i = 0; i < input.byteLength; i++) {\n hasher.writeByte(input[i]!)\n }\n return hasher.digest()\n}\n\nfunction hashPlainObject(input: object, marker: number): number {\n const hasher = new MurmurHashStream()\n\n // Mark the type of the input\n hasher.update(marker)\n const keys = Object.keys(input)\n keys.sort(keySort)\n for (const key of keys) {\n hasher.update(KEY)\n hasher.update(key)\n updateHasher(hasher, input[key as keyof typeof input])\n }\n\n return hasher.digest()\n}\n\nfunction updateHasher(hasher: Hasher, input: unknown): void {\n if (input === null) {\n hasher.update(NULL)\n return\n }\n switch (typeof input) {\n case `undefined`:\n hasher.update(UNDEFINED)\n return\n case `boolean`:\n hasher.update(input ? TRUE : FALSE)\n return\n case `number`:\n // Normalize NaNs and -0\n hasher.update(isNaN(input) ? NaN : input === 0 ? 0 : input)\n return\n case `bigint`:\n case `string`:\n case `symbol`:\n hasher.update(input)\n return\n case `object`:\n hasher.update(getCachedHash(input))\n return\n case `function`:\n // Functions are assigned a globally unique ID\n // and that ID is cached in the weak map\n hasher.update(cachedReferenceHash(input))\n return\n default:\n console.warn(\n `Ignored input during hashing because it is of type ${typeof input} which is not supported`,\n )\n }\n}\n\nfunction getCachedHash(input: object): number {\n let valueHash = hashCache.get(input)\n if (valueHash === undefined) {\n valueHash = hashObject(input)\n }\n return valueHash\n}\n\nlet nextRefId = 1\nfunction cachedReferenceHash(fn: object): number {\n let valueHash = hashCache.get(fn)\n if (valueHash === undefined) {\n valueHash = nextRefId ^ FUNCTIONS\n nextRefId++\n hashCache.set(fn, valueHash)\n }\n return valueHash\n}\n\n/**\n * Strings sorted lexicographically.\n */\nfunction keySort(a: string, b: string): number {\n return a.localeCompare(b)\n}\n"],"names":[],"mappings":"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"}
@@ -20,7 +20,7 @@ export declare class MurmurHashStream implements Hasher {
20
20
  private carry;
21
21
  private carryBytes;
22
22
  private _mix;
23
- private _writeByte;
23
+ writeByte(byte: number): void;
24
24
  update(chunk: symbol | string | number | bigint): void;
25
25
  digest(): number;
26
26
  }
@@ -24,7 +24,7 @@ class MurmurHashStream {
24
24
  this.hash = this.hash << 13 | this.hash >>> 19;
25
25
  this.hash = Math.imul(this.hash, 5) + 3864292196;
26
26
  }
27
- _writeByte(byte) {
27
+ writeByte(byte) {
28
28
  this.carry |= (byte & 255) << 8 * this.carryBytes;
29
29
  this.carryBytes++;
30
30
  this.length++;
@@ -44,8 +44,8 @@ class MurmurHashStream {
44
44
  }
45
45
  for (let i = 0; i < description.length; i++) {
46
46
  const code = description.charCodeAt(i);
47
- this._writeByte(code & 255);
48
- this._writeByte(code >>> 8 & 255);
47
+ this.writeByte(code & 255);
48
+ this.writeByte(code >>> 8 & 255);
49
49
  }
50
50
  return;
51
51
  }
@@ -53,20 +53,20 @@ class MurmurHashStream {
53
53
  this.update(STRING_MARKER);
54
54
  for (let i = 0; i < chunk.length; i++) {
55
55
  const code = chunk.charCodeAt(i);
56
- this._writeByte(code & 255);
57
- this._writeByte(code >>> 8 & 255);
56
+ this.writeByte(code & 255);
57
+ this.writeByte(code >>> 8 & 255);
58
58
  }
59
59
  return;
60
60
  case `number`:
61
61
  dv.setFloat64(0, chunk, true);
62
- this._writeByte(u8[0]);
63
- this._writeByte(u8[1]);
64
- this._writeByte(u8[2]);
65
- this._writeByte(u8[3]);
66
- this._writeByte(u8[4]);
67
- this._writeByte(u8[5]);
68
- this._writeByte(u8[6]);
69
- this._writeByte(u8[7]);
62
+ this.writeByte(u8[0]);
63
+ this.writeByte(u8[1]);
64
+ this.writeByte(u8[2]);
65
+ this.writeByte(u8[3]);
66
+ this.writeByte(u8[4]);
67
+ this.writeByte(u8[5]);
68
+ this.writeByte(u8[6]);
69
+ this.writeByte(u8[7]);
70
70
  return;
71
71
  case `bigint`: {
72
72
  let value = chunk;
@@ -77,10 +77,10 @@ class MurmurHashStream {
77
77
  this.update(BIG_INT_MARKER);
78
78
  }
79
79
  while (value > 0n) {
80
- this._writeByte(Number(value & 0xffn));
80
+ this.writeByte(Number(value & 0xffn));
81
81
  value >>= 8n;
82
82
  }
83
- if (chunk === 0n) this._writeByte(0);
83
+ if (chunk === 0n) this.writeByte(0);
84
84
  return;
85
85
  }
86
86
  default:
@@ -1 +1 @@
1
- {"version":3,"file":"murmur.js","sources":["../../../src/hashing/murmur.ts"],"sourcesContent":["/*\n * Implementation of murmur hash based on the Composites polyfill implementation:\n * https://github.com/tc39/proposal-composites\n */\n\nconst RANDOM_SEED = randomHash()\nconst STRING_MARKER = randomHash()\nconst BIG_INT_MARKER = randomHash()\nconst NEG_BIG_INT_MARKER = randomHash()\nconst SYMBOL_MARKER = randomHash()\n\nexport type Hash = number\n\nexport function randomHash() {\n return (Math.random() * (2 ** 31 - 1)) >>> 0\n}\n\nexport interface Hasher {\n update: (val: symbol | string | number | bigint) => void\n digest: () => number\n}\n\n// Allocate these once, outside the hot path\nconst buf = new ArrayBuffer(8)\n// dv and u8 are 2 different views on the same buffer `buf`\nconst dv = new DataView(buf)\nconst u8 = new Uint8Array(buf)\n\n/**\n * This implementation of Murmur hash uses a random initial seed and random markers.\n * This means that hashes aren't deterministic across app restarts.\n * This is intentional in the composites polyfill to be resistent to hash-flooding attacks\n * where malicious users would precompute lots of different objects whose hashes collide with each other.\n *\n * Currently, for ts/db-ivm this is fine because we don't persist client state.\n * However, when we will introduce persistence we will either need to store the seeds or remove the randomness\n * to ensure deterministic hashes across app restarts.\n */\nexport class MurmurHashStream implements Hasher {\n private hash: number = RANDOM_SEED\n private length = 0\n private carry = 0\n private carryBytes = 0\n\n private _mix(k1: number): void {\n k1 = Math.imul(k1, 0xcc9e2d51)\n k1 = (k1 << 15) | (k1 >>> 17)\n k1 = Math.imul(k1, 0x1b873593)\n this.hash ^= k1\n this.hash = (this.hash << 13) | (this.hash >>> 19)\n this.hash = Math.imul(this.hash, 5) + 0xe6546b64\n }\n\n private _writeByte(byte: number): void {\n this.carry |= (byte & 0xff) << (8 * this.carryBytes)\n this.carryBytes++\n this.length++\n\n if (this.carryBytes === 4) {\n this._mix(this.carry >>> 0)\n this.carry = 0\n this.carryBytes = 0\n }\n }\n\n update(chunk: symbol | string | number | bigint): void {\n switch (typeof chunk) {\n case `symbol`: {\n this.update(SYMBOL_MARKER)\n const description = chunk.description\n if (!description) {\n return\n }\n\n for (let i = 0; i < description.length; i++) {\n const code = description.charCodeAt(i)\n this._writeByte(code & 0xff)\n this._writeByte((code >>> 8) & 0xff)\n }\n return\n }\n case `string`:\n this.update(STRING_MARKER)\n for (let i = 0; i < chunk.length; i++) {\n const code = chunk.charCodeAt(i)\n this._writeByte(code & 0xff)\n this._writeByte((code >>> 8) & 0xff)\n }\n return\n case `number`:\n dv.setFloat64(0, chunk, true) // fixed little-endian\n this._writeByte(u8[0]!)\n this._writeByte(u8[1]!)\n this._writeByte(u8[2]!)\n this._writeByte(u8[3]!)\n this._writeByte(u8[4]!)\n this._writeByte(u8[5]!)\n this._writeByte(u8[6]!)\n this._writeByte(u8[7]!)\n return\n case `bigint`: {\n let value = chunk\n if (value < 0n) {\n value = -value\n this.update(NEG_BIG_INT_MARKER)\n } else {\n this.update(BIG_INT_MARKER)\n }\n while (value > 0n) {\n this._writeByte(Number(value & 0xffn))\n value >>= 8n\n }\n if (chunk === 0n) this._writeByte(0)\n return\n }\n default:\n throw new TypeError(`Unsupported input type: ${typeof chunk}`)\n }\n }\n\n digest(): number {\n if (this.carryBytes > 0) {\n let k1 = this.carry >>> 0\n k1 = Math.imul(k1, 0xcc9e2d51)\n k1 = (k1 << 15) | (k1 >>> 17)\n k1 = Math.imul(k1, 0x1b873593)\n this.hash ^= k1\n }\n\n this.hash ^= this.length\n this.hash ^= this.hash >>> 16\n this.hash = Math.imul(this.hash, 0x85ebca6b)\n this.hash ^= this.hash >>> 13\n this.hash = Math.imul(this.hash, 0xc2b2ae35)\n this.hash ^= this.hash >>> 16\n\n return this.hash >>> 0\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"murmur.js","sources":["../../../src/hashing/murmur.ts"],"sourcesContent":["/*\n * Implementation of murmur hash based on the Composites polyfill implementation:\n * https://github.com/tc39/proposal-composites\n */\n\nconst RANDOM_SEED = randomHash()\nconst STRING_MARKER = randomHash()\nconst BIG_INT_MARKER = randomHash()\nconst NEG_BIG_INT_MARKER = randomHash()\nconst SYMBOL_MARKER = randomHash()\n\nexport type Hash = number\n\nexport function randomHash() {\n return (Math.random() * (2 ** 31 - 1)) >>> 0\n}\n\nexport interface Hasher {\n update: (val: symbol | string | number | bigint) => void\n digest: () => number\n}\n\n// Allocate these once, outside the hot path\nconst buf = new ArrayBuffer(8)\n// dv and u8 are 2 different views on the same buffer `buf`\nconst dv = new DataView(buf)\nconst u8 = new Uint8Array(buf)\n\n/**\n * This implementation of Murmur hash uses a random initial seed and random markers.\n * This means that hashes aren't deterministic across app restarts.\n * This is intentional in the composites polyfill to be resistent to hash-flooding attacks\n * where malicious users would precompute lots of different objects whose hashes collide with each other.\n *\n * Currently, for ts/db-ivm this is fine because we don't persist client state.\n * However, when we will introduce persistence we will either need to store the seeds or remove the randomness\n * to ensure deterministic hashes across app restarts.\n */\nexport class MurmurHashStream implements Hasher {\n private hash: number = RANDOM_SEED\n private length = 0\n private carry = 0\n private carryBytes = 0\n\n private _mix(k1: number): void {\n k1 = Math.imul(k1, 0xcc9e2d51)\n k1 = (k1 << 15) | (k1 >>> 17)\n k1 = Math.imul(k1, 0x1b873593)\n this.hash ^= k1\n this.hash = (this.hash << 13) | (this.hash >>> 19)\n this.hash = Math.imul(this.hash, 5) + 0xe6546b64\n }\n\n writeByte(byte: number): void {\n this.carry |= (byte & 0xff) << (8 * this.carryBytes)\n this.carryBytes++\n this.length++\n\n if (this.carryBytes === 4) {\n this._mix(this.carry >>> 0)\n this.carry = 0\n this.carryBytes = 0\n }\n }\n\n update(chunk: symbol | string | number | bigint): void {\n switch (typeof chunk) {\n case `symbol`: {\n this.update(SYMBOL_MARKER)\n const description = chunk.description\n if (!description) {\n return\n }\n\n for (let i = 0; i < description.length; i++) {\n const code = description.charCodeAt(i)\n this.writeByte(code & 0xff)\n this.writeByte((code >>> 8) & 0xff)\n }\n return\n }\n case `string`:\n this.update(STRING_MARKER)\n for (let i = 0; i < chunk.length; i++) {\n const code = chunk.charCodeAt(i)\n this.writeByte(code & 0xff)\n this.writeByte((code >>> 8) & 0xff)\n }\n return\n case `number`:\n dv.setFloat64(0, chunk, true) // fixed little-endian\n this.writeByte(u8[0]!)\n this.writeByte(u8[1]!)\n this.writeByte(u8[2]!)\n this.writeByte(u8[3]!)\n this.writeByte(u8[4]!)\n this.writeByte(u8[5]!)\n this.writeByte(u8[6]!)\n this.writeByte(u8[7]!)\n return\n case `bigint`: {\n let value = chunk\n if (value < 0n) {\n value = -value\n this.update(NEG_BIG_INT_MARKER)\n } else {\n this.update(BIG_INT_MARKER)\n }\n while (value > 0n) {\n this.writeByte(Number(value & 0xffn))\n value >>= 8n\n }\n if (chunk === 0n) this.writeByte(0)\n return\n }\n default:\n throw new TypeError(`Unsupported input type: ${typeof chunk}`)\n }\n }\n\n digest(): number {\n if (this.carryBytes > 0) {\n let k1 = this.carry >>> 0\n k1 = Math.imul(k1, 0xcc9e2d51)\n k1 = (k1 << 15) | (k1 >>> 17)\n k1 = Math.imul(k1, 0x1b873593)\n this.hash ^= k1\n }\n\n this.hash ^= this.length\n this.hash ^= this.hash >>> 16\n this.hash = Math.imul(this.hash, 0x85ebca6b)\n this.hash ^= this.hash >>> 13\n this.hash = Math.imul(this.hash, 0xc2b2ae35)\n this.hash ^= this.hash >>> 16\n\n return this.hash >>> 0\n }\n}\n"],"names":[],"mappings":"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"}
@@ -2,3 +2,4 @@ export * from './d2.js';
2
2
  export * from './multiset.js';
3
3
  export * from './operators/index.js';
4
4
  export * from './types.js';
5
+ export { compareKeys } from './utils.js';
package/dist/esm/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { D2, RootStreamBuilder, StreamBuilder } from "./d2.js";
2
2
  import { MultiSet } from "./multiset.js";
3
+ import { compareKeys } from "./utils.js";
3
4
  import { groupBy, groupByOperators } from "./operators/groupBy.js";
4
5
  import { pipe } from "./operators/pipe.js";
5
6
  import { MapOperator, map } from "./operators/map.js";
@@ -38,6 +39,7 @@ export {
38
39
  TapOperator,
39
40
  TopKWithFractionalIndexOperator,
40
41
  antiJoin,
42
+ compareKeys,
41
43
  concat,
42
44
  consolidate,
43
45
  count,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;"}