@tanstack/db 0.5.33 → 0.6.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 (273) hide show
  1. package/dist/cjs/collection/change-events.cjs.map +1 -1
  2. package/dist/cjs/collection/change-events.d.cts +3 -2
  3. package/dist/cjs/collection/changes.cjs +13 -4
  4. package/dist/cjs/collection/changes.cjs.map +1 -1
  5. package/dist/cjs/collection/changes.d.cts +10 -1
  6. package/dist/cjs/collection/cleanup-queue.cjs +89 -0
  7. package/dist/cjs/collection/cleanup-queue.cjs.map +1 -0
  8. package/dist/cjs/collection/cleanup-queue.d.cts +30 -0
  9. package/dist/cjs/collection/events.cjs +14 -0
  10. package/dist/cjs/collection/events.cjs.map +1 -1
  11. package/dist/cjs/collection/events.d.cts +39 -1
  12. package/dist/cjs/collection/index.cjs +66 -28
  13. package/dist/cjs/collection/index.cjs.map +1 -1
  14. package/dist/cjs/collection/index.d.cts +49 -36
  15. package/dist/cjs/collection/indexes.cjs +211 -62
  16. package/dist/cjs/collection/indexes.cjs.map +1 -1
  17. package/dist/cjs/collection/indexes.d.cts +27 -17
  18. package/dist/cjs/collection/lifecycle.cjs +5 -22
  19. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  20. package/dist/cjs/collection/lifecycle.d.cts +0 -1
  21. package/dist/cjs/collection/mutations.cjs +18 -0
  22. package/dist/cjs/collection/mutations.cjs.map +1 -1
  23. package/dist/cjs/collection/mutations.d.cts +1 -0
  24. package/dist/cjs/collection/state.cjs +381 -53
  25. package/dist/cjs/collection/state.cjs.map +1 -1
  26. package/dist/cjs/collection/state.d.cts +65 -1
  27. package/dist/cjs/collection/subscription.cjs +6 -0
  28. package/dist/cjs/collection/subscription.cjs.map +1 -1
  29. package/dist/cjs/collection/subscription.d.cts +4 -0
  30. package/dist/cjs/collection/sync.cjs +108 -1
  31. package/dist/cjs/collection/sync.cjs.map +1 -1
  32. package/dist/cjs/collection/sync.d.cts +2 -0
  33. package/dist/cjs/collection/transaction-metadata.cjs +5 -0
  34. package/dist/cjs/collection/transaction-metadata.cjs.map +1 -0
  35. package/dist/cjs/collection/transaction-metadata.d.cts +1 -0
  36. package/dist/cjs/errors.cjs +8 -0
  37. package/dist/cjs/errors.cjs.map +1 -1
  38. package/dist/cjs/errors.d.cts +3 -0
  39. package/dist/cjs/index.cjs +22 -4
  40. package/dist/cjs/index.cjs.map +1 -1
  41. package/dist/cjs/index.d.cts +11 -3
  42. package/dist/cjs/indexes/auto-index.cjs +13 -6
  43. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  44. package/dist/cjs/indexes/base-index.cjs +0 -3
  45. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  46. package/dist/cjs/indexes/base-index.d.cts +2 -6
  47. package/dist/cjs/indexes/basic-index.cjs +361 -0
  48. package/dist/cjs/indexes/basic-index.cjs.map +1 -0
  49. package/dist/cjs/indexes/basic-index.d.cts +102 -0
  50. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  51. package/dist/cjs/indexes/btree-index.d.cts +1 -1
  52. package/dist/cjs/indexes/index-options.d.cts +8 -9
  53. package/dist/cjs/indexes/index-registry.cjs +89 -0
  54. package/dist/cjs/indexes/index-registry.cjs.map +1 -0
  55. package/dist/cjs/indexes/index-registry.d.cts +61 -0
  56. package/dist/cjs/local-only.cjs +5 -0
  57. package/dist/cjs/local-only.cjs.map +1 -1
  58. package/dist/cjs/query/builder/functions.cjs +27 -11
  59. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  60. package/dist/cjs/query/builder/functions.d.cts +25 -3
  61. package/dist/cjs/query/builder/index.cjs +200 -39
  62. package/dist/cjs/query/builder/index.cjs.map +1 -1
  63. package/dist/cjs/query/builder/index.d.cts +4 -3
  64. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  65. package/dist/cjs/query/builder/ref-proxy.d.cts +14 -3
  66. package/dist/cjs/query/builder/types.d.cts +84 -19
  67. package/dist/cjs/query/compiler/evaluators.cjs +51 -0
  68. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  69. package/dist/cjs/query/compiler/group-by.cjs +100 -28
  70. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  71. package/dist/cjs/query/compiler/group-by.d.cts +4 -2
  72. package/dist/cjs/query/compiler/index.cjs +283 -11
  73. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  74. package/dist/cjs/query/compiler/index.d.cts +30 -2
  75. package/dist/cjs/query/compiler/order-by.cjs +29 -10
  76. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  77. package/dist/cjs/query/compiler/order-by.d.cts +1 -1
  78. package/dist/cjs/query/compiler/select.cjs +8 -0
  79. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  80. package/dist/cjs/query/index.d.cts +2 -1
  81. package/dist/cjs/query/ir.cjs +18 -1
  82. package/dist/cjs/query/ir.cjs.map +1 -1
  83. package/dist/cjs/query/ir.d.cts +21 -1
  84. package/dist/cjs/query/live/collection-config-builder.cjs +501 -5
  85. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  86. package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
  87. package/dist/cjs/query/live/types.d.cts +3 -3
  88. package/dist/cjs/query/live/utils.cjs +43 -3
  89. package/dist/cjs/query/live/utils.cjs.map +1 -1
  90. package/dist/cjs/query/live/utils.d.cts +1 -0
  91. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  92. package/dist/cjs/query/live-query-collection.d.cts +9 -6
  93. package/dist/cjs/query/query-once.cjs.map +1 -1
  94. package/dist/cjs/query/query-once.d.cts +7 -5
  95. package/dist/cjs/query/subset-dedupe.cjs +9 -3
  96. package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
  97. package/dist/cjs/types.d.cts +42 -8
  98. package/dist/cjs/utils/array-utils.cjs +27 -0
  99. package/dist/cjs/utils/array-utils.cjs.map +1 -0
  100. package/dist/cjs/utils/array-utils.d.cts +16 -0
  101. package/dist/cjs/utils/comparison.cjs +11 -0
  102. package/dist/cjs/utils/comparison.cjs.map +1 -1
  103. package/dist/cjs/utils/index-optimization.cjs +4 -0
  104. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  105. package/dist/cjs/utils.cjs +7 -9
  106. package/dist/cjs/utils.cjs.map +1 -1
  107. package/dist/cjs/utils.d.cts +6 -1
  108. package/dist/cjs/virtual-props.cjs +33 -0
  109. package/dist/cjs/virtual-props.cjs.map +1 -0
  110. package/dist/cjs/virtual-props.d.cts +196 -0
  111. package/dist/esm/collection/change-events.d.ts +3 -2
  112. package/dist/esm/collection/change-events.js.map +1 -1
  113. package/dist/esm/collection/changes.d.ts +10 -1
  114. package/dist/esm/collection/changes.js +13 -4
  115. package/dist/esm/collection/changes.js.map +1 -1
  116. package/dist/esm/collection/cleanup-queue.d.ts +30 -0
  117. package/dist/esm/collection/cleanup-queue.js +89 -0
  118. package/dist/esm/collection/cleanup-queue.js.map +1 -0
  119. package/dist/esm/collection/events.d.ts +39 -1
  120. package/dist/esm/collection/events.js +14 -0
  121. package/dist/esm/collection/events.js.map +1 -1
  122. package/dist/esm/collection/index.d.ts +49 -36
  123. package/dist/esm/collection/index.js +67 -29
  124. package/dist/esm/collection/index.js.map +1 -1
  125. package/dist/esm/collection/indexes.d.ts +27 -17
  126. package/dist/esm/collection/indexes.js +211 -62
  127. package/dist/esm/collection/indexes.js.map +1 -1
  128. package/dist/esm/collection/lifecycle.d.ts +0 -1
  129. package/dist/esm/collection/lifecycle.js +5 -22
  130. package/dist/esm/collection/lifecycle.js.map +1 -1
  131. package/dist/esm/collection/mutations.d.ts +1 -0
  132. package/dist/esm/collection/mutations.js +18 -0
  133. package/dist/esm/collection/mutations.js.map +1 -1
  134. package/dist/esm/collection/state.d.ts +65 -1
  135. package/dist/esm/collection/state.js +381 -53
  136. package/dist/esm/collection/state.js.map +1 -1
  137. package/dist/esm/collection/subscription.d.ts +4 -0
  138. package/dist/esm/collection/subscription.js +6 -0
  139. package/dist/esm/collection/subscription.js.map +1 -1
  140. package/dist/esm/collection/sync.d.ts +2 -0
  141. package/dist/esm/collection/sync.js +108 -1
  142. package/dist/esm/collection/sync.js.map +1 -1
  143. package/dist/esm/collection/transaction-metadata.d.ts +1 -0
  144. package/dist/esm/collection/transaction-metadata.js +5 -0
  145. package/dist/esm/collection/transaction-metadata.js.map +1 -0
  146. package/dist/esm/errors.d.ts +3 -0
  147. package/dist/esm/errors.js +8 -0
  148. package/dist/esm/errors.js.map +1 -1
  149. package/dist/esm/index.d.ts +11 -3
  150. package/dist/esm/index.js +25 -7
  151. package/dist/esm/index.js.map +1 -1
  152. package/dist/esm/indexes/auto-index.js +13 -6
  153. package/dist/esm/indexes/auto-index.js.map +1 -1
  154. package/dist/esm/indexes/base-index.d.ts +2 -6
  155. package/dist/esm/indexes/base-index.js +1 -4
  156. package/dist/esm/indexes/base-index.js.map +1 -1
  157. package/dist/esm/indexes/basic-index.d.ts +102 -0
  158. package/dist/esm/indexes/basic-index.js +361 -0
  159. package/dist/esm/indexes/basic-index.js.map +1 -0
  160. package/dist/esm/indexes/btree-index.d.ts +1 -1
  161. package/dist/esm/indexes/btree-index.js.map +1 -1
  162. package/dist/esm/indexes/index-options.d.ts +8 -9
  163. package/dist/esm/indexes/index-registry.d.ts +61 -0
  164. package/dist/esm/indexes/index-registry.js +89 -0
  165. package/dist/esm/indexes/index-registry.js.map +1 -0
  166. package/dist/esm/local-only.js +5 -0
  167. package/dist/esm/local-only.js.map +1 -1
  168. package/dist/esm/query/builder/functions.d.ts +25 -3
  169. package/dist/esm/query/builder/functions.js +27 -11
  170. package/dist/esm/query/builder/functions.js.map +1 -1
  171. package/dist/esm/query/builder/index.d.ts +4 -3
  172. package/dist/esm/query/builder/index.js +201 -40
  173. package/dist/esm/query/builder/index.js.map +1 -1
  174. package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
  175. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  176. package/dist/esm/query/builder/types.d.ts +84 -19
  177. package/dist/esm/query/compiler/evaluators.js +51 -0
  178. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  179. package/dist/esm/query/compiler/group-by.d.ts +4 -2
  180. package/dist/esm/query/compiler/group-by.js +101 -29
  181. package/dist/esm/query/compiler/group-by.js.map +1 -1
  182. package/dist/esm/query/compiler/index.d.ts +30 -2
  183. package/dist/esm/query/compiler/index.js +285 -13
  184. package/dist/esm/query/compiler/index.js.map +1 -1
  185. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  186. package/dist/esm/query/compiler/order-by.js +30 -11
  187. package/dist/esm/query/compiler/order-by.js.map +1 -1
  188. package/dist/esm/query/compiler/select.js +8 -0
  189. package/dist/esm/query/compiler/select.js.map +1 -1
  190. package/dist/esm/query/index.d.ts +2 -1
  191. package/dist/esm/query/ir.d.ts +21 -1
  192. package/dist/esm/query/ir.js +18 -1
  193. package/dist/esm/query/ir.js.map +1 -1
  194. package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
  195. package/dist/esm/query/live/collection-config-builder.js +503 -7
  196. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  197. package/dist/esm/query/live/types.d.ts +3 -3
  198. package/dist/esm/query/live/utils.d.ts +1 -0
  199. package/dist/esm/query/live/utils.js +43 -3
  200. package/dist/esm/query/live/utils.js.map +1 -1
  201. package/dist/esm/query/live-query-collection.d.ts +9 -6
  202. package/dist/esm/query/live-query-collection.js.map +1 -1
  203. package/dist/esm/query/query-once.d.ts +7 -5
  204. package/dist/esm/query/query-once.js.map +1 -1
  205. package/dist/esm/query/subset-dedupe.js +9 -3
  206. package/dist/esm/query/subset-dedupe.js.map +1 -1
  207. package/dist/esm/types.d.ts +42 -8
  208. package/dist/esm/utils/array-utils.d.ts +16 -0
  209. package/dist/esm/utils/array-utils.js +27 -0
  210. package/dist/esm/utils/array-utils.js.map +1 -0
  211. package/dist/esm/utils/comparison.js +11 -0
  212. package/dist/esm/utils/comparison.js.map +1 -1
  213. package/dist/esm/utils/index-optimization.js +4 -0
  214. package/dist/esm/utils/index-optimization.js.map +1 -1
  215. package/dist/esm/utils.d.ts +6 -1
  216. package/dist/esm/utils.js +7 -9
  217. package/dist/esm/utils.js.map +1 -1
  218. package/dist/esm/virtual-props.d.ts +196 -0
  219. package/dist/esm/virtual-props.js +33 -0
  220. package/dist/esm/virtual-props.js.map +1 -0
  221. package/package.json +2 -2
  222. package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
  223. package/src/collection/change-events.ts +13 -9
  224. package/src/collection/changes.ts +30 -7
  225. package/src/collection/cleanup-queue.ts +105 -0
  226. package/src/collection/events.ts +65 -0
  227. package/src/collection/index.ts +110 -45
  228. package/src/collection/indexes.ts +283 -76
  229. package/src/collection/lifecycle.ts +5 -26
  230. package/src/collection/mutations.ts +21 -0
  231. package/src/collection/state.ts +545 -71
  232. package/src/collection/subscription.ts +7 -0
  233. package/src/collection/sync.ts +137 -0
  234. package/src/collection/transaction-metadata.ts +1 -0
  235. package/src/errors.ts +9 -0
  236. package/src/index.ts +46 -3
  237. package/src/indexes/auto-index.ts +18 -8
  238. package/src/indexes/base-index.ts +2 -10
  239. package/src/indexes/basic-index.ts +507 -0
  240. package/src/indexes/btree-index.ts +1 -1
  241. package/src/indexes/index-options.ts +17 -37
  242. package/src/indexes/index-registry.ts +174 -0
  243. package/src/local-only.ts +7 -0
  244. package/src/query/builder/functions.ts +84 -7
  245. package/src/query/builder/index.ts +329 -9
  246. package/src/query/builder/ref-proxy.ts +22 -4
  247. package/src/query/builder/types.ts +257 -62
  248. package/src/query/compiler/evaluators.ts +57 -0
  249. package/src/query/compiler/group-by.ts +156 -35
  250. package/src/query/compiler/index.ts +445 -15
  251. package/src/query/compiler/order-by.ts +51 -12
  252. package/src/query/compiler/select.ts +9 -0
  253. package/src/query/index.ts +7 -0
  254. package/src/query/ir.ts +23 -2
  255. package/src/query/live/collection-config-builder.ts +809 -9
  256. package/src/query/live/types.ts +10 -4
  257. package/src/query/live/utils.ts +64 -3
  258. package/src/query/live-query-collection.ts +43 -18
  259. package/src/query/query-once.ts +31 -12
  260. package/src/query/subset-dedupe.ts +11 -7
  261. package/src/types.ts +49 -9
  262. package/src/utils/array-utils.ts +49 -0
  263. package/src/utils/comparison.ts +14 -0
  264. package/src/utils/index-optimization.ts +4 -0
  265. package/src/utils.ts +12 -9
  266. package/src/virtual-props.ts +282 -0
  267. package/dist/cjs/indexes/lazy-index.cjs +0 -190
  268. package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
  269. package/dist/cjs/indexes/lazy-index.d.cts +0 -96
  270. package/dist/esm/indexes/lazy-index.d.ts +0 -96
  271. package/dist/esm/indexes/lazy-index.js +0 -190
  272. package/dist/esm/indexes/lazy-index.js.map +0 -1
  273. package/src/indexes/lazy-index.ts +0 -251
