@tanstack/db 0.5.32 → 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 (287) 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 +24 -4
  40. package/dist/cjs/index.cjs.map +1 -1
  41. package/dist/cjs/index.d.cts +12 -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/effect.cjs +602 -0
  81. package/dist/cjs/query/effect.cjs.map +1 -0
  82. package/dist/cjs/query/effect.d.cts +94 -0
  83. package/dist/cjs/query/index.d.cts +2 -1
  84. package/dist/cjs/query/ir.cjs +18 -1
  85. package/dist/cjs/query/ir.cjs.map +1 -1
  86. package/dist/cjs/query/ir.d.cts +21 -1
  87. package/dist/cjs/query/live/collection-config-builder.cjs +493 -66
  88. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  89. package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
  90. package/dist/cjs/query/live/collection-subscriber.cjs +33 -100
  91. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  92. package/dist/cjs/query/live/collection-subscriber.d.cts +0 -1
  93. package/dist/cjs/query/live/types.d.cts +3 -3
  94. package/dist/cjs/query/live/utils.cjs +219 -0
  95. package/dist/cjs/query/live/utils.cjs.map +1 -0
  96. package/dist/cjs/query/live/utils.d.cts +110 -0
  97. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  98. package/dist/cjs/query/live-query-collection.d.cts +9 -6
  99. package/dist/cjs/query/query-once.cjs.map +1 -1
  100. package/dist/cjs/query/query-once.d.cts +7 -5
  101. package/dist/cjs/query/subset-dedupe.cjs +9 -3
  102. package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
  103. package/dist/cjs/types.d.cts +42 -8
  104. package/dist/cjs/utils/array-utils.cjs +27 -0
  105. package/dist/cjs/utils/array-utils.cjs.map +1 -0
  106. package/dist/cjs/utils/array-utils.d.cts +16 -0
  107. package/dist/cjs/utils/comparison.cjs +11 -0
  108. package/dist/cjs/utils/comparison.cjs.map +1 -1
  109. package/dist/cjs/utils/index-optimization.cjs +4 -0
  110. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  111. package/dist/cjs/utils.cjs +7 -9
  112. package/dist/cjs/utils.cjs.map +1 -1
  113. package/dist/cjs/utils.d.cts +6 -1
  114. package/dist/cjs/virtual-props.cjs +33 -0
  115. package/dist/cjs/virtual-props.cjs.map +1 -0
  116. package/dist/cjs/virtual-props.d.cts +196 -0
  117. package/dist/esm/collection/change-events.d.ts +3 -2
  118. package/dist/esm/collection/change-events.js.map +1 -1
  119. package/dist/esm/collection/changes.d.ts +10 -1
  120. package/dist/esm/collection/changes.js +13 -4
  121. package/dist/esm/collection/changes.js.map +1 -1
  122. package/dist/esm/collection/cleanup-queue.d.ts +30 -0
  123. package/dist/esm/collection/cleanup-queue.js +89 -0
  124. package/dist/esm/collection/cleanup-queue.js.map +1 -0
  125. package/dist/esm/collection/events.d.ts +39 -1
  126. package/dist/esm/collection/events.js +14 -0
  127. package/dist/esm/collection/events.js.map +1 -1
  128. package/dist/esm/collection/index.d.ts +49 -36
  129. package/dist/esm/collection/index.js +67 -29
  130. package/dist/esm/collection/index.js.map +1 -1
  131. package/dist/esm/collection/indexes.d.ts +27 -17
  132. package/dist/esm/collection/indexes.js +211 -62
  133. package/dist/esm/collection/indexes.js.map +1 -1
  134. package/dist/esm/collection/lifecycle.d.ts +0 -1
  135. package/dist/esm/collection/lifecycle.js +5 -22
  136. package/dist/esm/collection/lifecycle.js.map +1 -1
  137. package/dist/esm/collection/mutations.d.ts +1 -0
  138. package/dist/esm/collection/mutations.js +18 -0
  139. package/dist/esm/collection/mutations.js.map +1 -1
  140. package/dist/esm/collection/state.d.ts +65 -1
  141. package/dist/esm/collection/state.js +381 -53
  142. package/dist/esm/collection/state.js.map +1 -1
  143. package/dist/esm/collection/subscription.d.ts +4 -0
  144. package/dist/esm/collection/subscription.js +6 -0
  145. package/dist/esm/collection/subscription.js.map +1 -1
  146. package/dist/esm/collection/sync.d.ts +2 -0
  147. package/dist/esm/collection/sync.js +108 -1
  148. package/dist/esm/collection/sync.js.map +1 -1
  149. package/dist/esm/collection/transaction-metadata.d.ts +1 -0
  150. package/dist/esm/collection/transaction-metadata.js +5 -0
  151. package/dist/esm/collection/transaction-metadata.js.map +1 -0
  152. package/dist/esm/errors.d.ts +3 -0
  153. package/dist/esm/errors.js +8 -0
  154. package/dist/esm/errors.js.map +1 -1
  155. package/dist/esm/index.d.ts +12 -3
  156. package/dist/esm/index.js +27 -7
  157. package/dist/esm/index.js.map +1 -1
  158. package/dist/esm/indexes/auto-index.js +13 -6
  159. package/dist/esm/indexes/auto-index.js.map +1 -1
  160. package/dist/esm/indexes/base-index.d.ts +2 -6
  161. package/dist/esm/indexes/base-index.js +1 -4
  162. package/dist/esm/indexes/base-index.js.map +1 -1
  163. package/dist/esm/indexes/basic-index.d.ts +102 -0
  164. package/dist/esm/indexes/basic-index.js +361 -0
  165. package/dist/esm/indexes/basic-index.js.map +1 -0
  166. package/dist/esm/indexes/btree-index.d.ts +1 -1
  167. package/dist/esm/indexes/btree-index.js.map +1 -1
  168. package/dist/esm/indexes/index-options.d.ts +8 -9
  169. package/dist/esm/indexes/index-registry.d.ts +61 -0
  170. package/dist/esm/indexes/index-registry.js +89 -0
  171. package/dist/esm/indexes/index-registry.js.map +1 -0
  172. package/dist/esm/local-only.js +5 -0
  173. package/dist/esm/local-only.js.map +1 -1
  174. package/dist/esm/query/builder/functions.d.ts +25 -3
  175. package/dist/esm/query/builder/functions.js +27 -11
  176. package/dist/esm/query/builder/functions.js.map +1 -1
  177. package/dist/esm/query/builder/index.d.ts +4 -3
  178. package/dist/esm/query/builder/index.js +201 -40
  179. package/dist/esm/query/builder/index.js.map +1 -1
  180. package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
  181. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  182. package/dist/esm/query/builder/types.d.ts +84 -19
  183. package/dist/esm/query/compiler/evaluators.js +51 -0
  184. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  185. package/dist/esm/query/compiler/group-by.d.ts +4 -2
  186. package/dist/esm/query/compiler/group-by.js +101 -29
  187. package/dist/esm/query/compiler/group-by.js.map +1 -1
  188. package/dist/esm/query/compiler/index.d.ts +30 -2
  189. package/dist/esm/query/compiler/index.js +285 -13
  190. package/dist/esm/query/compiler/index.js.map +1 -1
  191. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  192. package/dist/esm/query/compiler/order-by.js +30 -11
  193. package/dist/esm/query/compiler/order-by.js.map +1 -1
  194. package/dist/esm/query/compiler/select.js +8 -0
  195. package/dist/esm/query/compiler/select.js.map +1 -1
  196. package/dist/esm/query/effect.d.ts +94 -0
  197. package/dist/esm/query/effect.js +602 -0
  198. package/dist/esm/query/effect.js.map +1 -0
  199. package/dist/esm/query/index.d.ts +2 -1
  200. package/dist/esm/query/ir.d.ts +21 -1
  201. package/dist/esm/query/ir.js +18 -1
  202. package/dist/esm/query/ir.js.map +1 -1
  203. package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
  204. package/dist/esm/query/live/collection-config-builder.js +492 -65
  205. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  206. package/dist/esm/query/live/collection-subscriber.d.ts +0 -1
  207. package/dist/esm/query/live/collection-subscriber.js +31 -98
  208. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  209. package/dist/esm/query/live/types.d.ts +3 -3
  210. package/dist/esm/query/live/utils.d.ts +110 -0
  211. package/dist/esm/query/live/utils.js +219 -0
  212. package/dist/esm/query/live/utils.js.map +1 -0
  213. package/dist/esm/query/live-query-collection.d.ts +9 -6
  214. package/dist/esm/query/live-query-collection.js.map +1 -1
  215. package/dist/esm/query/query-once.d.ts +7 -5
  216. package/dist/esm/query/query-once.js.map +1 -1
  217. package/dist/esm/query/subset-dedupe.js +9 -3
  218. package/dist/esm/query/subset-dedupe.js.map +1 -1
  219. package/dist/esm/types.d.ts +42 -8
  220. package/dist/esm/utils/array-utils.d.ts +16 -0
  221. package/dist/esm/utils/array-utils.js +27 -0
  222. package/dist/esm/utils/array-utils.js.map +1 -0
  223. package/dist/esm/utils/comparison.js +11 -0
  224. package/dist/esm/utils/comparison.js.map +1 -1
  225. package/dist/esm/utils/index-optimization.js +4 -0
  226. package/dist/esm/utils/index-optimization.js.map +1 -1
  227. package/dist/esm/utils.d.ts +6 -1
  228. package/dist/esm/utils.js +7 -9
  229. package/dist/esm/utils.js.map +1 -1
  230. package/dist/esm/virtual-props.d.ts +196 -0
  231. package/dist/esm/virtual-props.js +33 -0
  232. package/dist/esm/virtual-props.js.map +1 -0
  233. package/package.json +2 -2
  234. package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
  235. package/src/collection/change-events.ts +13 -9
  236. package/src/collection/changes.ts +30 -7
  237. package/src/collection/cleanup-queue.ts +105 -0
  238. package/src/collection/events.ts +65 -0
  239. package/src/collection/index.ts +110 -45
  240. package/src/collection/indexes.ts +283 -76
  241. package/src/collection/lifecycle.ts +5 -26
  242. package/src/collection/mutations.ts +21 -0
  243. package/src/collection/state.ts +545 -71
  244. package/src/collection/subscription.ts +7 -0
  245. package/src/collection/sync.ts +137 -0
  246. package/src/collection/transaction-metadata.ts +1 -0
  247. package/src/errors.ts +9 -0
  248. package/src/index.ts +57 -3
  249. package/src/indexes/auto-index.ts +18 -8
  250. package/src/indexes/base-index.ts +2 -10
  251. package/src/indexes/basic-index.ts +507 -0
  252. package/src/indexes/btree-index.ts +1 -1
  253. package/src/indexes/index-options.ts +17 -37
  254. package/src/indexes/index-registry.ts +174 -0
  255. package/src/local-only.ts +7 -0
  256. package/src/query/builder/functions.ts +84 -7
  257. package/src/query/builder/index.ts +329 -9
  258. package/src/query/builder/ref-proxy.ts +22 -4
  259. package/src/query/builder/types.ts +257 -62
  260. package/src/query/compiler/evaluators.ts +57 -0
  261. package/src/query/compiler/group-by.ts +156 -35
  262. package/src/query/compiler/index.ts +445 -15
  263. package/src/query/compiler/order-by.ts +51 -12
  264. package/src/query/compiler/select.ts +9 -0
  265. package/src/query/effect.ts +1119 -0
  266. package/src/query/index.ts +7 -0
  267. package/src/query/ir.ts +23 -2
  268. package/src/query/live/collection-config-builder.ts +778 -104
  269. package/src/query/live/collection-subscriber.ts +40 -156
  270. package/src/query/live/types.ts +10 -4
  271. package/src/query/live/utils.ts +417 -0
  272. package/src/query/live-query-collection.ts +43 -18
  273. package/src/query/query-once.ts +31 -12
  274. package/src/query/subset-dedupe.ts +11 -7
  275. package/src/types.ts +49 -9
  276. package/src/utils/array-utils.ts +49 -0
  277. package/src/utils/comparison.ts +14 -0
  278. package/src/utils/index-optimization.ts +4 -0
  279. package/src/utils.ts +12 -9
  280. package/src/virtual-props.ts +282 -0
  281. package/dist/cjs/indexes/lazy-index.cjs +0 -190
  282. package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
  283. package/dist/cjs/indexes/lazy-index.d.cts +0 -96
  284. package/dist/esm/indexes/lazy-index.d.ts +0 -96
  285. package/dist/esm/indexes/lazy-index.js +0 -190
  286. package/dist/esm/indexes/lazy-index.js.map +0 -1
  287. package/src/indexes/lazy-index.ts +0 -251
