@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
@@ -1,5 +1,6 @@
1
- import { D2, output } from '@tanstack/db-ivm'
2
- import { compileQuery } from '../compiler/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'
3
4
  import {
4
5
  MissingAliasInputsError,
5
6
  SetWindowRequiresOrderByError,
@@ -16,13 +17,17 @@ import {
16
17
  extractCollectionsFromQuery,
17
18
  } from './utils.js'
18
19
  import type { LiveQueryInternalUtils } from './internal.js'
19
- import type { WindowOptions } from '../compiler/index.js'
20
+ import type {
21
+ IncludesCompilationResult,
22
+ WindowOptions,
23
+ } from '../compiler/index.js'
20
24
  import type { SchedulerContextId } from '../../scheduler.js'
21
25
  import type { CollectionSubscription } from '../../collection/subscription.js'
22
26
  import type { RootStreamBuilder } from '@tanstack/db-ivm'
23
27
  import type { OrderByOptimizationInfo } from '../compiler/order-by.js'
24
28
  import type { Collection } from '../../collection/index.js'
25
29
  import type {
30
+ ChangeMessage,
26
31
  CollectionConfigSingleRowOption,
27
32
  KeyedStream,
28
33
  ResultStream,
@@ -31,7 +36,12 @@ import type {
31
36
  UtilsRecord,
32
37
  } from '../../types.js'
33
38
  import type { Context, GetResult } from '../builder/types.js'
34
- import type { BasicExpression, QueryIR } from '../ir.js'
39
+ import type {
40
+ BasicExpression,
41
+ IncludesMaterialization,
42
+ PropRef,
43
+ QueryIR,
44
+ } from '../ir.js'
35
45
  import type { LazyCollectionCallbacks } from '../compiler/joins.js'
36
46
  import type {
37
47
  Changes,
@@ -140,6 +150,7 @@ export class CollectionConfigBuilder<
140
150
  public sourceWhereClausesCache:
141
151
  | Map<string, BasicExpression<boolean>>
142
152
  | undefined
153
+ private includesCache: Array<IncludesCompilationResult> | undefined
143
154
 
144
155
  // Map of source alias to subscription
145
156
  readonly subscriptions: Record<string, CollectionSubscription> = {}
@@ -156,7 +167,10 @@ export class CollectionConfigBuilder<
156
167
  // Generate a unique ID if not provided
157
168
  this.id = config.id || `live-query-${++liveQueryCollectionCounter}`
158
169
 
159
- this.query = buildQueryFromConfig(config)
170
+ this.query = buildQueryFromConfig({
171
+ query: config.query,
172
+ requireObjectResult: true,
173
+ })
160
174
  this.collections = extractCollectionsFromQuery(this.query)
161
175
  const collectionAliasesById = extractCollectionAliases(this.query)
162
176
 
@@ -632,6 +646,7 @@ export class CollectionConfigBuilder<
632
646
  this.inputsCache = undefined
633
647
  this.pipelineCache = undefined
634
648
  this.sourceWhereClausesCache = undefined
649
+ this.includesCache = undefined
635
650
 
636
651
  // Reset lazy source alias state
637
652
  this.lazySources.clear()
@@ -680,6 +695,7 @@ export class CollectionConfigBuilder<
680
695
  this.pipelineCache = compilation.pipeline
681
696
  this.sourceWhereClausesCache = compilation.sourceWhereClauses
682
697
  this.compiledAliasToCollectionId = compilation.aliasToCollectionId
698
+ this.includesCache = compilation.includes
683
699
 
684
700
  // Defensive check: verify all compiled aliases have corresponding inputs
685
701
  // This should never happen since all aliases come from user declarations,
@@ -727,10 +743,19 @@ export class CollectionConfigBuilder<
727
743
  }),
728
744
  )
729
745
 
746
+ // Set up includes output routing and child collection lifecycle
747
+ const includesState = this.setupIncludesOutput(
748
+ this.includesCache,
749
+ syncState,
750
+ )
751
+
730
752
  // Flush pending changes and reset the accumulator.
731
753
  // Called at the end of each graph run to commit all accumulated changes.
732
754
  syncState.flushPendingChanges = () => {
733
- if (pendingChanges.size === 0) {
755
+ const hasParentChanges = pendingChanges.size > 0
756
+ const hasChildChanges = hasPendingIncludesChanges(includesState)
757
+
758
+ if (!hasParentChanges && !hasChildChanges) {
734
759
  return
735
760
  }
736
761
 
@@ -762,10 +787,22 @@ export class CollectionConfigBuilder<
762
787
  changesToApply = merged
763
788
  }
764
789
 
765
- begin()
766
- changesToApply.forEach(this.applyChanges.bind(this, config))
767
- commit()
790
+ // 1. Flush parent changes
791
+ if (hasParentChanges) {
792
+ begin()
793
+ changesToApply.forEach(this.applyChanges.bind(this, config))
794
+ commit()
795
+ }
768
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
+ )
769
806
  }
770
807
 
771
808
  graph.finalize()
@@ -778,6 +815,88 @@ export class CollectionConfigBuilder<
778
815
  return syncState as FullSyncState
779
816
  }
780
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
+
781
900
  private applyChanges(
782
901
  config: SyncMethods<TResult>,
783
902
  changes: {
@@ -1013,6 +1132,687 @@ function createOrderByComparator<T extends object>(
1013
1132
  }
1014
1133
  }
1015
1134
 
1135
+ /**
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
1150
+ */
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 []
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
1212
+ }
1213
+
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()
1311
+ }
1312
+
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 })
1363
+ }
1364
+ }
1365
+
1366
+ dirtyCorrelationKeys.add(parentCorrelationKey)
1367
+ toDelete.push(nestedCorrelationKey)
1368
+ }
1369
+
1370
+ for (const key of toDelete) {
1371
+ setup.buffer.delete(key)
1372
+ }
1373
+ }
1374
+
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
+ }
1440
+
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
+ }
1458
+ }
1459
+
1460
+ /**
1461
+ * Recursively checks whether any nested buffer has pending changes.
1462
+ */
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
1468
+ }
1469
+ return false
1470
+ }
1471
+
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])
1483
+ }
1484
+
1485
+ /**
1486
+ * Creates a child Collection entry for includes subqueries.
1487
+ * The child Collection is a full-fledged Collection instance that starts syncing immediately.
1488
+ */
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
+ })
1519
+
1520
+ const entry: ChildCollectionEntry = {
1521
+ collection,
1522
+ get syncMethods() {
1523
+ return syncMethods
1524
+ },
1525
+ resultKeys,
1526
+ orderByIndices,
1527
+ }
1528
+
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
+ }
1598
+ }
1599
+ }
1600
+
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
+ }
1700
+
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
+ }
1744
+
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
+ }
1767
+ }
1768
+ }
1769
+ }
1770
+
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
+ }
1778
+
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
+ }
1814
+ }
1815
+
1016
1816
  function accumulateChanges<T>(
1017
1817
  acc: Map<unknown, Changes<T>>,
1018
1818
  [[key, tupleData], multiplicity]: [