@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
package/README.md CHANGED
@@ -11,7 +11,7 @@ The API is almost identical to D2TS, but without the need to specify a version w
11
11
  Here's a simple example that demonstrates the core concepts:
12
12
 
13
13
  ```typescript
14
- import { D2, map, filter, debug, MultiSet } from '@tanstack/db-ivm'
14
+ import { D2, map, filter, output, MultiSet } from '@tanstack/db-ivm'
15
15
 
16
16
  // Create a new D2 graph
17
17
  const graph = new D2()
@@ -25,10 +25,10 @@ const input = graph.newInput<number>()
25
25
  // 2. Adds 5 to each number
26
26
  // 3. Filters to keep only even numbers
27
27
  // Pipelines can have multiple inputs and outputs.
28
- const output = input.pipe(
28
+ input.pipe(
29
29
  map((x) => x + 5),
30
30
  filter((x) => x % 2 === 0),
31
- debug('output'),
31
+ output((message) => console.log(message.getInner())),
32
32
  )
33
33
 
34
34
  // Finalize the pipeline, after this point we can no longer add operators or
@@ -52,7 +52,6 @@ input.sendData(
52
52
  // Process the data
53
53
  graph.run()
54
54
 
55
- // Output will show:
56
- // 6 (from 1 + 5)
57
- // 8 (from 3 + 5)
55
+ // Output will show [[6, 1], [8, 1]]: 6 (from 1 + 5) and 8 (from 3 + 5),
56
+ // each with a multiplicity of 1
58
57
  ```
