@tanstack/db 0.4.3 → 0.4.5

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 (168) hide show
  1. package/dist/cjs/collection/change-events.cjs +1 -1
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/changes.cjs +7 -3
  4. package/dist/cjs/collection/changes.cjs.map +1 -1
  5. package/dist/cjs/collection/events.cjs +3 -6
  6. package/dist/cjs/collection/events.cjs.map +1 -1
  7. package/dist/cjs/collection/index.cjs +4 -1
  8. package/dist/cjs/collection/index.cjs.map +1 -1
  9. package/dist/cjs/collection/index.d.cts +16 -4
  10. package/dist/cjs/collection/mutations.cjs +13 -20
  11. package/dist/cjs/collection/mutations.cjs.map +1 -1
  12. package/dist/cjs/collection/state.cjs +14 -6
  13. package/dist/cjs/collection/state.cjs.map +1 -1
  14. package/dist/cjs/collection/subscription.cjs +3 -4
  15. package/dist/cjs/collection/subscription.cjs.map +1 -1
  16. package/dist/cjs/collection/subscription.d.cts +2 -2
  17. package/dist/cjs/collection/sync.cjs +10 -2
  18. package/dist/cjs/collection/sync.cjs.map +1 -1
  19. package/dist/cjs/indexes/auto-index.cjs +4 -3
  20. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  21. package/dist/cjs/indexes/auto-index.d.cts +2 -1
  22. package/dist/cjs/indexes/base-index.cjs +26 -0
  23. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  24. package/dist/cjs/indexes/base-index.d.cts +47 -2
  25. package/dist/cjs/indexes/btree-index.cjs +45 -9
  26. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  27. package/dist/cjs/indexes/btree-index.d.cts +15 -0
  28. package/dist/cjs/indexes/lazy-index.cjs +3 -6
  29. package/dist/cjs/indexes/lazy-index.cjs.map +1 -1
  30. package/dist/cjs/indexes/reverse-index.cjs +78 -0
  31. package/dist/cjs/indexes/reverse-index.cjs.map +1 -0
  32. package/dist/cjs/indexes/reverse-index.d.cts +30 -0
  33. package/dist/cjs/proxy.cjs +1 -1
  34. package/dist/cjs/proxy.cjs.map +1 -1
  35. package/dist/cjs/query/builder/index.cjs +21 -0
  36. package/dist/cjs/query/builder/index.cjs.map +1 -1
  37. package/dist/cjs/query/builder/index.d.cts +16 -1
  38. package/dist/cjs/query/builder/types.d.cts +7 -0
  39. package/dist/cjs/query/compiler/evaluators.cjs +1 -1
  40. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  41. package/dist/cjs/query/compiler/group-by.cjs +2 -10
  42. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  43. package/dist/cjs/query/compiler/joins.cjs +6 -6
  44. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/order-by.cjs +4 -5
  46. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  47. package/dist/cjs/query/compiler/order-by.d.cts +2 -2
  48. package/dist/cjs/query/compiler/select.cjs +1 -1
  49. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  50. package/dist/cjs/query/index.d.cts +1 -1
  51. package/dist/cjs/query/ir.cjs.map +1 -1
  52. package/dist/cjs/query/ir.d.cts +1 -0
  53. package/dist/cjs/query/live/collection-config-builder.cjs +3 -2
  54. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  55. package/dist/cjs/query/live/collection-config-builder.d.cts +2 -2
  56. package/dist/cjs/query/live/collection-subscriber.cjs +2 -3
  57. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  58. package/dist/cjs/query/live/types.d.cts +4 -0
  59. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  60. package/dist/cjs/query/live-query-collection.d.cts +7 -4
  61. package/dist/cjs/query/optimizer.cjs +2 -4
  62. package/dist/cjs/query/optimizer.cjs.map +1 -1
  63. package/dist/cjs/transactions.cjs +2 -3
  64. package/dist/cjs/transactions.cjs.map +1 -1
  65. package/dist/cjs/types.d.cts +13 -0
  66. package/dist/cjs/utils/btree.cjs +1 -1
  67. package/dist/cjs/utils/btree.cjs.map +1 -1
  68. package/dist/cjs/utils/index-optimization.cjs +7 -2
  69. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  70. package/dist/cjs/utils/index-optimization.d.cts +3 -2
  71. package/dist/cjs/utils.cjs +6 -0
  72. package/dist/cjs/utils.cjs.map +1 -1
  73. package/dist/cjs/utils.d.cts +2 -3
  74. package/dist/esm/collection/change-events.js +1 -1
  75. package/dist/esm/collection/change-events.js.map +1 -1
  76. package/dist/esm/collection/changes.js +7 -3
  77. package/dist/esm/collection/changes.js.map +1 -1
  78. package/dist/esm/collection/events.js +3 -6
  79. package/dist/esm/collection/events.js.map +1 -1
  80. package/dist/esm/collection/index.d.ts +16 -4
  81. package/dist/esm/collection/index.js +4 -1
  82. package/dist/esm/collection/index.js.map +1 -1
  83. package/dist/esm/collection/mutations.js +13 -20
  84. package/dist/esm/collection/mutations.js.map +1 -1
  85. package/dist/esm/collection/state.js +14 -6
  86. package/dist/esm/collection/state.js.map +1 -1
  87. package/dist/esm/collection/subscription.d.ts +2 -2
  88. package/dist/esm/collection/subscription.js +3 -4
  89. package/dist/esm/collection/subscription.js.map +1 -1
  90. package/dist/esm/collection/sync.js +10 -2
  91. package/dist/esm/collection/sync.js.map +1 -1
  92. package/dist/esm/indexes/auto-index.d.ts +2 -1
  93. package/dist/esm/indexes/auto-index.js +4 -3
  94. package/dist/esm/indexes/auto-index.js.map +1 -1
  95. package/dist/esm/indexes/base-index.d.ts +47 -2
  96. package/dist/esm/indexes/base-index.js +26 -0
  97. package/dist/esm/indexes/base-index.js.map +1 -1
  98. package/dist/esm/indexes/btree-index.d.ts +15 -0
  99. package/dist/esm/indexes/btree-index.js +45 -9
  100. package/dist/esm/indexes/btree-index.js.map +1 -1
  101. package/dist/esm/indexes/lazy-index.js +3 -6
  102. package/dist/esm/indexes/lazy-index.js.map +1 -1
  103. package/dist/esm/indexes/reverse-index.d.ts +30 -0
  104. package/dist/esm/indexes/reverse-index.js +78 -0
  105. package/dist/esm/indexes/reverse-index.js.map +1 -0
  106. package/dist/esm/proxy.js +1 -1
  107. package/dist/esm/proxy.js.map +1 -1
  108. package/dist/esm/query/builder/index.d.ts +16 -1
  109. package/dist/esm/query/builder/index.js +21 -0
  110. package/dist/esm/query/builder/index.js.map +1 -1
  111. package/dist/esm/query/builder/types.d.ts +7 -0
  112. package/dist/esm/query/compiler/evaluators.js +1 -1
  113. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  114. package/dist/esm/query/compiler/group-by.js +3 -11
  115. package/dist/esm/query/compiler/group-by.js.map +1 -1
  116. package/dist/esm/query/compiler/joins.js +6 -6
  117. package/dist/esm/query/compiler/joins.js.map +1 -1
  118. package/dist/esm/query/compiler/order-by.d.ts +2 -2
  119. package/dist/esm/query/compiler/order-by.js +4 -5
  120. package/dist/esm/query/compiler/order-by.js.map +1 -1
  121. package/dist/esm/query/compiler/select.js +1 -1
  122. package/dist/esm/query/compiler/select.js.map +1 -1
  123. package/dist/esm/query/index.d.ts +1 -1
  124. package/dist/esm/query/ir.d.ts +1 -0
  125. package/dist/esm/query/ir.js.map +1 -1
  126. package/dist/esm/query/live/collection-config-builder.d.ts +2 -2
  127. package/dist/esm/query/live/collection-config-builder.js +3 -2
  128. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  129. package/dist/esm/query/live/collection-subscriber.js +2 -3
  130. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  131. package/dist/esm/query/live/types.d.ts +4 -0
  132. package/dist/esm/query/live-query-collection.d.ts +7 -4
  133. package/dist/esm/query/live-query-collection.js.map +1 -1
  134. package/dist/esm/query/optimizer.js +2 -4
  135. package/dist/esm/query/optimizer.js.map +1 -1
  136. package/dist/esm/transactions.js +2 -3
  137. package/dist/esm/transactions.js.map +1 -1
  138. package/dist/esm/types.d.ts +13 -0
  139. package/dist/esm/utils/btree.js +1 -1
  140. package/dist/esm/utils/btree.js.map +1 -1
  141. package/dist/esm/utils/index-optimization.d.ts +3 -2
  142. package/dist/esm/utils/index-optimization.js +7 -2
  143. package/dist/esm/utils/index-optimization.js.map +1 -1
  144. package/dist/esm/utils.d.ts +2 -3
  145. package/dist/esm/utils.js +6 -0
  146. package/dist/esm/utils.js.map +1 -1
  147. package/package.json +1 -1
  148. package/src/collection/changes.ts +10 -4
  149. package/src/collection/index.ts +38 -5
  150. package/src/collection/state.ts +28 -11
  151. package/src/collection/subscription.ts +3 -3
  152. package/src/collection/sync.ts +17 -2
  153. package/src/indexes/auto-index.ts +8 -3
  154. package/src/indexes/base-index.ts +94 -4
  155. package/src/indexes/btree-index.ts +58 -7
  156. package/src/indexes/reverse-index.ts +120 -0
  157. package/src/query/builder/index.ts +30 -2
  158. package/src/query/builder/types.ts +12 -0
  159. package/src/query/compiler/group-by.ts +1 -10
  160. package/src/query/compiler/order-by.ts +15 -18
  161. package/src/query/index.ts +1 -0
  162. package/src/query/ir.ts +1 -0
  163. package/src/query/live/collection-config-builder.ts +3 -2
  164. package/src/query/live/types.ts +5 -0
  165. package/src/query/live-query-collection.ts +34 -8
  166. package/src/types.ts +22 -0
  167. package/src/utils/index-optimization.ts +19 -5
  168. package/src/utils.ts +8 -0
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Generic utility functions\n */\n\ninterface TypedArray {\n length: number\n [index: number]: number\n}\n\n/**\n * Deep equality function that compares two values recursively\n * Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects\n *\n * @param a - First value to compare\n * @param b - Second value to compare\n * @returns True if the values are deeply equal, false otherwise\n *\n * @example\n * ```typescript\n * deepEquals({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (property order doesn't matter)\n * deepEquals([1, { x: 2 }], [1, { x: 2 }]) // true\n * deepEquals({ a: 1 }, { a: 2 }) // false\n * deepEquals(new Date('2023-01-01'), new Date('2023-01-01')) // true\n * deepEquals(new Map([['a', 1]]), new Map([['a', 1]])) // true\n * ```\n */\nexport function deepEquals(a: any, b: any): boolean {\n return deepEqualsInternal(a, b, new Map())\n}\n\n/**\n * Internal implementation with cycle detection to prevent infinite recursion\n */\nfunction deepEqualsInternal(\n a: any,\n b: any,\n visited: Map<object, object>\n): boolean {\n // Handle strict equality (primitives, same reference)\n if (a === b) return true\n\n // Handle null/undefined\n if (a == null || b == null) return false\n\n // Handle different types\n if (typeof a !== typeof b) return false\n\n // Handle Date objects\n if (a instanceof Date) {\n if (!(b instanceof Date)) return false\n return a.getTime() === b.getTime()\n }\n\n // Handle RegExp objects\n if (a instanceof RegExp) {\n if (!(b instanceof RegExp)) return false\n return a.source === b.source && a.flags === b.flags\n }\n\n // Handle Map objects - only if both are Maps\n if (a instanceof Map) {\n if (!(b instanceof Map)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const entries = Array.from(a.entries())\n const result = entries.every(([key, val]) => {\n return b.has(key) && deepEqualsInternal(val, b.get(key), visited)\n })\n\n visited.delete(a)\n return result\n }\n\n // Handle Set objects - only if both are Sets\n if (a instanceof Set) {\n if (!(b instanceof Set)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Convert to arrays for comparison\n const aValues = Array.from(a)\n const bValues = Array.from(b)\n\n // Simple comparison for primitive values\n if (aValues.every((val) => typeof val !== `object`)) {\n visited.delete(a)\n return aValues.every((val) => b.has(val))\n }\n\n // For objects in sets, we need to do a more complex comparison\n // This is a simplified approach and may not work for all cases\n const result = aValues.length === bValues.length\n visited.delete(a)\n return result\n }\n\n // Handle TypedArrays\n if (\n ArrayBuffer.isView(a) &&\n ArrayBuffer.isView(b) &&\n !(a instanceof DataView) &&\n !(b instanceof DataView)\n ) {\n const typedA = a as unknown as TypedArray\n const typedB = b as unknown as TypedArray\n if (typedA.length !== typedB.length) return false\n\n for (let i = 0; i < typedA.length; i++) {\n if (typedA[i] !== typedB[i]) return false\n }\n\n return true\n }\n\n // Handle Temporal objects\n // Check if both are Temporal objects of the same type\n if (isTemporal(a) && isTemporal(b)) {\n const aTag = getStringTag(a)\n const bTag = getStringTag(b)\n\n // If they're different Temporal types, they're not equal\n if (aTag !== bTag) return false\n\n // Use Temporal's built-in equals method if available\n if (typeof a.equals === `function`) {\n return a.equals(b)\n }\n\n // Fallback to toString comparison for other types\n return a.toString() === b.toString()\n }\n\n // Handle arrays\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const result = a.every((item, index) =>\n deepEqualsInternal(item, b[index], visited)\n )\n visited.delete(a)\n return result\n }\n\n // Handle objects\n if (typeof a === `object`) {\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Get all keys from both objects\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n\n // Check if they have the same number of keys\n if (keysA.length !== keysB.length) {\n visited.delete(a)\n return false\n }\n\n // Check if all keys exist in both objects and their values are equal\n const result = keysA.every(\n (key) => key in b && deepEqualsInternal(a[key], b[key], visited)\n )\n\n visited.delete(a)\n return result\n }\n\n // For primitives that aren't strictly equal\n return false\n}\n\nconst temporalTypes = [\n `Temporal.Duration`,\n `Temporal.Instant`,\n `Temporal.PlainDate`,\n `Temporal.PlainDateTime`,\n `Temporal.PlainMonthDay`,\n `Temporal.PlainTime`,\n `Temporal.PlainYearMonth`,\n `Temporal.ZonedDateTime`,\n]\n\nfunction getStringTag(a: any): any {\n return a[Symbol.toStringTag]\n}\n\n/** Checks if the value is a Temporal object by checking for the Temporal brand */\nexport function isTemporal(a: any): boolean {\n const tag = getStringTag(a)\n return typeof tag === `string` && temporalTypes.includes(tag)\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Generic utility functions\n */\n\nimport type { CompareOptions } from \"./query/builder/types\"\n\ninterface TypedArray {\n length: number\n [index: number]: number\n}\n\n/**\n * Deep equality function that compares two values recursively\n * Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects\n *\n * @param a - First value to compare\n * @param b - Second value to compare\n * @returns True if the values are deeply equal, false otherwise\n *\n * @example\n * ```typescript\n * deepEquals({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (property order doesn't matter)\n * deepEquals([1, { x: 2 }], [1, { x: 2 }]) // true\n * deepEquals({ a: 1 }, { a: 2 }) // false\n * deepEquals(new Date('2023-01-01'), new Date('2023-01-01')) // true\n * deepEquals(new Map([['a', 1]]), new Map([['a', 1]])) // true\n * ```\n */\nexport function deepEquals(a: any, b: any): boolean {\n return deepEqualsInternal(a, b, new Map())\n}\n\n/**\n * Internal implementation with cycle detection to prevent infinite recursion\n */\nfunction deepEqualsInternal(\n a: any,\n b: any,\n visited: Map<object, object>\n): boolean {\n // Handle strict equality (primitives, same reference)\n if (a === b) return true\n\n // Handle null/undefined\n if (a == null || b == null) return false\n\n // Handle different types\n if (typeof a !== typeof b) return false\n\n // Handle Date objects\n if (a instanceof Date) {\n if (!(b instanceof Date)) return false\n return a.getTime() === b.getTime()\n }\n\n // Handle RegExp objects\n if (a instanceof RegExp) {\n if (!(b instanceof RegExp)) return false\n return a.source === b.source && a.flags === b.flags\n }\n\n // Handle Map objects - only if both are Maps\n if (a instanceof Map) {\n if (!(b instanceof Map)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const entries = Array.from(a.entries())\n const result = entries.every(([key, val]) => {\n return b.has(key) && deepEqualsInternal(val, b.get(key), visited)\n })\n\n visited.delete(a)\n return result\n }\n\n // Handle Set objects - only if both are Sets\n if (a instanceof Set) {\n if (!(b instanceof Set)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Convert to arrays for comparison\n const aValues = Array.from(a)\n const bValues = Array.from(b)\n\n // Simple comparison for primitive values\n if (aValues.every((val) => typeof val !== `object`)) {\n visited.delete(a)\n return aValues.every((val) => b.has(val))\n }\n\n // For objects in sets, we need to do a more complex comparison\n // This is a simplified approach and may not work for all cases\n const result = aValues.length === bValues.length\n visited.delete(a)\n return result\n }\n\n // Handle TypedArrays\n if (\n ArrayBuffer.isView(a) &&\n ArrayBuffer.isView(b) &&\n !(a instanceof DataView) &&\n !(b instanceof DataView)\n ) {\n const typedA = a as unknown as TypedArray\n const typedB = b as unknown as TypedArray\n if (typedA.length !== typedB.length) return false\n\n for (let i = 0; i < typedA.length; i++) {\n if (typedA[i] !== typedB[i]) return false\n }\n\n return true\n }\n\n // Handle Temporal objects\n // Check if both are Temporal objects of the same type\n if (isTemporal(a) && isTemporal(b)) {\n const aTag = getStringTag(a)\n const bTag = getStringTag(b)\n\n // If they're different Temporal types, they're not equal\n if (aTag !== bTag) return false\n\n // Use Temporal's built-in equals method if available\n if (typeof a.equals === `function`) {\n return a.equals(b)\n }\n\n // Fallback to toString comparison for other types\n return a.toString() === b.toString()\n }\n\n // Handle arrays\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const result = a.every((item, index) =>\n deepEqualsInternal(item, b[index], visited)\n )\n visited.delete(a)\n return result\n }\n\n // Handle objects\n if (typeof a === `object`) {\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Get all keys from both objects\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n\n // Check if they have the same number of keys\n if (keysA.length !== keysB.length) {\n visited.delete(a)\n return false\n }\n\n // Check if all keys exist in both objects and their values are equal\n const result = keysA.every(\n (key) => key in b && deepEqualsInternal(a[key], b[key], visited)\n )\n\n visited.delete(a)\n return result\n }\n\n // For primitives that aren't strictly equal\n return false\n}\n\nconst temporalTypes = [\n `Temporal.Duration`,\n `Temporal.Instant`,\n `Temporal.PlainDate`,\n `Temporal.PlainDateTime`,\n `Temporal.PlainMonthDay`,\n `Temporal.PlainTime`,\n `Temporal.PlainYearMonth`,\n `Temporal.ZonedDateTime`,\n]\n\nfunction getStringTag(a: any): any {\n return a[Symbol.toStringTag]\n}\n\n/** Checks if the value is a Temporal object by checking for the Temporal brand */\nexport function isTemporal(a: any): boolean {\n const tag = getStringTag(a)\n return typeof tag === `string` && temporalTypes.includes(tag)\n}\n\nexport const DEFAULT_COMPARE_OPTIONS: CompareOptions = {\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.4.3",
4
+ "version": "0.4.5",
5
5
  "dependencies": {
6
6
  "@standard-schema/spec": "^1.0.0",
7
7
  "@tanstack/db-ivm": "0.1.9"
@@ -68,14 +68,20 @@ export class CollectionChangesManager<
68
68
  // Either we're not batching, or we're forcing emission (user action or ending batch cycle)
69
69
  let eventsToEmit = changes
70
70
 
71
- // If we have batched events and this is a forced emit, combine them
72
- if (this.batchedEvents.length > 0 && forceEmit) {
73
- eventsToEmit = [...this.batchedEvents, ...changes]
71
+ if (forceEmit) {
72
+ // Force emit is used to end a batch (e.g. after a sync commit). Combine any
73
+ // buffered optimistic events with the final changes so subscribers see the
74
+ // whole picture, even if the sync diff is empty.
75
+ if (this.batchedEvents.length > 0) {
76
+ eventsToEmit = [...this.batchedEvents, ...changes]
77
+ }
74
78
  this.batchedEvents = []
75
79
  this.shouldBatchEvents = false
76
80
  }
77
81
 
78
- if (eventsToEmit.length === 0) return
82
+ if (eventsToEmit.length === 0) {
83
+ return
84
+ }
79
85
 
80
86
  // Emit to all listeners
81
87
  for (const subscription of this.changeSubscriptions) {
@@ -24,7 +24,9 @@ import type {
24
24
  InferSchemaInput,
25
25
  InferSchemaOutput,
26
26
  InsertConfig,
27
+ NonSingleResult,
27
28
  OperationConfig,
29
+ SingleResult,
28
30
  SubscribeChangesOptions,
29
31
  Transaction as TransactionType,
30
32
  UtilsRecord,
@@ -50,6 +52,7 @@ export interface Collection<
50
52
  TInsertInput extends object = T,
51
53
  > extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {
52
54
  readonly utils: TUtils
55
+ readonly singleResult?: true
53
56
  }
54
57
 
55
58
  /**
@@ -132,8 +135,22 @@ export function createCollection<
132
135
  options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
133
136
  schema: T
134
137
  utils?: TUtils
135
- }
136
- ): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>>
138
+ } & NonSingleResult
139
+ ): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &
140
+ NonSingleResult
141
+
142
+ // Overload for when schema is provided and singleResult is true
143
+ export function createCollection<
144
+ T extends StandardSchemaV1,
145
+ TKey extends string | number = string | number,
146
+ TUtils extends UtilsRecord = {},
147
+ >(
148
+ options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
149
+ schema: T
150
+ utils?: TUtils
151
+ } & SingleResult
152
+ ): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &
153
+ SingleResult
137
154
 
138
155
  // Overload for when no schema is provided
139
156
  // the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
@@ -145,8 +162,21 @@ export function createCollection<
145
162
  options: CollectionConfig<T, TKey, never> & {
146
163
  schema?: never // prohibit schema if an explicit type is provided
147
164
  utils?: TUtils
148
- }
149
- ): Collection<T, TKey, TUtils, never, T>
165
+ } & NonSingleResult
166
+ ): Collection<T, TKey, TUtils, never, T> & NonSingleResult
167
+
168
+ // Overload for when no schema is provided and singleResult is true
169
+ // the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
170
+ export function createCollection<
171
+ T extends object,
172
+ TKey extends string | number = string | number,
173
+ TUtils extends UtilsRecord = {},
174
+ >(
175
+ options: CollectionConfig<T, TKey, never> & {
176
+ schema?: never // prohibit schema if an explicit type is provided
177
+ utils?: TUtils
178
+ } & SingleResult
179
+ ): Collection<T, TKey, TUtils, never, T> & SingleResult
150
180
 
151
181
  // Implementation
152
182
  export function createCollection(
@@ -428,7 +458,10 @@ export class CollectionImpl<
428
458
  * // Create a ordered index with custom options
429
459
  * const ageIndex = collection.createIndex((row) => row.age, {
430
460
  * indexType: BTreeIndex,
431
- * options: { compareFn: customComparator },
461
+ * options: {
462
+ * compareFn: customComparator,
463
+ * compareOptions: { direction: 'asc', nulls: 'first', stringSort: 'lexical' }
464
+ * },
432
465
  * name: 'age_btree'
433
466
  * })
434
467
  *
@@ -217,7 +217,11 @@ export class CollectionStateManager<
217
217
  triggeredByUserAction: boolean = false
218
218
  ): void {
219
219
  // Skip redundant recalculations when we're in the middle of committing sync transactions
220
- if (this.isCommittingSyncTransactions) {
220
+ // While the sync pipeline is replaying a large batch we still want to honour
221
+ // fresh optimistic mutations from the UI. Only skip recompute for the
222
+ // internal sync-driven redraws; user-triggered work (triggeredByUserAction)
223
+ // must run so live queries stay responsive during long commits.
224
+ if (this.isCommittingSyncTransactions && !triggeredByUserAction) {
221
225
  return
222
226
  }
223
227
 
@@ -708,10 +712,23 @@ export class CollectionStateManager<
708
712
 
709
713
  // Check if this sync operation is redundant with a completed optimistic operation
710
714
  const completedOp = completedOptimisticOps.get(key)
711
- const isRedundantSync =
712
- completedOp &&
713
- newVisibleValue !== undefined &&
714
- deepEquals(completedOp.value, newVisibleValue)
715
+ let isRedundantSync = false
716
+
717
+ if (completedOp) {
718
+ if (
719
+ completedOp.type === `delete` &&
720
+ previousVisibleValue !== undefined &&
721
+ newVisibleValue === undefined &&
722
+ deepEquals(completedOp.value, previousVisibleValue)
723
+ ) {
724
+ isRedundantSync = true
725
+ } else if (
726
+ newVisibleValue !== undefined &&
727
+ deepEquals(completedOp.value, newVisibleValue)
728
+ ) {
729
+ isRedundantSync = true
730
+ }
731
+ }
715
732
 
716
733
  if (!isRedundantSync) {
717
734
  if (
@@ -808,9 +825,6 @@ export class CollectionStateManager<
808
825
  public capturePreSyncVisibleState(): void {
809
826
  if (this.pendingSyncedTransactions.length === 0) return
810
827
 
811
- // Clear any previous capture
812
- this.preSyncVisibleState.clear()
813
-
814
828
  // Get all keys that will be affected by sync operations
815
829
  const syncedKeys = new Set<TKey>()
816
830
  for (const transaction of this.pendingSyncedTransactions) {
@@ -826,10 +840,13 @@ export class CollectionStateManager<
826
840
 
827
841
  // Only capture current visible state for keys that will be affected by sync operations
828
842
  // This is much more efficient than capturing the entire collection state
843
+ // Only capture keys that haven't been captured yet to preserve earlier captures
829
844
  for (const key of syncedKeys) {
830
- const currentValue = this.get(key)
831
- if (currentValue !== undefined) {
832
- this.preSyncVisibleState.set(key, currentValue)
845
+ if (!this.preSyncVisibleState.has(key)) {
846
+ const currentValue = this.get(key)
847
+ if (currentValue !== undefined) {
848
+ this.preSyncVisibleState.set(key, currentValue)
849
+ }
833
850
  }
834
851
  }
835
852
  }
@@ -5,7 +5,7 @@ import {
5
5
  createFilteredCallback,
6
6
  } from "./change-events.js"
7
7
  import type { BasicExpression } from "../query/ir.js"
8
- import type { BaseIndex } from "../indexes/base-index.js"
8
+ import type { IndexInterface } from "../indexes/base-index.js"
9
9
  import type { ChangeMessage } from "../types.js"
10
10
  import type { CollectionImpl } from "./index.js"
11
11
 
@@ -38,7 +38,7 @@ export class CollectionSubscription {
38
38
 
39
39
  private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => void
40
40
 
41
- private orderByIndex: BaseIndex<string | number> | undefined
41
+ private orderByIndex: IndexInterface<string | number> | undefined
42
42
 
43
43
  constructor(
44
44
  private collection: CollectionImpl<any, any, any, any, any>,
@@ -65,7 +65,7 @@ export class CollectionSubscription {
65
65
  : this.callback
66
66
  }
67
67
 
68
- setOrderByIndex(index: BaseIndex<any>) {
68
+ setOrderByIndex(index: IndexInterface<any>) {
69
69
  this.orderByIndex = index
70
70
  }
71
71
 
@@ -7,6 +7,7 @@ import {
7
7
  SyncTransactionAlreadyCommittedError,
8
8
  SyncTransactionAlreadyCommittedWriteError,
9
9
  } from "../errors"
10
+ import { deepEquals } from "../utils"
10
11
  import type { StandardSchemaV1 } from "@standard-schema/spec"
11
12
  import type { ChangeMessage, CollectionConfig } from "../types"
12
13
  import type { CollectionImpl } from "./index.js"
@@ -84,6 +85,8 @@ export class CollectionSyncManager<
84
85
  }
85
86
  const key = this.config.getKey(messageWithoutKey.value)
86
87
 
88
+ let messageType = messageWithoutKey.type
89
+
87
90
  // Check if an item with this key already exists when inserting
88
91
  if (messageWithoutKey.type === `insert`) {
89
92
  const insertingIntoExistingSynced = state.syncedData.has(key)
@@ -96,17 +99,29 @@ export class CollectionSyncManager<
96
99
  !hasPendingDeleteForKey &&
97
100
  !isTruncateTransaction
98
101
  ) {
99
- throw new DuplicateKeySyncError(key, this.id)
102
+ const existingValue = state.syncedData.get(key)
103
+ if (
104
+ existingValue !== undefined &&
105
+ deepEquals(existingValue, messageWithoutKey.value)
106
+ ) {
107
+ // The "insert" is an echo of a value we already have locally.
108
+ // Treat it as an update so we preserve optimistic intent without
109
+ // throwing a duplicate-key error during reconciliation.
110
+ messageType = `update`
111
+ } else {
112
+ throw new DuplicateKeySyncError(key, this.id)
113
+ }
100
114
  }
101
115
  }
102
116
 
103
117
  const message: ChangeMessage<TOutput> = {
104
118
  ...messageWithoutKey,
119
+ type: messageType,
105
120
  key,
106
121
  }
107
122
  pendingTransaction.operations.push(message)
108
123
 
109
- if (messageWithoutKey.type === `delete`) {
124
+ if (messageType === `delete`) {
110
125
  pendingTransaction.deletedKeys.add(key)
111
126
  }
112
127
  },
@@ -1,4 +1,6 @@
1
+ import { DEFAULT_COMPARE_OPTIONS } from "../utils"
1
2
  import { BTreeIndex } from "./btree-index"
3
+ import type { CompareOptions } from "../query/builder/types"
2
4
  import type { BasicExpression } from "../query/ir"
3
5
  import type { CollectionImpl } from "../collection/index.js"
4
6
 
@@ -30,6 +32,7 @@ export function ensureIndexForField<
30
32
  fieldName: string,
31
33
  fieldPath: Array<string>,
32
34
  collection: CollectionImpl<T, TKey, any, any, any>,
35
+ compareOptions: CompareOptions = DEFAULT_COMPARE_OPTIONS,
33
36
  compareFn?: (a: any, b: any) => number
34
37
  ) {
35
38
  if (!shouldAutoIndex(collection)) {
@@ -37,8 +40,10 @@ export function ensureIndexForField<
37
40
  }
38
41
 
39
42
  // Check if we already have an index for this field
40
- const existingIndex = Array.from(collection.indexes.values()).find((index) =>
41
- index.matchesField(fieldPath)
43
+ const existingIndex = Array.from(collection.indexes.values()).find(
44
+ (index) =>
45
+ index.matchesField(fieldPath) &&
46
+ index.matchesCompareOptions(compareOptions)
42
47
  )
43
48
 
44
49
  if (existingIndex) {
@@ -50,7 +55,7 @@ export function ensureIndexForField<
50
55
  collection.createIndex((row) => (row as any)[fieldName], {
51
56
  name: `auto_${fieldName}`,
52
57
  indexType: BTreeIndex,
53
- options: compareFn ? { compareFn } : {},
58
+ options: compareFn ? { compareFn, compareOptions } : {},
54
59
  })
55
60
  } catch (error) {
56
61
  console.warn(`Failed to create auto-index for field "${fieldName}":`, error)
@@ -1,6 +1,9 @@
1
1
  import { compileSingleRowExpression } from "../query/compiler/evaluators.js"
2
2
  import { comparisonFunctions } from "../query/builder/functions.js"
3
- import type { BasicExpression } from "../query/ir.js"
3
+ import { DEFAULT_COMPARE_OPTIONS, deepEquals } from "../utils.js"
4
+ import type { RangeQueryOptions } from "./btree-index.js"
5
+ import type { CompareOptions } from "../query/builder/types.js"
6
+ import type { BasicExpression, OrderByDirection } from "../query/ir.js"
4
7
 
5
8
  /**
6
9
  * Operations that indexes can support, imported from available comparison functions
@@ -22,12 +25,57 @@ export interface IndexStats {
22
25
  readonly lastUpdated: Date
23
26
  }
24
27
 
28
+ export interface IndexInterface<
29
+ TKey extends string | number = string | number,
30
+ > {
31
+ add: (key: TKey, item: any) => void
32
+ remove: (key: TKey, item: any) => void
33
+ update: (key: TKey, oldItem: any, newItem: any) => void
34
+
35
+ build: (entries: Iterable<[TKey, any]>) => void
36
+ clear: () => void
37
+
38
+ lookup: (operation: IndexOperation, value: any) => Set<TKey>
39
+
40
+ equalityLookup: (value: any) => Set<TKey>
41
+ inArrayLookup: (values: Array<any>) => Set<TKey>
42
+
43
+ rangeQuery: (options: RangeQueryOptions) => Set<TKey>
44
+ rangeQueryReversed: (options: RangeQueryOptions) => Set<TKey>
45
+
46
+ take: (
47
+ n: number,
48
+ from?: TKey,
49
+ filterFn?: (key: TKey) => boolean
50
+ ) => Array<TKey>
51
+ takeReversed: (
52
+ n: number,
53
+ from?: TKey,
54
+ filterFn?: (key: TKey) => boolean
55
+ ) => Array<TKey>
56
+
57
+ get keyCount(): number
58
+ get orderedEntriesArray(): Array<[any, Set<TKey>]>
59
+ get orderedEntriesArrayReversed(): Array<[any, Set<TKey>]>
60
+
61
+ get indexedKeysSet(): Set<TKey>
62
+ get valueMapData(): Map<any, Set<TKey>>
63
+
64
+ supports: (operation: IndexOperation) => boolean
65
+
66
+ matchesField: (fieldPath: Array<string>) => boolean
67
+ matchesCompareOptions: (compareOptions: CompareOptions) => boolean
68
+ matchesDirection: (direction: OrderByDirection) => boolean
69
+
70
+ getStats: () => IndexStats
71
+ }
72
+
25
73
  /**
26
74
  * Base abstract class that all index types extend
27
75
  */
28
- export abstract class BaseIndex<
29
- TKey extends string | number = string | number,
30
- > {
76
+ export abstract class BaseIndex<TKey extends string | number = string | number>
77
+ implements IndexInterface<TKey>
78
+ {
31
79
  public readonly id: number
32
80
  public readonly name?: string
33
81
  public readonly expression: BasicExpression
@@ -36,6 +84,7 @@ export abstract class BaseIndex<
36
84
  protected lookupCount = 0
37
85
  protected totalLookupTime = 0
38
86
  protected lastUpdated = new Date()
87
+ protected compareOptions: CompareOptions
39
88
 
40
89
  constructor(
41
90
  id: number,
@@ -45,6 +94,7 @@ export abstract class BaseIndex<
45
94
  ) {
46
95
  this.id = id
47
96
  this.expression = expression
97
+ this.compareOptions = DEFAULT_COMPARE_OPTIONS
48
98
  this.name = name
49
99
  this.initialize(options)
50
100
  }
@@ -61,7 +111,20 @@ export abstract class BaseIndex<
61
111
  from?: TKey,
62
112
  filterFn?: (key: TKey) => boolean
63
113
  ): Array<TKey>
114
+ abstract takeReversed(
115
+ n: number,
116
+ from?: TKey,
117
+ filterFn?: (key: TKey) => boolean
118
+ ): Array<TKey>
64
119
  abstract get keyCount(): number
120
+ abstract equalityLookup(value: any): Set<TKey>
121
+ abstract inArrayLookup(values: Array<any>): Set<TKey>
122
+ abstract rangeQuery(options: RangeQueryOptions): Set<TKey>
123
+ abstract rangeQueryReversed(options: RangeQueryOptions): Set<TKey>
124
+ abstract get orderedEntriesArray(): Array<[any, Set<TKey>]>
125
+ abstract get orderedEntriesArrayReversed(): Array<[any, Set<TKey>]>
126
+ abstract get indexedKeysSet(): Set<TKey>
127
+ abstract get valueMapData(): Map<any, Set<TKey>>
65
128
 
66
129
  // Common methods
67
130
  supports(operation: IndexOperation): boolean {
@@ -76,6 +139,33 @@ export abstract class BaseIndex<
76
139
  )
77
140
  }
78
141
 
142
+ /**
143
+ * Checks if the compare options match the index's compare options.
144
+ * The direction is ignored because the index can be reversed if the direction is different.
145
+ */
146
+ matchesCompareOptions(compareOptions: CompareOptions): boolean {
147
+ const thisCompareOptionsWithoutDirection = {
148
+ ...this.compareOptions,
149
+ direction: undefined,
150
+ }
151
+ const compareOptionsWithoutDirection = {
152
+ ...compareOptions,
153
+ direction: undefined,
154
+ }
155
+
156
+ return deepEquals(
157
+ thisCompareOptionsWithoutDirection,
158
+ compareOptionsWithoutDirection
159
+ )
160
+ }
161
+
162
+ /**
163
+ * Checks if the index matches the provided direction.
164
+ */
165
+ matchesDirection(direction: OrderByDirection): boolean {
166
+ return this.compareOptions.direction === direction
167
+ }
168
+
79
169
  getStats(): IndexStats {
80
170
  return {
81
171
  entryCount: this.keyCount,
@@ -1,6 +1,7 @@
1
1
  import { BTree } from "../utils/btree.js"
2
2
  import { defaultComparator, normalizeValue } from "../utils/comparison.js"
3
3
  import { BaseIndex } from "./base-index.js"
4
+ import type { CompareOptions } from "../query/builder/types.js"
4
5
  import type { BasicExpression } from "../query/ir.js"
5
6
  import type { IndexOperation } from "./base-index.js"
6
7
 
@@ -9,6 +10,7 @@ import type { IndexOperation } from "./base-index.js"
9
10
  */
10
11
  export interface BTreeIndexOptions {
11
12
  compareFn?: (a: any, b: any) => number
13
+ compareOptions?: CompareOptions
12
14
  }
13
15
 
14
16
  /**
@@ -53,6 +55,9 @@ export class BTreeIndex<
53
55
  ) {
54
56
  super(id, expression, name, options)
55
57
  this.compareFn = options?.compareFn ?? defaultComparator
58
+ if (options?.compareOptions) {
59
+ this.compareOptions = options!.compareOptions
60
+ }
56
61
  this.orderedEntries = new BTree(this.compareFn)
57
62
  }
58
63
 
@@ -240,18 +245,31 @@ export class BTreeIndex<
240
245
  }
241
246
 
242
247
  /**
243
- * Returns the next n items after the provided item or the first n items if no from item is provided.
244
- * @param n - The number of items to return
245
- * @param from - The item to start from (exclusive). Starts from the smallest item (inclusive) if not provided.
246
- * @returns The next n items after the provided key. Returns the first n items if no from item is provided.
248
+ * Performs a reversed range query
247
249
  */
248
- take(n: number, from?: any, filterFn?: (key: TKey) => boolean): Array<TKey> {
250
+ rangeQueryReversed(options: RangeQueryOptions = {}): Set<TKey> {
251
+ const { from, to, fromInclusive = true, toInclusive = true } = options
252
+ return this.rangeQuery({
253
+ from: to ?? this.orderedEntries.maxKey(),
254
+ to: from ?? this.orderedEntries.minKey(),
255
+ fromInclusive: toInclusive,
256
+ toInclusive: fromInclusive,
257
+ })
258
+ }
259
+
260
+ private takeInternal(
261
+ n: number,
262
+ nextPair: (k?: any) => [any, any] | undefined,
263
+ from?: any,
264
+ filterFn?: (key: TKey) => boolean
265
+ ): Array<TKey> {
249
266
  const keysInResult: Set<TKey> = new Set()
250
267
  const result: Array<TKey> = []
251
- const nextKey = (k?: any) => this.orderedEntries.nextHigherKey(k)
268
+ let pair: [any, any] | undefined
252
269
  let key = normalizeValue(from)
253
270
 
254
- while ((key = nextKey(key)) && result.length < n) {
271
+ while ((pair = nextPair(key)) !== undefined && result.length < n) {
272
+ key = pair[0]
255
273
  const keys = this.valueMap.get(key)
256
274
  if (keys) {
257
275
  const it = keys.values()
@@ -268,6 +286,32 @@ export class BTreeIndex<
268
286
  return result
269
287
  }
270
288
 
289
+ /**
290
+ * Returns the next n items after the provided item or the first n items if no from item is provided.
291
+ * @param n - The number of items to return
292
+ * @param from - The item to start from (exclusive). Starts from the smallest item (inclusive) if not provided.
293
+ * @returns The next n items after the provided key. Returns the first n items if no from item is provided.
294
+ */
295
+ take(n: number, from?: any, filterFn?: (key: TKey) => boolean): Array<TKey> {
296
+ const nextPair = (k?: any) => this.orderedEntries.nextHigherPair(k)
297
+ return this.takeInternal(n, nextPair, from, filterFn)
298
+ }
299
+
300
+ /**
301
+ * Returns the next n items **before** the provided item (in descending order) or the last n items if no from item is provided.
302
+ * @param n - The number of items to return
303
+ * @param from - The item to start from (exclusive). Starts from the largest item (inclusive) if not provided.
304
+ * @returns The next n items **before** the provided key. Returns the last n items if no from item is provided.
305
+ */
306
+ takeReversed(
307
+ n: number,
308
+ from?: any,
309
+ filterFn?: (key: TKey) => boolean
310
+ ): Array<TKey> {
311
+ const nextPair = (k?: any) => this.orderedEntries.nextLowerPair(k)
312
+ return this.takeInternal(n, nextPair, from, filterFn)
313
+ }
314
+
271
315
  /**
272
316
  * Performs an IN array lookup
273
317
  */
@@ -296,6 +340,13 @@ export class BTreeIndex<
296
340
  .map((key) => [key, this.valueMap.get(key) ?? new Set()])
297
341
  }
298
342
 
343
+ get orderedEntriesArrayReversed(): Array<[any, Set<TKey>]> {
344
+ return this.takeReversed(this.orderedEntries.size).map((key) => [
345
+ key,
346
+ this.valueMap.get(key) ?? new Set(),
347
+ ])
348
+ }
349
+
299
350
  get valueMapData(): Map<any, Set<TKey>> {
300
351
  return this.valueMap
301
352
  }