@tanstack/db-ivm 0.1.26 → 0.2.0

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 (136) hide show
  1. package/README.md +5 -6
  2. package/dist/cjs/hashing/hash.cjs +58 -3
  3. package/dist/cjs/hashing/hash.cjs.map +1 -1
  4. package/dist/cjs/hashing/hash.d.cts +9 -0
  5. package/dist/cjs/hashing/murmur.cjs +26 -15
  6. package/dist/cjs/hashing/murmur.cjs.map +1 -1
  7. package/dist/cjs/hashing/murmur.d.cts +2 -0
  8. package/dist/cjs/index.cjs +0 -26
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/indexes.cjs +52 -16
  11. package/dist/cjs/indexes.cjs.map +1 -1
  12. package/dist/cjs/indexes.d.cts +4 -1
  13. package/dist/cjs/operators/groupBy.cjs +33 -36
  14. package/dist/cjs/operators/groupBy.cjs.map +1 -1
  15. package/dist/cjs/operators/groupBy.d.cts +1 -5
  16. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs +3 -9
  17. package/dist/cjs/operators/groupedTopKWithFractionalIndex.cjs.map +1 -1
  18. package/dist/cjs/operators/groupedTopKWithFractionalIndex.d.cts +1 -15
  19. package/dist/cjs/operators/index.d.cts +0 -6
  20. package/dist/cjs/operators/join.cjs +0 -20
  21. package/dist/cjs/operators/join.cjs.map +1 -1
  22. package/dist/cjs/operators/join.d.cts +0 -40
  23. package/dist/cjs/operators/orderBy.cjs +2 -77
  24. package/dist/cjs/operators/orderBy.cjs.map +1 -1
  25. package/dist/cjs/operators/orderBy.d.cts +2 -31
  26. package/dist/cjs/operators/reduce.cjs +36 -5
  27. package/dist/cjs/operators/reduce.cjs.map +1 -1
  28. package/dist/cjs/operators/reduce.d.cts +11 -2
  29. package/dist/cjs/operators/topKArray.cjs +9 -0
  30. package/dist/cjs/operators/topKArray.cjs.map +1 -1
  31. package/dist/cjs/operators/topKArray.d.cts +6 -0
  32. package/dist/cjs/operators/topKState.cjs +3 -0
  33. package/dist/cjs/operators/topKState.cjs.map +1 -1
  34. package/dist/cjs/operators/topKState.d.cts +1 -0
  35. package/dist/cjs/operators/topKWithFractionalIndex.cjs +5 -9
  36. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  37. package/dist/cjs/operators/topKWithFractionalIndex.d.cts +7 -12
  38. package/dist/cjs/utils.cjs.map +1 -1
  39. package/dist/cjs/utils.d.cts +0 -7
  40. package/dist/esm/hashing/hash.d.ts +9 -0
  41. package/dist/esm/hashing/hash.js +60 -5
  42. package/dist/esm/hashing/hash.js.map +1 -1
  43. package/dist/esm/hashing/murmur.d.ts +2 -0
  44. package/dist/esm/hashing/murmur.js +26 -15
  45. package/dist/esm/hashing/murmur.js.map +1 -1
  46. package/dist/esm/index.js +3 -29
  47. package/dist/esm/index.js.map +1 -1
  48. package/dist/esm/indexes.d.ts +4 -1
  49. package/dist/esm/indexes.js +52 -16
  50. package/dist/esm/indexes.js.map +1 -1
  51. package/dist/esm/operators/groupBy.d.ts +1 -5
  52. package/dist/esm/operators/groupBy.js +33 -36
  53. package/dist/esm/operators/groupBy.js.map +1 -1
  54. package/dist/esm/operators/groupedTopKWithFractionalIndex.d.ts +1 -6
  55. package/dist/esm/operators/groupedTopKWithFractionalIndex.js +3 -9
  56. package/dist/esm/operators/groupedTopKWithFractionalIndex.js.map +1 -1
  57. package/dist/esm/operators/index.d.ts +0 -6
  58. package/dist/esm/operators/join.d.ts +0 -25
  59. package/dist/esm/operators/join.js +1 -21
  60. package/dist/esm/operators/join.js.map +1 -1
  61. package/dist/esm/operators/orderBy.d.ts +2 -22
  62. package/dist/esm/operators/orderBy.js +3 -78
  63. package/dist/esm/operators/orderBy.js.map +1 -1
  64. package/dist/esm/operators/reduce.d.ts +11 -2
  65. package/dist/esm/operators/reduce.js +36 -5
  66. package/dist/esm/operators/reduce.js.map +1 -1
  67. package/dist/esm/operators/topKArray.d.ts +6 -0
  68. package/dist/esm/operators/topKArray.js +9 -0
  69. package/dist/esm/operators/topKArray.js.map +1 -1
  70. package/dist/esm/operators/topKState.d.ts +1 -0
  71. package/dist/esm/operators/topKState.js +3 -0
  72. package/dist/esm/operators/topKState.js.map +1 -1
  73. package/dist/esm/operators/topKWithFractionalIndex.d.ts +7 -3
  74. package/dist/esm/operators/topKWithFractionalIndex.js +6 -10
  75. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  76. package/dist/esm/utils.d.ts +0 -7
  77. package/dist/esm/utils.js.map +1 -1
  78. package/package.json +2 -4
  79. package/src/hashing/hash.ts +87 -5
  80. package/src/hashing/murmur.ts +31 -17
  81. package/src/indexes.ts +65 -16
  82. package/src/operators/groupBy.ts +48 -58
  83. package/src/operators/groupedTopKWithFractionalIndex.ts +4 -15
  84. package/src/operators/index.ts +0 -6
  85. package/src/operators/join.ts +0 -90
  86. package/src/operators/orderBy.ts +6 -141
  87. package/src/operators/reduce.ts +60 -3
  88. package/src/operators/topKArray.ts +15 -0
  89. package/src/operators/topKState.ts +7 -0
  90. package/src/operators/topKWithFractionalIndex.ts +15 -16
  91. package/src/utils.ts +0 -30
  92. package/dist/cjs/operators/count.cjs +0 -35
  93. package/dist/cjs/operators/count.cjs.map +0 -1
  94. package/dist/cjs/operators/count.d.cts +0 -19
  95. package/dist/cjs/operators/debug.cjs +0 -39
  96. package/dist/cjs/operators/debug.cjs.map +0 -1
  97. package/dist/cjs/operators/debug.d.cts +0 -16
  98. package/dist/cjs/operators/filterBy.cjs +0 -19
  99. package/dist/cjs/operators/filterBy.cjs.map +0 -1
  100. package/dist/cjs/operators/filterBy.d.cts +0 -9
  101. package/dist/cjs/operators/keying.cjs +0 -16
  102. package/dist/cjs/operators/keying.cjs.map +0 -1
  103. package/dist/cjs/operators/keying.d.cts +0 -22
  104. package/dist/cjs/operators/negate.cjs +0 -27
  105. package/dist/cjs/operators/negate.cjs.map +0 -1
  106. package/dist/cjs/operators/negate.d.cts +0 -13
  107. package/dist/cjs/operators/topK.cjs +0 -39
  108. package/dist/cjs/operators/topK.cjs.map +0 -1
  109. package/dist/cjs/operators/topK.d.cts +0 -34
  110. package/dist/cjs/operators/topKWithFractionalIndexBTree.d.cts +0 -43
  111. package/dist/esm/operators/count.d.ts +0 -13
  112. package/dist/esm/operators/count.js +0 -35
  113. package/dist/esm/operators/count.js.map +0 -1
  114. package/dist/esm/operators/debug.d.ts +0 -16
  115. package/dist/esm/operators/debug.js +0 -39
  116. package/dist/esm/operators/debug.js.map +0 -1
  117. package/dist/esm/operators/filterBy.d.ts +0 -9
  118. package/dist/esm/operators/filterBy.js +0 -19
  119. package/dist/esm/operators/filterBy.js.map +0 -1
  120. package/dist/esm/operators/keying.d.ts +0 -19
  121. package/dist/esm/operators/keying.js +0 -16
  122. package/dist/esm/operators/keying.js.map +0 -1
  123. package/dist/esm/operators/negate.d.ts +0 -13
  124. package/dist/esm/operators/negate.js +0 -27
  125. package/dist/esm/operators/negate.js.map +0 -1
  126. package/dist/esm/operators/topK.d.ts +0 -31
  127. package/dist/esm/operators/topK.js +0 -39
  128. package/dist/esm/operators/topK.js.map +0 -1
  129. package/dist/esm/operators/topKWithFractionalIndexBTree.d.ts +0 -28
  130. package/src/operators/count.ts +0 -51
  131. package/src/operators/debug.ts +0 -57
  132. package/src/operators/filterBy.ts +0 -30
  133. package/src/operators/keying.ts +0 -34
  134. package/src/operators/negate.ts +0 -32
  135. package/src/operators/topK.ts +0 -91
  136. package/src/operators/topKWithFractionalIndexBTree.ts +0 -297