@@ -45,7 +45,9 @@ function hashObject(input, context) {
45
45
  if (body) updateHasher(hasher, value, context);
46
46
  else hasher.update(value);
47
47
  if (body) {
48
- const keys = Object.keys(body).sort(keySort);
48
+ const keys = Object.keys(body);
49
+ const denseArray = Array.isArray(body) && keys.length === body.length && (keys.length === 0 || keys[keys.length - 1] === String(body.length - 1));
50
+ if (!denseArray) keys.sort();
49
51
  keys.push(
50
52
  ...Object.getOwnPropertySymbols(body).filter(
51
53
  (key) => Object.prototype.propertyIsEnumerable.call(body, key)
@@ -179,10 +181,63 @@ function cachedReferenceHash(fn) {
179
181
  }
180
182
  return valueHash;
181
183
  }
182
- function keySort(a, b) {
183
- return a.localeCompare(b);
184
+ const MARKER_TAGS = /* @__PURE__ */ new Map([
185
+ [ARRAY_MARKER, `[`],
186
+ [OBJECT_MARKER, `{`],
187
+ [DATE_MARKER, `<`],
188
+ [REGEXP_MARKER, `/`],
189
+ [MAP_MARKER, `&`],
190
+ [SET_MARKER, `*`],
191
+ [UINT8ARRAY_MARKER, `^`],
192
+ [TEMPORAL_MARKER, `?`]
193
+ ]);
194
+ const MAX_IDENTITY_DEPTH = 4;
195
+ function structuralIdentity(value, depth = 0) {
196
+ switch (typeof value) {
197
+ case `number`:
198
+ return `#${value === 0 ? 0 : value}`;
199
+ case `string`:
200
+ return `"${value.length}:${value}`;
201
+ case `boolean`:
202
+ return value ? `+` : `-`;
203
+ case `undefined`:
204
+ return `~`;
205
+ case `bigint`:
206
+ return `%${value}`;
207
+ case `symbol`:
208
+ return `$${murmur.getSymbolIdentity(value)}`;
209
+ case `object`: {
210
+ if (value === null) return `!`;
211
+ if (depth >= MAX_IDENTITY_DEPTH || isReferenceHashedObject(value))
212
+ return void 0;
213
+ const [marker, header, body] = objectParts(value);
214
+ let head = MARKER_TAGS.get(marker);
215
+ for (const part of header) {
216
+ if (Number.isNaN(part)) return void 0;
217
+ head += structuralIdentity(part, depth + 1);
218
+ }
219
+ if (!body) return head;
220
+ if (Object.getOwnPropertySymbols(body).some(
221
+ (key) => Object.prototype.propertyIsEnumerable.call(body, key)
222
+ ))
223
+ return void 0;
224
+ let parts = ``;
225
+ for (const key of Object.keys(body).sort()) {
226
+ const part = structuralIdentity(
227
+ body[key],
228
+ depth + 1
229
+ );
230
+ if (part === void 0) return void 0;
231
+ parts += `${key.length}:${key}=${part}|`;
232
+ }
233
+ return `${head}:${parts.length}:${parts}`;
234
+ }
235
+ default:
236
+ return void 0;
237
+ }
184
238
  }
185
239
  exports.equalHashValues = equalHashValues;
186
240
  exports.hash = hash;
187
241
  exports.registerOpaqueHash = registerOpaqueHash;
242
+ exports.structuralIdentity = structuralIdentity;
188
243
  //# sourceMappingURL=hash.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"hash.cjs","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":["randomHash","MurmurHashStream","getSymbolIdentity","isTemporal"],"mappings":"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"}
1
+ {"version":3,"file":"hash.cjs","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":["randomHash","MurmurHashStream","getSymbolIdentity","isTemporal"],"mappings":"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"}
@@ -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;
@@ -38,9 +38,7 @@ function createSymbolIdStore() {
38
38
  function randomHash() {
39
39
  return Math.random() * (2 ** 31 - 1) >>> 0;
40
40
  }
41
- const buf = new ArrayBuffer(8);
42
- const dv = new DataView(buf);
43
- const u8 = new Uint8Array(buf);
41
+ const dv = new DataView(new ArrayBuffer(8));
44
42
  class MurmurHashStream {
45
43
  constructor() {
46
44
  this.hash = RANDOM_SEED;
@@ -66,6 +64,17 @@ class MurmurHashStream {
66
64
  this.carryBytes = 0;
67
65
  }
68
66
  }
67
+ /** Write four bytes, low byte first, as four `writeByte` calls would. */
68
+ writeWord(word) {
69
+ this.length += 4;
70
+ if (this.carryBytes === 0) {
71
+ this._mix(word >>> 0);
72
+ return;
73
+ }
74
+ const shift = 8 * this.carryBytes;
75
+ this._mix((this.carry | word << shift) >>> 0);
76
+ this.carry = word >>> 32 - shift;
77
+ }
69
78
  update(chunk) {
70
79
  switch (typeof chunk) {
71
80
  case `symbol`: {
@@ -75,22 +84,24 @@ class MurmurHashStream {
75
84
  }
76
85
  case `string`:
77
86
  this.update(STRING_MARKER);
78
- for (let i = 0; i < chunk.length; i++) {
79
- const code = chunk.charCodeAt(i);
80
- this.writeByte(code & 255);
81
- this.writeByte(code >>> 8 & 255);
87
+ {
88
+ const pairs = chunk.length - chunk.length % 2;
89
+ for (let i = 0; i < pairs; i += 2) {
90
+ this.writeWord(
91
+ chunk.charCodeAt(i) | chunk.charCodeAt(i + 1) << 16
92
+ );
93
+ }
94
+ if (pairs < chunk.length) {
95
+ const code = chunk.charCodeAt(pairs);
96
+ this.writeByte(code & 255);
97
+ this.writeByte(code >>> 8 & 255);
98
+ }
82
99
  }
83
100
  return;
84
101
  case `number`:
85
102
  dv.setFloat64(0, chunk, true);
86
- this.writeByte(u8[0]);
87
- this.writeByte(u8[1]);
88
- this.writeByte(u8[2]);
89
- this.writeByte(u8[3]);
90
- this.writeByte(u8[4]);
91
- this.writeByte(u8[5]);
92
- this.writeByte(u8[6]);
93
- this.writeByte(u8[7]);
103
+ this.writeWord(dv.getUint32(0, true));
104
+ this.writeWord(dv.getUint32(4, true));
94
105
  return;
95
106
  case `bigint`: {
96
107
  let value = chunk;
@@ -1 +1 @@
1
- {"version":3,"file":"murmur.cjs","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.cjs","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":";;AAKA,MAAM,cAAc,WAAA;AACpB,MAAM,gBAAgB,WAAA;AACtB,MAAM,iBAAiB,WAAA;AACvB,MAAM,qBAAqB,WAAA;AAC3B,MAAM,gBAAgB,WAAA;AAOtB,MAAM,YAAY,oBAAA;AAClB,MAAM,0CAA0B,IAAA;AAChC,IAAI,eAAe;AAEZ,SAAS,kBAAkB,QAAwB;AACxD,QAAM,gBAAgB,OAAO,OAAO,MAAM;AAC1C,MAAI,kBAAkB,QAAW;AAC/B,QAAIA,MAAK,oBAAoB,IAAI,aAAa;AAC9C,QAAIA,QAAO,QAAW;AACpBA,YAAK,EAAE;AACP,0BAAoB,IAAI,eAAeA,GAAE;AAAA,IAC3C;AACA,WAAOA;AAAAA,EACT;AAEA,MAAI,KAAK,UAAU,IAAI,MAAM;AAC7B,MAAI,OAAO,QAAW;AACpB,SAAK,EAAE;AACP,cAAU,IAAI,QAAQ,EAAE;AAAA,EAC1B;AACA,SAAO;AACT;AAEA,SAAS,sBAAqC;AAC5C,QAAM,8BAAc,QAAA;AACpB,QAAM,QAAQ,uBAAA;AAEd,MAAI;AACF,YAAQ,IAAI,OAAO,CAAC;AACpB,QAAI,QAAQ,IAAI,KAAK,MAAM,EAAG,QAAO;AAAA,EACvC,QAAQ;AAAA,EAGR;AAEA,6BAAW,IAAA;AACb;AAIO,SAAS,aAAa;AAC3B,SAAQ,KAAK,OAAA,KAAY,KAAK,KAAK,OAAQ;AAC7C;AAQA,MAAM,KAAK,IAAI,SAAS,IAAI,YAAY,CAAC,CAAC;AAYnC,MAAM,iBAAmC;AAAA,EAAzC,cAAA;AACL,SAAQ,OAAe;AACvB,SAAQ,SAAS;AACjB,SAAQ,QAAQ;AAChB,SAAQ,aAAa;AAAA,EAAA;AAAA,EAEb,KAAK,IAAkB;AAC7B,SAAK,KAAK,KAAK,IAAI,UAAU;AAC7B,SAAM,MAAM,KAAO,OAAO;AAC1B,SAAK,KAAK,KAAK,IAAI,SAAU;AAC7B,SAAK,QAAQ;AACb,SAAK,OAAQ,KAAK,QAAQ,KAAO,KAAK,SAAS;AAC/C,SAAK,OAAO,KAAK,KAAK,KAAK,MAAM,CAAC,IAAI;AAAA,EACxC;AAAA,EAEA,UAAU,MAAoB;AAC5B,SAAK,UAAU,OAAO,QAAU,IAAI,KAAK;AACzC,SAAK;AACL,SAAK;AAEL,QAAI,KAAK,eAAe,GAAG;AACzB,WAAK,KAAK,KAAK,UAAU,CAAC;AAC1B,WAAK,QAAQ;AACb,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA,EAGQ,UAAU,MAAoB;AACpC,SAAK,UAAU;AACf,QAAI,KAAK,eAAe,GAAG;AACzB,WAAK,KAAK,SAAS,CAAC;AACpB;AAAA,IACF;AAGA,UAAM,QAAQ,IAAI,KAAK;AACvB,SAAK,MAAM,KAAK,QAAS,QAAQ,WAAY,CAAC;AAC9C,SAAK,QAAQ,SAAU,KAAK;AAAA,EAC9B;AAAA,EAEA,OAAO,OAAgD;AACrD,YAAQ,OAAO,OAAA;AAAA,MACb,KAAK,UAAU;AACb,aAAK,OAAO,aAAa;AACzB,aAAK,OAAO,kBAAkB,KAAK,CAAC;AACpC;AAAA,MACF;AAAA,MACA,KAAK;AACH,aAAK,OAAO,aAAa;AACzB;AAEE,gBAAM,QAAQ,MAAM,SAAU,MAAM,SAAS;AAC7C,mBAAS,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;AACjC,iBAAK;AAAA,cACH,MAAM,WAAW,CAAC,IAAK,MAAM,WAAW,IAAI,CAAC,KAAK;AAAA,YAAA;AAAA,UAEtD;AACA,cAAI,QAAQ,MAAM,QAAQ;AACxB,kBAAM,OAAO,MAAM,WAAW,KAAK;AACnC,iBAAK,UAAU,OAAO,GAAI;AAC1B,iBAAK,UAAW,SAAS,IAAK,GAAI;AAAA,UACpC;AAAA,QACF;AACA;AAAA,MACF,KAAK;AACH,WAAG,WAAW,GAAG,OAAO,IAAI;AAC5B,aAAK,UAAU,GAAG,UAAU,GAAG,IAAI,CAAC;AACpC,aAAK,UAAU,GAAG,UAAU,GAAG,IAAI,CAAC;AACpC;AAAA,MACF,KAAK,UAAU;AACb,YAAI,QAAQ;AACZ,YAAI,QAAQ,IAAI;AACd,kBAAQ,CAAC;AACT,eAAK,OAAO,kBAAkB;AAAA,QAChC,OAAO;AACL,eAAK,OAAO,cAAc;AAAA,QAC5B;AACA,eAAO,QAAQ,IAAI;AACjB,eAAK,UAAU,OAAO,QAAQ,KAAK,CAAC;AACpC,oBAAU;AAAA,QACZ;AACA,YAAI,UAAU,GAAI,MAAK,UAAU,CAAC;AAClC;AAAA,MACF;AAAA,MACA;AACE,cAAM,IAAI,UAAU,2BAA2B,OAAO,KAAK,EAAE;AAAA,IAAA;AAAA,EAEnE;AAAA,EAEA,SAAiB;AACf,QAAI,KAAK,aAAa,GAAG;AACvB,UAAI,KAAK,KAAK,UAAU;AACxB,WAAK,KAAK,KAAK,IAAI,UAAU;AAC7B,WAAM,MAAM,KAAO,OAAO;AAC1B,WAAK,KAAK,KAAK,IAAI,SAAU;AAC7B,WAAK,QAAQ;AAAA,IACf;AAEA,SAAK,QAAQ,KAAK;AAClB,SAAK,QAAQ,KAAK,SAAS;AAC3B,SAAK,OAAO,KAAK,KAAK,KAAK,MAAM,UAAU;AAC3C,SAAK,QAAQ,KAAK,SAAS;AAC3B,SAAK,OAAO,KAAK,KAAK,KAAK,MAAM,UAAU;AAC3C,SAAK,QAAQ,KAAK,SAAS;AAE3B,WAAO,KAAK,SAAS;AAAA,EACvB;AACF;;;;"}
@@ -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
  }
@@ -6,25 +6,19 @@ const utils = require("./utils.cjs");
6
6
  const hash = require("./hashing/hash.cjs");
7
7
  const concat = require("./operators/concat.cjs");
8
8
  const consolidate = require("./operators/consolidate.cjs");
9
- const count = require("./operators/count.cjs");
10
- const debug = require("./operators/debug.cjs");
11
9
  const distinct = require("./operators/distinct.cjs");
12
10
  const filter = require("./operators/filter.cjs");
13
11
  const groupedTopKWithFractionalIndex = require("./operators/groupedTopKWithFractionalIndex.cjs");
14
12
  const join = require("./operators/join.cjs");
15
13
  const map = require("./operators/map.cjs");
16
- const negate = require("./operators/negate.cjs");
17
14
  const output = require("./operators/output.cjs");
18
15
  const reduce = require("./operators/reduce.cjs");
19
16
  const tap = require("./operators/tap.cjs");
20
17
  const topKWithFractionalIndex = require("./operators/topKWithFractionalIndex.cjs");
21
- const filterBy = require("./operators/filterBy.cjs");
22
18
  const groupBy = require("./operators/groupBy.cjs");
23
19
  const groupedOrderBy = require("./operators/groupedOrderBy.cjs");
24
- const keying = require("./operators/keying.cjs");
25
20
  const orderBy = require("./operators/orderBy.cjs");
26
21
  const pipe = require("./operators/pipe.cjs");
27
- const topK = require("./operators/topK.cjs");
28
22
  exports.D2 = d2.D2;
29
23
  exports.RootStreamBuilder = d2.RootStreamBuilder;
30
24
  exports.StreamBuilder = d2.StreamBuilder;
@@ -36,10 +30,6 @@ exports.ConcatOperator = concat.ConcatOperator;
36
30
  exports.concat = concat.concat;
37
31
  exports.ConsolidateOperator = consolidate.ConsolidateOperator;
38
32
  exports.consolidate = consolidate.consolidate;
39
- exports.CountOperator = count.CountOperator;
40
- exports.count = count.count;
41
- exports.DebugOperator = debug.DebugOperator;
42
- exports.debug = debug.debug;
43
33
  exports.DistinctOperator = distinct.DistinctOperator;
44
34
  exports.distinct = distinct.distinct;
45
35
  exports.FilterOperator = filter.FilterOperator;
@@ -47,16 +37,9 @@ exports.filter = filter.filter;
47
37
  exports.GroupedTopKWithFractionalIndexOperator = groupedTopKWithFractionalIndex.GroupedTopKWithFractionalIndexOperator;
48
38
  exports.groupedTopKWithFractionalIndex = groupedTopKWithFractionalIndex.groupedTopKWithFractionalIndex;
49
39
  exports.JoinOperator = join.JoinOperator;
50
- exports.antiJoin = join.antiJoin;
51
- exports.fullJoin = join.fullJoin;
52
- exports.innerJoin = join.innerJoin;
53
40
  exports.join = join.join;
54
- exports.leftJoin = join.leftJoin;
55
- exports.rightJoin = join.rightJoin;
56
41
  exports.MapOperator = map.MapOperator;
57
42
  exports.map = map.map;
58
- exports.NegateOperator = negate.NegateOperator;
59
- exports.negate = negate.negate;
60
43
  exports.OutputOperator = output.OutputOperator;
61
44
  exports.output = output.output;
62
45
  exports.ReduceOperator = reduce.ReduceOperator;
@@ -65,18 +48,9 @@ exports.TapOperator = tap.TapOperator;
65
48
  exports.tap = tap.tap;
66
49
  exports.TopKWithFractionalIndexOperator = topKWithFractionalIndex.TopKWithFractionalIndexOperator;
67
50
  exports.topKWithFractionalIndex = topKWithFractionalIndex.topKWithFractionalIndex;
68
- exports.filterBy = filterBy.filterBy;
69
51
  exports.groupBy = groupBy.groupBy;
70
52
  exports.groupByOperators = groupBy.groupByOperators;
71
53
  exports.groupedOrderByWithFractionalIndex = groupedOrderBy.groupedOrderByWithFractionalIndex;
72
- exports.keyBy = keying.keyBy;
73
- exports.rekey = keying.rekey;
74
- exports.unkey = keying.unkey;
75
- exports.orderBy = orderBy.orderBy;
76
54
  exports.orderByWithFractionalIndex = orderBy.orderByWithFractionalIndex;
77
- exports.orderByWithFractionalIndexBase = orderBy.orderByWithFractionalIndexBase;
78
- exports.orderByWithIndex = orderBy.orderByWithIndex;
79
55
  exports.pipe = pipe.pipe;
80
- exports.topK = topK.topK;
81
- exports.topKWithIndex = topK.topKWithIndex;
82
56
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -6,8 +6,9 @@ const NO_PREFIX = /* @__PURE__ */ Symbol(`NO_PREFIX`);
6
6
  class PrefixMap extends Map {
7
7
  /**
8
8
  * Add a value to the PrefixMap. Returns true if the map becomes empty after the operation.
9
+ * With `prefixIdentity`, values that share a prefix are the same value.
9
10
  */
10
- addValue(value, multiplicity) {
11
+ addValue(value, multiplicity, prefixIdentity = false) {
11
12
  if (multiplicity === 0) return this.size === 0;
12
13
  const prefix = getPrefix(value);
13
14
  const valueMapOrSingleValue = this.get(prefix);
@@ -17,7 +18,7 @@ class PrefixMap extends Map {
17
18
  if (!isSamePrefix(currentPrefix, prefix)) {
18
19
  throw new Error(`Mismatching prefixes, this should never happen`);
19
20
  }
20
- if (currentValue === value || hash.hash(currentValue) === hash.hash(value)) {
21
+ if (currentValue === value || prefixIdentity && prefix !== NO_PREFIX || hash.hash(currentValue) === hash.hash(value)) {
21
22
  const newMultiplicity = currentMultiplicity + multiplicity;
22
23
  if (newMultiplicity === 0) {
23
24
  this.delete(prefix);
@@ -78,10 +79,20 @@ class Index {
78
79
  * hash to identify identical values, storing them in a third level value map.
79
80
  */
80
81
  #inner;
81
- #consolidatedMultiplicity = /* @__PURE__ */ new Map();
82
- // sum of multiplicities per key
83
- constructor() {
82
+ // Sum of multiplicities per key, for presence. Only joins read presence; a
83
+ // reduce index is built without it.
84
+ #consolidatedMultiplicity;
85
+ /**
86
+ * When true, the producer guarantees that a value's string or number
87
+ * prefix determines the value: two values under one key with an equal
88
+ * prefix are equal, so they merge without a structural hash.
89
+ */
90
+ #prefixIdentity;
91
+ constructor(options) {
84
92
  this.#inner = /* @__PURE__ */ new Map();
93
+ if (options?.trackPresence ?? true)
94
+ this.#consolidatedMultiplicity = /* @__PURE__ */ new Map();
95
+ this.#prefixIdentity = options?.prefixIdentity ?? false;
85
96
  }
86
97
  /**
87
98
  * Create an Index from multiple MultiSet messages.
@@ -116,6 +127,11 @@ class Index {
116
127
  get size() {
117
128
  return this.#inner.size;
118
129
  }
130
+ #presence() {
131
+ if (this.#consolidatedMultiplicity === void 0)
132
+ throw new Error(`This Index does not track presence`);
133
+ return this.#consolidatedMultiplicity;
134
+ }
119
135
  /**
120
136
  * This method checks if the index has a given key.
121
137
  * @param key - The key to check.
@@ -130,7 +146,7 @@ class Index {
130
146
  * @returns True if the key has non-zero consolidated multiplicity, false otherwise.
131
147
  */
132
148
  hasPresence(key) {
133
- return (this.#consolidatedMultiplicity.get(key) || 0) !== 0;
149
+ return (this.#presence().get(key) || 0) !== 0;
134
150
  }
135
151
  /**
136
152
  * Get the consolidated multiplicity (sum of multiplicities) for a key.
@@ -138,14 +154,14 @@ class Index {
138
154
  * @returns The consolidated multiplicity for the key.
139
155
  */
140
156
  getConsolidatedMultiplicity(key) {
141
- return this.#consolidatedMultiplicity.get(key) || 0;
157
+ return this.#presence().get(key) || 0;
142
158
  }
143
159
  /**
144
160
  * Get all keys that have presence (non-zero consolidated multiplicity).
145
161
  * @returns An iterator of keys with non-zero consolidated multiplicity.
146
162
  */
147
163
  getPresenceKeys() {
148
- return this.#consolidatedMultiplicity.keys();
164
+ return this.#presence().keys();
149
165
  }
150
166
  /**
151
167
  * This method returns all values for a given key.
@@ -153,7 +169,20 @@ class Index {
153
169
  * @returns An array of value tuples [value, multiplicity].
154
170
  */
155
171
  get(key) {
156
- return [...this.getIterator(key)];
172
+ const mapOrSingleValue = this.#inner.get(key);
173
+ if (mapOrSingleValue === void 0) return [];
174
+ if (isSingleValue(mapOrSingleValue)) return [mapOrSingleValue];
175
+ if (mapOrSingleValue instanceof ValueMap)
176
+ return [...mapOrSingleValue.values()];
177
+ const result = [];
178
+ for (const singleValueOrValueMap of mapOrSingleValue.values()) {
179
+ if (isSingleValue(singleValueOrValueMap))
180
+ result.push(singleValueOrValueMap);
181
+ else
182
+ for (const valueTuple of singleValueOrValueMap.values())
183
+ result.push(valueTuple);
184
+ }
185
+ return result;
157
186
  }
158
187
  /**
159
188
  * This method returns an iterator over all values for a given key.
@@ -212,11 +241,14 @@ class Index {
212
241
  addValue(key, valueTuple) {
213
242
  const [value, multiplicity] = valueTuple;
214
243
  if (multiplicity === 0) return;
215
- const newConsolidatedMultiplicity = (this.#consolidatedMultiplicity.get(key) || 0) + multiplicity;
216
- if (newConsolidatedMultiplicity === 0) {
217
- this.#consolidatedMultiplicity.delete(key);
218
- } else {
219
- this.#consolidatedMultiplicity.set(key, newConsolidatedMultiplicity);
244
+ const presence = this.#consolidatedMultiplicity;
245
+ if (presence) {
246
+ const newConsolidatedMultiplicity = (presence.get(key) || 0) + multiplicity;
247
+ if (newConsolidatedMultiplicity === 0) {
248
+ presence.delete(key);
249
+ } else {
250
+ presence.set(key, newConsolidatedMultiplicity);
251
+ }
220
252
  }
221
253
  const mapOrSingleValue = this.#inner.get(key);
222
254
  if (mapOrSingleValue === void 0) {
@@ -246,7 +278,11 @@ class Index {
246
278
  }
247
279
  }
248
280
  } else {
249
- const isEmpty = mapOrSingleValue.addValue(value, multiplicity);
281
+ const isEmpty = mapOrSingleValue.addValue(
282
+ value,
283
+ multiplicity,
284
+ this.#prefixIdentity
285
+ );
250
286
  if (isEmpty) {
251
287
  this.#inner.delete(key);
252
288
  }
@@ -268,7 +304,7 @@ class Index {
268
304
  }
269
305
  const newPrefix = getPrefix(newValue);
270
306
  const currentPrefix = getPrefix(currentValue);
271
- if (isSamePrefix(currentPrefix, newPrefix) && (currentValue === newValue || hash.hash(currentValue) === hash.hash(newValue))) {
307
+ if (isSamePrefix(currentPrefix, newPrefix) && (this.#prefixIdentity && newPrefix !== NO_PREFIX || hash.hash(currentValue) === hash.hash(newValue))) {
272
308
  const newMultiplicity = currentMultiplicity + multiplicity;
273
309
  if (newMultiplicity === 0) {
274
310
  this.#inner.delete(key);