@tanstack/db 0.5.33 → 0.6.1

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 (282) 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 +4 -3
  222. package/skills/db-core/SKILL.md +4 -2
  223. package/skills/db-core/collection-setup/SKILL.md +30 -11
  224. package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
  225. package/skills/db-core/collection-setup/references/powersync-adapter.md +4 -0
  226. package/skills/db-core/collection-setup/references/query-adapter.md +32 -0
  227. package/skills/db-core/custom-adapter/SKILL.md +58 -9
  228. package/skills/db-core/live-queries/SKILL.md +162 -2
  229. package/skills/db-core/mutations-optimistic/SKILL.md +1 -1
  230. package/skills/db-core/persistence/SKILL.md +241 -0
  231. package/skills/meta-framework/SKILL.md +1 -1
  232. package/src/collection/change-events.ts +13 -9
  233. package/src/collection/changes.ts +30 -7
  234. package/src/collection/cleanup-queue.ts +105 -0
  235. package/src/collection/events.ts +65 -0
  236. package/src/collection/index.ts +110 -45
  237. package/src/collection/indexes.ts +283 -76
  238. package/src/collection/lifecycle.ts +5 -26
  239. package/src/collection/mutations.ts +21 -0
  240. package/src/collection/state.ts +545 -71
  241. package/src/collection/subscription.ts +7 -0
  242. package/src/collection/sync.ts +137 -0
  243. package/src/collection/transaction-metadata.ts +1 -0
  244. package/src/errors.ts +9 -0
  245. package/src/index.ts +46 -3
  246. package/src/indexes/auto-index.ts +18 -8
  247. package/src/indexes/base-index.ts +2 -10
  248. package/src/indexes/basic-index.ts +507 -0
  249. package/src/indexes/btree-index.ts +1 -1
  250. package/src/indexes/index-options.ts +17 -37
  251. package/src/indexes/index-registry.ts +174 -0
  252. package/src/local-only.ts +7 -0
  253. package/src/query/builder/functions.ts +84 -7
  254. package/src/query/builder/index.ts +329 -9
  255. package/src/query/builder/ref-proxy.ts +22 -4
  256. package/src/query/builder/types.ts +257 -62
  257. package/src/query/compiler/evaluators.ts +57 -0
  258. package/src/query/compiler/group-by.ts +156 -35
  259. package/src/query/compiler/index.ts +445 -15
  260. package/src/query/compiler/order-by.ts +51 -12
  261. package/src/query/compiler/select.ts +9 -0
  262. package/src/query/index.ts +7 -0
  263. package/src/query/ir.ts +23 -2
  264. package/src/query/live/collection-config-builder.ts +809 -9
  265. package/src/query/live/types.ts +10 -4
  266. package/src/query/live/utils.ts +64 -3
  267. package/src/query/live-query-collection.ts +43 -18
  268. package/src/query/query-once.ts +31 -12
  269. package/src/query/subset-dedupe.ts +11 -7
  270. package/src/types.ts +49 -9
  271. package/src/utils/array-utils.ts +49 -0
  272. package/src/utils/comparison.ts +14 -0
  273. package/src/utils/index-optimization.ts +4 -0
  274. package/src/utils.ts +12 -9
  275. package/src/virtual-props.ts +282 -0
  276. package/dist/cjs/indexes/lazy-index.cjs +0 -190
  277. package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
  278. package/dist/cjs/indexes/lazy-index.d.cts +0 -96
  279. package/dist/esm/indexes/lazy-index.d.ts +0 -96
  280. package/dist/esm/indexes/lazy-index.js +0 -190
  281. package/dist/esm/indexes/lazy-index.js.map +0 -1
  282. 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.1",
4
4
  "description": "A reactive client store for building super fast apps on sync",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -12,7 +12,8 @@
12
12
  "homepage": "https://tanstack.com/db",
13
13
  "keywords": [
14
14
  "optimistic",
15
- "typescript"
15
+ "typescript",
16
+ "tanstack-intent"
16
17
  ],
17
18
  "type": "module",
18
19
  "main": "dist/cjs/index.cjs",
@@ -40,7 +41,7 @@
40
41
  "dependencies": {
41
42
  "@standard-schema/spec": "^1.1.0",
42
43
  "@tanstack/pacer-lite": "^0.2.1",
43
- "@tanstack/db-ivm": "0.1.17"
44
+ "@tanstack/db-ivm": "0.1.18"
44
45
  },