@@ -1,6 +1,6 @@
1
- import { D2, output } from '@tanstack/db-ivm'
2
- import { compileQuery } from '../compiler/index.js'
3
- import { buildQuery, getQueryIR } from '../builder/index.js'
1
+ import { D2, output, serializeValue } from '@tanstack/db-ivm'
2
+ import { INCLUDES_ROUTING, compileQuery } from '../compiler/index.js'
3
+ import { createCollection } from '../../collection/index.js'
4
4
  import {
5
5
  MissingAliasInputsError,
6
6
  SetWindowRequiresOrderByError,
@@ -10,14 +10,24 @@ import { getActiveTransaction } from '../../transactions.js'
10
10
  import { CollectionSubscriber } from './collection-subscriber.js'
11
11
  import { getCollectionBuilder } from './collection-registry.js'
12
12
  import { LIVE_QUERY_INTERNAL } from './internal.js'
13
+ import {
14
+ buildQueryFromConfig,
15
+ extractCollectionAliases,
16
+ extractCollectionFromSource,
17
+ extractCollectionsFromQuery,
18
+ } from './utils.js'
13
19
  import type { LiveQueryInternalUtils } from './internal.js'
14
- import type { WindowOptions } from '../compiler/index.js'
20
+ import type {
21
+ IncludesCompilationResult,
22
+ WindowOptions,
23
+ } from '../compiler/index.js'
15
24
  import type { SchedulerContextId } from '../../scheduler.js'
16
25
  import type { CollectionSubscription } from '../../collection/subscription.js'
17
26
  import type { RootStreamBuilder } from '@tanstack/db-ivm'
18
27
  import type { OrderByOptimizationInfo } from '../compiler/order-by.js'
19
28
  import type { Collection } from '../../collection/index.js'
20
29
  import type {
30
+ ChangeMessage,
21
31
  CollectionConfigSingleRowOption,
22
32
  KeyedStream,
23
33
  ResultStream,
@@ -26,7 +36,12 @@ import type {
26
36
  UtilsRecord,
27
37
  } from '../../types.js'
28
38
  import type { Context, GetResult } from '../builder/types.js'
29
- import type { BasicExpression, QueryIR } from '../ir.js'
39
+ import type {
40
+ BasicExpression,
41
+ IncludesMaterialization,
42
+ PropRef,
43
+ QueryIR,
44
+ } from '../ir.js'
30
45
  import type { LazyCollectionCallbacks } from '../compiler/joins.js'
31
46
  import type {
32
47
  Changes,
@@ -135,6 +150,7 @@ export class CollectionConfigBuilder<
135
150
  public sourceWhereClausesCache:
136
151
  | Map<string, BasicExpression<boolean>>
137
152
  | undefined
153
+ private includesCache: Array<IncludesCompilationResult> | undefined
138
154
 
139
155
  // Map of source alias to subscription
140
156
  readonly subscriptions: Record<string, CollectionSubscription> = {}
@@ -151,7 +167,10 @@ export class CollectionConfigBuilder<
151
167
  // Generate a unique ID if not provided
152
168
  this.id = config.id || `live-query-${++liveQueryCollectionCounter}`
153
169
 
154
- this.query = buildQueryFromConfig(config)
170
+ this.query = buildQueryFromConfig({
171
+ query: config.query,
172
+ requireObjectResult: true,
173
+ })
155
174
  this.collections = extractCollectionsFromQuery(this.query)
156
175
  const collectionAliasesById = extractCollectionAliases(this.query)
157
176
 
@@ -627,6 +646,7 @@ export class CollectionConfigBuilder<
627
646
  this.inputsCache = undefined
628
647
  this.pipelineCache = undefined
629
648
  this.sourceWhereClausesCache = undefined
649
+ this.includesCache = undefined
630
650
 
631
651
  // Reset lazy source alias state
632
652
  this.lazySources.clear()
@@ -675,6 +695,7 @@ export class CollectionConfigBuilder<
675
695
  this.pipelineCache = compilation.pipeline
676
696
  this.sourceWhereClausesCache = compilation.sourceWhereClauses
677
697
  this.compiledAliasToCollectionId = compilation.aliasToCollectionId
698
+ this.includesCache = compilation.includes
678
699
 
679
700
  // Defensive check: verify all compiled aliases have corresponding inputs
680
701
  // This should never happen since all aliases come from user declarations,
@@ -722,10 +743,19 @@ export class CollectionConfigBuilder<
722
743
  }),
723
744
  )
