@tanstack/db 0.4.8 → 0.4.9
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/errors.cjs +51 -17
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +38 -8
- package/dist/cjs/index.cjs +8 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +42 -19
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +33 -8
- package/dist/cjs/query/compiler/joins.cjs +88 -66
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +5 -2
- package/dist/cjs/query/compiler/order-by.cjs +2 -0
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +276 -42
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +84 -8
- package/dist/cjs/query/live/collection-registry.cjs +16 -0
- package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
- package/dist/cjs/query/live/collection-registry.d.cts +26 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +57 -58
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
- package/dist/cjs/query/live-query-collection.cjs +11 -5
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +10 -3
- package/dist/cjs/query/optimizer.cjs +44 -7
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +4 -4
- package/dist/cjs/scheduler.cjs +137 -0
- package/dist/cjs/scheduler.cjs.map +1 -0
- package/dist/cjs/scheduler.d.cts +56 -0
- package/dist/cjs/transactions.cjs +7 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/esm/errors.d.ts +38 -8
- package/dist/esm/errors.js +52 -18
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +9 -5
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +33 -8
- package/dist/esm/query/compiler/index.js +42 -19
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +5 -2
- package/dist/esm/query/compiler/joins.js +90 -68
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -0
- package/dist/esm/query/compiler/order-by.js +2 -0
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +84 -8
- package/dist/esm/query/live/collection-config-builder.js +276 -42
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-registry.d.ts +26 -0
- package/dist/esm/query/live/collection-registry.js +16 -0
- package/dist/esm/query/live/collection-registry.js.map +1 -0
- package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
- package/dist/esm/query/live/collection-subscriber.js +57 -58
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +10 -3
- package/dist/esm/query/live-query-collection.js +11 -5
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +4 -4
- package/dist/esm/query/optimizer.js +44 -7
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/scheduler.d.ts +56 -0
- package/dist/esm/scheduler.js +137 -0
- package/dist/esm/scheduler.js.map +1 -0
- package/dist/esm/transactions.js +7 -1
- package/dist/esm/transactions.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +79 -13
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +115 -32
- package/src/query/compiler/joins.ts +180 -127
- package/src/query/compiler/order-by.ts +7 -0
- package/src/query/compiler/select.ts +2 -3
- package/src/query/live/collection-config-builder.ts +450 -58
- package/src/query/live/collection-registry.ts +47 -0
- package/src/query/live/collection-subscriber.ts +88 -106
- package/src/query/live-query-collection.ts +39 -14
- package/src/query/optimizer.ts +85 -15
- package/src/scheduler.ts +198 -0
- package/src/transactions.ts +12 -1
|
@@ -1,35 +1,107 @@
|
|
|
1
|
+
import { SchedulerContextId } from '../../scheduler.js';
|
|
1
2
|
import { CollectionSubscription } from '../../collection/subscription.js';
|
|
2
3
|
import { OrderByOptimizationInfo } from '../compiler/order-by.js';
|
|
3
|
-
import { CollectionConfigSingleRowOption, SyncConfig } from '../../types.js';
|
|
4
|
+
import { CollectionConfigSingleRowOption, SyncConfig, UtilsRecord } from '../../types.js';
|
|
4
5
|
import { Context, GetResult } from '../builder/types.js';
|
|
5
6
|
import { BasicExpression, QueryIR } from '../ir.js';
|
|
6
7
|
import { LazyCollectionCallbacks } from '../compiler/joins.js';
|
|
7
8
|
import { FullSyncState, LiveQueryCollectionConfig } from './types.js';
|
|
8
|
-
type
|
|
9
|
+
export type LiveQueryCollectionUtils = UtilsRecord & {
|
|
10
|
+
getRunCount: () => number;
|
|
11
|
+
getBuilder: () => CollectionConfigBuilder<any, any>;
|
|
12
|
+
};
|
|
9
13
|
export declare class CollectionConfigBuilder<TContext extends Context, TResult extends object = GetResult<TContext>> {
|
|
10
14
|
private readonly config;
|
|
11
15
|
private readonly id;
|
|
12
16
|
readonly query: QueryIR;
|
|
13
17
|
private readonly collections;
|
|
18
|
+
private readonly collectionByAlias;
|
|
19
|
+
private compiledAliasToCollectionId;
|
|
14
20
|
private readonly resultKeys;
|
|
15
21
|
private readonly orderByIndices;
|
|
16
22
|
private readonly compare?;
|
|
17
23
|
private isGraphRunning;
|
|
24
|
+
private runCount;
|
|
25
|
+
currentSyncConfig: Parameters<SyncConfig<TResult>[`sync`]>[0] | undefined;
|
|
26
|
+
currentSyncState: FullSyncState | undefined;
|
|
18
27
|
private isInErrorState;
|
|
19
28
|
private liveQueryCollection?;
|
|
29
|
+
private readonly aliasDependencies;
|
|
30
|
+
private readonly builderDependencies;
|
|
31
|
+
private readonly pendingGraphRuns;
|
|
32
|
+
private unsubscribeFromSchedulerClears?;
|
|
20
33
|
private graphCache;
|
|
21
34
|
private inputsCache;
|
|
22
35
|
private pipelineCache;
|
|
23
|
-
|
|
36
|
+
sourceWhereClausesCache: Map<string, BasicExpression<boolean>> | undefined;
|
|
24
37
|
readonly subscriptions: Record<string, CollectionSubscription>;
|
|
25
|
-
|
|
26
|
-
readonly
|
|
38
|
+
lazySourcesCallbacks: Record<string, LazyCollectionCallbacks>;
|
|
39
|
+
readonly lazySources: Set<string>;
|
|
27
40
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>;
|
|
28
41
|
constructor(config: LiveQueryCollectionConfig<TContext, TResult>);
|
|
29
|
-
getConfig(): CollectionConfigSingleRowOption<TResult
|
|
30
|
-
|
|
42
|
+
getConfig(): CollectionConfigSingleRowOption<TResult> & {
|
|
43
|
+
utils: LiveQueryCollectionUtils;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Resolves a collection alias to its collection ID.
|
|
47
|
+
*
|
|
48
|
+
* Uses a two-tier lookup strategy:
|
|
49
|
+
* 1. First checks compiled aliases (includes subquery inner aliases)
|
|
50
|
+
* 2. Falls back to declared aliases from the query's from/join clauses
|
|
51
|
+
*
|
|
52
|
+
* @param alias - The alias to resolve (e.g., "employee", "manager")
|
|
53
|
+
* @returns The collection ID that the alias references
|
|
54
|
+
* @throws {Error} If the alias is not found in either lookup
|
|
55
|
+
*/
|
|
56
|
+
getCollectionIdForAlias(alias: string): string;
|
|
57
|
+
isLazyAlias(alias: string): boolean;
|
|
58
|
+
maybeRunGraph(callback?: () => boolean): void;
|
|
59
|
+
/**
|
|
60
|
+
* Schedules a graph run with the transaction-scoped scheduler.
|
|
61
|
+
* Ensures each builder runs at most once per transaction, with automatic dependency tracking
|
|
62
|
+
* to run parent queries before child queries. Outside a transaction, runs immediately.
|
|
63
|
+
*
|
|
64
|
+
* Multiple calls during a transaction are coalesced into a single execution.
|
|
65
|
+
* Dependencies are auto-discovered from subscribed live queries, or can be overridden.
|
|
66
|
+
* Load callbacks are combined when entries merge.
|
|
67
|
+
*
|
|
68
|
+
* Uses the current sync session's config and syncState from instance properties.
|
|
69
|
+
*
|
|
70
|
+
* @param callback - Optional callback to load more data if needed (returns true when done)
|
|
71
|
+
* @param options - Optional scheduling configuration
|
|
72
|
+
* @param options.contextId - Transaction ID to group work; defaults to active transaction
|
|
73
|
+
* @param options.jobId - Unique identifier for this job; defaults to this builder instance
|
|
74
|
+
* @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies
|
|
75
|
+
* @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies
|
|
76
|
+
*/
|
|
77
|
+
scheduleGraphRun(callback?: () => boolean, options?: {
|
|
78
|
+
contextId?: SchedulerContextId;
|
|
79
|
+
jobId?: unknown;
|
|
80
|
+
alias?: string;
|
|
81
|
+
dependencies?: Array<CollectionConfigBuilder<any, any>>;
|
|
82
|
+
}): void;
|
|
83
|
+
/**
|
|
84
|
+
* Clears pending graph run state for a specific context.
|
|
85
|
+
* Called when the scheduler clears a context (e.g., transaction rollback/abort).
|
|
86
|
+
*/
|
|
87
|
+
clearPendingGraphRun(contextId: SchedulerContextId): void;
|
|
88
|
+
/**
|
|
89
|
+
* Executes a pending graph run. Called by the scheduler when dependencies are satisfied.
|
|
90
|
+
* Clears the pending state BEFORE execution so that any re-schedules during the run
|
|
91
|
+
* create fresh state and don't interfere with the current execution.
|
|
92
|
+
* Uses instance sync state - if sync has ended, gracefully returns without executing.
|
|
93
|
+
*
|
|
94
|
+
* @param contextId - Optional context ID to look up pending state
|
|
95
|
+
* @param pendingParam - For immediate execution (no context), pending state is passed directly
|
|
96
|
+
*/
|
|
97
|
+
private executeGraphRun;
|
|
31
98
|
private getSyncConfig;
|
|
99
|
+
incrementRunCount(): void;
|
|
100
|
+
getRunCount(): number;
|
|
32
101
|
private syncFn;
|
|
102
|
+
/**
|
|
103
|
+
* Compiles the query pipeline with all declared aliases.
|
|
104
|
+
*/
|
|
33
105
|
private compileBasePipeline;
|
|
34
106
|
private maybeCompileBasePipeline;
|
|
35
107
|
private extendPipelineWithChangeProcessing;
|
|
@@ -47,6 +119,10 @@ export declare class CollectionConfigBuilder<TContext extends Context, TResult e
|
|
|
47
119
|
*/
|
|
48
120
|
private transitionToError;
|
|
49
121
|
private allCollectionsReady;
|
|
122
|
+
/**
|
|
123
|
+
* Creates per-alias subscriptions enabling self-join support.
|
|
124
|
+
* Each alias gets its own subscription with independent filters, even for the same collection.
|
|
125
|
+
* Example: `{ employee: col, manager: col }` creates two separate subscriptions.
|
|
126
|
+
*/
|
|
50
127
|
private subscribeToAllCollections;
|
|
51
128
|
}
|
|
52
|
-
export {};
|
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
import { D2, output } from "@tanstack/db-ivm";
|
|
2
2
|
import { compileQuery } from "../compiler/index.js";
|
|
3
3
|
import { buildQuery, getQueryIR } from "../builder/index.js";
|
|
4
|
+
import { MissingAliasInputsError } from "../../errors.js";
|
|
5
|
+
import { transactionScopedScheduler } from "../../scheduler.js";
|
|
6
|
+
import { getActiveTransaction } from "../../transactions.js";
|
|
4
7
|
import { CollectionSubscriber } from "./collection-subscriber.js";
|
|
8
|
+
import { getCollectionBuilder } from "./collection-registry.js";
|
|
5
9
|
let liveQueryCollectionCounter = 0;
|
|
6
10
|
class CollectionConfigBuilder {
|
|
7
11
|
constructor(config) {
|
|
8
12
|
this.config = config;
|
|
13
|
+
this.compiledAliasToCollectionId = {};
|
|
9
14
|
this.resultKeys = /* @__PURE__ */ new WeakMap();
|
|
10
15
|
this.orderByIndices = /* @__PURE__ */ new WeakMap();
|
|
11
16
|
this.isGraphRunning = false;
|
|
17
|
+
this.runCount = 0;
|
|
12
18
|
this.isInErrorState = false;
|
|
19
|
+
this.aliasDependencies = {};
|
|
20
|
+
this.builderDependencies = /* @__PURE__ */ new Set();
|
|
21
|
+
this.pendingGraphRuns = /* @__PURE__ */ new Map();
|
|
13
22
|
this.subscriptions = {};
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
23
|
+
this.lazySourcesCallbacks = {};
|
|
24
|
+
this.lazySources = /* @__PURE__ */ new Set();
|
|
16
25
|
this.optimizableOrderByCollections = {};
|
|
17
26
|
this.id = config.id || `live-query-${++liveQueryCollectionCounter}`;
|
|
18
27
|
this.query = buildQueryFromConfig(config);
|
|
19
28
|
this.collections = extractCollectionsFromQuery(this.query);
|
|
29
|
+
const collectionAliasesById = extractCollectionAliases(this.query);
|
|
30
|
+
this.collectionByAlias = {};
|
|
31
|
+
for (const [collectionId, aliases] of collectionAliasesById.entries()) {
|
|
32
|
+
const collection = this.collections[collectionId];
|
|
33
|
+
if (!collection) continue;
|
|
34
|
+
for (const alias of aliases) {
|
|
35
|
+
this.collectionByAlias[alias] = collection;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
20
38
|
if (this.query.orderBy && this.query.orderBy.length > 0) {
|
|
21
39
|
this.compare = createOrderByComparator(this.orderByIndices);
|
|
22
40
|
}
|
|
@@ -35,9 +53,38 @@ class CollectionConfigBuilder {
|
|
|
35
53
|
onUpdate: this.config.onUpdate,
|
|
36
54
|
onDelete: this.config.onDelete,
|
|
37
55
|
startSync: this.config.startSync,
|
|
38
|
-
singleResult: this.query.singleResult
|
|
56
|
+
singleResult: this.query.singleResult,
|
|
57
|
+
utils: {
|
|
58
|
+
getRunCount: this.getRunCount.bind(this),
|
|
59
|
+
getBuilder: () => this
|
|
60
|
+
}
|
|
39
61
|
};
|
|
40
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolves a collection alias to its collection ID.
|
|
65
|
+
*
|
|
66
|
+
* Uses a two-tier lookup strategy:
|
|
67
|
+
* 1. First checks compiled aliases (includes subquery inner aliases)
|
|
68
|
+
* 2. Falls back to declared aliases from the query's from/join clauses
|
|
69
|
+
*
|
|
70
|
+
* @param alias - The alias to resolve (e.g., "employee", "manager")
|
|
71
|
+
* @returns The collection ID that the alias references
|
|
72
|
+
* @throws {Error} If the alias is not found in either lookup
|
|
73
|
+
*/
|
|
74
|
+
getCollectionIdForAlias(alias) {
|
|
75
|
+
const compiled = this.compiledAliasToCollectionId[alias];
|
|
76
|
+
if (compiled) {
|
|
77
|
+
return compiled;
|
|
78
|
+
}
|
|
79
|
+
const collection = this.collectionByAlias[alias];
|
|
80
|
+
if (collection) {
|
|
81
|
+
return collection.id;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Unknown source alias "${alias}"`);
|
|
84
|
+
}
|
|
85
|
+
isLazyAlias(alias) {
|
|
86
|
+
return this.lazySources.has(alias);
|
|
87
|
+
}
|
|
41
88
|
// The callback function is called after the graph has run.
|
|
42
89
|
// This gives the callback a chance to load more data if needed,
|
|
43
90
|
// that's used to optimize orderBy operators that set a limit,
|
|
@@ -45,14 +92,20 @@ class CollectionConfigBuilder {
|
|
|
45
92
|
// That can happen because even though we load N rows, the pipeline might filter some of these rows out
|
|
46
93
|
// causing the orderBy operator to receive less than N rows or even no rows at all.
|
|
47
94
|
// So this callback would notice that it doesn't have enough rows and load some more.
|
|
48
|
-
// The callback returns a boolean, when it's true it's done loading data.
|
|
49
|
-
maybeRunGraph(
|
|
95
|
+
// The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.
|
|
96
|
+
maybeRunGraph(callback) {
|
|
50
97
|
if (this.isGraphRunning) {
|
|
51
98
|
return;
|
|
52
99
|
}
|
|
100
|
+
if (!this.currentSyncConfig || !this.currentSyncState) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`maybeRunGraph called without active sync session. This should not happen.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
53
105
|
this.isGraphRunning = true;
|
|
54
106
|
try {
|
|
55
|
-
const { begin, commit } =
|
|
107
|
+
const { begin, commit } = this.currentSyncConfig;
|
|
108
|
+
const syncState = this.currentSyncState;
|
|
56
109
|
if (this.isInErrorState) {
|
|
57
110
|
return;
|
|
58
111
|
}
|
|
@@ -64,21 +117,136 @@ class CollectionConfigBuilder {
|
|
|
64
117
|
if (syncState.messagesCount === 0) {
|
|
65
118
|
begin();
|
|
66
119
|
commit();
|
|
67
|
-
this.updateLiveQueryStatus(
|
|
120
|
+
this.updateLiveQueryStatus(this.currentSyncConfig);
|
|
68
121
|
}
|
|
69
122
|
}
|
|
70
123
|
} finally {
|
|
71
124
|
this.isGraphRunning = false;
|
|
72
125
|
}
|
|
73
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Schedules a graph run with the transaction-scoped scheduler.
|
|
129
|
+
* Ensures each builder runs at most once per transaction, with automatic dependency tracking
|
|
130
|
+
* to run parent queries before child queries. Outside a transaction, runs immediately.
|
|
131
|
+
*
|
|
132
|
+
* Multiple calls during a transaction are coalesced into a single execution.
|
|
133
|
+
* Dependencies are auto-discovered from subscribed live queries, or can be overridden.
|
|
134
|
+
* Load callbacks are combined when entries merge.
|
|
135
|
+
*
|
|
136
|
+
* Uses the current sync session's config and syncState from instance properties.
|
|
137
|
+
*
|
|
138
|
+
* @param callback - Optional callback to load more data if needed (returns true when done)
|
|
139
|
+
* @param options - Optional scheduling configuration
|
|
140
|
+
* @param options.contextId - Transaction ID to group work; defaults to active transaction
|
|
141
|
+
* @param options.jobId - Unique identifier for this job; defaults to this builder instance
|
|
142
|
+
* @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies
|
|
143
|
+
* @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies
|
|
144
|
+
*/
|
|
145
|
+
scheduleGraphRun(callback, options) {
|
|
146
|
+
const contextId = options?.contextId ?? getActiveTransaction()?.id;
|
|
147
|
+
const jobId = options?.jobId ?? this;
|
|
148
|
+
const dependentBuilders = (() => {
|
|
149
|
+
if (options?.dependencies) {
|
|
150
|
+
return options.dependencies;
|
|
151
|
+
}
|
|
152
|
+
const deps = new Set(this.builderDependencies);
|
|
153
|
+
if (options?.alias) {
|
|
154
|
+
const aliasDeps = this.aliasDependencies[options.alias];
|
|
155
|
+
if (aliasDeps) {
|
|
156
|
+
for (const dep of aliasDeps) {
|
|
157
|
+
deps.add(dep);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
deps.delete(this);
|
|
162
|
+
return Array.from(deps);
|
|
163
|
+
})();
|
|
164
|
+
if (!this.currentSyncConfig || !this.currentSyncState) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`scheduleGraphRun called without active sync session. This should not happen.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
let pending = contextId ? this.pendingGraphRuns.get(contextId) : void 0;
|
|
170
|
+
if (!pending) {
|
|
171
|
+
pending = {
|
|
172
|
+
loadCallbacks: /* @__PURE__ */ new Set()
|
|
173
|
+
};
|
|
174
|
+
if (contextId) {
|
|
175
|
+
this.pendingGraphRuns.set(contextId, pending);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (callback) {
|
|
179
|
+
pending.loadCallbacks.add(callback);
|
|
180
|
+
}
|
|
181
|
+
const pendingToPass = contextId ? void 0 : pending;
|
|
182
|
+
transactionScopedScheduler.schedule({
|
|
183
|
+
contextId,
|
|
184
|
+
jobId,
|
|
185
|
+
dependencies: dependentBuilders,
|
|
186
|
+
run: () => this.executeGraphRun(contextId, pendingToPass)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Clears pending graph run state for a specific context.
|
|
191
|
+
* Called when the scheduler clears a context (e.g., transaction rollback/abort).
|
|
192
|
+
*/
|
|
193
|
+
clearPendingGraphRun(contextId) {
|
|
194
|
+
this.pendingGraphRuns.delete(contextId);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Executes a pending graph run. Called by the scheduler when dependencies are satisfied.
|
|
198
|
+
* Clears the pending state BEFORE execution so that any re-schedules during the run
|
|
199
|
+
* create fresh state and don't interfere with the current execution.
|
|
200
|
+
* Uses instance sync state - if sync has ended, gracefully returns without executing.
|
|
201
|
+
*
|
|
202
|
+
* @param contextId - Optional context ID to look up pending state
|
|
203
|
+
* @param pendingParam - For immediate execution (no context), pending state is passed directly
|
|
204
|
+
*/
|
|
205
|
+
executeGraphRun(contextId, pendingParam) {
|
|
206
|
+
const pending = pendingParam ?? (contextId ? this.pendingGraphRuns.get(contextId) : void 0);
|
|
207
|
+
if (contextId) {
|
|
208
|
+
this.pendingGraphRuns.delete(contextId);
|
|
209
|
+
}
|
|
210
|
+
if (!pending) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!this.currentSyncConfig || !this.currentSyncState) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.incrementRunCount();
|
|
217
|
+
const combinedLoader = () => {
|
|
218
|
+
let allDone = true;
|
|
219
|
+
let firstError;
|
|
220
|
+
pending.loadCallbacks.forEach((loader) => {
|
|
221
|
+
try {
|
|
222
|
+
allDone = loader() && allDone;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
allDone = false;
|
|
225
|
+
firstError ??= error;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
if (firstError) {
|
|
229
|
+
throw firstError;
|
|
230
|
+
}
|
|
231
|
+
return allDone;
|
|
232
|
+
};
|
|
233
|
+
this.maybeRunGraph(combinedLoader);
|
|
234
|
+
}
|
|
74
235
|
getSyncConfig() {
|
|
75
236
|
return {
|
|
76
237
|
rowUpdateMode: `full`,
|
|
77
238
|
sync: this.syncFn.bind(this)
|
|
78
239
|
};
|
|
79
240
|
}
|
|
241
|
+
incrementRunCount() {
|
|
242
|
+
this.runCount++;
|
|
243
|
+
}
|
|
244
|
+
getRunCount() {
|
|
245
|
+
return this.runCount;
|
|
246
|
+
}
|
|
80
247
|
syncFn(config) {
|
|
81
248
|
this.liveQueryCollection = config.collection;
|
|
249
|
+
this.currentSyncConfig = config;
|
|
82
250
|
const syncState = {
|
|
83
251
|
messagesCount: 0,
|
|
84
252
|
subscribedToAllCollections: false,
|
|
@@ -88,44 +256,66 @@ class CollectionConfigBuilder {
|
|
|
88
256
|
config,
|
|
89
257
|
syncState
|
|
90
258
|
);
|
|
259
|
+
this.currentSyncState = fullSyncState;
|
|
260
|
+
this.unsubscribeFromSchedulerClears = transactionScopedScheduler.onClear(
|
|
261
|
+
(contextId) => {
|
|
262
|
+
this.clearPendingGraphRun(contextId);
|
|
263
|
+
}
|
|
264
|
+
);
|
|
91
265
|
const loadMoreDataCallbacks = this.subscribeToAllCollections(
|
|
92
266
|
config,
|
|
93
267
|
fullSyncState
|
|
94
268
|
);
|
|
95
|
-
this.
|
|
269
|
+
this.scheduleGraphRun(loadMoreDataCallbacks);
|
|
96
270
|
return () => {
|
|
97
271
|
syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
|
|
272
|
+
this.currentSyncConfig = void 0;
|
|
273
|
+
this.currentSyncState = void 0;
|
|
274
|
+
this.pendingGraphRuns.clear();
|
|
98
275
|
this.graphCache = void 0;
|
|
99
276
|
this.inputsCache = void 0;
|
|
100
277
|
this.pipelineCache = void 0;
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
278
|
+
this.sourceWhereClausesCache = void 0;
|
|
279
|
+
this.lazySources.clear();
|
|
103
280
|
this.optimizableOrderByCollections = {};
|
|
104
|
-
this.
|
|
281
|
+
this.lazySourcesCallbacks = {};
|
|
282
|
+
Object.keys(this.subscriptions).forEach(
|
|
283
|
+
(key) => delete this.subscriptions[key]
|
|
284
|
+
);
|
|
285
|
+
this.compiledAliasToCollectionId = {};
|
|
286
|
+
this.unsubscribeFromSchedulerClears?.();
|
|
287
|
+
this.unsubscribeFromSchedulerClears = void 0;
|
|
105
288
|
};
|
|
106
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Compiles the query pipeline with all declared aliases.
|
|
292
|
+
*/
|
|
107
293
|
compileBasePipeline() {
|
|
108
294
|
this.graphCache = new D2();
|
|
109
295
|
this.inputsCache = Object.fromEntries(
|
|
110
|
-
Object.
|
|
111
|
-
|
|
296
|
+
Object.keys(this.collectionByAlias).map((alias) => [
|
|
297
|
+
alias,
|
|
112
298
|
this.graphCache.newInput()
|
|
113
299
|
])
|
|
114
300
|
);
|
|
115
|
-
const
|
|
116
|
-
pipeline: pipelineCache,
|
|
117
|
-
collectionWhereClauses: collectionWhereClausesCache
|
|
118
|
-
} = compileQuery(
|
|
301
|
+
const compilation = compileQuery(
|
|
119
302
|
this.query,
|
|
120
303
|
this.inputsCache,
|
|
121
304
|
this.collections,
|
|
122
305
|
this.subscriptions,
|
|
123
|
-
this.
|
|
124
|
-
this.
|
|
306
|
+
this.lazySourcesCallbacks,
|
|
307
|
+
this.lazySources,
|
|
125
308
|
this.optimizableOrderByCollections
|
|
126
309
|
);
|
|
127
|
-
this.pipelineCache =
|
|
128
|
-
this.
|
|
310
|
+
this.pipelineCache = compilation.pipeline;
|
|
311
|
+
this.sourceWhereClausesCache = compilation.sourceWhereClauses;
|
|
312
|
+
this.compiledAliasToCollectionId = compilation.aliasToCollectionId;
|
|
313
|
+
const missingAliases = Object.keys(this.compiledAliasToCollectionId).filter(
|
|
314
|
+
(alias) => !Object.hasOwn(this.inputsCache, alias)
|
|
315
|
+
);
|
|
316
|
+
if (missingAliases.length > 0) {
|
|
317
|
+
throw new MissingAliasInputsError(missingAliases);
|
|
318
|
+
}
|
|
129
319
|
}
|
|
130
320
|
maybeCompileBasePipeline() {
|
|
131
321
|
if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {
|
|
@@ -235,29 +425,45 @@ class CollectionConfigBuilder {
|
|
|
235
425
|
(collection) => collection.isReady()
|
|
236
426
|
);
|
|
237
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Creates per-alias subscriptions enabling self-join support.
|
|
430
|
+
* Each alias gets its own subscription with independent filters, even for the same collection.
|
|
431
|
+
* Example: `{ employee: col, manager: col }` creates two separate subscriptions.
|
|
432
|
+
*/
|
|
238
433
|
subscribeToAllCollections(config, syncState) {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
syncState.unsubscribeCallbacks.add(statusUnsubscribe);
|
|
254
|
-
const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(
|
|
255
|
-
collectionSubscriber,
|
|
256
|
-
subscription
|
|
257
|
-
);
|
|
258
|
-
return loadMore;
|
|
434
|
+
const compiledAliases = Object.entries(this.compiledAliasToCollectionId);
|
|
435
|
+
if (compiledAliases.length === 0) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
`Compiler returned no alias metadata for query '${this.id}'. This should not happen; please report.`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const loaders = compiledAliases.map(([alias, collectionId]) => {
|
|
441
|
+
const collection = this.collectionByAlias[alias] ?? this.collections[collectionId];
|
|
442
|
+
const dependencyBuilder = getCollectionBuilder(collection);
|
|
443
|
+
if (dependencyBuilder && dependencyBuilder !== this) {
|
|
444
|
+
this.aliasDependencies[alias] = [dependencyBuilder];
|
|
445
|
+
this.builderDependencies.add(dependencyBuilder);
|
|
446
|
+
} else {
|
|
447
|
+
this.aliasDependencies[alias] = [];
|
|
259
448
|
}
|
|
260
|
-
|
|
449
|
+
const collectionSubscriber = new CollectionSubscriber(
|
|
450
|
+
alias,
|
|
451
|
+
collectionId,
|
|
452
|
+
collection,
|
|
453
|
+
this
|
|
454
|
+
);
|
|
455
|
+
const statusUnsubscribe = collection.on(`status:change`, (event) => {
|
|
456
|
+
this.handleSourceStatusChange(config, collectionId, event);
|
|
457
|
+
});
|
|
458
|
+
syncState.unsubscribeCallbacks.add(statusUnsubscribe);
|
|
459
|
+
const subscription = collectionSubscriber.subscribe();
|
|
460
|
+
this.subscriptions[alias] = subscription;
|
|
461
|
+
const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(
|
|
462
|
+
collectionSubscriber,
|
|
463
|
+
subscription
|
|
464
|
+
);
|
|
465
|
+
return loadMore;
|
|
466
|
+
});
|
|
261
467
|
const loadMoreDataCallback = () => {
|
|
262
468
|
loaders.map((loader) => loader());
|
|
263
469
|
return true;
|
|
@@ -313,6 +519,34 @@ function extractCollectionsFromQuery(query) {
|
|
|
313
519
|
extractFromQuery(query);
|
|
314
520
|
return collections;
|
|
315
521
|
}
|
|
522
|
+
function extractCollectionAliases(query) {
|
|
523
|
+
const aliasesById = /* @__PURE__ */ new Map();
|
|
524
|
+
function recordAlias(source) {
|
|
525
|
+
if (!source) return;
|
|
526
|
+
if (source.type === `collectionRef`) {
|
|
527
|
+
const { id } = source.collection;
|
|
528
|
+
const existing = aliasesById.get(id);
|
|
529
|
+
if (existing) {
|
|
530
|
+
existing.add(source.alias);
|
|
531
|
+
} else {
|
|
532
|
+
aliasesById.set(id, /* @__PURE__ */ new Set([source.alias]));
|
|
533
|
+
}
|
|
534
|
+
} else if (source.type === `queryRef`) {
|
|
535
|
+
traverse(source.query);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function traverse(q) {
|
|
539
|
+
if (!q) return;
|
|
540
|
+
recordAlias(q.from);
|
|
541
|
+
if (q.join) {
|
|
542
|
+
for (const joinClause of q.join) {
|
|
543
|
+
recordAlias(joinClause.from);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
traverse(query);
|
|
548
|
+
return aliasesById;
|
|
549
|
+
}
|
|
316
550
|
function accumulateChanges(acc, [[key, tupleData], multiplicity]) {
|
|
317
551
|
const [value, orderByIndex] = tupleData;
|
|
318
552
|
const changes = acc.get(key) || {
|