@@ -1 +1 @@
1
- {"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["import { isTemporal } from './hashing/temporal.js'\n\n/**\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// 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 * A string identity within one consolidation: primitives by value, objects,\n * functions, and symbols by identity.\n */\nexport function getStringId(\n value: unknown,\n referenceIds: Map<object | symbol, number>,\n): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n const type = typeof value\n if (type === `object` || type === `function` || type === `symbol`) {\n const reference = value as object | symbol\n let id = referenceIds.get(reference)\n if (id === undefined) referenceIds.set(reference, (id = referenceIds.size))\n return `ref_${id}`\n }\n return `${type}_${String(value)}`\n}\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U,\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (typeof a === `number` && typeof b === `number`) {\n const aIsNaN = Number.isNaN(a)\n const bIsNaN = Number.isNaN(b)\n if (aIsNaN || bIsNaN) {\n if (aIsNaN && bIsNaN) return 0\n return aIsNaN ? 1 : -1\n }\n }\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\ntype CanonicalValue =\n | readonly [`undefined`]\n | readonly [`null`]\n | readonly [`boolean`, boolean]\n | readonly [`number`, number | `NaN` | `Infinity` | `-Infinity`]\n | readonly [`bigint`, string]\n | readonly [`string`, string]\n | readonly [`date`, number | `Invalid`]\n | readonly [`temporal`, string, string]\n | readonly [`regexp`, string, string]\n | readonly [`bytes`, Array<number>]\n | readonly [`array`, Array<CanonicalValue>]\n | readonly [`map`, Array<readonly [CanonicalValue, CanonicalValue]>]\n | readonly [`set`, Array<CanonicalValue>]\n | readonly [`object`, Array<readonly [string, CanonicalValue]>]\n\n/**\n * Serializes a supported query value into one canonical key.\n *\n * JSON's native encoding is not suitable for relation keys: it merges BigInt\n * with strings when a replacer is used, merges NaN with null, drops undefined,\n * and depends on object insertion order. Ordinary JSON values keep their\n * established wire form. Values that need richer types use a reserved prefix\n * plus a structural, type-tagged encoding.\n */\nexport function serializeValue(value: unknown): string {\n if (isJsonSafeStructuralValue(value, new Set())) {\n return JSON.stringify(toStableJsonValue(value))\n }\n\n return `~${JSON.stringify(toCanonicalValue(value, new Set()))}`\n}\n\ntype JsonValue =\n | null\n | boolean\n | number\n | string\n | Array<JsonValue>\n | { [key: string]: JsonValue }\n\nfunction isJsonSafeStructuralValue(\n value: unknown,\n ancestors: Set<object>,\n): boolean {\n if (value === null) return true\n\n switch (typeof value) {\n case `boolean`:\n case `string`:\n return true\n case `number`:\n return Number.isFinite(value)\n case `undefined`:\n case `bigint`:\n case `symbol`:\n case `function`:\n return false\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (\n value instanceof Date ||\n value instanceof RegExp ||\n value instanceof Uint8Array ||\n isTemporal(value) ||\n value instanceof Map ||\n value instanceof Set\n ) {\n return false\n }\n\n return Array.isArray(value)\n ? value.every((item) => isJsonSafeStructuralValue(item, ancestors))\n : Object.keys(value).every((key) =>\n isJsonSafeStructuralValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n )\n })\n}\n\nfunction toStableJsonValue(value: unknown): JsonValue {\n if (\n value === null ||\n typeof value === `boolean` ||\n typeof value === `number` ||\n typeof value === `string`\n ) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map(toStableJsonValue)\n }\n\n return Object.fromEntries(\n Object.keys(value as object)\n .sort()\n .map((key) => [\n key,\n toStableJsonValue((value as Record<string, unknown>)[key]),\n ]),\n )\n}\n\nfunction toCanonicalValue(\n value: unknown,\n ancestors: Set<object>,\n): CanonicalValue {\n if (value === undefined) return [`undefined`]\n if (value === null) return [`null`]\n\n switch (typeof value) {\n case `boolean`:\n return [`boolean`, value]\n case `number`:\n if (Number.isNaN(value)) return [`number`, `NaN`]\n if (value === Infinity) return [`number`, `Infinity`]\n if (value === -Infinity) return [`number`, `-Infinity`]\n return [`number`, value === 0 ? 0 : value]\n case `bigint`:\n return [`bigint`, value.toString()]\n case `string`:\n return [`string`, value]\n case `symbol`:\n case `function`:\n throw new TypeError(\n `Cannot serialize ${typeof value} as a structural relation key`,\n )\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (value instanceof Date) {\n const timestamp = value.getTime()\n return Number.isNaN(timestamp)\n ? ([`date`, `Invalid`] as const)\n : ([`date`, timestamp] as const)\n }\n\n if (value instanceof RegExp) {\n return [`regexp`, value.source, value.flags]\n }\n\n if (value instanceof Uint8Array) {\n return [`bytes`, Array.from(value)]\n }\n\n if (isTemporal(value)) {\n return [`temporal`, value[Symbol.toStringTag], value.toString()]\n }\n\n if (Array.isArray(value)) {\n return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]\n }\n\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([key, entryValue]) =>\n [\n toCanonicalValue(key, ancestors),\n toCanonicalValue(entryValue, ancestors),\n ] as const,\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`map`, entries]\n }\n\n if (value instanceof Set) {\n const entries = [...value].map((entry) =>\n toCanonicalValue(entry, ancestors),\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`set`, entries]\n }\n\n const entries = Object.keys(value)\n .sort()\n .map(\n (key) =>\n [\n key,\n toCanonicalValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n ] as const,\n )\n return [`object`, entries]\n })\n}\n\nfunction compareSerializedValues(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0\n}\n\nfunction withAcyclicValue<T>(\n value: object,\n ancestors: Set<object>,\n encode: () => T,\n): T {\n if (ancestors.has(value)) {\n throw new TypeError(`Cannot serialize a cyclic structural relation key`)\n }\n\n ancestors.add(value)\n try {\n return encode()\n } finally {\n ancestors.delete(value)\n }\n}\n"],"names":["isTemporal","entries"],"mappings":";;;AAmBA,MAAM,YAAY;AACX,SAAS,iBAAiB,OAAuB,OAAuB;AAC7E,MAAI,MAAM,UAAU,WAAW;AAC7B,UAAM,KAAK,GAAG,KAAK;AAAA,EACrB,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,YAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,SAAS;AAC1C,YAAM,KAAK,GAAG,KAAK;AAAA,IACrB;AAAA,EACF;AACF;AAEO,SAAS,aACd,OACA,OACA,YACQ;AACR,MAAI,MAAM;AACV,MAAI,OAAO,MAAM;AACjB,SAAO,MAAM,MAAM;AACjB,UAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,CAAC;AACvC,UAAM,aAAa,WAAW,MAAM,GAAG,GAAI,KAAK;AAChD,QAAI,aAAa,GAAG;AAClB,YAAM,MAAM;AAAA,IACd,WAAW,aAAa,GAAG;AACzB,aAAO;AAAA,IACT,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAMO,SAAS,YACd,OACA,cACQ;AACR,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,UAAU,OAAW,QAAO;AAChC,QAAM,OAAO,OAAO;AACpB,MAAI,SAAS,YAAY,SAAS,cAAc,SAAS,UAAU;AACjE,UAAM,YAAY;AAClB,QAAI,KAAK,aAAa,IAAI,SAAS;AACnC,QAAI,OAAO,OAAW,cAAa,IAAI,WAAY,KAAK,aAAa,IAAK;AAC1E,WAAO,OAAO,EAAE;AAAA,EAClB;AACA,SAAO,GAAG,IAAI,IAAI,OAAO,KAAK,CAAC;AACjC;AA2BO,SAAS,aAAa,GAAW,GAAW;AACjD,QAAM,CAAC,IAAI,EAAE,IAAI;AACjB,QAAM,CAAC,IAAI,EAAE,IAAI;AAGjB,QAAM,UAAyB;AAAA,IAC7B,GAAG,MAAM,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;AAAA;AAAA,IAC7B,GAAG,MAAM,KAAK,IAAI,IAAI,EAAE,GAAG,EAAE;AAAA;AAAA,EAAA;AAI/B,QAAM,UAAyB;AAAA,IAC7B,GAAG,MAAM,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;AAAA,IAC7B,GAAG,MAAM,KAAK,IAAI,IAAI,EAAE,GAAG,EAAE;AAAA,EAAA;AAG/B,SAAO,EAAE,SAAS,QAAA;AACpB;AAEA,SAAS,MAAM,OAAe,KAA4B;AACxD,QAAM,MAAqB,CAAA;AAC3B,WAAS,IAAI,OAAO,IAAI,KAAK,IAAK,KAAI,KAAK,CAAC;AAC5C,SAAO;AACT;AAMO,SAAS,YAAY,GAAoB,GAA4B;AAE1E,MAAI,OAAO,MAAM,OAAO,GAAG;AACzB,QAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;AAClD,YAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,YAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,UAAI,UAAU,QAAQ;AACpB,YAAI,UAAU,OAAQ,QAAO;AAC7B,eAAO,SAAS,IAAI;AAAA,MACtB;AAAA,IACF;AACA,QAAI,IAAI,EAAG,QAAO;AAClB,QAAI,IAAI,EAAG,QAAO;AAClB,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,MAAM,WAAW,KAAK;AACtC;AA2BO,SAAS,eAAe,OAAwB;AACrD,MAAI,0BAA0B,OAAO,oBAAI,IAAA,CAAK,GAAG;AAC/C,WAAO,KAAK,UAAU,kBAAkB,KAAK,CAAC;AAAA,EAChD;AAEA,SAAO,IAAI,KAAK,UAAU,iBAAiB,OAAO,oBAAI,KAAK,CAAC,CAAC;AAC/D;AAUA,SAAS,0BACP,OACA,WACS;AACT,MAAI,UAAU,KAAM,QAAO;AAE3B,UAAQ,OAAO,OAAA;AAAA,IACb,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,OAAO,SAAS,KAAK;AAAA,IAC9B,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,EAAA;AAGX,SAAO,iBAAiB,OAAO,WAAW,MAAM;AAC9C,QACE,iBAAiB,QACjB,iBAAiB,UACjB,iBAAiB,cACjBA,SAAAA,WAAW,KAAK,KAChB,iBAAiB,OACjB,iBAAiB,KACjB;AACA,aAAO;AAAA,IACT;AAEA,WAAO,MAAM,QAAQ,KAAK,IACtB,MAAM,MAAM,CAAC,SAAS,0BAA0B,MAAM,SAAS,CAAC,IAChE,OAAO,KAAK,KAAK,EAAE;AAAA,MAAM,CAAC,QACxB;AAAA,QACG,MAAkC,GAAG;AAAA,QACtC;AAAA,MAAA;AAAA,IACF;AAAA,EAER,CAAC;AACH;AAEA,SAAS,kBAAkB,OAA2B;AACpD,MACE,UAAU,QACV,OAAO,UAAU,aACjB,OAAO,UAAU,YACjB,OAAO,UAAU,UACjB;AACA,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,iBAAiB;AAAA,EACpC;AAEA,SAAO,OAAO;AAAA,IACZ,OAAO,KAAK,KAAe,EACxB,OACA,IAAI,CAAC,QAAQ;AAAA,MACZ;AAAA,MACA,kBAAmB,MAAkC,GAAG,CAAC;AAAA,IAAA,CAC1D;AAAA,EAAA;AAEP;AAEA,SAAS,iBACP,OACA,WACgB;AAChB,MAAI,UAAU,OAAW,QAAO,CAAC,WAAW;AAC5C,MAAI,UAAU,KAAM,QAAO,CAAC,MAAM;AAElC,UAAQ,OAAO,OAAA;AAAA,IACb,KAAK;AACH,aAAO,CAAC,WAAW,KAAK;AAAA,IAC1B,KAAK;AACH,UAAI,OAAO,MAAM,KAAK,EAAG,QAAO,CAAC,UAAU,KAAK;AAChD,UAAI,UAAU,SAAU,QAAO,CAAC,UAAU,UAAU;AACpD,UAAI,UAAU,UAAW,QAAO,CAAC,UAAU,WAAW;AACtD,aAAO,CAAC,UAAU,UAAU,IAAI,IAAI,KAAK;AAAA,IAC3C,KAAK;AACH,aAAO,CAAC,UAAU,MAAM,SAAA,CAAU;AAAA,IACpC,KAAK;AACH,aAAO,CAAC,UAAU,KAAK;AAAA,IACzB,KAAK;AAAA,IACL,KAAK;AACH,YAAM,IAAI;AAAA,QACR,oBAAoB,OAAO,KAAK;AAAA,MAAA;AAAA,EAClC;AAGJ,SAAO,iBAAiB,OAAO,WAAW,MAAM;AAC9C,QAAI,iBAAiB,MAAM;AACzB,YAAM,YAAY,MAAM,QAAA;AACxB,aAAO,OAAO,MAAM,SAAS,IACxB,CAAC,QAAQ,SAAS,IAClB,CAAC,QAAQ,SAAS;AAAA,IACzB;AAEA,QAAI,iBAAiB,QAAQ;AAC3B,aAAO,CAAC,UAAU,MAAM,QAAQ,MAAM,KAAK;AAAA,IAC7C;AAEA,QAAI,iBAAiB,YAAY;AAC/B,aAAO,CAAC,SAAS,MAAM,KAAK,KAAK,CAAC;AAAA,IACpC;AAEA,QAAIA,SAAAA,WAAW,KAAK,GAAG;AACrB,aAAO,CAAC,YAAY,MAAM,OAAO,WAAW,GAAG,MAAM,UAAU;AAAA,IACjE;AAEA,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,aAAO,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,iBAAiB,MAAM,SAAS,CAAC,CAAC;AAAA,IACzE;AAEA,QAAI,iBAAiB,KAAK;AACxB,YAAMC,WAAU,CAAC,GAAG,MAAM,QAAA,CAAS,EAAE;AAAA,QACnC,CAAC,CAAC,KAAK,UAAU,MACf;AAAA,UACE,iBAAiB,KAAK,SAAS;AAAA,UAC/B,iBAAiB,YAAY,SAAS;AAAA,QAAA;AAAA,MACxC;AAEJA,eAAQ;AAAA,QAAK,CAAC,MAAM,UAClB,wBAAwB,KAAK,UAAU,IAAI,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,MAAA;AAErE,aAAO,CAAC,OAAOA,QAAO;AAAA,IACxB;AAEA,QAAI,iBAAiB,KAAK;AACxB,YAAMA,WAAU,CAAC,GAAG,KAAK,EAAE;AAAA,QAAI,CAAC,UAC9B,iBAAiB,OAAO,SAAS;AAAA,MAAA;AAEnCA,eAAQ;AAAA,QAAK,CAAC,MAAM,UAClB,wBAAwB,KAAK,UAAU,IAAI,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,MAAA;AAErE,aAAO,CAAC,OAAOA,QAAO;AAAA,IACxB;AAEA,UAAM,UAAU,OAAO,KAAK,KAAK,EAC9B,OACA;AAAA,MACC,CAAC,QACC;AAAA,QACE;AAAA,QACA;AAAA,UACG,MAAkC,GAAG;AAAA,UACtC;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAEN,WAAO,CAAC,UAAU,OAAO;AAAA,EAC3B,CAAC;AACH;AAEA,SAAS,wBAAwB,MAAc,OAAuB;AACpE,SAAO,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI;AAChD;AAEA,SAAS,iBACP,OACA,WACA,QACG;AACH,MAAI,UAAU,IAAI,KAAK,GAAG;AACxB,UAAM,IAAI,UAAU,mDAAmD;AAAA,EACzE;AAEA,YAAU,IAAI,KAAK;AACnB,MAAI;AACF,WAAO,OAAA;AAAA,EACT,UAAA;AACE,cAAU,OAAO,KAAK;AAAA,EACxB;AACF;;;;;;;"}
1
+ {"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["import { isTemporal } from './hashing/temporal.js'\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 * A string identity within one consolidation: primitives by value, objects,\n * functions, and symbols by identity.\n */\nexport function getStringId(\n value: unknown,\n referenceIds: Map<object | symbol, number>,\n): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n const type = typeof value\n if (type === `object` || type === `function` || type === `symbol`) {\n const reference = value as object | symbol\n let id = referenceIds.get(reference)\n if (id === undefined) referenceIds.set(reference, (id = referenceIds.size))\n return `ref_${id}`\n }\n return `${type}_${String(value)}`\n}\n\nexport type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive\n\n/**\n * Computes the difference between two half-open ranges.\n * @param a - The first half-open range\n * @param b - The second half-open range\n * @returns The difference between the two ranges\n */\nexport function diffHalfOpen(a: HRange, b: HRange) {\n const [a1, a2] = a\n const [b1, b2] = b\n\n // A \\ B can be up to two segments (left and right of the overlap)\n const onlyInA: Array<number> = [\n ...range(a1, Math.min(a2, b1)), // left side of A outside B\n ...range(Math.max(a1, b2), a2), // right side of A outside B\n ]\n\n // B \\ A similarly\n const onlyInB: Array<number> = [\n ...range(b1, Math.min(b2, a1)),\n ...range(Math.max(b1, a2), b2),\n ]\n\n return { onlyInA, onlyInB }\n}\n\nfunction range(start: number, end: number): Array<number> {\n const out: Array<number> = []\n for (let i = start; i < end; i++) out.push(i)\n return out\n}\n\n/**\n * Compares two keys (string | number) in a consistent, deterministic way.\n * Handles mixed types by ordering strings before numbers.\n */\nexport function compareKeys(a: string | number, b: string | number): number {\n // Same type: compare directly\n if (typeof a === typeof b) {\n if (typeof a === `number` && typeof b === `number`) {\n const aIsNaN = Number.isNaN(a)\n const bIsNaN = Number.isNaN(b)\n if (aIsNaN || bIsNaN) {\n if (aIsNaN && bIsNaN) return 0\n return aIsNaN ? 1 : -1\n }\n }\n if (a < b) return -1\n if (a > b) return 1\n return 0\n }\n // Different types: strings come before numbers\n return typeof a === `string` ? -1 : 1\n}\n\ntype CanonicalValue =\n | readonly [`undefined`]\n | readonly [`null`]\n | readonly [`boolean`, boolean]\n | readonly [`number`, number | `NaN` | `Infinity` | `-Infinity`]\n | readonly [`bigint`, string]\n | readonly [`string`, string]\n | readonly [`date`, number | `Invalid`]\n | readonly [`temporal`, string, string]\n | readonly [`regexp`, string, string]\n | readonly [`bytes`, Array<number>]\n | readonly [`array`, Array<CanonicalValue>]\n | readonly [`map`, Array<readonly [CanonicalValue, CanonicalValue]>]\n | readonly [`set`, Array<CanonicalValue>]\n | readonly [`object`, Array<readonly [string, CanonicalValue]>]\n\n/**\n * Serializes a supported query value into one canonical key.\n *\n * JSON's native encoding is not suitable for relation keys: it merges BigInt\n * with strings when a replacer is used, merges NaN with null, drops undefined,\n * and depends on object insertion order. Ordinary JSON values keep their\n * established wire form. Values that need richer types use a reserved prefix\n * plus a structural, type-tagged encoding.\n */\nexport function serializeValue(value: unknown): string {\n if (isJsonSafeStructuralValue(value, new Set())) {\n return JSON.stringify(toStableJsonValue(value))\n }\n\n return `~${JSON.stringify(toCanonicalValue(value, new Set()))}`\n}\n\ntype JsonValue =\n | null\n | boolean\n | number\n | string\n | Array<JsonValue>\n | { [key: string]: JsonValue }\n\nfunction isJsonSafeStructuralValue(\n value: unknown,\n ancestors: Set<object>,\n): boolean {\n if (value === null) return true\n\n switch (typeof value) {\n case `boolean`:\n case `string`:\n return true\n case `number`:\n return Number.isFinite(value)\n case `undefined`:\n case `bigint`:\n case `symbol`:\n case `function`:\n return false\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (\n value instanceof Date ||\n value instanceof RegExp ||\n value instanceof Uint8Array ||\n isTemporal(value) ||\n value instanceof Map ||\n value instanceof Set\n ) {\n return false\n }\n\n return Array.isArray(value)\n ? value.every((item) => isJsonSafeStructuralValue(item, ancestors))\n : Object.keys(value).every((key) =>\n isJsonSafeStructuralValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n )\n })\n}\n\nfunction toStableJsonValue(value: unknown): JsonValue {\n if (\n value === null ||\n typeof value === `boolean` ||\n typeof value === `number` ||\n typeof value === `string`\n ) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map(toStableJsonValue)\n }\n\n return Object.fromEntries(\n Object.keys(value as object)\n .sort()\n .map((key) => [\n key,\n toStableJsonValue((value as Record<string, unknown>)[key]),\n ]),\n )\n}\n\nfunction toCanonicalValue(\n value: unknown,\n ancestors: Set<object>,\n): CanonicalValue {\n if (value === undefined) return [`undefined`]\n if (value === null) return [`null`]\n\n switch (typeof value) {\n case `boolean`:\n return [`boolean`, value]\n case `number`:\n if (Number.isNaN(value)) return [`number`, `NaN`]\n if (value === Infinity) return [`number`, `Infinity`]\n if (value === -Infinity) return [`number`, `-Infinity`]\n return [`number`, value === 0 ? 0 : value]\n case `bigint`:\n return [`bigint`, value.toString()]\n case `string`:\n return [`string`, value]\n case `symbol`:\n case `function`:\n throw new TypeError(\n `Cannot serialize ${typeof value} as a structural relation key`,\n )\n }\n\n return withAcyclicValue(value, ancestors, () => {\n if (value instanceof Date) {\n const timestamp = value.getTime()\n return Number.isNaN(timestamp)\n ? ([`date`, `Invalid`] as const)\n : ([`date`, timestamp] as const)\n }\n\n if (value instanceof RegExp) {\n return [`regexp`, value.source, value.flags]\n }\n\n if (value instanceof Uint8Array) {\n return [`bytes`, Array.from(value)]\n }\n\n if (isTemporal(value)) {\n return [`temporal`, value[Symbol.toStringTag], value.toString()]\n }\n\n if (Array.isArray(value)) {\n return [`array`, value.map((item) => toCanonicalValue(item, ancestors))]\n }\n\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([key, entryValue]) =>\n [\n toCanonicalValue(key, ancestors),\n toCanonicalValue(entryValue, ancestors),\n ] as const,\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`map`, entries]\n }\n\n if (value instanceof Set) {\n const entries = [...value].map((entry) =>\n toCanonicalValue(entry, ancestors),\n )\n entries.sort((left, right) =>\n compareSerializedValues(JSON.stringify(left), JSON.stringify(right)),\n )\n return [`set`, entries]\n }\n\n const entries = Object.keys(value)\n .sort()\n .map(\n (key) =>\n [\n key,\n toCanonicalValue(\n (value as Record<string, unknown>)[key],\n ancestors,\n ),\n ] as const,\n )\n return [`object`, entries]\n })\n}\n\nfunction compareSerializedValues(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0\n}\n\nfunction withAcyclicValue<T>(\n value: object,\n ancestors: Set<object>,\n encode: () => T,\n): T {\n if (ancestors.has(value)) {\n throw new TypeError(`Cannot serialize a cyclic structural relation key`)\n }\n\n ancestors.add(value)\n try {\n return encode()\n } finally {\n ancestors.delete(value)\n }\n}\n"],"names":["isTemporal","entries"],"mappings":"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"}
@@ -1,8 +1,3 @@
1
- /**
2
- * Simple assertion function for runtime checks.
3
- * Throws an error if the condition is false.
4
- */
5
- export declare function assert(condition: unknown, message?: string): asserts condition;
6
1
  export declare function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>): void;
