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