@tanstack/db-ivm 0.1.6 → 0.1.8

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 (104) hide show
  1. package/dist/cjs/d2.cjs +1 -8
  2. package/dist/cjs/d2.cjs.map +1 -1
  3. package/dist/cjs/d2.d.cts +0 -1
  4. package/dist/cjs/indexes.cjs +243 -85
  5. package/dist/cjs/indexes.cjs.map +1 -1
  6. package/dist/cjs/indexes.d.cts +58 -7
  7. package/dist/cjs/operators/concat.cjs +0 -1
  8. package/dist/cjs/operators/concat.cjs.map +1 -1
  9. package/dist/cjs/operators/consolidate.cjs +0 -1
  10. package/dist/cjs/operators/consolidate.cjs.map +1 -1
  11. package/dist/cjs/operators/count.cjs +0 -1
  12. package/dist/cjs/operators/count.cjs.map +1 -1
  13. package/dist/cjs/operators/debug.cjs +0 -1
  14. package/dist/cjs/operators/debug.cjs.map +1 -1
  15. package/dist/cjs/operators/distinct.cjs +2 -3
  16. package/dist/cjs/operators/distinct.cjs.map +1 -1
  17. package/dist/cjs/operators/distinct.d.cts +6 -4
  18. package/dist/cjs/operators/filter.cjs +0 -1
  19. package/dist/cjs/operators/filter.cjs.map +1 -1
  20. package/dist/cjs/operators/join.cjs +0 -1
  21. package/dist/cjs/operators/join.cjs.map +1 -1
  22. package/dist/cjs/operators/map.cjs +0 -1
  23. package/dist/cjs/operators/map.cjs.map +1 -1
  24. package/dist/cjs/operators/negate.cjs +0 -1
  25. package/dist/cjs/operators/negate.cjs.map +1 -1
  26. package/dist/cjs/operators/output.cjs +0 -1
  27. package/dist/cjs/operators/output.cjs.map +1 -1
  28. package/dist/cjs/operators/reduce.cjs +0 -1
  29. package/dist/cjs/operators/reduce.cjs.map +1 -1
  30. package/dist/cjs/operators/tap.cjs +0 -1
  31. package/dist/cjs/operators/tap.cjs.map +1 -1
  32. package/dist/cjs/operators/topKWithFractionalIndex.cjs +0 -1
  33. package/dist/cjs/operators/topKWithFractionalIndex.cjs.map +1 -1
  34. package/dist/cjs/types.d.cts +0 -1
  35. package/dist/cjs/utils.cjs +0 -12
  36. package/dist/cjs/utils.cjs.map +1 -1
  37. package/dist/esm/d2.d.ts +0 -1
  38. package/dist/esm/d2.js +1 -8
  39. package/dist/esm/d2.js.map +1 -1
  40. package/dist/esm/indexes.d.ts +58 -7
  41. package/dist/esm/indexes.js +243 -85
  42. package/dist/esm/indexes.js.map +1 -1
  43. package/dist/esm/operators/concat.js +0 -1
  44. package/dist/esm/operators/concat.js.map +1 -1
  45. package/dist/esm/operators/consolidate.js +0 -1
  46. package/dist/esm/operators/consolidate.js.map +1 -1
  47. package/dist/esm/operators/count.js +0 -1
  48. package/dist/esm/operators/count.js.map +1 -1
  49. package/dist/esm/operators/debug.js +0 -1
  50. package/dist/esm/operators/debug.js.map +1 -1
  51. package/dist/esm/operators/distinct.d.ts +6 -4
  52. package/dist/esm/operators/distinct.js +2 -3
  53. package/dist/esm/operators/distinct.js.map +1 -1
  54. package/dist/esm/operators/filter.js +0 -1
  55. package/dist/esm/operators/filter.js.map +1 -1
  56. package/dist/esm/operators/join.js +0 -1
  57. package/dist/esm/operators/join.js.map +1 -1
  58. package/dist/esm/operators/map.js +0 -1
  59. package/dist/esm/operators/map.js.map +1 -1
  60. package/dist/esm/operators/negate.js +0 -1
  61. package/dist/esm/operators/negate.js.map +1 -1
  62. package/dist/esm/operators/output.js +0 -1
  63. package/dist/esm/operators/output.js.map +1 -1
  64. package/dist/esm/operators/reduce.js +0 -1
  65. package/dist/esm/operators/reduce.js.map +1 -1
  66. package/dist/esm/operators/tap.js +0 -1
  67. package/dist/esm/operators/tap.js.map +1 -1
  68. package/dist/esm/operators/topKWithFractionalIndex.js +0 -1
  69. package/dist/esm/operators/topKWithFractionalIndex.js.map +1 -1
  70. package/dist/esm/types.d.ts +0 -1
  71. package/dist/esm/utils.js +1 -13
  72. package/dist/esm/utils.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/d2.ts +0 -7
  75. package/src/indexes.ts +365 -90
  76. package/src/operators/concat.ts +0 -1
  77. package/src/operators/consolidate.ts +0 -1
  78. package/src/operators/count.ts +0 -1
  79. package/src/operators/debug.ts +0 -1
  80. package/src/operators/distinct.ts +13 -9
  81. package/src/operators/filter.ts +0 -1
  82. package/src/operators/join.ts +0 -1
  83. package/src/operators/map.ts +0 -1
  84. package/src/operators/negate.ts +0 -1
  85. package/src/operators/output.ts +0 -1
  86. package/src/operators/reduce.ts +0 -1
  87. package/src/operators/tap.ts +0 -1
  88. package/src/operators/topKWithFractionalIndex.ts +0 -1
  89. package/src/operators/topKWithFractionalIndexBTree.ts +0 -1
  90. package/src/types.ts +0 -1
  91. package/dist/cjs/hashIndex.cjs +0 -83
  92. package/dist/cjs/hashIndex.cjs.map +0 -1
  93. package/dist/cjs/hashIndex.d.cts +0 -25
  94. package/dist/cjs/valueIndex.cjs +0 -74
  95. package/dist/cjs/valueIndex.cjs.map +0 -1
  96. package/dist/cjs/valueIndex.d.cts +0 -17
  97. package/dist/esm/hashIndex.d.ts +0 -25
  98. package/dist/esm/hashIndex.js +0 -83
  99. package/dist/esm/hashIndex.js.map +0 -1
  100. package/dist/esm/valueIndex.d.ts +0 -17
  101. package/dist/esm/valueIndex.js +0 -74
  102. package/dist/esm/valueIndex.js.map +0 -1
  103. package/src/hashIndex.ts +0 -94
  104. package/src/valueIndex.ts +0 -78
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n"],"names":[],"mappings":"AAgBO,MAAM,mBAAyB,IAAU;AAAA,EAC9C,YACU,cACR,SACA;AACA,UAAM,OAAO;AAHL,SAAA,eAAA;AAAA,EAIV;AAAA,EAEA,IAAI,KAAW;AACb,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAElB,aAAO,KAAK,aAAA;AAAA,IACd;AACA,WAAO,MAAM,IAAI,GAAG;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,KAAQ,SAA6B;AAC1C,UAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,UAAM,WAAW,QAAQ,KAAK;AAC9B,SAAK,IAAI,KAAK,QAAQ;AACtB,WAAO;AAAA,EACT;AACF;AAMA,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,MAAM,kBAAkB;AAAA,EAAxB,cAAA;AACL,SAAQ,gCAAgB,QAAA;AACxB,SAAQ,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,MAAM,OAAoB;AAExB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,YAAM,MAAM,OAAO,KAAK;AACxB,UAAI,YAAY;AAChB,eAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,cAAM,OAAO,IAAI,WAAW,CAAC;AAC7B,qBAAa,aAAa,KAAK,YAAY;AAC3C,oBAAY,YAAY;AAAA,MAC1B;AACA,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,KAAK,QAAQ;AAAA,IACzC;AACA,WAAO,KAAK,UAAU,IAAI,KAAK;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,OAAoB;AAC9B,QAAI,UAAU,KAAM,QAAO;AAC3B,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,OAAO,UAAU,iBAAiB,OAAO,OAAO,KAAK,CAAC;AAE1D,WAAO,OAAO,KAAK,MAAM,KAAK,CAAC;AAAA,EACjC;AACF;AAKO,MAAM,0BAA0B,IAAI,kBAAA;AAEpC,UAAU,kBACZ,WACU;AACb,aAAW,YAAY,WAAW;AAChC,WAAO;AAAA,EACT;AACF;AAEO,UAAU,YACf,IACA,IACa;AACb,aAAW,KAAK,IAAI;AAClB,UAAM,GAAG,CAAC;AAAA,EACZ;AACF;"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Simple assertion function for runtime checks.\n * Throws an error if the condition is false.\n */\nexport function assert(\n condition: unknown,\n message?: string\n): asserts condition {\n if (!condition) {\n throw new Error(message || `Assertion failed`)\n }\n}\n\n/**\n * A map that returns a default value for keys that are not present.\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n constructor(\n private defaultValue: () => V,\n entries?: Iterable<[K, V]>\n ) {\n super(entries)\n }\n\n get(key: K): V {\n if (!this.has(key)) {\n // this.set(key, this.defaultValue())\n return this.defaultValue()\n }\n return super.get(key)!\n }\n\n /**\n * Update the value for a key using a function.\n */\n update(key: K, updater: (value: V) => V): V {\n const value = this.get(key)\n const newValue = updater(value)\n this.set(key, newValue)\n return newValue\n }\n}\n\n// JS engines have various limits on how many args can be passed to a function\n// with a spread operator, so we need to split the operation into chunks\n// 32767 is the max for Chrome 14, all others are higher\n// TODO: investigate the performance of this and other approaches\nconst chunkSize = 30000\nexport function chunkedArrayPush(array: Array<unknown>, other: Array<unknown>) {\n if (other.length <= chunkSize) {\n array.push(...other)\n } else {\n for (let i = 0; i < other.length; i += chunkSize) {\n const chunk = other.slice(i, i + chunkSize)\n array.push(...chunk)\n }\n }\n}\n\nexport function binarySearch<T>(\n array: Array<T>,\n value: T,\n comparator: (a: T, b: T) => number\n): number {\n let low = 0\n let high = array.length\n while (low < high) {\n const mid = Math.floor((low + high) / 2)\n const comparison = comparator(array[mid]!, value)\n if (comparison < 0) {\n low = mid + 1\n } else if (comparison > 0) {\n high = mid\n } else {\n return mid\n }\n }\n return low\n}\n\n/**\n * Utility for generating unique IDs for objects and values.\n * Uses WeakMap for object reference tracking and consistent hashing for primitives.\n */\nexport class ObjectIdGenerator {\n private objectIds = new WeakMap<object, number>()\n private nextId = 0\n\n /**\n * Get a unique identifier for any value.\n * - Objects: Uses WeakMap for reference-based identity\n * - Primitives: Uses consistent string-based hashing\n */\n getId(value: any): number {\n // For primitives, use a simple hash of their string representation\n if (typeof value !== `object` || value === null) {\n const str = String(value)\n let hashValue = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hashValue = (hashValue << 5) - hashValue + char\n hashValue = hashValue & hashValue // Convert to 32-bit integer\n }\n return hashValue\n }\n\n // For objects, use WeakMap to assign unique IDs\n if (!this.objectIds.has(value)) {\n this.objectIds.set(value, this.nextId++)\n }\n return this.objectIds.get(value)!\n }\n\n /**\n * Get a string representation of the ID for use in composite keys.\n */\n getStringId(value: any): string {\n if (value === null) return `null`\n if (value === undefined) return `undefined`\n if (typeof value !== `object`) return `str_${String(value)}`\n\n return `obj_${this.getId(value)}`\n }\n}\n\n/**\n * Global instance for cases where a shared object ID space is needed.\n */\nexport const globalObjectIdGenerator = new ObjectIdGenerator()\n\nexport function* concatIterable<T>(\n ...iterables: Array<Iterable<T>>\n): Iterable<T> {\n for (const iterable of iterables) {\n yield* iterable\n }\n}\n\nexport function* mapIterable<T, U>(\n it: Iterable<T>,\n fn: (t: T) => U\n): Iterable<U> {\n for (const t of it) {\n yield fn(t)\n }\n}\n"],"names":[],"mappings":"AAgBO,MAAM,mBAAyB,IAAU;AAAA,EAC9C,YACU,cACR,SACA;AACA,UAAM,OAAO;AAHL,SAAA,eAAA;AAAA,EAIV;AAAA,EAEA,IAAI,KAAW;AACb,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAElB,aAAO,KAAK,aAAA;AAAA,IACd;AACA,WAAO,MAAM,IAAI,GAAG;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,KAAQ,SAA6B;AAC1C,UAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,UAAM,WAAW,QAAQ,KAAK;AAC9B,SAAK,IAAI,KAAK,QAAQ;AACtB,WAAO;AAAA,EACT;AACF;AAMA,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,MAAM,kBAAkB;AAAA,EAAxB,cAAA;AACL,SAAQ,gCAAgB,QAAA;AACxB,SAAQ,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,MAAM,OAAoB;AAExB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,YAAM,MAAM,OAAO,KAAK;AACxB,UAAI,YAAY;AAChB,eAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,cAAM,OAAO,IAAI,WAAW,CAAC;AAC7B,qBAAa,aAAa,KAAK,YAAY;AAC3C,oBAAY,YAAY;AAAA,MAC1B;AACA,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,KAAK,QAAQ;AAAA,IACzC;AACA,WAAO,KAAK,UAAU,IAAI,KAAK;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,OAAoB;AAC9B,QAAI,UAAU,KAAM,QAAO;AAC3B,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,OAAO,UAAU,iBAAiB,OAAO,OAAO,KAAK,CAAC;AAE1D,WAAO,OAAO,KAAK,MAAM,KAAK,CAAC;AAAA,EACjC;AACF;AAKO,MAAM,0BAA0B,IAAI,kBAAA;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/db-ivm",
3
3
  "description": "Incremental View Maintenance for TanStack DB based on Differential Dataflow",