7
2
  export declare function binarySearch<T>(array: Array<T>, value: T, comparator: (a: T, b: T) => number): number;
8
3
  /**
@@ -10,8 +5,6 @@ export declare function binarySearch<T>(array: Array<T>, value: T, comparator: (
10
5
  * functions, and symbols by identity.
11
6
  */
12
7
  export declare function getStringId(value: unknown, referenceIds: Map<object | symbol, number>): string;
13
- export declare function concatIterable<T>(...iterables: Array<Iterable<T>>): Iterable<T>;
14
- export declare function mapIterable<T, U>(it: Iterable<T>, fn: (t: T) => U): Iterable<U>;
15
8
  export type HRange = [
16
9
  number,
17
10
  number
@@ -6,3 +6,12 @@ export declare function hash(input: any): number;
6
6
  * Reference-valued leaves remain opaque, including registered mutable handles.
7
7
  */
8
8
  export declare function equalHashValues(left: unknown, right: unknown): boolean;
9
+ /**
10
+ * @internal An exact identity string for a value, or undefined when it has
11
+ * no cheap one. Two values that `equalHashValues` treats as equal get the
12
+ * same result, both undefined or one string; two that it treats as different
13
+ * never share a string. The decision uses only what equalHashValues reads:
14
+ * the type, the `objectParts` marker and headers, and enumerable own keys. groupBy uses it
15
+ * to merge equal contributions without hashing them.
16
+ */
17
+ export declare function structuralIdentity(value: unknown, depth?: number): string | undefined;
@@ -1,4 +1,4 @@
1
- import { MurmurHashStream, randomHash, getSymbolIdentity } from "./murmur.js";
1
+ import { MurmurHashStream, getSymbolIdentity, randomHash } from "./murmur.js";
2
2
  import { isTemporal } from "./temporal.js";
3
3
  const TRUE = randomHash();
4
4
  const FALSE = randomHash();
@@ -43,7 +43,9 @@ function hashObject(input, context) {
43
43
  if (body) updateHasher(hasher, value, context);
44
44
  else hasher.update(value);
45
45
  if (body) {
46
- const keys = Object.keys(body).sort(keySort);
46
+ const keys = Object.keys(body);
47
+ const denseArray = Array.isArray(body) && keys.length === body.length && (keys.length === 0 || keys[keys.length - 1] === String(body.length - 1));
48
+ if (!denseArray) keys.sort();
47
49
  keys.push(
48
50
  ...Object.getOwnPropertySymbols(body).filter(
49
51
  (key) => Object.prototype.propertyIsEnumerable.call(body, key)
@@ -177,12 +179,65 @@ function cachedReferenceHash(fn) {
177
179
  }
178
180
  return valueHash;
179
181
  }
180
- function keySort(a, b) {
181
- return a.localeCompare(b);
182
+ const MARKER_TAGS = /* @__PURE__ */ new Map([
183
+ [ARRAY_MARKER, `[`],
184
+ [OBJECT_MARKER, `{`],
185
+ [DATE_MARKER, `<`],
186
+ [REGEXP_MARKER, `/`],
187
+ [MAP_MARKER, `&`],
188
+ [SET_MARKER, `*`],
189
+ [UINT8ARRAY_MARKER, `^`],
190
+ [TEMPORAL_MARKER, `?`]
191
+ ]);
192
+ const MAX_IDENTITY_DEPTH = 4;
193
+ function structuralIdentity(value, depth = 0) {
194
+ switch (typeof value) {
195
+ case `number`:
196
+ return `#${value === 0 ? 0 : value}`;
197
+ case `string`:
198
+ return `"${value.length}:${value}`;
199
+ case `boolean`:
200
+ return value ? `+` : `-`;
201
+ case `undefined`:
202
+ return `~`;
203
+ case `bigint`:
204
+ return `%${value}`;
205
+ case `symbol`:
206
+ return `$${getSymbolIdentity(value)}`;
207
+ case `object`: {
208
+ if (value === null) return `!`;
209
+ if (depth >= MAX_IDENTITY_DEPTH || isReferenceHashedObject(value))
210
+ return void 0;
211
+ const [marker, header, body] = objectParts(value);
212
+ let head = MARKER_TAGS.get(marker);
213
+ for (const part of header) {
214
+ if (Number.isNaN(part)) return void 0;
215
+ head += structuralIdentity(part, depth + 1);
216
+ }
217
+ if (!body) return head;
218
+ if (Object.getOwnPropertySymbols(body).some(
219
+ (key) => Object.prototype.propertyIsEnumerable.call(body, key)
220
+ ))
221
+ return void 0;
222
+ let parts = ``;
223
+ for (const key of Object.keys(body).sort()) {
224
+ const part = structuralIdentity(
225
+ body[key],
226
+ depth + 1
227
+ );
228
+ if (part === void 0) return void 0;
229
+ parts += `${key.length}:${key}=${part}|`;
230
+ }
231
+ return `${head}:${parts.length}:${parts}`;
232
+ }
233
+ default:
234
+ return void 0;
235
+ }
182
236
  }
183
237
  export {
184
238
  equalHashValues,
185
239
  hash,
186
- registerOpaqueHash
240
+ registerOpaqueHash,
241
+ structuralIdentity
187
242
  };
188
243
  //# sourceMappingURL=hash.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hash.js","sources":["../../../src/hashing/hash.ts"],"sourcesContent":["import { MurmurHashStream, getSymbolIdentity, randomHash } from './murmur.js'\nimport { isTemporal } from './temporal.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()\n// Identity is distinct even if two random hash values happen to collide.\nconst DATE_MARKER = { hash: randomHash() }\nconst REGEXP_MARKER = { hash: randomHash() }\nconst OBJECT_MARKER = { hash: randomHash() }\nconst ARRAY_MARKER = { hash: randomHash() }\nconst MAP_MARKER = { hash: randomHash() }\nconst SET_MARKER = { hash: randomHash() }\nconst UINT8ARRAY_MARKER = { hash: randomHash() }\nconst TEMPORAL_MARKER = { hash: randomHash() }\n// Bound structural recursion and value visits. Shared acyclic subtrees are\n// cached; cycles are rejected rather than given context-dependent hashes.\nconst MAX_STRUCTURAL_HASH_WORK = 1_000_000\nconst MAX_STRUCTURAL_HASH_DEPTH = 768\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>()\nconst referenceValues = new WeakSet<object>()\n\n/** @internal Register a mutable handle before it enters a structural value. */\nexport function registerOpaqueHash(value: object): void {\n cachedReferenceHash(value)\n}\n\ntype HashContext = {\n activeObjects: Set<object>\n work: number\n pendingHashes: Map<object, number>\n}\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, context: HashContext): number {\n if (context.activeObjects.size >= MAX_STRUCTURAL_HASH_DEPTH) {\n throw new RangeError(\n `Value is too complex to hash safely: structural depth`,\n )\n }\n\n context.activeObjects.add(input)\n\n let valueHash: number | undefined\n try {\n const [marker, header, body] = objectParts(input)\n const hasher = new MurmurHashStream()\n hasher.update(marker.hash)\n // Header values of a type with a body (array length, RegExp fields)\n // count toward the work cap; Date, binary, and Temporal headers do not.\n for (const value of header)\n if (body) updateHasher(hasher, value, context)\n else hasher.update(value as string | number)\n if (body) {\n // Sorted string keys, then enumerable symbol keys by identity.\n const keys: Array<string | symbol> = Object.keys(body).sort(keySort)\n keys.push(\n ...Object.getOwnPropertySymbols(body)\n .filter((key) =>\n Object.prototype.propertyIsEnumerable.call(body, key),\n )\n .sort(\n (left, right) => getSymbolIdentity(left) - getSymbolIdentity(right),\n ),\n )\n for (const key of keys) {\n hasher.update(KEY)\n hasher.update(key)\n updateHasher(hasher, body[key as keyof typeof body], context)\n }\n }\n valueHash = hasher.digest()\n } finally {\n context.activeObjects.delete(input)\n }\n\n context.pendingHashes.set(input, valueHash)\n return valueHash\n}\n\n/**\n * The one type dispatch shared by structural hashing and equality: a type\n * marker, primitive header values, and an optional object whose enumerable\n * own properties are compared structurally.\n */\ntype ObjectParts = [\n marker: Readonly<{ hash: number }>,\n // A RegExp `lastIndex` can hold any value; the other headers are strings\n // or numbers.\n header: ReadonlyArray<unknown>,\n body?: object,\n]\n\nfunction objectParts(input: object): ObjectParts {\n if (input instanceof Date) {\n const time = input.getTime()\n // Invalid dates are equal to each other; headers compare with `===`.\n return [DATE_MARKER, [Number.isNaN(time) ? `invalid` : time]]\n }\n if (isBinaryValue(input))\n return [\n UINT8ARRAY_MARKER,\n [String.fromCharCode.apply(null, input as unknown as Array<number>)],\n ]\n if (isTemporal(input))\n return [TEMPORAL_MARKER, [input[Symbol.toStringTag], input.toString()]]\n if (input instanceof RegExp)\n return [REGEXP_MARKER, [input.source, input.flags, input.lastIndex], input]\n if (input instanceof Map) return [MAP_MARKER, [], [...input.entries()]]\n if (input instanceof Set) return [SET_MARKER, [], [...input.values()]]\n return input instanceof Array\n ? [ARRAY_MARKER, [input.length], input]\n : [OBJECT_MARKER, [], input]\n}\n\nfunction updateHasher(\n hasher: Hasher,\n input: unknown,\n context?: HashContext,\n): void {\n if (context && ++context.work > MAX_STRUCTURAL_HASH_WORK) {\n throw new RangeError(`Value is too complex to hash safely: structural work`)\n }\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, context))\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 }\n}\n\nfunction getCachedHash(input: object, context?: HashContext): number {\n // Active objects are neither cached nor pending, so the cycle check below\n // still sees every back-reference.\n const cached = hashCache.get(input) ?? context?.pendingHashes.get(input)\n if (cached !== undefined) return cached\n\n // Opaque leaves cannot contain structural back-references. Resolve them\n // before entering structural recursion, even when they have user properties.\n if (isReferenceHashedObject(input)) return cachedReferenceHash(input)\n\n if (context) {\n if (context.activeObjects.has(input)) {\n throw new TypeError(`Cannot hash cyclic structural values`)\n }\n return hashObject(input, context)\n }\n\n // Only an uncached structural root needs graph traversal state. Commit its\n // cache entries after success so a failed traversal cannot poison retries.\n const root: HashContext = {\n activeObjects: new Set(),\n work: 0,\n pendingHashes: new Map(),\n }\n const result = hashObject(input, root)\n for (const [object, valueHash] of root.pendingHashes) {\n hashCache.set(object, valueHash)\n }\n return result\n}\n\nfunction isReferenceHashedObject(input: object): boolean {\n return (\n referenceValues.has(input) ||\n (typeof File !== `undefined` && input instanceof File) ||\n (isBinaryValue(input) &&\n input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD)\n )\n}\n\n// Buffer can come from another realm than Uint8Array (for example under jsdom).\nfunction isBinaryValue(input: object): input is Uint8Array {\n return (\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n )\n}\n\n/** @internal Compare immutable structural values without computing a digest.\n * Pair memoization also permits cyclic values that structural hashing rejects.\n * Reference-valued leaves remain opaque, including registered mutable handles.\n */\nexport function equalHashValues(left: unknown, right: unknown): boolean {\n const compared = new Map<object, Set<object>>()\n const keys = (value: object) =>\n Reflect.ownKeys(value).filter((key) =>\n Object.prototype.propertyIsEnumerable.call(value, key),\n )\n function equal(a: unknown, b: unknown): boolean {\n if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true\n if (\n a === null ||\n b === null ||\n typeof a !== `object` ||\n typeof b !== `object` ||\n isReferenceHashedObject(a) ||\n isReferenceHashedObject(b)\n )\n return false\n // Arrays from another realm take the object marker but keep this check.\n if (Array.isArray(a) && Array.isArray(b) && a.length !== b.length)\n return false\n\n // Revisited pairs close cycles and avoid expanding shared subtrees. Check\n // before objectParts copies Map or Set entries; record only pairs with a\n // body, after their markers and headers match.\n const peers = compared.get(a)\n if (peers?.has(b)) return true\n const [aMarker, aHeader, aBody] = objectParts(a)\n const [bMarker, bHeader, bBody] = objectParts(b)\n if (\n aMarker !== bMarker ||\n !aHeader.every((value, index) => value === bHeader[index])\n )\n return false\n if (!aBody || !bBody) return true\n if (peers) peers.add(b)\n else compared.set(a, new Set([b]))\n const aKeys = keys(aBody)\n return (\n aKeys.length === keys(bBody).length &&\n aKeys.every(\n (key) =>\n Object.prototype.propertyIsEnumerable.call(bBody, key) &&\n equal(aBody[key as keyof object], bBody[key as keyof object]),\n )\n )\n }\n return equal(left, right)\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 referenceValues.add(fn)\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, getSymbolIdentity, randomHash } from './murmur.js'\nimport { isTemporal } from './temporal.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()\n// Identity is distinct even if two random hash values happen to collide.\nconst DATE_MARKER = { hash: randomHash() }\nconst REGEXP_MARKER = { hash: randomHash() }\nconst OBJECT_MARKER = { hash: randomHash() }\nconst ARRAY_MARKER = { hash: randomHash() }\nconst MAP_MARKER = { hash: randomHash() }\nconst SET_MARKER = { hash: randomHash() }\nconst UINT8ARRAY_MARKER = { hash: randomHash() }\nconst TEMPORAL_MARKER = { hash: randomHash() }\n// Bound structural recursion and value visits. Shared acyclic subtrees are\n// cached; cycles are rejected rather than given context-dependent hashes.\nconst MAX_STRUCTURAL_HASH_WORK = 1_000_000\nconst MAX_STRUCTURAL_HASH_DEPTH = 768\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>()\nconst referenceValues = new WeakSet<object>()\n\n/** @internal Register a mutable handle before it enters a structural value. */\nexport function registerOpaqueHash(value: object): void {\n cachedReferenceHash(value)\n}\n\ntype HashContext = {\n activeObjects: Set<object>\n work: number\n pendingHashes: Map<object, number>\n}\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, context: HashContext): number {\n if (context.activeObjects.size >= MAX_STRUCTURAL_HASH_DEPTH) {\n throw new RangeError(\n `Value is too complex to hash safely: structural depth`,\n )\n }\n\n context.activeObjects.add(input)\n\n let valueHash: number | undefined\n try {\n const [marker, header, body] = objectParts(input)\n const hasher = new MurmurHashStream()\n hasher.update(marker.hash)\n // Header values of a type with a body (array length, RegExp fields)\n // count toward the work cap; Date, binary, and Temporal headers do not.\n for (const value of header)\n if (body) updateHasher(hasher, value, context)\n else hasher.update(value as string | number)\n if (body) {\n // String keys in UTF-16 code-unit order, then enumerable symbol keys by\n // identity. Code-unit order is total; locale collation can rank two\n // distinct keys as equal and leave their insertion order in the hash.\n const keys: Array<string | symbol> = Object.keys(body)\n // A dense array with no other keys lists its indices in one order for\n // every equal array: integer keys come first, so the last key is the\n // last index. Anything else sorts.\n const denseArray =\n Array.isArray(body) &&\n keys.length === body.length &&\n (keys.length === 0 || keys[keys.length - 1] === String(body.length - 1))\n if (!denseArray) keys.sort()\n keys.push(\n ...Object.getOwnPropertySymbols(body)\n .filter((key) =>\n Object.prototype.propertyIsEnumerable.call(body, key),\n )\n .sort(\n (left, right) => getSymbolIdentity(left) - getSymbolIdentity(right),\n ),\n )\n for (const key of keys) {\n hasher.update(KEY)\n hasher.update(key)\n updateHasher(hasher, body[key as keyof typeof body], context)\n }\n }\n valueHash = hasher.digest()\n } finally {\n context.activeObjects.delete(input)\n }\n\n context.pendingHashes.set(input, valueHash)\n return valueHash\n}\n\n/**\n * The one type dispatch shared by structural hashing and equality: a type\n * marker, primitive header values, and an optional object whose enumerable\n * own properties are compared structurally.\n */\ntype ObjectParts = [\n marker: Readonly<{ hash: number }>,\n // A RegExp `lastIndex` can hold any value; the other headers are strings\n // or numbers.\n header: ReadonlyArray<unknown>,\n body?: object,\n]\n\nfunction objectParts(input: object): ObjectParts {\n if (input instanceof Date) {\n const time = input.getTime()\n // Invalid dates are equal to each other; headers compare with `===`.\n return [DATE_MARKER, [Number.isNaN(time) ? `invalid` : time]]\n }\n if (isBinaryValue(input))\n return [\n UINT8ARRAY_MARKER,\n [String.fromCharCode.apply(null, input as unknown as Array<number>)],\n ]\n if (isTemporal(input))\n return [TEMPORAL_MARKER, [input[Symbol.toStringTag], input.toString()]]\n if (input instanceof RegExp)\n return [REGEXP_MARKER, [input.source, input.flags, input.lastIndex], input]\n if (input instanceof Map) return [MAP_MARKER, [], [...input.entries()]]\n if (input instanceof Set) return [SET_MARKER, [], [...input.values()]]\n return input instanceof Array\n ? [ARRAY_MARKER, [input.length], input]\n : [OBJECT_MARKER, [], input]\n}\n\nfunction updateHasher(\n hasher: Hasher,\n input: unknown,\n context?: HashContext,\n): void {\n if (context && ++context.work > MAX_STRUCTURAL_HASH_WORK) {\n throw new RangeError(`Value is too complex to hash safely: structural work`)\n }\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, context))\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 }\n}\n\nfunction getCachedHash(input: object, context?: HashContext): number {\n // Active objects are neither cached nor pending, so the cycle check below\n // still sees every back-reference.\n const cached = hashCache.get(input) ?? context?.pendingHashes.get(input)\n if (cached !== undefined) return cached\n\n // Opaque leaves cannot contain structural back-references. Resolve them\n // before entering structural recursion, even when they have user properties.\n if (isReferenceHashedObject(input)) return cachedReferenceHash(input)\n\n if (context) {\n if (context.activeObjects.has(input)) {\n throw new TypeError(`Cannot hash cyclic structural values`)\n }\n return hashObject(input, context)\n }\n\n // Only an uncached structural root needs graph traversal state. Commit its\n // cache entries after success so a failed traversal cannot poison retries.\n const root: HashContext = {\n activeObjects: new Set(),\n work: 0,\n pendingHashes: new Map(),\n }\n const result = hashObject(input, root)\n for (const [object, valueHash] of root.pendingHashes) {\n hashCache.set(object, valueHash)\n }\n return result\n}\n\nfunction isReferenceHashedObject(input: object): boolean {\n return (\n referenceValues.has(input) ||\n (typeof File !== `undefined` && input instanceof File) ||\n (isBinaryValue(input) &&\n input.byteLength > UINT8ARRAY_CONTENT_HASH_THRESHOLD)\n )\n}\n\n// Buffer can come from another realm than Uint8Array (for example under jsdom).\nfunction isBinaryValue(input: object): input is Uint8Array {\n return (\n (typeof Buffer !== `undefined` && input instanceof Buffer) ||\n input instanceof Uint8Array\n )\n}\n\n/** @internal Compare immutable structural values without computing a digest.\n * Pair memoization also permits cyclic values that structural hashing rejects.\n * Reference-valued leaves remain opaque, including registered mutable handles.\n */\nexport function equalHashValues(left: unknown, right: unknown): boolean {\n const compared = new Map<object, Set<object>>()\n const keys = (value: object) =>\n Reflect.ownKeys(value).filter((key) =>\n Object.prototype.propertyIsEnumerable.call(value, key),\n )\n function equal(a: unknown, b: unknown): boolean {\n if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true\n if (\n a === null ||\n b === null ||\n typeof a !== `object` ||\n typeof b !== `object` ||\n isReferenceHashedObject(a) ||\n isReferenceHashedObject(b)\n )\n return false\n // Arrays from another realm take the object marker but keep this check.\n if (Array.isArray(a) && Array.isArray(b) && a.length !== b.length)\n return false\n\n // Revisited pairs close cycles and avoid expanding shared subtrees. Check\n // before objectParts copies Map or Set entries; record only pairs with a\n // body, after their markers and headers match.\n const peers = compared.get(a)\n if (peers?.has(b)) return true\n const [aMarker, aHeader, aBody] = objectParts(a)\n const [bMarker, bHeader, bBody] = objectParts(b)\n if (\n aMarker !== bMarker ||\n !aHeader.every((value, index) => value === bHeader[index])\n )\n return false\n if (!aBody || !bBody) return true\n if (peers) peers.add(b)\n else compared.set(a, new Set([b]))\n const aKeys = keys(aBody)\n return (\n aKeys.length === keys(bBody).length &&\n aKeys.every(\n (key) =>\n Object.prototype.propertyIsEnumerable.call(bBody, key) &&\n equal(aBody[key as keyof object], bBody[key as keyof object]),\n )\n )\n }\n return equal(left, right)\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 referenceValues.add(fn)\n }\n return valueHash\n}\n\n// One punctuation tag per objectParts marker, so values of different kinds\n// never share an identity (and no tag reads as an identifier in this source).\nconst MARKER_TAGS = new Map<{ hash: number }, string>([\n [ARRAY_MARKER, `[`],\n [OBJECT_MARKER, `{`],\n [DATE_MARKER, `<`],\n [REGEXP_MARKER, `/`],\n [MAP_MARKER, `&`],\n [SET_MARKER, `*`],\n [UINT8ARRAY_MARKER, `^`],\n [TEMPORAL_MARKER, `?`],\n])\n\n// Nesting a structural identity follows: a min or max input is an array of\n// exact keys, and an exact key can hold a reference tuple.\nconst MAX_IDENTITY_DEPTH = 4\n\n/**\n * @internal An exact identity string for a value, or undefined when it has\n * no cheap one. Two values that `equalHashValues` treats as equal get the\n * same result, both undefined or one string; two that it treats as different\n * never share a string. The decision uses only what equalHashValues reads:\n * the type, the `objectParts` marker and headers, and enumerable own keys. groupBy uses it\n * to merge equal contributions without hashing them.\n */\nexport function structuralIdentity(\n value: unknown,\n depth = 0,\n): string | undefined {\n switch (typeof value) {\n case `number`:\n // The hash treats -0 as 0 and every NaN as one value.\n return `#${value === 0 ? 0 : value}`\n case `string`:\n return `\"${value.length}:${value}`\n case `boolean`:\n return value ? `+` : `-`\n case `undefined`:\n return `~`\n case `bigint`:\n return `%${value}`\n case `symbol`:\n return `$${getSymbolIdentity(value)}`\n case `object`: {\n if (value === null) return `!`\n if (depth >= MAX_IDENTITY_DEPTH || isReferenceHashedObject(value))\n return undefined\n const [marker, header, body] = objectParts(value)\n // equalHashValues compares headers with ===, so a NaN header is never\n // equal to another value's, and gets no identity.\n let head = MARKER_TAGS.get(marker)!\n for (const part of header) {\n if (Number.isNaN(part)) return undefined\n head += structuralIdentity(part, depth + 1)\n }\n if (!body) return head\n if (\n Object.getOwnPropertySymbols(body).some((key) =>\n Object.prototype.propertyIsEnumerable.call(body, key),\n )\n )\n return undefined\n let parts = ``\n for (const key of Object.keys(body).sort()) {\n const part = structuralIdentity(\n (body as Record<string, unknown>)[key],\n depth + 1,\n )\n if (part === undefined) return undefined\n parts += `${key.length}:${key}=${part}|`\n }\n // An array's header is its length, so a hole differs from undefined.\n return `${head}:${parts.length}:${parts}`\n }\n default:\n return undefined\n }\n}\n"],"names":[],"mappings":"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"}
@@ -22,6 +22,8 @@ export declare class MurmurHashStream implements Hasher {
22
22
  private carryBytes;
23
23
  private _mix;
24
24
  writeByte(byte: number): void;
25
+ /** Write four bytes, low byte first, as four `writeByte` calls would. */
26
+ private writeWord;
25
27
  update(chunk: symbol | string | number | bigint): void;
26
28
  digest(): number;
27
29
  }
@@ -36,9 +36,7 @@ function createSymbolIdStore() {
36
36
  function randomHash() {
37
37
  return Math.random() * (2 ** 31 - 1) >>> 0;
38
38
  }
39
- const buf = new ArrayBuffer(8);
40
- const dv = new DataView(buf);
41
- const u8 = new Uint8Array(buf);
39
+ const dv = new DataView(new ArrayBuffer(8));
42
40
  class MurmurHashStream {
43
41
  constructor() {
44
42
  this.hash = RANDOM_SEED;
@@ -64,6 +62,17 @@ class MurmurHashStream {
64
62
  this.carryBytes = 0;
65
63
  }
66
64
  }
65
+ /** Write four bytes, low byte first, as four `writeByte` calls would. */
66
+ writeWord(word) {
67
+ this.length += 4;
68
+ if (this.carryBytes === 0) {
69
+ this._mix(word >>> 0);
70
+ return;
71
+ }
72
+ const shift = 8 * this.carryBytes;
73
+ this._mix((this.carry | word << shift) >>> 0);
74
+ this.carry = word >>> 32 - shift;
75
+ }
67
76
  update(chunk) {
68
77
  switch (typeof chunk) {
69
78
  case `symbol`: {
@@ -73,22 +82,24 @@ class MurmurHashStream {
73
82
  }
74
83
  case `string`:
75
84
  this.update(STRING_MARKER);
76
- for (let i = 0; i < chunk.length; i++) {
77
- const code = chunk.charCodeAt(i);
78
- this.writeByte(code & 255);
79
- this.writeByte(code >>> 8 & 255);
85
+ {
86
+ const pairs = chunk.length - chunk.length % 2;
87
+ for (let i = 0; i < pairs; i += 2) {
88
+ this.writeWord(
89
+ chunk.charCodeAt(i) | chunk.charCodeAt(i + 1) << 16
90
+ );
91
+ }
92
+ if (pairs < chunk.length) {
93
+ const code = chunk.charCodeAt(pairs);
94
+ this.writeByte(code & 255);
95
+ this.writeByte(code >>> 8 & 255);
96
+ }
80
97
  }
81
98
  return;
82
99
  case `number`:
83
100
  dv.setFloat64(0, chunk, true);
84
- this.writeByte(u8[0]);
85
- this.writeByte(u8[1]);
86
- this.writeByte(u8[2]);
87
- this.writeByte(u8[3]);
88
- this.writeByte(u8[4]);
89
- this.writeByte(u8[5]);
90
- this.writeByte(u8[6]);
91
- this.writeByte(u8[7]);
101
+ this.writeWord(dv.getUint32(0, true));
102
+ this.writeWord(dv.getUint32(4, true));
92
103
  return;
93
104
  case `bigint`: {
94
105
  let value = chunk;
@@ -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\ntype SymbolIdStore = {\n get: (key: symbol) => number | undefined\n set: (key: symbol, value: number) => unknown\n}\n\nconst symbolIds = createSymbolIdStore()\nconst registeredSymbolIds = new Map<string, number>()\nlet nextSymbolId = 0\n\nexport function getSymbolIdentity(symbol: symbol): number {\n const registeredKey = Symbol.keyFor(symbol)\n if (registeredKey !== undefined) {\n let id = registeredSymbolIds.get(registeredKey)\n if (id === undefined) {\n id = ++nextSymbolId\n registeredSymbolIds.set(registeredKey, id)\n }\n return id\n }\n\n let id = symbolIds.get(symbol)\n if (id === undefined) {\n id = ++nextSymbolId\n symbolIds.set(symbol, id)\n }\n return id\n}\n\nfunction createSymbolIdStore(): SymbolIdStore {\n const weakIds = new WeakMap<object, number>() as unknown as SymbolIdStore\n const probe = Symbol()\n\n try {\n weakIds.set(probe, 0)\n if (weakIds.get(probe) === 0) return weakIds\n } catch {\n // Older runtimes reject symbols as weak keys. Retain them rather than\n // merge distinct symbols and corrupt differential state.\n }\n\n return new Map<symbol, number>()\n}\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 this.update(getSymbolIdentity(chunk))\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":["id"],"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\ntype SymbolIdStore = {\n get: (key: symbol) => number | undefined\n set: (key: symbol, value: number) => unknown\n}\n\nconst symbolIds = createSymbolIdStore()\nconst registeredSymbolIds = new Map<string, number>()\nlet nextSymbolId = 0\n\nexport function getSymbolIdentity(symbol: symbol): number {\n const registeredKey = Symbol.keyFor(symbol)\n if (registeredKey !== undefined) {\n let id = registeredSymbolIds.get(registeredKey)\n if (id === undefined) {\n id = ++nextSymbolId\n registeredSymbolIds.set(registeredKey, id)\n }\n return id\n }\n\n let id = symbolIds.get(symbol)\n if (id === undefined) {\n id = ++nextSymbolId\n symbolIds.set(symbol, id)\n }\n return id\n}\n\nfunction createSymbolIdStore(): SymbolIdStore {\n const weakIds = new WeakMap<object, number>() as unknown as SymbolIdStore\n const probe = Symbol()\n\n try {\n weakIds.set(probe, 0)\n if (weakIds.get(probe) === 0) return weakIds\n } catch {\n // Older runtimes reject symbols as weak keys. Retain them rather than\n // merge distinct symbols and corrupt differential state.\n }\n\n return new Map<symbol, number>()\n}\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 this once, outside the hot path\nconst dv = new DataView(new ArrayBuffer(8))\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 /** Write four bytes, low byte first, as four `writeByte` calls would. */\n private writeWord(word: number): void {\n this.length += 4\n if (this.carryBytes === 0) {\n this._mix(word >>> 0)\n return\n }\n // The carry's bytes come first. The word's low bytes complete that block,\n // and its high bytes become the new carry, which keeps its byte count.\n const shift = 8 * this.carryBytes\n this._mix((this.carry | (word << shift)) >>> 0)\n this.carry = word >>> (32 - shift)\n }\n\n update(chunk: symbol | string | number | bigint): void {\n switch (typeof chunk) {\n case `symbol`: {\n this.update(SYMBOL_MARKER)\n this.update(getSymbolIdentity(chunk))\n return\n }\n case `string`:\n this.update(STRING_MARKER)\n {\n // Each code unit is two bytes, so a pair of them is one word.\n const pairs = chunk.length - (chunk.length % 2)\n for (let i = 0; i < pairs; i += 2) {\n this.writeWord(\n chunk.charCodeAt(i) | (chunk.charCodeAt(i + 1) << 16),\n )\n }\n if (pairs < chunk.length) {\n const code = chunk.charCodeAt(pairs)\n this.writeByte(code & 0xff)\n this.writeByte((code >>> 8) & 0xff)\n }\n }\n return\n case `number`:\n dv.setFloat64(0, chunk, true) // fixed little-endian\n this.writeWord(dv.getUint32(0, true))\n this.writeWord(dv.getUint32(4, true))\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":["id"],"mappings":"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"}
package/dist/esm/index.js CHANGED
@@ -4,79 +4,53 @@ import { compareKeys, serializeValue } from "./utils.js";
4
4
  import { registerOpaqueHash } from "./hashing/hash.js";
5
5
  import { ConcatOperator, concat } from "./operators/concat.js";
6
6
  import { ConsolidateOperator, consolidate } from "./operators/consolidate.js";
7
- import { CountOperator, count } from "./operators/count.js";
8
- import { DebugOperator, debug } from "./operators/debug.js";
9
7
  import { DistinctOperator, distinct } from "./operators/distinct.js";
10
8
  import { FilterOperator, filter } from "./operators/filter.js";
11
9
  import { GroupedTopKWithFractionalIndexOperator, groupedTopKWithFractionalIndex } from "./operators/groupedTopKWithFractionalIndex.js";
12
- import { JoinOperator, antiJoin, fullJoin, innerJoin, join, leftJoin, rightJoin } from "./operators/join.js";
10
+ import { JoinOperator, join } from "./operators/join.js";
13
11
  import { MapOperator, map } from "./operators/map.js";
14
- import { NegateOperator, negate } from "./operators/negate.js";
15
12
  import { OutputOperator, output } from "./operators/output.js";
16
13
  import { ReduceOperator, reduce } from "./operators/reduce.js";
17
14
  import { TapOperator, tap } from "./operators/tap.js";
18
15
  import { TopKWithFractionalIndexOperator, topKWithFractionalIndex } from "./operators/topKWithFractionalIndex.js";
19
- import { filterBy } from "./operators/filterBy.js";
20
16
  import { groupBy, groupByOperators } from "./operators/groupBy.js";
21
17
  import { groupedOrderByWithFractionalIndex } from "./operators/groupedOrderBy.js";
22
- import { keyBy, rekey, unkey } from "./operators/keying.js";
23
- import { orderBy, orderByWithFractionalIndex, orderByWithFractionalIndexBase, orderByWithIndex } from "./operators/orderBy.js";
18
+ import { orderByWithFractionalIndex } from "./operators/orderBy.js";
24
19
  import { pipe } from "./operators/pipe.js";
25
- import { topK, topKWithIndex } from "./operators/topK.js";
26
20
  export {
27
21
  ConcatOperator,
28
22
  ConsolidateOperator,
29
- CountOperator,
30
23
  D2,
31
- DebugOperator,
32
24
  DistinctOperator,
33
25
  FilterOperator,
34
26
  GroupedTopKWithFractionalIndexOperator,
35
27
  JoinOperator,
36
28
  MapOperator,
37
29
  MultiSet,
38
- NegateOperator,
39
30
  OutputOperator,
40
31
  ReduceOperator,
41
32
  RootStreamBuilder,
42
33
  StreamBuilder,
43
34
  TapOperator,
44
35
  TopKWithFractionalIndexOperator,
45
- antiJoin,
46
36
  compareKeys,
47
37
  concat,
48
38
  consolidate,
49
- count,
50
- debug,
51
39
  distinct,
52
40
  filter,
53
- filterBy,
54
- fullJoin,
55
41
  groupBy,
56
42
  groupByOperators,
57
43
  groupedOrderByWithFractionalIndex,
58
44
  groupedTopKWithFractionalIndex,
59
- innerJoin,
60
45
  join,
61
- keyBy,
62
- leftJoin,
63
46
  map,
64
- negate,
65
- orderBy,
66
47
  orderByWithFractionalIndex,
67
- orderByWithFractionalIndexBase,
68
- orderByWithIndex,
69
48
  output,
70
49
  pipe,
71
50
  reduce,
72
51
  registerOpaqueHash,
73
- rekey,
74
- rightJoin,
75
52
  serializeValue,
76
53
  tap,
77
- topK,
78
- topKWithFractionalIndex,
79
- topKWithIndex,
80
- unkey
54
+ topKWithFractionalIndex
81
55
  };
82
56
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
@@ -7,7 +7,10 @@ type SingleValue<TValue> = [TValue, number];
7
7
  */
8
8
  export declare class Index<TKey, TValue, TPrefix = any> {
9
9
  #private;
10
- constructor();
10
+ constructor(options?: {
11
+ prefixIdentity?: boolean;
12
+ trackPresence?: boolean;
13
+ });
11
14
  /**
12
15
  * Create an Index from multiple MultiSet messages.
13
16
  * @param messages - Array of MultiSet messages to build the index from.
@@ -4,8 +4,9 @@ const NO_PREFIX = /* @__PURE__ */ Symbol(`NO_PREFIX`);
4
4
  class PrefixMap extends Map {
5
5
  /**
6
6
  * Add a value to the PrefixMap. Returns true if the map becomes empty after the operation.
7
+ * With `prefixIdentity`, values that share a prefix are the same value.
7
8
  */
8
- addValue(value, multiplicity) {
9
+ addValue(value, multiplicity, prefixIdentity = false) {
9
10
  if (multiplicity === 0) return this.size === 0;
10
11
  const prefix = getPrefix(value);
11
12
  const valueMapOrSingleValue = this.get(prefix);
@@ -15,7 +16,7 @@ class PrefixMap extends Map {
15
16
  if (!isSamePrefix(currentPrefix, prefix)) {
16
17
  throw new Error(`Mismatching prefixes, this should never happen`);
17
18
  }
18
- if (currentValue === value || hash(currentValue) === hash(value)) {
19
+ if (currentValue === value || prefixIdentity && prefix !== NO_PREFIX || hash(currentValue) === hash(value)) {
19
20
  const newMultiplicity = currentMultiplicity + multiplicity;
20
21
  if (newMultiplicity === 0) {
21
22
  this.delete(prefix);
@@ -76,10 +77,20 @@ class Index {
76
77
  * hash to identify identical values, storing them in a third level value map.
77
78
  */
78
79
  #inner;
79
- #consolidatedMultiplicity = /* @__PURE__ */ new Map();
80
- // sum of multiplicities per key
81
- constructor() {
80
+ // Sum of multiplicities per key, for presence. Only joins read presence; a
81
+ // reduce index is built without it.
82
+ #consolidatedMultiplicity;
83
+ /**
84
+ * When true, the producer guarantees that a value's string or number
85
+ * prefix determines the value: two values under one key with an equal
86
+ * prefix are equal, so they merge without a structural hash.
87
+ */
88
+ #prefixIdentity;
89
+ constructor(options) {
82
90
  this.#inner = /* @__PURE__ */ new Map();
91
+ if (options?.trackPresence ?? true)
92
+ this.#consolidatedMultiplicity = /* @__PURE__ */ new Map();
93
+ this.#prefixIdentity = options?.prefixIdentity ?? false;
83
94
  }
84
95
  /**
85
96
  * Create an Index from multiple MultiSet messages.
@@ -114,6 +125,11 @@ class Index {
114
125
  get size() {
115
126
  return this.#inner.size;
116
127
  }
128
+ #presence() {
129
+ if (this.#consolidatedMultiplicity === void 0)
130
+ throw new Error(`This Index does not track presence`);
131
+ return this.#consolidatedMultiplicity;
132
+ }
117
133
  /**
118
134
  * This method checks if the index has a given key.
119
135
  * @param key - The key to check.
@@ -128,7 +144,7 @@ class Index {
128
144
  * @returns True if the key has non-zero consolidated multiplicity, false otherwise.
129
145
  */
130
146
  hasPresence(key) {
131
- return (this.#consolidatedMultiplicity.get(key) || 0) !== 0;
147
+ return (this.#presence().get(key) || 0) !== 0;
132
148
  }
133
149
  /**
134
150
  * Get the consolidated multiplicity (sum of multiplicities) for a key.
@@ -136,14 +152,14 @@ class Index {
136
152
  * @returns The consolidated multiplicity for the key.
137
153
  */
138
154
  getConsolidatedMultiplicity(key) {
139
- return this.#consolidatedMultiplicity.get(key) || 0;
155
+ return this.#presence().get(key) || 0;
140
156
  }
141
157
  /**
142
158
  * Get all keys that have presence (non-zero consolidated multiplicity).
143
159
  * @returns An iterator of keys with non-zero consolidated multiplicity.
144
160
  */
145
161
  getPresenceKeys() {
146
- return this.#consolidatedMultiplicity.keys();
162
+ return this.#presence().keys();
147
163
  }
148
164
  /**
149
165
  * This method returns all values for a given key.
@@ -151,7 +167,20 @@ class Index {
151
167
  * @returns An array of value tuples [value, multiplicity].
152
168
  */
153
169
  get(key) {
154
- return [...this.getIterator(key)];
170
+ const mapOrSingleValue = this.#inner.get(key);
171
+ if (mapOrSingleValue === void 0) return [];
172
+ if (isSingleValue(mapOrSingleValue)) return [mapOrSingleValue];
173
+ if (mapOrSingleValue instanceof ValueMap)
174
+ return [...mapOrSingleValue.values()];
175
+ const result = [];
176
+ for (const singleValueOrValueMap of mapOrSingleValue.values()) {
177
+ if (isSingleValue(singleValueOrValueMap))
178
+ result.push(singleValueOrValueMap);
179
+ else
180
+ for (const valueTuple of singleValueOrValueMap.values())
181
+ result.push(valueTuple);
182
+ }
183
+ return result;
155
184
  }
156
185
  /**
157
186
  * This method returns an iterator over all values for a given key.
@@ -210,11 +239,14 @@ class Index {
210
239
  addValue(key, valueTuple) {
211
240
  const [value, multiplicity] = valueTuple;
212
241
  if (multiplicity === 0) return;
213
- const newConsolidatedMultiplicity = (this.#consolidatedMultiplicity.get(key) || 0) + multiplicity;
214
- if (newConsolidatedMultiplicity === 0) {
215
- this.#consolidatedMultiplicity.delete(key);
216
- } else {
217
- this.#consolidatedMultiplicity.set(key, newConsolidatedMultiplicity);
242
+ const presence = this.#consolidatedMultiplicity;
243
+ if (presence) {
244
+ const newConsolidatedMultiplicity = (presence.get(key) || 0) + multiplicity;
245
+ if (newConsolidatedMultiplicity === 0) {
246
+ presence.delete(key);
247
+ } else {
248
+ presence.set(key, newConsolidatedMultiplicity);
249
+ }
218
250
  }
219
251
  const mapOrSingleValue = this.#inner.get(key);
220
252
  if (mapOrSingleValue === void 0) {
@@ -244,7 +276,11 @@ class Index {
244
276
  }
245
277
  }
246
278
  } else {
247
- const isEmpty = mapOrSingleValue.addValue(value, multiplicity);
279
+ const isEmpty = mapOrSingleValue.addValue(
280
+ value,
281
+ multiplicity,
282
+ this.#prefixIdentity
283
+ );
248
284
  if (isEmpty) {
249
285
  this.#inner.delete(key);
250
286
  }
@@ -266,7 +302,7 @@ class Index {
266
302
  }
267
303
  const newPrefix = getPrefix(newValue);
268
304
  const currentPrefix = getPrefix(currentValue);
269
- if (isSamePrefix(currentPrefix, newPrefix) && (currentValue === newValue || hash(currentValue) === hash(newValue))) {
305
+ if (isSamePrefix(currentPrefix, newPrefix) && (this.#prefixIdentity && newPrefix !== NO_PREFIX || hash(currentValue) === hash(newValue))) {
270
306
  const newMultiplicity = currentMultiplicity + multiplicity;
271
307
  if (newMultiplicity === 0) {
272
308
  this.#inner.delete(key);