724
745
 
746
+ // Set up includes output routing and child collection lifecycle
747
+ const includesState = this.setupIncludesOutput(
748
+ this.includesCache,
749
+ syncState,
750
+ )
751
+
725
752
  // Flush pending changes and reset the accumulator.
726
753
  // Called at the end of each graph run to commit all accumulated changes.
727
754
  syncState.flushPendingChanges = () => {
728
- if (pendingChanges.size === 0) {
755
+ const hasParentChanges = pendingChanges.size > 0
756
+ const hasChildChanges = hasPendingIncludesChanges(includesState)
757
+
758
+ if (!hasParentChanges && !hasChildChanges) {
729
759
  return
730
760
  }
731
761
 
@@ -757,10 +787,22 @@ export class CollectionConfigBuilder<
757
787
  changesToApply = merged
758
788
  }
759
789
 
760
- begin()
761
- changesToApply.forEach(this.applyChanges.bind(this, config))
762
- commit()
790
+ // 1. Flush parent changes
791
+ if (hasParentChanges) {
792
+ begin()
793
+ changesToApply.forEach(this.applyChanges.bind(this, config))
794
+ commit()
795
+ }
763
796
  pendingChanges = new Map()
797
+
798
+ // 2. Process includes: create/dispose child Collections, route child changes
799
+ flushIncludesState(
800
+ includesState,
801
+ config.collection,
802
+ this.id,
803
+ hasParentChanges ? changesToApply : null,
804
+ config,
805
+ )
764
806
  }
765
807
 
766
808
  graph.finalize()
@@ -773,6 +815,88 @@ export class CollectionConfigBuilder<
773
815
  return syncState as FullSyncState
774
816
  }
775
817
 
818
+ /**
819
+ * Sets up output callbacks for includes child pipelines.
820
+ * Each includes entry gets its own output callback that accumulates child changes,
821
+ * and a child registry that maps correlation key → child Collection.
822
+ */
823
+ private setupIncludesOutput(
824
+ includesEntries: Array<IncludesCompilationResult> | undefined,
825
+ syncState: SyncState,
826
+ ): Array<IncludesOutputState> {
827
+ if (!includesEntries || includesEntries.length === 0) {
828
+ return []
829
+ }
830
+
831
+ return includesEntries.map((entry) => {
832
+ const state: IncludesOutputState = {
833
+ fieldName: entry.fieldName,
834
+ childCorrelationField: entry.childCorrelationField,
835
+ hasOrderBy: entry.hasOrderBy,
836
+ materialization: entry.materialization,
837
+ scalarField: entry.scalarField,
838
+ childRegistry: new Map(),
839
+ pendingChildChanges: new Map(),
840
+ correlationToParentKeys: new Map(),
841
+ }
842
+
843
+ // Attach output callback on the child pipeline
844
+ entry.pipeline.pipe(
845
+ output((data) => {
846
+ const messages = data.getInner()
847
+ syncState.messagesCount += messages.length
848
+
849
+ for (const [[childKey, tupleData], multiplicity] of messages) {
850
+ const [childResult, _orderByIndex, correlationKey, parentContext] =
851
+ tupleData as unknown as [
852
+ any,
853
+ string | undefined,
854
+ unknown,
855
+ Record<string, any> | null,
856
+ ]
857
+
858
+ const routingKey = computeRoutingKey(correlationKey, parentContext)
859
+
860
+ // Accumulate by [routingKey, childKey]
861
+ let byChild = state.pendingChildChanges.get(routingKey)
862
+ if (!byChild) {
863
+ byChild = new Map()
864
+ state.pendingChildChanges.set(routingKey, byChild)
865
+ }
866
+
867
+ const existing = byChild.get(childKey) || {
868
+ deletes: 0,
869
+ inserts: 0,
870
+ value: childResult,
871
+ orderByIndex: _orderByIndex,
872
+ }
873
+
874
+ if (multiplicity < 0) {
875
+ existing.deletes += Math.abs(multiplicity)
876
+ } else if (multiplicity > 0) {
877
+ existing.inserts += multiplicity
878
+ existing.value = childResult
879
+ }
880
+
881
+ byChild.set(childKey, existing)
882
+ }
883
+ }),
884
+ )
885
+
886
+ // Set up shared buffers for nested includes (e.g., comments inside issues)
887
+ if (entry.childCompilationResult.includes) {
888
+ state.nestedSetups = setupNestedPipelines(
889
+ entry.childCompilationResult.includes,
890
+ syncState,
891
+ )
892
+ state.nestedRoutingIndex = new Map()
893
+ state.nestedRoutingReverseIndex = new Map()
894
+ }
895
+
896
+ return state
897
+ })
898
+ }
899
+
776
900
  private applyChanges(
777
901
  config: SyncMethods<TResult>,
778
902
  changes: {
@@ -984,16 +1108,6 @@ export class CollectionConfigBuilder<
984
1108
  }
985
1109
  }