4
- "version": "0.1.6",
4
+ "version": "0.1.8",
5
5
  "dependencies": {
6
6
  "fractional-indexing": "^3.2.0",
7
7
  "sorted-btree": "^1.8.1"
package/src/d2.ts CHANGED
@@ -8,7 +8,6 @@ import type { MultiSet, MultiSetArray } from "./multiset.js"
8
8
  import type { ID2, IStreamBuilder, PipedOperator } from "./types.js"
9
9
 
10
10
  export class D2 implements ID2 {
11
- #streams: Array<DifferenceStreamReader<any>> = []
12
11
  #operators: Array<UnaryOperator<any> | BinaryOperator<any>> = []
13
12
  #nextOperatorId = 0
14
13
  #finalized = false
@@ -31,7 +30,6 @@ export class D2 implements ID2 {
31
30
  const writer = new DifferenceStreamWriter<T>()
32
31
  // Use the root stream builder that exposes the sendData and sendFrontier methods
33
32
  const streamBuilder = new RootStreamBuilder<T>(this, writer)
34
- this.#streams.push(streamBuilder.connectReader())
35
33
  return streamBuilder
36
34
  }
37
35
 
@@ -40,11 +38,6 @@ export class D2 implements ID2 {
40
38
  this.#operators.push(operator)
41
39
  }
42
40
 
43
- addStream(stream: DifferenceStreamReader<any>): void {
44
- this.#checkNotFinalized()
45
- this.#streams.push(stream)
46
- }
47
-
48
41
  finalize() {
49
42
  this.#checkNotFinalized()
50
43
  this.#finalized = true
package/src/indexes.ts CHANGED
@@ -1,60 +1,238 @@
1
+ /**
2
+ * # Optimized Index Data Structure
3
+ *
4
+ * Multi-level index that adapts storage strategy based on data patterns to minimize memory
5
+ * usage, eliminate wasteful lookups, and avoid hashing whenever possible.
6
+ *
7
+ * ## Storage Strategy
8
+ *
9
+ * **Single value**: `IndexMap['key'] → [value, multiplicity]` (no hashing needed)
10
+ *
11
+ * **Multiple unprefixed values**: Direct ValueMap (avoids NO_PREFIX lookup)
12
+ * ```
13
+ * IndexMap['key'] → ValueMap { hash(value1) → [value1, mult1], ... }
14
+ * ```
15
+ *
16
+ * **Values with prefixes**: PrefixMap uses prefix keys directly (no hashing)
17
+ * ```
18
+ * IndexMap['key'] → PrefixMap { 'prefix1' → [value1, mult1], NO_PREFIX → ValueMap{...} }
19
+ * ```
20
+ *
21
+ * **Multiple values per prefix**: ValueMap within PrefixMap (hash only suffixes)
22
+ * ```
23
+ * PrefixMap['prefix'] → ValueMap { hash(suffix1) → [full_value1, mult1], ... }
24
+ * ```
25
+ *
26
+ * ## Dynamic Evolution
27
+ *
28
+ * Structure automatically evolves as data is added:
29
+ * - Single → ValueMap (when both values unprefixed)
30
+ * - Single → PrefixMap (when at least one prefixed)
31
+ * - ValueMap → PrefixMap (adding prefixed value to unprefixed)
32
+ *
33
+ * Prefixes extracted from array values: `['prefix', 'suffix']` → prefix='prefix'
34
+ */
35
+
1
36
  import { MultiSet } from "./multiset.js"
2
- import { HashIndex } from "./hashIndex.js"
3
- import { ValueIndex } from "./valueIndex.js"
4
- import { concatIterable, mapIterable } from "./utils.js"
37
+ import { hash } from "./hashing/index.js"
38
+ import type { Hash } from "./hashing/index.js"
39
+
40
+ // We use a symbol to represent the absence of a prefix, unprefixed values a stored
41
+ // against this key.
42
+ const NO_PREFIX = Symbol(`NO_PREFIX`)
43
+ type NO_PREFIX = typeof NO_PREFIX
44
+
45
+ // A single value is a tuple of the value and the multiplicity.
46
+ type SingleValue<TValue> = [TValue, number]
47
+
48
+ // Base map type for the index. Stores single values, prefix maps, or value maps against a key.
49
+ type IndexMap<TKey, TValue, TPrefix> = Map<
50
+ TKey,
51
+ SingleValue<TValue> | PrefixMap<TValue, TPrefix> | ValueMap<TValue>
52
+ >
53
+
54
+ // Second level map type for the index, stores single values or value maps against a prefix.
55
+ class PrefixMap<TValue, TPrefix> extends Map<
56
+ TPrefix | NO_PREFIX,
57
+ SingleValue<TValue> | ValueMap<TValue>
58
+ > {
59
+ /**
60
+ * Add a value to the PrefixMap. Returns true if the map becomes empty after the operation.
61
+ */
62
+ addValue(value: TValue, multiplicity: number): boolean {
63
+ if (multiplicity === 0) return this.size === 0
64
+
65
+ const prefix = getPrefix<TValue, TPrefix>(value)
66
+ const valueMapOrSingleValue = this.get(prefix)
67
+
68
+ if (isSingleValue(valueMapOrSingleValue)) {
69
+ const [currentValue, currentMultiplicity] = valueMapOrSingleValue
70
+ const currentPrefix = getPrefix<TValue, TPrefix>(currentValue)
71
+
72
+ if (currentPrefix !== prefix) {
73
+ throw new Error(`Mismatching prefixes, this should never happen`)
74
+ }
75
+
76
+ if (currentValue === value || hash(currentValue) === hash(value)) {
77
+ // Same value, update multiplicity
78
+ const newMultiplicity = currentMultiplicity + multiplicity
79
+ if (newMultiplicity === 0) {
80
+ this.delete(prefix)
81
+ } else {
82
+ this.set(prefix, [value, newMultiplicity])
83
+ }
84
+ } else {
85
+ // Different suffixes, need to create ValueMap
86
+ const valueMap = new ValueMap<TValue>()
87
+ valueMap.set(hash(currentValue), valueMapOrSingleValue)
88
+ valueMap.set(hash(value), [value, multiplicity])
89
+ this.set(prefix, valueMap)
90
+ }
91
+ } else if (valueMapOrSingleValue === undefined) {
92
+ // No existing value for this prefix
93
+ this.set(prefix, [value, multiplicity])
94
+ } else {
95
+ // Existing ValueMap
96
+ const isEmpty = valueMapOrSingleValue.addValue(value, multiplicity)
97
+ if (isEmpty) {
98
+ this.delete(prefix)
99
+ }
100
+ }
101
+
102
+ return this.size === 0
103
+ }
104
+ }
105
+
106
+ // Third level map type for the index, stores single values or value maps against a hash.
107
+ class ValueMap<TValue> extends Map<Hash, [TValue, number]> {
108
+ /**
109
+ * Add a value to the ValueMap. Returns true if the map becomes empty after the operation.
110
+ * @param value - The full value to store
111
+ * @param multiplicity - The multiplicity to add
112
+ * @param hashKey - Optional hash key to use instead of hashing the full value (used when in PrefixMap context)
113
+ */
114
+ addValue(value: TValue, multiplicity: number): boolean {
115
+ if (multiplicity === 0) return this.size === 0
116
+
117
+ const key = hash(value)
118
+ const currentValue = this.get(key)
119
+
120
+ if (currentValue) {
121
+ const [, currentMultiplicity] = currentValue
122
+ const newMultiplicity = currentMultiplicity + multiplicity
123
+ if (newMultiplicity === 0) {
124
+ this.delete(key)
125
+ } else {
126
+ this.set(key, [value, newMultiplicity])
127
+ }
128
+ } else {
129
+ this.set(key, [value, multiplicity])
130
+ }
131
+
132
+ return this.size === 0
133
+ }
134
+ }
5
135
 
6
136
  /**
7
137
  * A map from a difference collection trace's keys -> (value, multiplicities) that changed.
8
138
  * Used in operations like join and reduce where the operation needs to
9
139
  * exploit the key-value structure of the data to run efficiently.
10
140
  */
11
- export class Index<K, V> {
141
+ export class Index<TKey, TValue, TPrefix = any> {
12
142
  /*
13
- * This is a hybrid Index that composes a ValueIndex and a HashIndex.
14
- * Keys that have only one value are stored in the ValueIndex.
15
- * Keys that have multiple values are stored in the HashIndex, the hash distinguishes between the values.
16
- * This reduces the amount of hashes we need to compute since often times only a small portion of the keys are updated
17
- * so we don't have to hash the keys that are never updated.
18
- *
19
- * Note: The `valueIndex` and `hashIndex` have disjoint keys.
20
- * When a key that has only one value gets a new distinct value,
21
- * it is added to the `hashIndex` and removed from the `valueIndex` and vice versa.
143
+ * This index maintains a nested map of keys -> (value, multiplicities), where:
144
+ * - initially the values are stored against the key as a single value tuple
145
+ * - when a key gets additional values, the values are stored against the key in a
146
+ * prefix map
147
+ * - the prefix is extract where possible from values that are structured as
148
+ * [rowPrimaryKey, rowValue], as they are in the Tanstack DB query pipeline.
149
+ * - only when there are multiple values for a given prefix do we fall back to a
150
+ * hash to identify identical values, storing them in a third level value map.
22
151
  */
23
- #valueIndex: ValueIndex<K, V>
24
- #hashIndex: HashIndex<K, V>
152
+ #inner: IndexMap<TKey, TValue, TPrefix>
25
153
 
26
154
  constructor() {
27
- this.#valueIndex = new ValueIndex<K, V>()
28
- this.#hashIndex = new HashIndex<K, V>()
155
+ this.#inner = new Map()
29
156
  }
30
157
 
158
+ /**
159
+ * This method returns a string representation of the index.
160
+ * @param indent - Whether to indent the string representation.
161
+ * @returns A string representation of the index.
162
+ */
31
163
  toString(indent = false): string {
32
- return `Index(\n ${this.#valueIndex.toString(indent)},\n ${this.#hashIndex.toString(indent)}\n)`
164
+ return `Index(${JSON.stringify(
165
+ [...this.entries()],
166
+ undefined,
167
+ indent ? 2 : undefined
168
+ )})`
33
169
  }
34
170
 
35
- get(key: K): Array<[V, number]> {
36
- if (this.#valueIndex.has(key)) {
37
- return [this.#valueIndex.get(key)!]
38
- }
39
- return this.#hashIndex.get(key)
171
+ /**
172
+ * The size of the index.
173
+ */
174
+ get size(): number {
175
+ return this.#inner.size
176
+ }
177
+
178
+ /**
179
+ * This method checks if the index has a given key.
180
+ * @param key - The key to check.
181
+ * @returns True if the index has the key, false otherwise.
182
+ */
183
+ has(key: TKey): boolean {
184
+ return this.#inner.has(key)
40
185
  }
41
186
 
42
- getMultiplicity(key: K, value: V): number {
43
- if (this.#valueIndex.has(key)) {
44
- return this.#valueIndex.getMultiplicity(key)
187
+ /**
188
+ * This method returns all values for a given key.
189
+ * @param key - The key to get the values for.
190
+ * @returns An array of value tuples [value, multiplicity].
191
+ */
192
+ get(key: TKey): Array<[TValue, number]> {
193
+ return [...this.getIterator(key)]
194
+ }
195
+
196
+ /**
197
+ * This method returns an iterator over all values for a given key.
198
+ * @param key - The key to get the values for.
199
+ * @returns An iterator of value tuples [value, multiplicity].
200
+ */
201
+ *getIterator(key: TKey): Iterable<[TValue, number]> {
202
+ const mapOrSingleValue = this.#inner.get(key)
203
+ if (isSingleValue(mapOrSingleValue)) {
204
+ yield mapOrSingleValue
205
+ } else if (mapOrSingleValue === undefined) {
206
+ return
207
+ } else if (mapOrSingleValue instanceof ValueMap) {
208
+ // Direct ValueMap - all values have NO_PREFIX
209
+ for (const valueTuple of mapOrSingleValue.values()) {
210
+ yield valueTuple
211
+ }
212
+ } else {
213
+ // PrefixMap - iterate through all prefixes
214
+ for (const singleValueOrValueMap of mapOrSingleValue.values()) {
215
+ if (isSingleValue(singleValueOrValueMap)) {
216
+ yield singleValueOrValueMap
217
+ } else {
218
+ for (const valueTuple of singleValueOrValueMap.values()) {
219
+ yield valueTuple
220
+ }
221
+ }
222
+ }
45
223
  }
46
- return this.#hashIndex.getMultiplicity(key, value)
47
224
  }
48
225
 
49
226
  /**
50
227
  * This returns an iterator that iterates over all key-value pairs.
51
228
  * @returns An iterable of all key-value pairs (and their multiplicities) in the index.
52
229
  */
53
- #entries(): Iterable<[K, [V, number]]> {
54
- return concatIterable(
55
- this.#valueIndex.entries(),
56
- this.#hashIndex.entriesIterator()
57
- )
230
+ *entries(): Iterable<[TKey, [TValue, number]]> {
231
+ for (const key of this.#inner.keys()) {
232
+ for (const valueTuple of this.getIterator(key)) {
233
+ yield [key, valueTuple]
234
+ }
235
+ }
58
236
  }
59
237
 
60
238
  /**
@@ -63,90 +241,156 @@ export class Index<K, V> {
63
241
  * It returns an iterator that you can use if you need to iterate over the values for a given key.
64
242
  * @returns An iterator of all *keys* in the index and their corresponding value iterator.
65
243
  */
66
- *#entriesIterators(): Iterable<[K, Iterable<[V, number]>]> {
67
- for (const [key, [value, multiplicity]] of this.#valueIndex.entries()) {
68
- yield [key, new Map<V, number>([[value, multiplicity]])]
69
- }
70
- for (const [key, valueMap] of this.#hashIndex.entries()) {
71
- yield [
72
- key,
73
- mapIterable(valueMap, ([_hash, [value, multiplicity]]) => [
74
- value,
75
- multiplicity,
76
- ]),
77
- ]
244
+ *entriesIterators(): Iterable<[TKey, Iterable<[TValue, number]>]> {
245
+ for (const key of this.#inner.keys()) {
246
+ yield [key, this.getIterator(key)]
78
247
  }
79
248
  }
80
249
 
81
- has(key: K): boolean {
82
- return this.#valueIndex.has(key) || this.#hashIndex.has(key)
83
- }
250
+ /**
251
+ * This method adds a value to the index.
252
+ * @param key - The key to add the value to.
253
+ * @param valueTuple - The value tuple [value, multiplicity] to add to the index.
254
+ */
255
+ addValue(key: TKey, valueTuple: SingleValue<TValue>) {
256
+ const [value, multiplicity] = valueTuple
257
+ // If the multiplicity is 0, do nothing
258
+ if (multiplicity === 0) return
84
259
 
85
- get size(): number {
86
- return this.#valueIndex.size + this.#hashIndex.size
87
- }
260
+ const mapOrSingleValue = this.#inner.get(key)
88
261
 
89
- addValue(key: K, value: [V, number]): void {
90
- const containedInValueIndex = this.#valueIndex.has(key)
91
- const containedInHashIndex = this.#hashIndex.has(key)
262
+ if (mapOrSingleValue === undefined) {
263
+ // First value for this key
264
+ this.#inner.set(key, valueTuple)
265
+ return
266
+ }
92
267
 
93
- if (containedInHashIndex && containedInValueIndex) {
94
- throw new Error(
95
- `Key ${key} is contained in both the value index and the hash index. This should never happen because they should have disjoint keysets.`
268
+ if (isSingleValue(mapOrSingleValue)) {
269
+ // Handle transition from single value to map
270
+ this.#handleSingleValueTransition(
271
+ key,
272
+ mapOrSingleValue,
273
+ value,
274
+ multiplicity
96
275
  )
276
+ return
97
277
  }
98
278
 
99
- if (!containedInValueIndex && !containedInHashIndex) {
100
- // This is the first time we see the key
101
- // Add it to the value index
102
- this.#valueIndex.addValue(key, value)
103
- return
279
+ if (mapOrSingleValue instanceof ValueMap) {
280
+ // Handle existing ValueMap
281
+ const prefix = getPrefix<TValue, TPrefix>(value)
282
+ if (prefix !== NO_PREFIX) {
283
+ // Convert ValueMap to PrefixMap since we have a prefixed value
284
+ const prefixMap = new PrefixMap<TValue, TPrefix>()
285
+ prefixMap.set(NO_PREFIX, mapOrSingleValue)
286
+ prefixMap.set(prefix, valueTuple)
287
+ this.#inner.set(key, prefixMap)
288
+ } else {
289
+ // Add to existing ValueMap
290
+ const isEmpty = mapOrSingleValue.addValue(value, multiplicity)
291
+ if (isEmpty) {
292
+ this.#inner.delete(key)
293
+ }
294
+ }
295
+ } else {
296
+ // Handle existing PrefixMap
297
+ const isEmpty = mapOrSingleValue.addValue(value, multiplicity)
298
+ if (isEmpty) {
299
+ this.#inner.delete(key)
300
+ }
104
301
  }
302
+ }
105
303
 
106
- if (containedInValueIndex) {
107
- // This key is already in the value index
108
- // It could be that it's the same value or a different one
109
- // If it's a different value we will need to remove the key from the value index
110
- // and add the key and its two values to the hash index
111
- try {
112
- this.#valueIndex.addValue(key, value)
113
- } catch {
114
- // This is a different value, need to move the key to the hash index
115
- const existingValue = this.#valueIndex.get(key)!
116
- this.#valueIndex.delete(key)
117
- this.#hashIndex.addValue(key, existingValue)
118
- this.#hashIndex.addValue(key, value)
304
+ /**
305
+ * Handle the transition from a single value to either a ValueMap or PrefixMap
306
+ */
307
+ #handleSingleValueTransition(
308
+ key: TKey,
309
+ currentSingleValue: SingleValue<TValue>,
310
+ newValue: TValue,
311
+ multiplicity: number
312
+ ) {
313
+ const [currentValue, currentMultiplicity] = currentSingleValue
314
+
315
+ // Check for exact same value (reference equality)
316
+ if (currentValue === newValue) {
317
+ const newMultiplicity = currentMultiplicity + multiplicity
318
+ if (newMultiplicity === 0) {
319
+ this.#inner.delete(key)
320
+ } else {
321
+ this.#inner.set(key, [newValue, newMultiplicity])
119
322
  }
120
323
  return
121
324
  }
122
325
 
123
- if (containedInHashIndex) {
124
- // This key is already in the hash index so it already has two or more values.
125
- // However, this new value and multiplicity could cause an existing value to be removed
126
- // and lead to the key having only a single value in which case we need to move it back to the value index
127
- const singleRemainingValue = this.#hashIndex.addValue(key, value)
128
- if (singleRemainingValue) {
129
- // The key only has a single remaining value so we need to move it back to the value index
130
- this.#hashIndex.delete(key)
131
- this.#valueIndex.addValue(key, singleRemainingValue)
326
+ // Get prefixes for both values
327
+ const newPrefix = getPrefix<TValue, TPrefix>(newValue)
328
+ const currentPrefix = getPrefix<TValue, TPrefix>(currentValue)
329
+
330
+ // Check if they're the same value by prefix/suffix comparison
331
+ if (
332
+ currentPrefix === newPrefix &&
333
+ (currentValue === newValue || hash(currentValue) === hash(newValue))
334
+ ) {
335
+ const newMultiplicity = currentMultiplicity + multiplicity
336
+ if (newMultiplicity === 0) {
337
+ this.#inner.delete(key)
338
+ } else {
339
+ this.#inner.set(key, [newValue, newMultiplicity])
132
340
  }
133
341
  return
134
342
  }
343
+
344
+ // Different values - choose appropriate map type
345
+ if (currentPrefix === NO_PREFIX && newPrefix === NO_PREFIX) {
346
+ // Both have NO_PREFIX, use ValueMap directly
347
+ const valueMap = new ValueMap<TValue>()
348
+ valueMap.set(hash(currentValue), currentSingleValue)
349
+ valueMap.set(hash(newValue), [newValue, multiplicity])
350
+ this.#inner.set(key, valueMap)
351
+ } else {
352
+ // At least one has a prefix, use PrefixMap
353
+ const prefixMap = new PrefixMap<TValue, TPrefix>()
354
+
355
+ if (currentPrefix === newPrefix) {
356
+ // Same prefix, different suffixes - need ValueMap within PrefixMap
357
+ const valueMap = new ValueMap<TValue>()
358
+ valueMap.set(hash(currentValue), currentSingleValue)
359
+ valueMap.set(hash(newValue), [newValue, multiplicity])
360
+ prefixMap.set(currentPrefix, valueMap)
361
+ } else {
362
+ // Different prefixes - store as separate single values
363
+ prefixMap.set(currentPrefix, currentSingleValue)
364
+ prefixMap.set(newPrefix, [newValue, multiplicity])
365
+ }
366
+
367
+ this.#inner.set(key, prefixMap)
368
+ }
135
369
  }
136
370
 
137
- append(other: Index<K, V>): void {
138
- for (const [key, value] of other.#entries()) {
371
+ /**
372
+ * This method appends another index to the current index.
373
+ * @param other - The index to append to the current index.
374
+ */
375
+ append(other: Index<TKey, TValue>): void {
376
+ for (const [key, value] of other.entries()) {
139
377
  this.addValue(key, value)
140
378
  }
141
379
  }
142
380
 
143
- join<V2>(other: Index<K, V2>): MultiSet<[K, [V, V2]]> {
144
- const result: Array<[[K, [V, V2]], number]> = []
145
-
381
+ /**
382
+ * This method joins two indexes.
383
+ * @param other - The index to join with the current index.
384
+ * @returns A multiset of the joined values.
385
+ */
386
+ join<TValue2>(
387
+ other: Index<TKey, TValue2>
388
+ ): MultiSet<[TKey, [TValue, TValue2]]> {
389
+ const result: Array<[[TKey, [TValue, TValue2]], number]> = []
146
390
  // We want to iterate over the smaller of the two indexes to reduce the
147
391
  // number of operations we need to do.
148
392
  if (this.size <= other.size) {
149
- for (const [key, valueIt] of this.#entriesIterators()) {
393
+ for (const [key, valueIt] of this.entriesIterators()) {
150
394
  if (!other.has(key)) continue
151
395
  const otherValues = other.get(key)
152
396
  for (const [val1, mul1] of valueIt) {
@@ -158,7 +402,7 @@ export class Index<K, V> {
158
402
  }
159
403
  }
160
404
  } else {
161
- for (const [key, otherValueIt] of other.#entriesIterators()) {
405
+ for (const [key, otherValueIt] of other.entriesIterators()) {
162
406
  if (!this.has(key)) continue
163
407
  const values = this.get(key)
164
408
  for (const [val2, mul2] of otherValueIt) {
@@ -174,3 +418,34 @@ export class Index<K, V> {
174
418
  return new MultiSet(result)
175
419
  }
176
420
  }
421
+
422
+ /**
423
+ * This function extracts the prefix from a value.
424
+ * @param value - The value to extract the prefix from.
425
+ * @returns The prefix and the suffix.
426
+ */
427
+ function getPrefix<TValue, TPrefix>(value: TValue): TPrefix | NO_PREFIX {
428
+ // If the value is an array and the first element is a string or number, then the
429
+ // first element is the prefix. This is used to distinguish between values without
430
+ // the need for hashing unless there are multiple values for the same prefix.
431
+ if (
432
+ Array.isArray(value) &&
433
+ (typeof value[0] === `string` ||
434
+ typeof value[0] === `number` ||
435
+ typeof value[0] === `bigint`)
436
+ ) {
437
+ return value[0] as TPrefix
438
+ }
439
+ return NO_PREFIX
440
+ }
441
+
442
+ /**
443
+ * This function checks if a value is a single value.
444
+ * @param value - The value to check.
445
+ * @returns True if the value is a single value, false otherwise.
446
+ */
447
+ function isSingleValue<TValue>(
448
+ value: SingleValue<TValue> | unknown
449
+ ): value is SingleValue<TValue> {
450
+ return Array.isArray(value)
451
+ }
@@ -39,7 +39,6 @@ export function concat<T, T2>(
39
39
  output.writer
40
40
  )
41
41
  stream.graph.addOperator(operator)
42
- stream.graph.addStream(output.connectReader())
43
42
  return output
44
43
  }
45
44
  }
@@ -44,7 +44,6 @@ export function consolidate<T>(): PipedOperator<T, T> {
44
44
  output.writer
45
45
  )
46
46
  stream.graph.addOperator(operator)
47
- stream.graph.addStream(output.connectReader())
48
47
  return output
49
48
  }
50
49
  }
@@ -46,7 +46,6 @@ export function count<
46
46
  output.writer
47
47
  )
48
48
  stream.graph.addOperator(operator)
49
- stream.graph.addStream(output.connectReader())
50
49
  return output
51
50
  }
52
51
  }
@@ -52,7 +52,6 @@ export function debug<T>(
52
52
  indent
53
53
  )
54
54
  stream.graph.addOperator(operator)
55
- stream.graph.addStream(output.connectReader())
56
55
  return output
57
56
  }
58
57
  }