@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.
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +3 -2
- package/dist/cjs/collection/changes.cjs +13 -4
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/changes.d.cts +10 -1
- package/dist/cjs/collection/cleanup-queue.cjs +89 -0
- package/dist/cjs/collection/cleanup-queue.cjs.map +1 -0
- package/dist/cjs/collection/cleanup-queue.d.cts +30 -0
- package/dist/cjs/collection/events.cjs +14 -0
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/events.d.cts +39 -1
- package/dist/cjs/collection/index.cjs +66 -28
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +49 -36
- package/dist/cjs/collection/indexes.cjs +211 -62
- package/dist/cjs/collection/indexes.cjs.map +1 -1
- package/dist/cjs/collection/indexes.d.cts +27 -17
- package/dist/cjs/collection/lifecycle.cjs +5 -22
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +0 -1
- package/dist/cjs/collection/mutations.cjs +18 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -1
- package/dist/cjs/collection/mutations.d.cts +1 -0
- package/dist/cjs/collection/state.cjs +381 -53
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +65 -1
- package/dist/cjs/collection/subscription.cjs +6 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +4 -0
- package/dist/cjs/collection/sync.cjs +108 -1
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +2 -0
- package/dist/cjs/collection/transaction-metadata.cjs +5 -0
- package/dist/cjs/collection/transaction-metadata.cjs.map +1 -0
- package/dist/cjs/collection/transaction-metadata.d.cts +1 -0
- package/dist/cjs/errors.cjs +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +22 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +11 -3
- package/dist/cjs/indexes/auto-index.cjs +13 -6
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.cjs +0 -3
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -6
- package/dist/cjs/indexes/basic-index.cjs +361 -0
- package/dist/cjs/indexes/basic-index.cjs.map +1 -0
- package/dist/cjs/indexes/basic-index.d.cts +102 -0
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/indexes/index-options.d.cts +8 -9
- package/dist/cjs/indexes/index-registry.cjs +89 -0
- package/dist/cjs/indexes/index-registry.cjs.map +1 -0
- package/dist/cjs/indexes/index-registry.d.cts +61 -0
- package/dist/cjs/local-only.cjs +5 -0
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +27 -11
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +25 -3
- package/dist/cjs/query/builder/index.cjs +200 -39
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +4 -3
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +14 -3
- package/dist/cjs/query/builder/types.d.cts +84 -19
- package/dist/cjs/query/compiler/evaluators.cjs +51 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +100 -28
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.d.cts +4 -2
- package/dist/cjs/query/compiler/index.cjs +283 -11
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +30 -2
- package/dist/cjs/query/compiler/order-by.cjs +29 -10
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/compiler/select.cjs +8 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/index.d.cts +2 -1
- package/dist/cjs/query/ir.cjs +18 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +21 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +501 -5
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
- package/dist/cjs/query/live/types.d.cts +3 -3
- package/dist/cjs/query/live/utils.cjs +43 -3
- package/dist/cjs/query/live/utils.cjs.map +1 -1
- package/dist/cjs/query/live/utils.d.cts +1 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +9 -6
- package/dist/cjs/query/query-once.cjs.map +1 -1
- package/dist/cjs/query/query-once.d.cts +7 -5
- package/dist/cjs/query/subset-dedupe.cjs +9 -3
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
- package/dist/cjs/types.d.cts +42 -8
- package/dist/cjs/utils/array-utils.cjs +27 -0
- package/dist/cjs/utils/array-utils.cjs.map +1 -0
- package/dist/cjs/utils/array-utils.d.cts +16 -0
- package/dist/cjs/utils/comparison.cjs +11 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.cjs +4 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils.cjs +7 -9
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +6 -1
- package/dist/cjs/virtual-props.cjs +33 -0
- package/dist/cjs/virtual-props.cjs.map +1 -0
- package/dist/cjs/virtual-props.d.cts +196 -0
- package/dist/esm/collection/change-events.d.ts +3 -2
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/changes.d.ts +10 -1
- package/dist/esm/collection/changes.js +13 -4
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/cleanup-queue.d.ts +30 -0
- package/dist/esm/collection/cleanup-queue.js +89 -0
- package/dist/esm/collection/cleanup-queue.js.map +1 -0
- package/dist/esm/collection/events.d.ts +39 -1
- package/dist/esm/collection/events.js +14 -0
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +49 -36
- package/dist/esm/collection/index.js +67 -29
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/indexes.d.ts +27 -17
- package/dist/esm/collection/indexes.js +211 -62
- package/dist/esm/collection/indexes.js.map +1 -1
- package/dist/esm/collection/lifecycle.d.ts +0 -1
- package/dist/esm/collection/lifecycle.js +5 -22
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/mutations.d.ts +1 -0
- package/dist/esm/collection/mutations.js +18 -0
- package/dist/esm/collection/mutations.js.map +1 -1
- package/dist/esm/collection/state.d.ts +65 -1
- package/dist/esm/collection/state.js +381 -53
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +4 -0
- package/dist/esm/collection/subscription.js +6 -0
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +2 -0
- package/dist/esm/collection/sync.js +108 -1
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/collection/transaction-metadata.d.ts +1 -0
- package/dist/esm/collection/transaction-metadata.js +5 -0
- package/dist/esm/collection/transaction-metadata.js.map +1 -0
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +8 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +11 -3
- package/dist/esm/index.js +25 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +13 -6
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -6
- package/dist/esm/indexes/base-index.js +1 -4
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/basic-index.d.ts +102 -0
- package/dist/esm/indexes/basic-index.js +361 -0
- package/dist/esm/indexes/basic-index.js.map +1 -0
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/index-options.d.ts +8 -9
- package/dist/esm/indexes/index-registry.d.ts +61 -0
- package/dist/esm/indexes/index-registry.js +89 -0
- package/dist/esm/indexes/index-registry.js.map +1 -0
- package/dist/esm/local-only.js +5 -0
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +25 -3
- package/dist/esm/query/builder/functions.js +27 -11
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.d.ts +4 -3
- package/dist/esm/query/builder/index.js +201 -40
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +84 -19
- package/dist/esm/query/compiler/evaluators.js +51 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.d.ts +4 -2
- package/dist/esm/query/compiler/group-by.js +101 -29
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +30 -2
- package/dist/esm/query/compiler/index.js +285 -13
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js +30 -11
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js +8 -0
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/index.d.ts +2 -1
- package/dist/esm/query/ir.d.ts +21 -1
- package/dist/esm/query/ir.js +18 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
- package/dist/esm/query/live/collection-config-builder.js +503 -7
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/types.d.ts +3 -3
- package/dist/esm/query/live/utils.d.ts +1 -0
- package/dist/esm/query/live/utils.js +43 -3
- package/dist/esm/query/live/utils.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +9 -6
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/query-once.d.ts +7 -5
- package/dist/esm/query/query-once.js.map +1 -1
- package/dist/esm/query/subset-dedupe.js +9 -3
- package/dist/esm/query/subset-dedupe.js.map +1 -1
- package/dist/esm/types.d.ts +42 -8
- package/dist/esm/utils/array-utils.d.ts +16 -0
- package/dist/esm/utils/array-utils.js +27 -0
- package/dist/esm/utils/array-utils.js.map +1 -0
- package/dist/esm/utils/comparison.js +11 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.js +4 -0
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/dist/esm/utils.d.ts +6 -1
- package/dist/esm/utils.js +7 -9
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/virtual-props.d.ts +196 -0
- package/dist/esm/virtual-props.js +33 -0
- package/dist/esm/virtual-props.js.map +1 -0
- package/package.json +4 -3
- package/skills/db-core/SKILL.md +4 -2
- package/skills/db-core/collection-setup/SKILL.md +30 -11
- package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
- package/skills/db-core/collection-setup/references/powersync-adapter.md +4 -0
- package/skills/db-core/collection-setup/references/query-adapter.md +32 -0
- package/skills/db-core/custom-adapter/SKILL.md +58 -9
- package/skills/db-core/live-queries/SKILL.md +162 -2
- package/skills/db-core/mutations-optimistic/SKILL.md +1 -1
- package/skills/db-core/persistence/SKILL.md +241 -0
- package/skills/meta-framework/SKILL.md +1 -1
- package/src/collection/change-events.ts +13 -9
- package/src/collection/changes.ts +30 -7
- package/src/collection/cleanup-queue.ts +105 -0
- package/src/collection/events.ts +65 -0
- package/src/collection/index.ts +110 -45
- package/src/collection/indexes.ts +283 -76
- package/src/collection/lifecycle.ts +5 -26
- package/src/collection/mutations.ts +21 -0
- package/src/collection/state.ts +545 -71
- package/src/collection/subscription.ts +7 -0
- package/src/collection/sync.ts +137 -0
- package/src/collection/transaction-metadata.ts +1 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +46 -3
- package/src/indexes/auto-index.ts +18 -8
- package/src/indexes/base-index.ts +2 -10
- package/src/indexes/basic-index.ts +507 -0
- package/src/indexes/btree-index.ts +1 -1
- package/src/indexes/index-options.ts +17 -37
- package/src/indexes/index-registry.ts +174 -0
- package/src/local-only.ts +7 -0
- package/src/query/builder/functions.ts +84 -7
- package/src/query/builder/index.ts +329 -9
- package/src/query/builder/ref-proxy.ts +22 -4
- package/src/query/builder/types.ts +257 -62
- package/src/query/compiler/evaluators.ts +57 -0
- package/src/query/compiler/group-by.ts +156 -35
- package/src/query/compiler/index.ts +445 -15
- package/src/query/compiler/order-by.ts +51 -12
- package/src/query/compiler/select.ts +9 -0
- package/src/query/index.ts +7 -0
- package/src/query/ir.ts +23 -2
- package/src/query/live/collection-config-builder.ts +809 -9
- package/src/query/live/types.ts +10 -4
- package/src/query/live/utils.ts +64 -3
- package/src/query/live-query-collection.ts +43 -18
- package/src/query/query-once.ts +31 -12
- package/src/query/subset-dedupe.ts +11 -7
- package/src/types.ts +49 -9
- package/src/utils/array-utils.ts +49 -0
- package/src/utils/comparison.ts +14 -0
- package/src/utils/index-optimization.ts +4 -0
- package/src/utils.ts +12 -9
- package/src/virtual-props.ts +282 -0
- package/dist/cjs/indexes/lazy-index.cjs +0 -190
- package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
- package/dist/cjs/indexes/lazy-index.d.cts +0 -96
- package/dist/esm/indexes/lazy-index.d.ts +0 -96
- package/dist/esm/indexes/lazy-index.js +0 -190
- package/dist/esm/indexes/lazy-index.js.map +0 -1
- 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 {
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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]: [
|