986
1110
 
987
- function buildQueryFromConfig<TContext extends Context>(
988
- config: LiveQueryCollectionConfig<any, any>,
989
- ) {
990
- // Build the query using the provided query builder function or instance
991
- if (typeof config.query === `function`) {
992
- return buildQuery<TContext>(config.query)
993
- }
994
- return getQueryIR(config.query)
995
- }
996
-
997
1111
  function createOrderByComparator<T extends object>(
998
1112
  orderByIndices: WeakMap<object, string>,
999
1113
  ) {
@@ -1019,124 +1133,684 @@ function createOrderByComparator<T extends object>(
1019
1133
  }
1020
1134
 
1021
1135
  /**
1022
- * Helper function to extract collections from a compiled query
1023
- * Traverses the query IR to find all collection references
1024
- * Maps collections by their ID (not alias) as expected by the compiler
1136
+ * Shared buffer setup for a single nested includes level.
1137
+ * Pipeline output writes into the buffer; during flush the buffer is drained
1138
+ * into per-entry states via the routing index.
1139
+ */
1140
+ type NestedIncludesSetup = {
1141
+ compilationResult: IncludesCompilationResult
1142
+ /** Shared buffer: nestedCorrelationKey → Map<childKey, Changes> */
1143
+ buffer: Map<unknown, Map<unknown, Changes<any>>>
1144
+ /** For 3+ levels of nesting */
1145
+ nestedSetups?: Array<NestedIncludesSetup>
1146
+ }
1147
+
1148
+ /**
1149
+ * State tracked per includes entry for output routing and child lifecycle
1025
1150
  */
1026
- function extractCollectionsFromQuery(
1027
- query: any,
1028
- ): Record<string, Collection<any, any, any>> {
1029
- const collections: Record<string, any> = {}
1030
-
1031
- // Helper function to recursively extract collections from a query or source
1032
- function extractFromSource(source: any) {
1033
- if (source.type === `collectionRef`) {
1034
- collections[source.collection.id] = source.collection
1035
- } else if (source.type === `queryRef`) {
1036
- // Recursively extract from subquery
1037
- extractFromQuery(source.query)
1151
+ type IncludesOutputState = {
1152
+ fieldName: string
1153
+ childCorrelationField: PropRef
1154
+ /** Whether the child query has an ORDER BY clause */
1155
+ hasOrderBy: boolean
1156
+ /** How the child result is materialized on the parent row */
1157
+ materialization: IncludesMaterialization
1158
+ /** Internal field used to unwrap scalar child selects */
1159
+ scalarField?: string
1160
+ /** Maps correlation key value child Collection entry */
1161
+ childRegistry: Map<unknown, ChildCollectionEntry>
1162
+ /** Pending child changes: correlationKey → Map<childKey, Changes> */
1163
+ pendingChildChanges: Map<unknown, Map<unknown, Changes<any>>>
1164
+ /** Reverse index: correlation key → Set of parent collection keys */
1165
+ correlationToParentKeys: Map<unknown, Set<unknown>>
1166
+ /** Shared nested pipeline setups (one per nested includes level) */
1167
+ nestedSetups?: Array<NestedIncludesSetup>
1168
+ /** nestedCorrelationKey → parentCorrelationKey */
1169
+ nestedRoutingIndex?: Map<unknown, unknown>
1170
+ /** parentCorrelationKey → Set<nestedCorrelationKeys> */
1171
+ nestedRoutingReverseIndex?: Map<unknown, Set<unknown>>
1172
+ }
1173
+
1174
+ type ChildCollectionEntry = {
1175
+ collection: Collection<any, any, any>
1176
+ syncMethods: SyncMethods<any> | null
1177
+ resultKeys: WeakMap<object, unknown>
1178
+ orderByIndices: WeakMap<object, string> | null
1179
+ /** Per-entry nested includes states (one per nested includes level) */
1180
+ includesStates?: Array<IncludesOutputState>
1181
+ }
1182
+
1183
+ function materializesInline(state: IncludesOutputState): boolean {
1184
+ return state.materialization !== `collection`
1185
+ }
1186
+
1187
+ function materializeIncludedValue(
1188
+ state: IncludesOutputState,
1189
+ entry: ChildCollectionEntry | undefined,
1190
+ ): unknown {
1191
+ if (!entry) {
1192
+ if (state.materialization === `array`) {
1193
+ return []
1038
1194
  }
1195
+ if (state.materialization === `concat`) {
1196
+ return ``
1197
+ }
1198
+ return undefined
1199
+ }
1200
+
1201
+ if (state.materialization === `collection`) {
1202
+ return entry.collection
1203
+ }
1204
+
1205
+ const rows = [...entry.collection.toArray]
1206
+ const values = state.scalarField
1207
+ ? rows.map((row) => row?.[state.scalarField!])
1208
+ : rows
1209
+
1210
+ if (state.materialization === `array`) {
1211
+ return values
1039
1212
  }
1040
1213
 
1041
- // Helper function to recursively extract collections from a query
1042
- function extractFromQuery(q: any) {
1043
- // Extract from FROM clause
1044
- if (q.from) {
1045
- extractFromSource(q.from)
1214
+ return values.map((value) => String(value ?? ``)).join(``)
1215
+ }
1216
+
1217
+ /**
1218
+ * Sets up shared buffers for nested includes pipelines.
1219
+ * Instead of writing directly into a single shared IncludesOutputState,
1220
+ * each nested pipeline writes into a buffer that is later drained per-entry.
1221
+ */
1222
+ function setupNestedPipelines(
1223
+ includes: Array<IncludesCompilationResult>,
1224
+ syncState: SyncState,
1225
+ ): Array<NestedIncludesSetup> {
1226
+ return includes.map((entry) => {
1227
+ const buffer: Map<unknown, Map<unknown, Changes<any>>> = new Map()
1228
+
1229
+ // Attach output callback that writes into the shared buffer
1230
+ entry.pipeline.pipe(
1231
+ output((data) => {
1232
+ const messages = data.getInner()
1233
+ syncState.messagesCount += messages.length
1234
+
1235
+ for (const [[childKey, tupleData], multiplicity] of messages) {
1236
+ const [childResult, _orderByIndex, correlationKey, parentContext] =
1237
+ tupleData as unknown as [
1238
+ any,
1239
+ string | undefined,
1240
+ unknown,
1241
+ Record<string, any> | null,
1242
+ ]
1243
+
1244
+ const routingKey = computeRoutingKey(correlationKey, parentContext)
1245
+
1246
+ let byChild = buffer.get(routingKey)
1247
+ if (!byChild) {
1248
+ byChild = new Map()
1249
+ buffer.set(routingKey, byChild)
1250
+ }
1251
+
1252
+ const existing = byChild.get(childKey) || {
1253
+ deletes: 0,
1254
+ inserts: 0,
1255
+ value: childResult,
1256
+ orderByIndex: _orderByIndex,
1257
+ }
1258
+
1259
+ if (multiplicity < 0) {
1260
+ existing.deletes += Math.abs(multiplicity)
1261
+ } else if (multiplicity > 0) {
1262
+ existing.inserts += multiplicity
1263
+ existing.value = childResult
1264
+ }
1265
+
1266
+ byChild.set(childKey, existing)
1267
+ }
1268
+ }),
1269
+ )
1270
+
1271
+ const setup: NestedIncludesSetup = {
1272
+ compilationResult: entry,
1273
+ buffer,
1274
+ }
1275
+
1276
+ // Recursively set up deeper levels
1277
+ if (entry.childCompilationResult.includes) {
1278
+ setup.nestedSetups = setupNestedPipelines(
1279
+ entry.childCompilationResult.includes,
1280
+ syncState,
1281
+ )
1282
+ }
1283
+
1284
+ return setup
1285
+ })
1286
+ }
1287
+
1288
+ /**
1289
+ * Creates fresh per-entry IncludesOutputState array from NestedIncludesSetup array.
1290
+ * Each entry gets its own isolated state for nested includes.
1291
+ */
1292
+ function createPerEntryIncludesStates(
1293
+ setups: Array<NestedIncludesSetup>,
1294
+ ): Array<IncludesOutputState> {
1295
+ return setups.map((setup) => {
1296
+ const state: IncludesOutputState = {
1297
+ fieldName: setup.compilationResult.fieldName,
1298
+ childCorrelationField: setup.compilationResult.childCorrelationField,
1299
+ hasOrderBy: setup.compilationResult.hasOrderBy,
1300
+ materialization: setup.compilationResult.materialization,
1301
+ scalarField: setup.compilationResult.scalarField,
1302
+ childRegistry: new Map(),
1303
+ pendingChildChanges: new Map(),
1304
+ correlationToParentKeys: new Map(),
1305
+ }
1306
+
1307
+ if (setup.nestedSetups) {
1308
+ state.nestedSetups = setup.nestedSetups
1309
+ state.nestedRoutingIndex = new Map()
1310
+ state.nestedRoutingReverseIndex = new Map()
1046
1311
  }
1047
1312
 
1048
- // Extract from JOIN clauses
1049
- if (q.join && Array.isArray(q.join)) {
1050
- for (const joinClause of q.join) {
1051
- if (joinClause.from) {
1052
- extractFromSource(joinClause.from)
1313
+ return state
1314
+ })
1315
+ }
1316
+
1317
+ /**
1318
+ * Drains shared buffers into per-entry states using the routing index.
1319
+ * Returns the set of parent correlation keys that had changes routed to them.
1320
+ */
1321
+ function drainNestedBuffers(state: IncludesOutputState): Set<unknown> {
1322
+ const dirtyCorrelationKeys = new Set<unknown>()
1323
+
1324
+ if (!state.nestedSetups) return dirtyCorrelationKeys
1325
+
1326
+ for (let i = 0; i < state.nestedSetups.length; i++) {
1327
+ const setup = state.nestedSetups[i]!
1328
+ const toDelete: Array<unknown> = []
1329
+
1330
+ for (const [nestedCorrelationKey, childChanges] of setup.buffer) {
1331
+ const parentCorrelationKey =
1332
+ state.nestedRoutingIndex!.get(nestedCorrelationKey)
1333
+ if (parentCorrelationKey === undefined) {
1334
+ // Unroutable — parent not yet seen; keep in buffer
1335
+ continue
1336
+ }
1337
+
1338
+ const entry = state.childRegistry.get(parentCorrelationKey)
1339
+ if (!entry || !entry.includesStates) {
1340
+ continue
1341
+ }
1342
+
1343
+ // Route changes into this entry's per-entry state at position i
1344
+ const entryState = entry.includesStates[i]!
1345
+ for (const [childKey, changes] of childChanges) {
1346
+ let byChild = entryState.pendingChildChanges.get(nestedCorrelationKey)
1347
+ if (!byChild) {
1348
+ byChild = new Map()
1349
+ entryState.pendingChildChanges.set(nestedCorrelationKey, byChild)
1350
+ }
1351
+ const existing = byChild.get(childKey)
1352
+ if (existing) {
1353
+ existing.inserts += changes.inserts
1354
+ existing.deletes += changes.deletes
1355
+ if (changes.inserts > 0) {
1356
+ existing.value = changes.value
1357
+ if (changes.orderByIndex !== undefined) {
1358
+ existing.orderByIndex = changes.orderByIndex
1359
+ }
1360
+ }
1361
+ } else {
1362
+ byChild.set(childKey, { ...changes })
1053
1363
  }
1054
1364
  }
1365
+
1366
+ dirtyCorrelationKeys.add(parentCorrelationKey)
1367
+ toDelete.push(nestedCorrelationKey)
1368
+ }
1369
+
1370
+ for (const key of toDelete) {
1371
+ setup.buffer.delete(key)
1055
1372
  }
1056
1373
  }
1057
1374
 
1058
- // Start extraction from the root query
1059
- extractFromQuery(query)
1375
+ return dirtyCorrelationKeys
1376
+ }
1377
+
1378
+ /**
1379
+ * Updates the routing index after processing child changes.
1380
+ * Maps nested correlation keys to parent correlation keys so that
1381
+ * grandchild changes can be routed to the correct per-entry state.
1382
+ */
1383
+ function updateRoutingIndex(
1384
+ state: IncludesOutputState,
1385
+ correlationKey: unknown,
1386
+ childChanges: Map<unknown, Changes<any>>,
1387
+ ): void {
1388
+ if (!state.nestedSetups) return
1389
+
1390
+ for (const setup of state.nestedSetups) {
1391
+ for (const [, change] of childChanges) {
1392
+ if (change.inserts > 0) {
1393
+ // Read the nested routing key from the INCLUDES_ROUTING stamp.
1394
+ // Must use the composite routing key (not raw correlationKey) to match
1395
+ // how nested buffers are keyed by computeRoutingKey.
1396
+ const nestedRouting =
1397
+ change.value[INCLUDES_ROUTING]?.[setup.compilationResult.fieldName]
1398
+ const nestedCorrelationKey = nestedRouting?.correlationKey
1399
+ const nestedParentContext = nestedRouting?.parentContext ?? null
1400
+ const nestedRoutingKey = computeRoutingKey(
1401
+ nestedCorrelationKey,
1402
+ nestedParentContext,
1403
+ )
1404
+
1405
+ if (nestedCorrelationKey != null) {
1406
+ state.nestedRoutingIndex!.set(nestedRoutingKey, correlationKey)
1407
+ let reverseSet = state.nestedRoutingReverseIndex!.get(correlationKey)
1408
+ if (!reverseSet) {
1409
+ reverseSet = new Set()
1410
+ state.nestedRoutingReverseIndex!.set(correlationKey, reverseSet)
1411
+ }
1412
+ reverseSet.add(nestedRoutingKey)
1413
+ }
1414
+ } else if (change.deletes > 0 && change.inserts === 0) {
1415
+ // Remove from routing index
1416
+ const nestedRouting2 =
1417
+ change.value[INCLUDES_ROUTING]?.[setup.compilationResult.fieldName]
1418
+ const nestedCorrelationKey = nestedRouting2?.correlationKey
1419
+ const nestedParentContext2 = nestedRouting2?.parentContext ?? null
1420
+ const nestedRoutingKey = computeRoutingKey(
1421
+ nestedCorrelationKey,
1422
+ nestedParentContext2,
1423
+ )
1424
+
1425
+ if (nestedCorrelationKey != null) {
1426
+ state.nestedRoutingIndex!.delete(nestedRoutingKey)
1427
+ const reverseSet =
1428
+ state.nestedRoutingReverseIndex!.get(correlationKey)
1429
+ if (reverseSet) {
1430
+ reverseSet.delete(nestedRoutingKey)
1431
+ if (reverseSet.size === 0) {
1432
+ state.nestedRoutingReverseIndex!.delete(correlationKey)
1433
+ }
1434
+ }
1435
+ }
1436
+ }
1437
+ }
1438
+ }
1439
+ }
1060
1440
 
1061
- return collections
1441
+ /**
1442
+ * Cleans routing index entries when a parent is deleted.
1443
+ * Uses the reverse index to find and remove all nested routing entries.
1444
+ */
1445
+ function cleanRoutingIndexOnDelete(
1446
+ state: IncludesOutputState,
1447
+ correlationKey: unknown,
1448
+ ): void {
1449
+ if (!state.nestedRoutingReverseIndex) return
1450
+
1451
+ const nestedKeys = state.nestedRoutingReverseIndex.get(correlationKey)
1452
+ if (nestedKeys) {
1453
+ for (const nestedKey of nestedKeys) {
1454
+ state.nestedRoutingIndex!.delete(nestedKey)
1455
+ }
1456
+ state.nestedRoutingReverseIndex.delete(correlationKey)
1457
+ }
1062
1458
  }
1063
1459
 
1064
1460
  /**
1065
- * Helper function to extract the collection that is referenced in the query's FROM clause.
1066
- * The FROM clause may refer directly to a collection or indirectly to a subquery.
1461
+ * Recursively checks whether any nested buffer has pending changes.
1067
1462
  */
1068
- function extractCollectionFromSource(query: any): Collection<any, any, any> {
1069
- const from = query.from
1070
-
1071
- if (from.type === `collectionRef`) {
1072
- return from.collection
1073
- } else if (from.type === `queryRef`) {
1074
- // Recursively extract from subquery
1075
- return extractCollectionFromSource(from.query)
1463
+ function hasNestedBufferChanges(setups: Array<NestedIncludesSetup>): boolean {
1464
+ for (const setup of setups) {
1465
+ if (setup.buffer.size > 0) return true
1466
+ if (setup.nestedSetups && hasNestedBufferChanges(setup.nestedSetups))
1467
+ return true
1076
1468
  }
1469
+ return false
1470
+ }
1077
1471
 
1078
- throw new Error(
1079
- `Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`,
1080
- )
1472
+ /**
1473
+ * Computes a composite routing key from correlation key and parent context.
1474
+ * When parentContext is null (no parent filters), returns the raw correlationKey
1475
+ * for zero behavioral change on existing queries.
1476
+ */
1477
+ function computeRoutingKey(
1478
+ correlationKey: unknown,
1479
+ parentContext: Record<string, any> | null,
1480
+ ): unknown {
1481
+ if (parentContext == null) return correlationKey
1482
+ return JSON.stringify([correlationKey, parentContext])
1081
1483
  }
1082
1484
 
1083
1485
  /**
1084
- * Extracts all aliases used for each collection across the entire query tree.
1085
- *
1086
- * Traverses the QueryIR recursively to build a map from collection ID to all aliases
1087
- * that reference that collection. This is essential for self-join support, where the
1088
- * same collection may be referenced multiple times with different aliases.
1089
- *
1090
- * For example, given a query like:
1091
- * ```ts
1092
- * q.from({ employee: employeesCollection })
1093
- * .join({ manager: employeesCollection }, ({ employee, manager }) =>
1094
- * eq(employee.managerId, manager.id)
1095
- * )
1096
- * ```
1097
- *
1098
- * This function would return:
1099
- * ```
1100
- * Map { "employees" => Set { "employee", "manager" } }
1101
- * ```
1102
- *
1103
- * @param query - The query IR to extract aliases from
1104
- * @returns A map from collection ID to the set of all aliases referencing that collection
1486
+ * Creates a child Collection entry for includes subqueries.
1487
+ * The child Collection is a full-fledged Collection instance that starts syncing immediately.
1105
1488
  */
1106
- function extractCollectionAliases(query: QueryIR): Map<string, Set<string>> {
1107
- const aliasesById = new Map<string, Set<string>>()
1489
+ function createChildCollectionEntry(
1490
+ parentId: string,
1491
+ fieldName: string,
1492
+ correlationKey: unknown,
1493
+ hasOrderBy: boolean,
1494
+ nestedSetups?: Array<NestedIncludesSetup>,
1495
+ ): ChildCollectionEntry {
1496
+ const resultKeys = new WeakMap<object, unknown>()
1497
+ const orderByIndices = hasOrderBy ? new WeakMap<object, string>() : null
1498
+ let syncMethods: SyncMethods<any> | null = null
1499
+
1500
+ const compare = orderByIndices
1501
+ ? createOrderByComparator(orderByIndices)
1502
+ : undefined
1503
+
1504
+ const collection = createCollection<any, string | number>({
1505
+ id: `__child-collection:${parentId}-${fieldName}-${serializeValue(correlationKey)}`,
1506
+ getKey: (item: any) => resultKeys.get(item) as string | number,
1507
+ compare,
1508
+ sync: {
1509
+ rowUpdateMode: `full`,
1510
+ sync: (methods) => {
1511
+ syncMethods = methods
1512
+ return () => {
1513
+ syncMethods = null
1514
+ }
1515
+ },
1516
+ },
1517
+ startSync: true,
1518
+ })
1108
1519
 
1109
- function recordAlias(source: any) {
1110
- if (!source) return
1520
+ const entry: ChildCollectionEntry = {
1521
+ collection,
1522
+ get syncMethods() {
1523
+ return syncMethods
1524
+ },
1525
+ resultKeys,
1526
+ orderByIndices,
1527
+ }
1111
1528
 
1112
- if (source.type === `collectionRef`) {
1113
- const { id } = source.collection
1114
- const existing = aliasesById.get(id)
1115
- if (existing) {
1116
- existing.add(source.alias)
1117
- } else {
1118
- aliasesById.set(id, new Set([source.alias]))
1529
+ if (nestedSetups) {
1530
+ entry.includesStates = createPerEntryIncludesStates(nestedSetups)
1531
+ }
1532
+
1533
+ return entry
1534
+ }
1535
+
1536
+ /**
1537
+ * Flushes includes state using a bottom-up per-entry approach.
1538
+ * Five phases ensure correct ordering:
1539
+ * 1. Parent INSERTs — create child entries with per-entry nested states
1540
+ * 2. Child changes — apply to child Collections, update routing index
1541
+ * 3. Drain nested buffers — route buffered grandchild changes to per-entry states
1542
+ * 4. Flush per-entry states — recursively flush nested includes on each entry
1543
+ * 5. Parent DELETEs — clean up child entries and routing index
1544
+ */
1545
+ function flushIncludesState(
1546
+ includesState: Array<IncludesOutputState>,
1547
+ parentCollection: Collection<any, any, any>,
1548
+ parentId: string,
1549
+ parentChanges: Map<unknown, Changes<any>> | null,
1550
+ parentSyncMethods: SyncMethods<any> | null,
1551
+ ): void {
1552
+ for (const state of includesState) {
1553
+ // Phase 1: Parent INSERTs — ensure a child Collection exists for every parent
1554
+ if (parentChanges) {
1555
+ for (const [parentKey, changes] of parentChanges) {
1556
+ if (changes.inserts > 0) {
1557
+ const parentResult = changes.value
1558
+ // Extract routing info from INCLUDES_ROUTING symbol (set by compiler)
1559
+ const routing = parentResult[INCLUDES_ROUTING]?.[state.fieldName]
1560
+ const correlationKey = routing?.correlationKey
1561
+ const parentContext = routing?.parentContext ?? null
1562
+ const routingKey = computeRoutingKey(correlationKey, parentContext)
1563
+
1564
+ if (correlationKey != null) {
1565
+ // Ensure child Collection exists for this routing key
1566
+ if (!state.childRegistry.has(routingKey)) {
1567
+ const entry = createChildCollectionEntry(
1568
+ parentId,
1569
+ state.fieldName,
1570
+ routingKey,
1571
+ state.hasOrderBy,
1572
+ state.nestedSetups,
1573
+ )
1574
+ state.childRegistry.set(routingKey, entry)
1575
+ }
1576
+ // Update reverse index: routing key → parent keys
1577
+ let parentKeys = state.correlationToParentKeys.get(routingKey)
1578
+ if (!parentKeys) {
1579
+ parentKeys = new Set()
1580
+ state.correlationToParentKeys.set(routingKey, parentKeys)
1581
+ }
1582
+ parentKeys.add(parentKey)
1583
+
1584
+ const childValue = materializeIncludedValue(
1585
+ state,
1586
+ state.childRegistry.get(routingKey),
1587
+ )
1588
+ parentResult[state.fieldName] = childValue
1589
+
1590
+ // Parent rows may already be materialized in the live collection by the
1591
+ // time includes state is flushed, so update the stored row as well.
1592
+ const storedParent = parentCollection.get(parentKey as any)
1593
+ if (storedParent && storedParent !== parentResult) {
1594
+ storedParent[state.fieldName] = childValue
1595
+ }
1596
+ }
1597
+ }
1119
1598
  }
1120
- } else if (source.type === `queryRef`) {
1121
- traverse(source.query)
1122
1599
  }
1123
- }
1124
1600
 
1125
- function traverse(q?: QueryIR) {
1126
- if (!q) return
1601
+ // Track affected correlation keys for inline materializations before clearing child changes.
1602
+ const affectedCorrelationKeys = materializesInline(state)
1603
+ ? new Set<unknown>(state.pendingChildChanges.keys())
1604
+ : null
1605
+
1606
+ // Phase 2: Child changes — apply to child Collections
1607
+ // Track which entries had child changes and capture their childChanges maps
1608
+ const entriesWithChildChanges = new Map<
1609
+ unknown,
1610
+ { entry: ChildCollectionEntry; childChanges: Map<unknown, Changes<any>> }
1611
+ >()
1612
+ if (state.pendingChildChanges.size > 0) {
1613
+ for (const [correlationKey, childChanges] of state.pendingChildChanges) {
1614
+ // Ensure child Collection exists for this correlation key
1615
+ let entry = state.childRegistry.get(correlationKey)
1616
+ if (!entry) {
1617
+ entry = createChildCollectionEntry(
1618
+ parentId,
1619
+ state.fieldName,
1620
+ correlationKey,
1621
+ state.hasOrderBy,
1622
+ state.nestedSetups,
1623
+ )
1624
+ state.childRegistry.set(correlationKey, entry)
1625
+ }
1626
+
1627
+ if (state.materialization === `collection`) {
1628
+ attachChildCollectionToParent(
1629
+ parentCollection,
1630
+ state.fieldName,
1631
+ correlationKey,
1632
+ state.correlationToParentKeys,
1633
+ entry.collection,
1634
+ )
1635
+ }
1636
+
1637
+ // Apply child changes to the child Collection
1638
+ if (entry.syncMethods) {
1639
+ entry.syncMethods.begin()
1640
+ for (const [childKey, change] of childChanges) {
1641
+ entry.resultKeys.set(change.value, childKey)
1642
+ if (entry.orderByIndices && change.orderByIndex !== undefined) {
1643
+ entry.orderByIndices.set(change.value, change.orderByIndex)
1644
+ }
1645
+ if (change.inserts > 0 && change.deletes === 0) {
1646
+ entry.syncMethods.write({ value: change.value, type: `insert` })
1647
+ } else if (
1648
+ change.inserts > change.deletes ||
1649
+ (change.inserts === change.deletes &&
1650
+ entry.syncMethods.collection.has(
1651
+ entry.syncMethods.collection.getKeyFromItem(change.value),
1652
+ ))
1653
+ ) {
1654
+ entry.syncMethods.write({ value: change.value, type: `update` })
1655
+ } else if (change.deletes > 0) {
1656
+ entry.syncMethods.write({ value: change.value, type: `delete` })
1657
+ }
1658
+ }
1659
+ entry.syncMethods.commit()
1660
+ }
1661
+
1662
+ // Update routing index for nested includes
1663
+ updateRoutingIndex(state, correlationKey, childChanges)
1664
+
1665
+ entriesWithChildChanges.set(correlationKey, { entry, childChanges })
1666
+ }
1667
+ state.pendingChildChanges.clear()
1668
+ }
1669
+
1670
+ // Phase 3: Drain nested buffers — route buffered grandchild changes to per-entry states
1671
+ const dirtyFromBuffers = drainNestedBuffers(state)
1672
+
1673
+ // Phase 4: Flush per-entry states
1674
+ // First: entries that had child changes in Phase 2
1675
+ for (const [, { entry, childChanges }] of entriesWithChildChanges) {
1676
+ if (entry.includesStates) {
1677
+ flushIncludesState(
1678
+ entry.includesStates,
1679
+ entry.collection,
1680
+ entry.collection.id,
1681
+ childChanges,
1682
+ entry.syncMethods,
1683
+ )
1684
+ }
1685
+ }
1686
+ // Then: entries that only had buffer-routed changes (no child changes at this level)
1687
+ for (const correlationKey of dirtyFromBuffers) {
1688
+ if (entriesWithChildChanges.has(correlationKey)) continue
1689
+ const entry = state.childRegistry.get(correlationKey)
1690
+ if (entry?.includesStates) {
1691
+ flushIncludesState(
1692
+ entry.includesStates,
1693
+ entry.collection,
1694
+ entry.collection.id,
1695
+ null,
1696
+ entry.syncMethods,
1697
+ )
1698
+ }
1699
+ }
1127
1700
 
1128
- recordAlias(q.from)
1701
+ // For inline materializations: re-emit affected parents with updated snapshots.
1702
+ // We mutate items in-place (so collection.get() reflects changes immediately)
1703
+ // and emit UPDATE events directly. We bypass the sync methods because
1704
+ // commitPendingTransactions compares previous vs new visible state using
1705
+ // deepEquals, but in-place mutation means both sides reference the same
1706
+ // object, so the comparison always returns true and suppresses the event.
1707
+ const inlineReEmitKeys = materializesInline(state)
1708
+ ? new Set([...(affectedCorrelationKeys || []), ...dirtyFromBuffers])
1709
+ : null
1710
+ if (parentSyncMethods && inlineReEmitKeys && inlineReEmitKeys.size > 0) {
1711
+ const events: Array<ChangeMessage<any>> = []
1712
+ for (const correlationKey of inlineReEmitKeys) {
1713
+ const parentKeys = state.correlationToParentKeys.get(correlationKey)
1714
+ if (!parentKeys) continue
1715
+ const entry = state.childRegistry.get(correlationKey)
1716
+ for (const parentKey of parentKeys) {
1717
+ const item = parentCollection.get(parentKey as any)
1718
+ if (item) {
1719
+ const key = parentSyncMethods.collection.getKeyFromItem(item)
1720
+ // Capture previous value before in-place mutation
1721
+ const previousValue = { ...item }
1722
+ item[state.fieldName] = materializeIncludedValue(state, entry)
1723
+ events.push({
1724
+ type: `update`,
1725
+ key,
1726
+ value: item,
1727
+ previousValue,
1728
+ })
1729
+ }
1730
+ }
1731
+ }
1732
+ if (events.length > 0) {
1733
+ // Emit directly — the in-place mutation already updated the data in
1734
+ // syncedData, so we only need to notify subscribers.
1735
+ const changesManager = (parentCollection as any)._changes as {
1736
+ emitEvents: (
1737
+ changes: Array<ChangeMessage<any>>,
1738
+ forceEmit?: boolean,
1739
+ ) => void
1740
+ }
1741
+ changesManager.emitEvents(events, true)
1742
+ }
1743
+ }
1129
1744
 
1130
- if (q.join) {
1131
- for (const joinClause of q.join) {
1132
- recordAlias(joinClause.from)
1745
+ // Phase 5: Parent DELETEs — dispose child Collections and clean up
1746
+ if (parentChanges) {
1747
+ for (const [parentKey, changes] of parentChanges) {
1748
+ if (changes.deletes > 0 && changes.inserts === 0) {
1749
+ const routing = changes.value[INCLUDES_ROUTING]?.[state.fieldName]
1750
+ const correlationKey = routing?.correlationKey
1751
+ const parentContext = routing?.parentContext ?? null
1752
+ const routingKey = computeRoutingKey(correlationKey, parentContext)
1753
+ if (correlationKey != null) {
1754
+ // Clean up reverse index first, only delete child collection
1755
+ // when the last parent referencing it is removed
1756
+ const parentKeys = state.correlationToParentKeys.get(routingKey)
1757
+ if (parentKeys) {
1758
+ parentKeys.delete(parentKey)
1759
+ if (parentKeys.size === 0) {
1760
+ cleanRoutingIndexOnDelete(state, routingKey)
1761
+ state.childRegistry.delete(routingKey)
1762
+ state.correlationToParentKeys.delete(routingKey)
1763
+ }
1764
+ }
1765
+ }
1766
+ }
1133
1767
  }
1134
1768
  }
1135
1769
  }
1136
1770
 
1137
- traverse(query)
1771
+ // Clean up the internal routing stamp from parent/child results
1772
+ if (parentChanges) {
1773
+ for (const [, changes] of parentChanges) {
1774
+ delete changes.value[INCLUDES_ROUTING]
1775
+ }
1776
+ }
1777
+ }
1138
1778
 
1139
- return aliasesById
1779
+ /**
1780
+ * Checks whether any includes state has pending changes that need to be flushed.
1781
+ * Checks direct pending child changes and shared nested buffers.
1782
+ */
1783
+ function hasPendingIncludesChanges(
1784
+ states: Array<IncludesOutputState>,
1785
+ ): boolean {
1786
+ for (const state of states) {
1787
+ if (state.pendingChildChanges.size > 0) return true
1788
+ if (state.nestedSetups && hasNestedBufferChanges(state.nestedSetups))
1789
+ return true
1790
+ }
1791
+ return false
1792
+ }
1793
+
1794
+ /**
1795
+ * Attaches a child Collection to parent rows that match a given correlation key.
1796
+ * Uses the reverse index to look up parent keys directly instead of scanning.
1797
+ */
1798
+ function attachChildCollectionToParent(
1799
+ parentCollection: Collection<any, any, any>,
1800
+ fieldName: string,
1801
+ correlationKey: unknown,
1802
+ correlationToParentKeys: Map<unknown, Set<unknown>>,
1803
+ childCollection: Collection<any, any, any>,
1804
+ ): void {
1805
+ const parentKeys = correlationToParentKeys.get(correlationKey)
1806
+ if (!parentKeys) return
1807
+
1808
+ for (const parentKey of parentKeys) {
1809
+ const item = parentCollection.get(parentKey as any)
1810
+ if (item) {
1811
+ item[fieldName] = childCollection
1812
+ }
1813
+ }
1140
1814
  }
1141
1815
 
1142
1816
  function accumulateChanges<T>(