45
46
  "peerDependencies": {
46
47
  "typescript": ">=4.7"
@@ -10,7 +10,7 @@ description: >
10
10
  createPacedMutations. Entry point for all TanStack DB skills.
11
11
  type: core
12
12
  library: db
13
- library_version: '0.5.30'
13
+ library_version: '0.6.0'
14
14
  ---
15
15
 
16
16
  # TanStack DB — Core Concepts
@@ -33,6 +33,7 @@ hooks. In framework projects, import from the framework package directly.
33
33
  | Query data with where, join, groupBy, select | db-core/live-queries/SKILL.md |
34
34
  | Insert, update, delete with optimistic UI | db-core/mutations-optimistic/SKILL.md |
35
35
  | Build a custom sync adapter | db-core/custom-adapter/SKILL.md |
36
+ | Persist collections to SQLite (offline cache) | db-core/persistence/SKILL.md |
36
37
  | Preload collections in route loaders | meta-framework/SKILL.md |
37
38
  | Add offline transaction queueing | offline/SKILL.md (in @tanstack/offline-transactions) |
38
39
 
@@ -54,8 +55,9 @@ For framework-specific hooks:
54
55
  - Using React hooks? → react-db
55
56
  - Preloading in route loaders (Start, Next, Remix)? → meta-framework
56
57
  - Building an adapter for a new backend? → db-core/custom-adapter
58
+ - Persisting collections to SQLite? → db-core/persistence
57
59
  - Need offline transaction persistence? → offline
58
60
 
59
61
  ## Version
60
62
 
61
- Targets @tanstack/db v0.5.30.
63
+ Targets @tanstack/db v0.6.0.
@@ -6,13 +6,14 @@ description: >
6
6
  (ElectricSQL real-time sync), powerSyncCollectionOptions (PowerSync SQLite),
7
7
  rxdbCollectionOptions (RxDB), trailbaseCollectionOptions (TrailBase),
8
8
  localOnlyCollectionOptions, localStorageCollectionOptions. CollectionConfig
9
- options: getKey, schema, sync, gcTime, autoIndex, syncMode (eager/on-demand/
10
- progressive). StandardSchema validation with Zod/Valibot/ArkType. Collection
11
- lifecycle (idle/loading/ready/error). Adapter-specific sync patterns including
12
- Electric txid tracking and Query direct writes.
9
+ options: getKey, schema, sync, gcTime, autoIndex (default off), defaultIndexType,
10
+ syncMode (eager/on-demand, plus progressive for Electric). StandardSchema validation
11
+ with Zod/Valibot/ArkType. Collection lifecycle (idle/loading/ready/error).
12
+ Adapter-specific sync patterns including Electric txid tracking, Query direct
13
+ writes, and PowerSync query-driven sync with onLoad/onLoadSubset hooks.
13
14
  type: sub-skill
14
15
  library: db
15
- library_version: '0.5.30'
16
+ library_version: '0.6.0'
16
17
  sources:
17
18
  - 'TanStack/db:docs/overview.md'
18
19
  - 'TanStack/db:docs/guides/schemas.md'
@@ -98,11 +99,29 @@ queryCollectionOptions({
98
99
  })
99
100
  ```
100
101
 
101
- | Mode | Best for | Data size |
102
- | ------------- | ---------------------------------------------- | --------- |
103
- | `eager` | Mostly-static datasets | <10k rows |
104
- | `on-demand` | Search, catalogs, large tables | >50k rows |
105
- | `progressive` | Collaborative apps needing instant first paint | Any |
102
+ | Mode | Best for | Data size |
103
+ | ------------- | -------------------------------------------------------------- | --------- |
104
+ | `eager` | Mostly-static datasets | <10k rows |
105
+ | `on-demand` | Search, catalogs, large tables | >50k rows |
106
+ | `progressive` | Collaborative apps needing instant first paint (Electric only) | Any |
107
+
108
+ ## Indexing
109
+
110
+ Indexing is opt-in. The `autoIndex` option defaults to `"off"`. To enable automatic indexing, set `autoIndex: "eager"` and provide a `defaultIndexType`:
111
+
112
+ ```ts
113
+ import { BasicIndex } from '@tanstack/db'
114
+
115
+ createCollection(
116
+ queryCollectionOptions({
117
+ autoIndex: 'eager',
118
+ defaultIndexType: BasicIndex,
119
+ // ...
120
+ }),
121
+ )
122
+ ```
123
+
124
+ Without `defaultIndexType`, setting `autoIndex: "eager"` throws a `CollectionConfigurationError`. You can also create indexes manually with `collection.createIndex()` and remove them with `collection.removeIndex()`.
106
125
 
107
126
  ## Core Patterns
108
127
 
@@ -255,7 +274,7 @@ app.post('/api/todos', async (req, res) => {
255
274
  })
256
275
  ```
257
276
 
258
- `pg_current_xact_id()` must be queried inside the same SQL transaction as the mutation. Otherwise the txid doesn't match and `awaitTxId` stalls forever.
277
+ `pg_current_xact_id()` must be queried inside the same SQL transaction as the mutation. Otherwise the txid doesn't match and `awaitTxId` times out (default 5 seconds).
259
278
 
260
279
  Source: docs/collections/electric-collection.md
261
280
 
@@ -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
@@ -196,6 +196,10 @@ await tx.commit()
196
196
  await tx.isPersisted.promise
197
197
  ```
198
198
 
199
+ ## On-Demand Sync Mode
200
+
201
+ PowerSync supports `on-demand` sync mode (query-driven sync), where only rows matching active live query predicates are loaded from SQLite into the collection. This can be combined with Sync Streams via `onLoad` (eager) or `onLoadSubset` (on-demand) hooks to also control which data the PowerSync Service syncs to the device. Use `extractSimpleComparisons` or `parseWhereExpression` to derive Sync Stream parameters dynamically from live query predicates.
202
+
199
203
  ## Complete Example
200
204
 
201
205
  ```typescript
@@ -176,6 +176,38 @@ const productsCollection = createCollection(
176
176
  )
177
177
  ```
178
178
 
179
+ ## Common Mistakes
180
+
181
+ ### HIGH Function-based queryKey without shared prefix
182
+
183
+ Wrong:
184
+
185
+ ```ts
186
+ queryCollectionOptions({
187
+ queryKey: (opts) => {
188
+ if (opts.where) {
189
+ return ['products-filtered', JSON.stringify(opts.where)]
190
+ }
191
+ return ['products-all']
192
+ },
193
+ })
194
+ ```
195
+
196
+ Correct:
197
+
198
+ ```ts
199
+ queryCollectionOptions({
200
+ queryKey: (opts) => {
201
+ if (opts.where) {
202
+ return ['products', JSON.stringify(opts.where)]
203
+ }
204
+ return ['products']
205
+ },
206
+ })
207
+ ```
208
+
209
+ When using a function-based `queryKey`, all derived keys must share the base key (`queryKey({})`) as a prefix. TanStack Query uses prefix matching for cache operations; if derived keys don't share the base prefix, cache updates silently miss entries, leading to stale data.
210
+
179
211
  ## Key Behaviors
180
212
 
181
213
  - `queryFn` result is treated as **complete state** -- missing items are deleted
@@ -2,15 +2,17 @@
2
2
  name: db-core/custom-adapter
3
3
  description: >
4
4
  Building custom collection adapters for new backends. SyncConfig interface:
5
- sync function receiving begin, write, commit, markReady, truncate primitives.
6
- ChangeMessage format (insert, update, delete). loadSubset for on-demand sync.
7
- LoadSubsetOptions (where, orderBy, limit, cursor). Expression parsing:
8
- parseWhereExpression, parseOrderByExpression, extractSimpleComparisons,
9
- parseLoadSubsetOptions. Collection options creator pattern. rowUpdateMode
10
- (partial vs full). Subscription lifecycle and cleanup functions.
5
+ sync function receiving begin, write, commit, markReady, truncate, metadata
6
+ primitives. ChangeMessage format (insert, update, delete). loadSubset for
7
+ on-demand sync. LoadSubsetOptions (where, orderBy, limit, cursor). Expression
8
+ parsing: parseWhereExpression, parseOrderByExpression,
9
+ extractSimpleComparisons, parseLoadSubsetOptions. Collection options creator
10
+ pattern. rowUpdateMode (partial vs full). Subscription lifecycle and cleanup
11
+ functions. Persisted sync metadata API (metadata.row and metadata.collection)
12
+ for storing per-row and per-collection adapter state.
11
13
  type: sub-skill
12
14
  library: db
13
- library_version: '0.5.30'
15
+ library_version: '0.6.0'
14
16
  sources:
15
17
  - 'TanStack/db:docs/guides/collection-options-creator.md'
16
18
  - 'TanStack/db:packages/db/src/collection/sync.ts'
@@ -38,7 +40,7 @@ function myBackendCollectionOptions<T>(config: {
38
40
  return {
39
41
  getKey: config.getKey,
40
42
  sync: {
41
- sync: ({ begin, write, commit, markReady, collection }) => {
43
+ sync: ({ begin, write, commit, markReady, metadata, collection }) => {
42
44
  let isInitialSyncComplete = false
43
45
  const bufferedEvents: Array<any> = []
44
46
 
@@ -157,6 +159,53 @@ Mutation handlers must not resolve until server changes have synced back to the
157
159
  4. **Version/timestamp**: wait until sync stream catches up to mutation time
158
160
  5. **Provider method**: `await backend.waitForPendingWrites()`
159
161
 
162
+ ### Persisted sync metadata
163
+
164
+ The `metadata` API on the sync config allows adapters to store per-row and per-collection metadata that persists across sync transactions. This is useful for tracking resume tokens, cursors, LSNs, or other adapter-specific state.
165
+
166
+ The `metadata` object is available as a property on the sync config argument alongside `begin`, `write`, `commit`, etc. It is always provided, but without persistence the metadata is in-memory only and does not survive reloads. With persistence, metadata is durable across sessions.
167
+
168
+ ```ts
169
+ sync: ({ begin, write, commit, markReady, metadata }) => {
170
+ // Row metadata: store per-row state (e.g. server version, ETag)
171
+ metadata.row.get(key) // => unknown | undefined
172
+ metadata.row.set(key, { version: 3, etag: 'abc' })
173
+ metadata.row.delete(key)
174
+
175
+ // Collection metadata: store per-collection state (e.g. resume cursor)
176
+ metadata.collection.get('cursor') // => unknown | undefined
177
+ metadata.collection.set('cursor', 'token_abc123')
178
+ metadata.collection.delete('cursor')
179
+ metadata.collection.list() // => [{ key: 'cursor', value: 'token_abc123' }]
180
+ metadata.collection.list('resume') // filter by prefix
181
+ }
182
+ ```
183
+
184
+ Row metadata writes are tied to the current transaction. When a row is deleted via `write({ type: 'delete', ... })`, its row metadata is automatically deleted. When a row is inserted, its metadata is set from `message.metadata` if provided, or deleted otherwise.
185
+
186
+ Collection metadata writes staged before `truncate()` are preserved and commit atomically with the truncate transaction.
187
+
188
+ **Typical usage — resume token:**
189
+
190
+ ```ts
191
+ sync: ({ begin, write, commit, markReady, metadata }) => {
192
+ const lastCursor = metadata.collection.get('cursor') as string | undefined
193
+
194
+ const stream = subscribeFromCursor(lastCursor)
195
+ stream.on('data', (batch) => {
196
+ begin()
197
+ for (const item of batch.items) {
198
+ write({ type: item.type, key: item.id, value: item.data })
199
+ }
200
+ metadata.collection.set('cursor', batch.cursor)
201
+ commit()
202
+ })
203
+
204
+ stream.on('ready', () => markReady())
205
+ return () => stream.close()
206
+ }
207
+ ```
208
+
160
209
  ### Expression parsing for predicate push-down
161
210
 
162
211
  ```ts
@@ -282,4 +331,4 @@ Source: packages/db/src/collection/sync.ts:110
282
331
 
283
332
  Getting-started simplicity (localOnly, eager mode) conflicts with production correctness (on-demand sync, race condition prevention, proper markReady handling). Agents optimizing for quick setup tend to skip buffering, markReady, and cleanup functions.
284
333
 
285
- See also: db-core/collection-setup/SKILL.md -- for built-in adapter patterns to model after.
334
+ See also: db-core/collection-setup/SKILL.md for built-in adapter patterns to model after.