package/dist/esm/utils.js CHANGED
@@ -61,8 +61,8 @@ function deepEqualsInternal(a, b, visited) {
61
61
  return false;
62
62
  }
63
63
  if (isTemporal(a) && isTemporal(b)) {
64
- const aTag = getStringTag(a);
65
- const bTag = getStringTag(b);
64
+ const aTag = a[Symbol.toStringTag];
65
+ const bTag = b[Symbol.toStringTag];
66
66
  if (aTag !== bTag) return false;
67
67
  if (typeof a.equals === `function`) {
68
68
  return a.equals(b);
@@ -102,7 +102,7 @@ function deepEqualsInternal(a, b, visited) {
102
102
  }
103
103
  return false;
104
104
  }
105
- const temporalTypes = [
105
+ const temporalTypes = /* @__PURE__ */ new Set([
106
106
  `Temporal.Duration`,
107
107
  `Temporal.Instant`,
108
108
  `Temporal.PlainDate`,
@@ -111,13 +111,11 @@ const temporalTypes = [
111
111
  `Temporal.PlainTime`,
112
112
  `Temporal.PlainYearMonth`,
113
113
  `Temporal.ZonedDateTime`
114
- ];
115
- function getStringTag(a) {
116
- return a[Symbol.toStringTag];
117
- }
114
+ ]);
118
115
  function isTemporal(a) {
119
- const tag = getStringTag(a);
120
- return typeof tag === `string` && temporalTypes.includes(tag);
116
+ if (a == null || typeof a !== `object`) return false;
117
+ const tag = a[Symbol.toStringTag];
118
+ return typeof tag === `string` && temporalTypes.has(tag);
121
119
  }
122
120
  const DEFAULT_COMPARE_OPTIONS = {
123
121
  direction: `asc`,
@@ -1 +1 @@
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 // Symmetric check: if b is Date but a is not, they're not equal\n if (b instanceof Date) return false\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 // Symmetric check: if b is RegExp but a is not, they're not equal\n if (b instanceof RegExp) return false\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 // Symmetric check: if b is Map but a is not, they're not equal\n if (b instanceof Map) return false\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 // Symmetric check: if b is Set but a is not, they're not equal\n if (b instanceof Set) return false\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 // Symmetric check: if b is TypedArray but a is not, they're not equal\n if (\n ArrayBuffer.isView(b) &&\n !(b instanceof DataView) &&\n !ArrayBuffer.isView(a)\n ) {\n return false\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 // Symmetric check: if b is Temporal but a is not, they're not equal\n if (isTemporal(b)) return false\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 // Symmetric check: if b is array but a is not, they're not equal\n if (Array.isArray(b)) return false\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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"}
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 // Symmetric check: if b is Date but a is not, they're not equal\n if (b instanceof Date) return false\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 // Symmetric check: if b is RegExp but a is not, they're not equal\n if (b instanceof RegExp) return false\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 // Symmetric check: if b is Map but a is not, they're not equal\n if (b instanceof Map) return false\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 // Symmetric check: if b is Set but a is not, they're not equal\n if (b instanceof Set) return false\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 // Symmetric check: if b is TypedArray but a is not, they're not equal\n if (\n ArrayBuffer.isView(b) &&\n !(b instanceof DataView) &&\n !ArrayBuffer.isView(a)\n ) {\n return false\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 = a[Symbol.toStringTag]\n const bTag = b[Symbol.toStringTag]\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 // Symmetric check: if b is Temporal but a is not, they're not equal\n if (isTemporal(b)) return false\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 // Symmetric check: if b is array but a is not, they're not equal\n if (Array.isArray(b)) return false\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 = new Set([\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\nexport interface TemporalLike {\n [Symbol.toStringTag]: string\n toString: () => string\n equals?: (other: unknown) => boolean\n}\n\n/** Checks if the value is a Temporal object by checking for the Temporal brand */\nexport function isTemporal(a: unknown): a is TemporalLike {\n if (a == null || typeof a !== `object`) return false\n const tag = (a as Record<symbol, unknown>)[Symbol.toStringTag]\n return typeof tag === `string` && temporalTypes.has(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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"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Virtual Properties for TanStack DB
3
+ *
4
+ * Virtual properties are computed, read-only properties that provide metadata about rows
5
+ * (sync status, source, selection state) without being part of the persisted data model.
6
+ *
7
+ * Virtual properties are prefixed with `$` to distinguish them from user data fields.
8
+ * User schemas should not include `$`-prefixed fields as they are reserved.
9
+ */
10
+ /**
11
+ * Origin of the last confirmed change to a row, from the current client's perspective.
12
+ *
13
+ * - `'local'`: The change originated from this client (e.g., a mutation made here)
14
+ * - `'remote'`: The change was received via sync from another client/server
15
+ *
16
+ * Note: This reflects the client's perspective, not the original creator.
17
+ * User A creates order → $origin = 'local' on User A's client
18
+ * Order syncs to server
19
+ * User B receives order → $origin = 'remote' on User B's client
20
+ */
21
+ export type VirtualOrigin = 'local' | 'remote';
22
+ /**
23
+ * Virtual properties available on every row in TanStack DB collections.
24
+ *
25
+ * These properties are:
26
+ * - Computed (not stored in the data model)
27
+ * - Read-only (cannot be mutated directly)
28
+ * - Available in queries (WHERE, ORDER BY, SELECT)
29
+ * - Included when spreading rows (`...user`)
30
+ *
31
+ * @template TKey - The type of the row's key (string or number)
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * // Accessing virtual properties on a row
36
+ * const user = collection.get('user-1')
37
+ * if (user.$synced) {
38
+ * console.log('Confirmed by backend')
39
+ * }
40
+ * if (user.$origin === 'local') {
41
+ * console.log('Created/modified locally')
42
+ * }
43
+ * ```
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Using virtual properties in queries
48
+ * const confirmedOrders = createLiveQueryCollection({
49
+ * query: (q) => q
50
+ * .from({ order: orders })
51
+ * .where(({ order }) => eq(order.$synced, true))
52
+ * })
53
+ * ```
54
+ */
55
+ export interface VirtualRowProps<TKey extends string | number = string | number> {
56
+ /**
57
+ * Whether this row reflects confirmed state from the backend.
58
+ *
59
+ * - `true`: Row is confirmed by the backend (no pending optimistic mutations)
60
+ * - `false`: Row has pending optimistic mutations that haven't been confirmed
61
+ *
62
+ * For local-only collections (no sync), this is always `true`.
63
+ * For live query collections, this is passed through from the source collection.
64
+ */
65
+ readonly $synced: boolean;
66
+ /**
67
+ * Origin of the last confirmed change to this row, from the current client's perspective.
68
+ *
69
+ * - `'local'`: The change originated from this client
70
+ * - `'remote'`: The change was received via sync
71
+ *
72
+ * For local-only collections, this is always `'local'`.
73
+ * For live query collections, this is passed through from the source collection.
74
+ */
75
+ readonly $origin: VirtualOrigin;
76
+ /**
77
+ * The row's key (primary identifier).
78
+ *
79
+ * This is the same value returned by `collection.config.getKey(row)`.
80
+ * Useful when you need the key in projections or computations.
81
+ */
82
+ readonly $key: TKey;
83
+ /**
84
+ * The ID of the source collection this row originated from.
85
+ *
86
+ * In joins, this can help identify which collection each row came from.
87
+ * For live query collections, this is the ID of the upstream collection.
88
+ */
89
+ readonly $collectionId: string;
90
+ }
91
+ /**
92
+ * Adds virtual properties to a row type.
93
+ *
94
+ * @template T - The base row type
95
+ * @template TKey - The type of the row's key
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * type User = { id: string; name: string }
100
+ * type UserWithVirtual = WithVirtualProps<User, string>
101
+ * // { id: string; name: string; $synced: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string }
102
+ * ```
103
+ */
104
+ export type WithVirtualProps<T extends object, TKey extends string | number = string | number> = T & VirtualRowProps<TKey>;
105
+ /**
106
+ * Extracts the base type from a type that may have virtual properties.
107
+ * Useful when you need to work with the raw data without virtual properties.
108
+ *
109
+ * @template T - The type that may include virtual properties
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * type UserWithVirtual = { id: string; name: string; $synced: boolean; $origin: 'local' | 'remote' }
114
+ * type User = WithoutVirtualProps<UserWithVirtual>
115
+ * // { id: string; name: string }
116
+ * ```
117
+ */
118
+ export type WithoutVirtualProps<T> = Omit<T, keyof VirtualRowProps>;
119
+ /**
120
+ * Checks if a value has virtual properties attached.
121
+ *
122
+ * @param value - The value to check
123
+ * @returns true if the value has virtual properties
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * if (hasVirtualProps(row)) {
128
+ * console.log('Synced:', row.$synced)
129
+ * }
130
+ * ```
131
+ */
132
+ export declare function hasVirtualProps(value: unknown): value is VirtualRowProps<string | number>;
133
+ /**
134
+ * Creates virtual properties for a row in a source collection.
135
+ *
136
+ * This is the internal function used by collections to add virtual properties
137
+ * to rows when emitting change messages.
138
+ *
139
+ * @param key - The row's key
140
+ * @param collectionId - The collection's ID
141
+ * @param isSynced - Whether the row is synced (not optimistic)
142
+ * @param origin - Whether the change was local or remote
143
+ * @returns Virtual properties object to merge with the row
144
+ *
145
+ * @internal
146
+ */
147
+ export declare function createVirtualProps<TKey extends string | number>(key: TKey, collectionId: string, isSynced: boolean, origin: VirtualOrigin): VirtualRowProps<TKey>;
148
+ /**
149
+ * Enriches a row with virtual properties using the "add-if-missing" pattern.
150
+ *
151
+ * If the row already has virtual properties (from an upstream collection),
152
+ * they are preserved. If not, new virtual properties are computed and added.
153
+ *
154
+ * This is the key function that enables pass-through semantics for nested
155
+ * live query collections.
156
+ *
157
+ * @param row - The row to enrich
158
+ * @param key - The row's key
159
+ * @param collectionId - The collection's ID
160
+ * @param computeSynced - Function to compute $synced if missing
161
+ * @param computeOrigin - Function to compute $origin if missing
162
+ * @returns The row with virtual properties (possibly the same object if already present)
163
+ *
164
+ * @internal
165
+ */
166
+ export declare function enrichRowWithVirtualProps<T extends object, TKey extends string | number>(row: T, key: TKey, collectionId: string, computeSynced: () => boolean, computeOrigin: () => VirtualOrigin): WithVirtualProps<T, TKey>;
167
+ /**
168
+ * Computes aggregate virtual properties for a group of rows.
169
+ *
170
+ * For aggregates:
171
+ * - `$synced`: true if ALL rows in the group are synced; false if ANY row is optimistic
172
+ * - `$origin`: 'local' if ANY row in the group is local; otherwise 'remote'
173
+ *
174
+ * @param rows - The rows in the group
175
+ * @param groupKey - The group key
176
+ * @param collectionId - The collection ID
177
+ * @returns Virtual properties for the aggregate row
178
+ *
179
+ * @internal
180
+ */
181
+ export declare function computeAggregateVirtualProps<TKey extends string | number>(rows: Array<Partial<VirtualRowProps<string | number>>>, groupKey: TKey, collectionId: string): VirtualRowProps<TKey>;
182
+ /**
183
+ * List of virtual property names for iteration and checking.
184
+ * @internal
185
+ */
186
+ export declare const VIRTUAL_PROP_NAMES: readonly ["$synced", "$origin", "$key", "$collectionId"];
187
+ /**
188
+ * Checks if a property name is a virtual property.
189
+ * @internal
190
+ */
191
+ export declare function isVirtualPropName(name: string): boolean;
192
+ /**
193
+ * Checks whether a property path references a virtual property.
194
+ * @internal
195
+ */
196
+ export declare function hasVirtualPropPath(path: Array<string>): boolean;
@@ -0,0 +1,33 @@
1
+ function hasVirtualProps(value) {
2
+ return typeof value === "object" && value !== null && VIRTUAL_PROP_NAMES.every((name) => name in value);
3
+ }
4
+ function enrichRowWithVirtualProps(row, key, collectionId, computeSynced, computeOrigin) {
5
+ const existingRow = row;
6
+ return {
7
+ ...row,
8
+ $synced: existingRow.$synced ?? computeSynced(),
9
+ $origin: existingRow.$origin ?? computeOrigin(),
10
+ $key: existingRow.$key ?? key,
11
+ $collectionId: existingRow.$collectionId ?? collectionId
12
+ };
13
+ }
14
+ const VIRTUAL_PROP_NAMES = [
15
+ "$synced",
16
+ "$origin",
17
+ "$key",
18
+ "$collectionId"
19
+ ];
20
+ function isVirtualPropName(name) {
21
+ return VIRTUAL_PROP_NAMES.includes(name);
22
+ }
23
+ function hasVirtualPropPath(path) {
24
+ return path.some((segment) => isVirtualPropName(segment));
25
+ }
26
+ export {
27
+ VIRTUAL_PROP_NAMES,
28
+ enrichRowWithVirtualProps,
29
+ hasVirtualPropPath,
30
+ hasVirtualProps,
31
+ isVirtualPropName
32
+ };
33
+ //# sourceMappingURL=virtual-props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtual-props.js","sources":["../../src/virtual-props.ts"],"sourcesContent":["/**\n * Virtual Properties for TanStack DB\n *\n * Virtual properties are computed, read-only properties that provide metadata about rows\n * (sync status, source, selection state) without being part of the persisted data model.\n *\n * Virtual properties are prefixed with `$` to distinguish them from user data fields.\n * User schemas should not include `$`-prefixed fields as they are reserved.\n */\n\n/**\n * Origin of the last confirmed change to a row, from the current client's perspective.\n *\n * - `'local'`: The change originated from this client (e.g., a mutation made here)\n * - `'remote'`: The change was received via sync from another client/server\n *\n * Note: This reflects the client's perspective, not the original creator.\n * User A creates order → $origin = 'local' on User A's client\n * Order syncs to server\n * User B receives order → $origin = 'remote' on User B's client\n */\nexport type VirtualOrigin = 'local' | 'remote'\n\n/**\n * Virtual properties available on every row in TanStack DB collections.\n *\n * These properties are:\n * - Computed (not stored in the data model)\n * - Read-only (cannot be mutated directly)\n * - Available in queries (WHERE, ORDER BY, SELECT)\n * - Included when spreading rows (`...user`)\n *\n * @template TKey - The type of the row's key (string or number)\n *\n * @example\n * ```typescript\n * // Accessing virtual properties on a row\n * const user = collection.get('user-1')\n * if (user.$synced) {\n * console.log('Confirmed by backend')\n * }\n * if (user.$origin === 'local') {\n * console.log('Created/modified locally')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Using virtual properties in queries\n * const confirmedOrders = createLiveQueryCollection({\n * query: (q) => q\n * .from({ order: orders })\n * .where(({ order }) => eq(order.$synced, true))\n * })\n * ```\n */\nexport interface VirtualRowProps<\n TKey extends string | number = string | number,\n> {\n /**\n * Whether this row reflects confirmed state from the backend.\n *\n * - `true`: Row is confirmed by the backend (no pending optimistic mutations)\n * - `false`: Row has pending optimistic mutations that haven't been confirmed\n *\n * For local-only collections (no sync), this is always `true`.\n * For live query collections, this is passed through from the source collection.\n */\n readonly $synced: boolean\n\n /**\n * Origin of the last confirmed change to this row, from the current client's perspective.\n *\n * - `'local'`: The change originated from this client\n * - `'remote'`: The change was received via sync\n *\n * For local-only collections, this is always `'local'`.\n * For live query collections, this is passed through from the source collection.\n */\n readonly $origin: VirtualOrigin\n\n /**\n * The row's key (primary identifier).\n *\n * This is the same value returned by `collection.config.getKey(row)`.\n * Useful when you need the key in projections or computations.\n */\n readonly $key: TKey\n\n /**\n * The ID of the source collection this row originated from.\n *\n * In joins, this can help identify which collection each row came from.\n * For live query collections, this is the ID of the upstream collection.\n */\n readonly $collectionId: string\n}\n\n/**\n * Adds virtual properties to a row type.\n *\n * @template T - The base row type\n * @template TKey - The type of the row's key\n *\n * @example\n * ```typescript\n * type User = { id: string; name: string }\n * type UserWithVirtual = WithVirtualProps<User, string>\n * // { id: string; name: string; $synced: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string }\n * ```\n */\nexport type WithVirtualProps<\n T extends object,\n TKey extends string | number = string | number,\n> = T & VirtualRowProps<TKey>\n\n/**\n * Extracts the base type from a type that may have virtual properties.\n * Useful when you need to work with the raw data without virtual properties.\n *\n * @template T - The type that may include virtual properties\n *\n * @example\n * ```typescript\n * type UserWithVirtual = { id: string; name: string; $synced: boolean; $origin: 'local' | 'remote' }\n * type User = WithoutVirtualProps<UserWithVirtual>\n * // { id: string; name: string }\n * ```\n */\nexport type WithoutVirtualProps<T> = Omit<T, keyof VirtualRowProps>\n\n/**\n * Checks if a value has virtual properties attached.\n *\n * @param value - The value to check\n * @returns true if the value has virtual properties\n *\n * @example\n * ```typescript\n * if (hasVirtualProps(row)) {\n * console.log('Synced:', row.$synced)\n * }\n * ```\n */\nexport function hasVirtualProps(\n value: unknown,\n): value is VirtualRowProps<string | number> {\n return (\n typeof value === 'object' &&\n value !== null &&\n VIRTUAL_PROP_NAMES.every((name) => name in value)\n )\n}\n\n/**\n * Creates virtual properties for a row in a source collection.\n *\n * This is the internal function used by collections to add virtual properties\n * to rows when emitting change messages.\n *\n * @param key - The row's key\n * @param collectionId - The collection's ID\n * @param isSynced - Whether the row is synced (not optimistic)\n * @param origin - Whether the change was local or remote\n * @returns Virtual properties object to merge with the row\n *\n * @internal\n */\nexport function createVirtualProps<TKey extends string | number>(\n key: TKey,\n collectionId: string,\n isSynced: boolean,\n origin: VirtualOrigin,\n): VirtualRowProps<TKey> {\n return {\n $synced: isSynced,\n $origin: origin,\n $key: key,\n $collectionId: collectionId,\n }\n}\n\n/**\n * Enriches a row with virtual properties using the \"add-if-missing\" pattern.\n *\n * If the row already has virtual properties (from an upstream collection),\n * they are preserved. If not, new virtual properties are computed and added.\n *\n * This is the key function that enables pass-through semantics for nested\n * live query collections.\n *\n * @param row - The row to enrich\n * @param key - The row's key\n * @param collectionId - The collection's ID\n * @param computeSynced - Function to compute $synced if missing\n * @param computeOrigin - Function to compute $origin if missing\n * @returns The row with virtual properties (possibly the same object if already present)\n *\n * @internal\n */\nexport function enrichRowWithVirtualProps<\n T extends object,\n TKey extends string | number,\n>(\n row: T,\n key: TKey,\n collectionId: string,\n computeSynced: () => boolean,\n computeOrigin: () => VirtualOrigin,\n): WithVirtualProps<T, TKey> {\n // Use nullish coalescing to preserve existing virtual properties (pass-through)\n // This is the \"add-if-missing\" pattern described in the RFC\n const existingRow = row as Partial<VirtualRowProps<TKey>>\n\n return {\n ...row,\n $synced: existingRow.$synced ?? computeSynced(),\n $origin: existingRow.$origin ?? computeOrigin(),\n $key: existingRow.$key ?? key,\n $collectionId: existingRow.$collectionId ?? collectionId,\n } as WithVirtualProps<T, TKey>\n}\n\n/**\n * Computes aggregate virtual properties for a group of rows.\n *\n * For aggregates:\n * - `$synced`: true if ALL rows in the group are synced; false if ANY row is optimistic\n * - `$origin`: 'local' if ANY row in the group is local; otherwise 'remote'\n *\n * @param rows - The rows in the group\n * @param groupKey - The group key\n * @param collectionId - The collection ID\n * @returns Virtual properties for the aggregate row\n *\n * @internal\n */\nexport function computeAggregateVirtualProps<TKey extends string | number>(\n rows: Array<Partial<VirtualRowProps<string | number>>>,\n groupKey: TKey,\n collectionId: string,\n): VirtualRowProps<TKey> {\n // $synced = true only if ALL rows are synced (false if ANY is optimistic)\n const allSynced = rows.every((row) => row.$synced ?? true)\n\n // $origin = 'local' if ANY row is local (consistent with \"local influence\" semantics)\n const hasLocal = rows.some((row) => row.$origin === 'local')\n\n return {\n $synced: allSynced,\n $origin: hasLocal ? 'local' : 'remote',\n $key: groupKey,\n $collectionId: collectionId,\n }\n}\n\n/**\n * List of virtual property names for iteration and checking.\n * @internal\n */\nexport const VIRTUAL_PROP_NAMES = [\n '$synced',\n '$origin',\n '$key',\n '$collectionId',\n] as const\n\n/**\n * Checks if a property name is a virtual property.\n * @internal\n */\nexport function isVirtualPropName(name: string): boolean {\n return VIRTUAL_PROP_NAMES.includes(name as any)\n}\n\n/**\n * Checks whether a property path references a virtual property.\n * @internal\n */\nexport function hasVirtualPropPath(path: Array<string>): boolean {\n return path.some((segment) => isVirtualPropName(segment))\n}\n"],"names":[],"mappings":"AAgJO,SAAS,gBACd,OAC2C;AAC3C,SACE,OAAO,UAAU,YACjB,UAAU,QACV,mBAAmB,MAAM,CAAC,SAAS,QAAQ,KAAK;AAEpD;AAgDO,SAAS,0BAId,KACA,KACA,cACA,eACA,eAC2B;AAG3B,QAAM,cAAc;AAEpB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS,YAAY,WAAW,cAAA;AAAA,IAChC,SAAS,YAAY,WAAW,cAAA;AAAA,IAChC,MAAM,YAAY,QAAQ;AAAA,IAC1B,eAAe,YAAY,iBAAiB;AAAA,EAAA;AAEhD;AAuCO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,SAAS,kBAAkB,MAAuB;AACvD,SAAO,mBAAmB,SAAS,IAAW;AAChD;AAMO,SAAS,mBAAmB,MAA8B;AAC/D,SAAO,KAAK,KAAK,CAAC,YAAY,kBAAkB,OAAO,CAAC;AAC1D;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
- "version": "0.5.33",
3
+ "version": "0.6.0",
4
4
  "description": "A reactive client store for building super fast apps on sync",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "dependencies": {
41
41
  "@standard-schema/spec": "^1.1.0",
42
42
  "@tanstack/pacer-lite": "^0.2.1",
43
- "@tanstack/db-ivm": "0.1.17"
43
+ "@tanstack/db-ivm": "0.1.18"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "typescript": ">=4.7"
@@ -78,7 +78,7 @@ onInsert: async ({ transaction }) => {
78
78
 
79
79
  ## Utility Methods (`collection.utils`)
80
80
 
81
- - `awaitTxId(txid, timeout?)` -- wait for txid in Electric stream; default timeout 30s
81
+ - `awaitTxId(txid, timeout?)` -- wait for txid in Electric stream; default timeout 5s
82
82
  - `awaitMatch(matchFn, timeout?)` -- wait for message matching predicate; default timeout 3000ms
83
83
 
84
84
  ### Helper Exports
@@ -22,6 +22,7 @@ import type {
22
22
  import type { CollectionImpl } from './index.js'
23
23
  import type { SingleRowRefProxy } from '../query/builder/ref-proxy'
24
24
  import type { BasicExpression, OrderBy } from '../query/ir.js'
25
+ import type { WithVirtualProps } from '../virtual-props.js'
25
26
 
26
27
  /**
27
28
  * Returns the current state of the collection as an array of changes
@@ -58,14 +59,14 @@ export function currentStateAsChanges<
58
59
  T extends object,
59
60
  TKey extends string | number,
60
61
  >(
61
- collection: CollectionLike<T, TKey>,
62
+ collection: CollectionLike<WithVirtualProps<T, TKey>, TKey>,
62
63
  options: CurrentStateAsChangesOptions = {},
63
- ): Array<ChangeMessage<T>> | void {
64
+ ): Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> | void {
64
65
  // Helper function to collect filtered results
65
66
  const collectFilteredResults = (
66
- filterFn?: (value: T) => boolean,
67
- ): Array<ChangeMessage<T>> => {
68
- const result: Array<ChangeMessage<T>> = []
67
+ filterFn?: (value: WithVirtualProps<T, TKey>) => boolean,
68
+ ): Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> => {
69
+ const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
69
70
  for (const [key, value] of collection.entries()) {
70
71
  // If no filter function is provided, include all items
71
72
  if (filterFn?.(value) ?? true) {
@@ -106,7 +107,7 @@ export function currentStateAsChanges<
106
107
  }
107
108
 
108
109
  // Convert keys to change messages
109
- const result: Array<ChangeMessage<T>> = []
110
+ const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
110
111
  for (const key of orderedKeys) {
111
112
  const value = collection.get(key)
112
113
  if (value !== undefined) {
@@ -138,7 +139,7 @@ export function currentStateAsChanges<
138
139
 
139
140
  if (optimizationResult.canOptimize) {
140
141
  // Use index optimization
141
- const result: Array<ChangeMessage<T>> = []
142
+ const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
142
143
  for (const key of optimizationResult.matchingKeys) {
143
144
  const value = collection.get(key)
144
145
  if (value !== undefined) {
@@ -241,9 +242,12 @@ export function createFilterFunctionFromExpression<T extends object>(
241
242
  * @param options - The subscription options containing the where clause
242
243
  * @returns A filtered callback function
243
244
  */
244
- export function createFilteredCallback<T extends object>(
245
+ export function createFilteredCallback<
246
+ T extends object,
247
+ TKey extends string | number = string | number,
248
+ >(
245
249
  originalCallback: (changes: Array<ChangeMessage<T>>) => void,
246
- options: SubscribeChangesOptions,
250
+ options: SubscribeChangesOptions<T, TKey>,
247
251
  ): (changes: Array<ChangeMessage<T>>) => void {
248
252
  const filterFn = createFilterFunctionFromExpression(options.whereExpression!)
249
253
 
@@ -10,6 +10,8 @@ import type { CollectionLifecycleManager } from './lifecycle.js'
10
10
  import type { CollectionSyncManager } from './sync.js'
11
11
  import type { CollectionEventsManager } from './events.js'
12
12
  import type { CollectionImpl } from './index.js'
13
+ import type { CollectionStateManager } from './state.js'
14
+ import type { WithVirtualProps } from '../virtual-props.js'
13
15
 
14
16
  export class CollectionChangesManager<
15
17
  TOutput extends object = Record<string, unknown>,
@@ -21,6 +23,7 @@ export class CollectionChangesManager<
21
23
  private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
22
24
  private events!: CollectionEventsManager
23
25
  private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
26
+ private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
24
27
 
25
28
  public activeSubscribersCount = 0
26
29
  public changeSubscriptions = new Set<CollectionSubscription>()
@@ -37,11 +40,13 @@ export class CollectionChangesManager<
37
40
  sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
38
41
  events: CollectionEventsManager
39
42
  collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
43
+ state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
40
44
  }) {
41
45
  this.lifecycle = deps.lifecycle
42
46
  this.sync = deps.sync
43
47
  this.events = deps.events
44
48
  this.collection = deps.collection
49
+ this.state = deps.state
45
50
  }
46
51
 
47
52
  /**
@@ -55,6 +60,16 @@ export class CollectionChangesManager<
55
60
  }
56
61
  }
57
62
 
63
+ /**
64
+ * Enriches a change message with virtual properties ($synced, $origin, $key, $collectionId).
65
+ * Uses the "add-if-missing" pattern to preserve virtual properties from upstream collections.
66
+ */
67
+ private enrichChangeWithVirtualProps(
68
+ change: ChangeMessage<TOutput, TKey>,
69
+ ): ChangeMessage<WithVirtualProps<TOutput, TKey>, TKey> {
70
+ return this.state.enrichChangeMessage(change)
71
+ }
72
+
58
73
  /**
59
74
  * Emit events either immediately or batch them for later emission
60
75
  */
@@ -70,26 +85,32 @@ export class CollectionChangesManager<
70
85
  }
71
86
 
72
87
  // Either we're not batching, or we're forcing emission (user action or ending batch cycle)
73
- let eventsToEmit = changes
88
+ let rawEvents = changes
74
89
 
75
90
  if (forceEmit) {
76
91
  // Force emit is used to end a batch (e.g. after a sync commit). Combine any
77
92
  // buffered optimistic events with the final changes so subscribers see the
78
93
  // whole picture, even if the sync diff is empty.
79
94
  if (this.batchedEvents.length > 0) {
80
- eventsToEmit = [...this.batchedEvents, ...changes]
95
+ rawEvents = [...this.batchedEvents, ...changes]
81
96
  }
82
97
  this.batchedEvents = []
83
98
  this.shouldBatchEvents = false
84
99
  }
85
100
 
86
- if (eventsToEmit.length === 0) {
101
+ if (rawEvents.length === 0) {
87
102
  return
88
103
  }
89
104
 
105
+ // Enrich all change messages with virtual properties
106
+ // This uses the "add-if-missing" pattern to preserve pass-through semantics
107
+ const enrichedEvents: Array<
108
+ ChangeMessage<WithVirtualProps<TOutput, TKey>, TKey>
109
+ > = rawEvents.map((change) => this.enrichChangeWithVirtualProps(change))
110
+
90
111
  // Emit to all listeners
91
112
  for (const subscription of this.changeSubscriptions) {
92
- subscription.emitEvents(eventsToEmit)
113
+ subscription.emitEvents(enrichedEvents)
93
114
  }
94
115
  }
95
116
 
@@ -97,8 +118,10 @@ export class CollectionChangesManager<
97
118
  * Subscribe to changes in the collection
98
119
  */
99
120
  public subscribeChanges(
100
- callback: (changes: Array<ChangeMessage<TOutput>>) => void,
101
- options: SubscribeChangesOptions<TOutput> = {},
121
+ callback: (
122
+ changes: Array<ChangeMessage<WithVirtualProps<TOutput, TKey>>>,
123
+ ) => void,
124
+ options: SubscribeChangesOptions<TOutput, TKey> = {},
102
125
  ): CollectionSubscription {
103
126
  // Start sync and track subscriber
104
127
  this.addSubscriber()
@@ -113,7 +136,7 @@ export class CollectionChangesManager<
113
136
  const { where, ...opts } = options
114
137
  let whereExpression = opts.whereExpression
115
138
  if (where) {
116
- const proxy = createSingleRowRefProxy<TOutput>()
139
+ const proxy = createSingleRowRefProxy<WithVirtualProps<TOutput, TKey>>()
117
140
  const result = where(proxy)
118
141
  whereExpression = toExpression(result)
119
142
  }
@@ -0,0 +1,105 @@
1
+ type CleanupTask = {
2
+ executeAt: number
3
+ callback: () => void
4
+ }
5
+
6
+ /**
7
+ * Batches many GC registrations behind a single shared timeout.
8
+ */
9
+ export class CleanupQueue {
10
+ private static instance: CleanupQueue | null = null
11
+
12
+ private tasks: Map<unknown, CleanupTask> = new Map()
13
+
14
+ private timeoutId: ReturnType<typeof setTimeout> | null = null
15
+ private microtaskScheduled = false
16
+
17
+ private constructor() {}
18
+
19
+ public static getInstance(): CleanupQueue {
20
+ if (!CleanupQueue.instance) {
21
+ CleanupQueue.instance = new CleanupQueue()
22
+ }
23
+ return CleanupQueue.instance
24
+ }
25
+
26
+ /**
27
+ * Queues a cleanup task and defers timeout selection to a microtask so
28
+ * multiple synchronous registrations can share one root timer.
29
+ */
30
+ public schedule(key: unknown, gcTime: number, callback: () => void): void {
31
+ const executeAt = Date.now() + gcTime
32
+ this.tasks.set(key, { executeAt, callback })
33
+
34
+ if (!this.microtaskScheduled) {
35
+ this.microtaskScheduled = true
36
+ Promise.resolve().then(() => {
37
+ this.microtaskScheduled = false
38
+ this.updateTimeout()
39
+ })
40
+ }
41
+ }
42
+
43
+ public cancel(key: unknown): void {
44
+ this.tasks.delete(key)
45
+ }
46
+
47
+ /**
48
+ * Keeps only one active timeout: whichever task is due next.
49
+ */
50
+ private updateTimeout(): void {
51
+ if (this.timeoutId !== null) {
52
+ clearTimeout(this.timeoutId)
53
+ this.timeoutId = null
54
+ }
55
+
56
+ if (this.tasks.size === 0) {
57
+ return
58
+ }
59
+
60
+ let earliestTime = Infinity
61
+ for (const task of this.tasks.values()) {
62
+ if (task.executeAt < earliestTime) {
63
+ earliestTime = task.executeAt
64
+ }
65
+ }
66
+
67
+ const delay = Math.max(0, earliestTime - Date.now())
68
+ this.timeoutId = setTimeout(() => this.process(), delay)
69
+ }
70
+
71
+ /**
72
+ * Runs every task whose deadline has passed, then schedules the next wakeup
73
+ * if there is still pending work.
74
+ */
75
+ private process(): void {
76
+ this.timeoutId = null
77
+ const now = Date.now()
78
+ for (const [key, task] of this.tasks.entries()) {
79
+ if (now >= task.executeAt) {
80
+ this.tasks.delete(key)
81
+ try {
82
+ task.callback()
83
+ } catch (error) {
84
+ console.error('Error in CleanupQueue task:', error)
85
+ }
86
+ }
87
+ }
88
+
89
+ if (this.tasks.size > 0) {
90
+ this.updateTimeout()
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Resets the singleton instance for tests.
96
+ */
97
+ public static resetInstance(): void {
98
+ if (CleanupQueue.instance) {
99
+ if (CleanupQueue.instance.timeoutId !== null) {
100
+ clearTimeout(CleanupQueue.instance.timeoutId)
101
+ }
102
+ CleanupQueue.instance = null
103
+ }
104
+ }
105
+ }