@tanstack/db 0.4.7 → 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.
Files changed (115) hide show
  1. package/dist/cjs/collection/index.cjs.map +1 -1
  2. package/dist/cjs/collection/index.d.cts +2 -1
  3. package/dist/cjs/collection/lifecycle.cjs +2 -3
  4. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  5. package/dist/cjs/collection/state.cjs +22 -33
  6. package/dist/cjs/collection/state.cjs.map +1 -1
  7. package/dist/cjs/collection/state.d.cts +6 -2
  8. package/dist/cjs/collection/sync.cjs +4 -3
  9. package/dist/cjs/collection/sync.cjs.map +1 -1
  10. package/dist/cjs/errors.cjs +51 -17
  11. package/dist/cjs/errors.cjs.map +1 -1
  12. package/dist/cjs/errors.d.cts +38 -8
  13. package/dist/cjs/index.cjs +8 -4
  14. package/dist/cjs/index.cjs.map +1 -1
  15. package/dist/cjs/indexes/auto-index.cjs +0 -3
  16. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  17. package/dist/cjs/query/builder/types.d.cts +1 -1
  18. package/dist/cjs/query/compiler/index.cjs +42 -19
  19. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  20. package/dist/cjs/query/compiler/index.d.cts +33 -8
  21. package/dist/cjs/query/compiler/joins.cjs +88 -66
  22. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  23. package/dist/cjs/query/compiler/joins.d.cts +5 -2
  24. package/dist/cjs/query/compiler/order-by.cjs +2 -0
  25. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  26. package/dist/cjs/query/compiler/order-by.d.cts +1 -0
  27. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  28. package/dist/cjs/query/live/collection-config-builder.cjs +322 -46
  29. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  30. package/dist/cjs/query/live/collection-config-builder.d.cts +98 -7
  31. package/dist/cjs/query/live/collection-registry.cjs +16 -0
  32. package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
  33. package/dist/cjs/query/live/collection-registry.d.cts +26 -0
  34. package/dist/cjs/query/live/collection-subscriber.cjs +57 -58
  35. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  36. package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
  37. package/dist/cjs/query/live-query-collection.cjs +11 -5
  38. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  39. package/dist/cjs/query/live-query-collection.d.cts +10 -3
  40. package/dist/cjs/query/optimizer.cjs +44 -7
  41. package/dist/cjs/query/optimizer.cjs.map +1 -1
  42. package/dist/cjs/query/optimizer.d.cts +4 -4
  43. package/dist/cjs/scheduler.cjs +137 -0
  44. package/dist/cjs/scheduler.cjs.map +1 -0
  45. package/dist/cjs/scheduler.d.cts +56 -0
  46. package/dist/cjs/transactions.cjs +7 -1
  47. package/dist/cjs/transactions.cjs.map +1 -1
  48. package/dist/cjs/types.d.cts +3 -5
  49. package/dist/esm/collection/index.d.ts +2 -1
  50. package/dist/esm/collection/index.js.map +1 -1
  51. package/dist/esm/collection/lifecycle.js +2 -3
  52. package/dist/esm/collection/lifecycle.js.map +1 -1
  53. package/dist/esm/collection/state.d.ts +6 -2
  54. package/dist/esm/collection/state.js +22 -33
  55. package/dist/esm/collection/state.js.map +1 -1
  56. package/dist/esm/collection/sync.js +4 -3
  57. package/dist/esm/collection/sync.js.map +1 -1
  58. package/dist/esm/errors.d.ts +38 -8
  59. package/dist/esm/errors.js +52 -18
  60. package/dist/esm/errors.js.map +1 -1
  61. package/dist/esm/index.js +9 -5
  62. package/dist/esm/indexes/auto-index.js +0 -3
  63. package/dist/esm/indexes/auto-index.js.map +1 -1
  64. package/dist/esm/query/builder/types.d.ts +1 -1
  65. package/dist/esm/query/compiler/index.d.ts +33 -8
  66. package/dist/esm/query/compiler/index.js +42 -19
  67. package/dist/esm/query/compiler/index.js.map +1 -1
  68. package/dist/esm/query/compiler/joins.d.ts +5 -2
  69. package/dist/esm/query/compiler/joins.js +90 -68
  70. package/dist/esm/query/compiler/joins.js.map +1 -1
  71. package/dist/esm/query/compiler/order-by.d.ts +1 -0
  72. package/dist/esm/query/compiler/order-by.js +2 -0
  73. package/dist/esm/query/compiler/order-by.js.map +1 -1
  74. package/dist/esm/query/compiler/select.js.map +1 -1
  75. package/dist/esm/query/live/collection-config-builder.d.ts +98 -7
  76. package/dist/esm/query/live/collection-config-builder.js +322 -46
  77. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  78. package/dist/esm/query/live/collection-registry.d.ts +26 -0
  79. package/dist/esm/query/live/collection-registry.js +16 -0
  80. package/dist/esm/query/live/collection-registry.js.map +1 -0
  81. package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
  82. package/dist/esm/query/live/collection-subscriber.js +57 -58
  83. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  84. package/dist/esm/query/live-query-collection.d.ts +10 -3
  85. package/dist/esm/query/live-query-collection.js +11 -5
  86. package/dist/esm/query/live-query-collection.js.map +1 -1
  87. package/dist/esm/query/optimizer.d.ts +4 -4
  88. package/dist/esm/query/optimizer.js +44 -7
  89. package/dist/esm/query/optimizer.js.map +1 -1
  90. package/dist/esm/scheduler.d.ts +56 -0
  91. package/dist/esm/scheduler.js +137 -0
  92. package/dist/esm/scheduler.js.map +1 -0
  93. package/dist/esm/transactions.js +7 -1
  94. package/dist/esm/transactions.js.map +1 -1
  95. package/dist/esm/types.d.ts +3 -5
  96. package/package.json +2 -2
  97. package/src/collection/index.ts +1 -1
  98. package/src/collection/lifecycle.ts +3 -4
  99. package/src/collection/state.ts +52 -48
  100. package/src/collection/sync.ts +7 -6
  101. package/src/errors.ts +79 -13
  102. package/src/indexes/auto-index.ts +0 -8
  103. package/src/query/builder/types.ts +1 -1
  104. package/src/query/compiler/index.ts +115 -32
  105. package/src/query/compiler/joins.ts +180 -127
  106. package/src/query/compiler/order-by.ts +7 -0
  107. package/src/query/compiler/select.ts +2 -3
  108. package/src/query/live/collection-config-builder.ts +542 -71
  109. package/src/query/live/collection-registry.ts +47 -0
  110. package/src/query/live/collection-subscriber.ts +87 -105
  111. package/src/query/live-query-collection.ts +39 -14
  112. package/src/query/optimizer.ts +85 -15
  113. package/src/scheduler.ts +198 -0
  114. package/src/transactions.ts +12 -1
  115. package/src/types.ts +3 -5
@@ -1,37 +1,128 @@
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';
9
+ export type LiveQueryCollectionUtils = UtilsRecord & {
10
+ getRunCount: () => number;
11
+ getBuilder: () => CollectionConfigBuilder<any, any>;
12
+ };
8
13
  export declare class CollectionConfigBuilder<TContext extends Context, TResult extends object = GetResult<TContext>> {
9
14
  private readonly config;
10
15
  private readonly id;
11
16
  readonly query: QueryIR;
12
17
  private readonly collections;
18
+ private readonly collectionByAlias;
19
+ private compiledAliasToCollectionId;
13
20
  private readonly resultKeys;
14
21
  private readonly orderByIndices;
15
22
  private readonly compare?;
16
23
  private isGraphRunning;
24
+ private runCount;
25
+ currentSyncConfig: Parameters<SyncConfig<TResult>[`sync`]>[0] | undefined;
26
+ currentSyncState: FullSyncState | undefined;
27
+ private isInErrorState;
28
+ private liveQueryCollection?;
29
+ private readonly aliasDependencies;
30
+ private readonly builderDependencies;
31
+ private readonly pendingGraphRuns;
32
+ private unsubscribeFromSchedulerClears?;
17
33
  private graphCache;
18
34
  private inputsCache;
19
35
  private pipelineCache;
20
- collectionWhereClausesCache: Map<string, BasicExpression<boolean>> | undefined;
36
+ sourceWhereClausesCache: Map<string, BasicExpression<boolean>> | undefined;
21
37
  readonly subscriptions: Record<string, CollectionSubscription>;
22
- lazyCollectionsCallbacks: Record<string, LazyCollectionCallbacks>;
23
- readonly lazyCollections: Set<string>;
38
+ lazySourcesCallbacks: Record<string, LazyCollectionCallbacks>;
39
+ readonly lazySources: Set<string>;
24
40
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>;
25
41
  constructor(config: LiveQueryCollectionConfig<TContext, TResult>);
26
- getConfig(): CollectionConfigSingleRowOption<TResult>;
27
- maybeRunGraph(config: Parameters<SyncConfig<TResult>[`sync`]>[0], syncState: FullSyncState, callback?: () => boolean): void;
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;
28
98
  private getSyncConfig;
99
+ incrementRunCount(): void;
100
+ getRunCount(): number;
29
101
  private syncFn;
102
+ /**
103
+ * Compiles the query pipeline with all declared aliases.
104
+ */
30
105
  private compileBasePipeline;
31
106
  private maybeCompileBasePipeline;
32
107
  private extendPipelineWithChangeProcessing;
33
108
  private applyChanges;
109
+ /**
110
+ * Handle status changes from source collections
111
+ */
112
+ private handleSourceStatusChange;
113
+ /**
114
+ * Update the live query status based on source collection statuses
115
+ */
116
+ private updateLiveQueryStatus;
117
+ /**
118
+ * Transition the live query to error state
119
+ */
120
+ private transitionToError;
34
121
  private allCollectionsReady;
35
- private allCollectionsReadyOrInitialCommit;
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
+ */
36
127
  private subscribeToAllCollections;
37
128
  }
@@ -1,21 +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;
18
+ this.isInErrorState = false;
19
+ this.aliasDependencies = {};
20
+ this.builderDependencies = /* @__PURE__ */ new Set();
21
+ this.pendingGraphRuns = /* @__PURE__ */ new Map();
12
22
  this.subscriptions = {};
13
- this.lazyCollectionsCallbacks = {};
14
- this.lazyCollections = /* @__PURE__ */ new Set();
23
+ this.lazySourcesCallbacks = {};
24
+ this.lazySources = /* @__PURE__ */ new Set();
15
25
  this.optimizableOrderByCollections = {};
16
26
  this.id = config.id || `live-query-${++liveQueryCollectionCounter}`;
17
27
  this.query = buildQueryFromConfig(config);
18
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
+ }
19
38
  if (this.query.orderBy && this.query.orderBy.length > 0) {
20
39
  this.compare = createOrderByComparator(this.orderByIndices);
21
40
  }
@@ -34,25 +53,63 @@ class CollectionConfigBuilder {
34
53
  onUpdate: this.config.onUpdate,
35
54
  onDelete: this.config.onDelete,
36
55
  startSync: this.config.startSync,
37
- singleResult: this.query.singleResult
56
+ singleResult: this.query.singleResult,
57
+ utils: {
58
+ getRunCount: this.getRunCount.bind(this),
59
+ getBuilder: () => this
60
+ }
38
61
  };
39
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
+ }
40
88
  // The callback function is called after the graph has run.
41
89
  // This gives the callback a chance to load more data if needed,
42
90
  // that's used to optimize orderBy operators that set a limit,
43
91
  // in order to load some more data if we still don't have enough rows after the pipeline has run.
44
- // That can happend because even though we load N rows, the pipeline might filter some of these rows out
92
+ // That can happen because even though we load N rows, the pipeline might filter some of these rows out
45
93
  // causing the orderBy operator to receive less than N rows or even no rows at all.
46
94
  // So this callback would notice that it doesn't have enough rows and load some more.
47
95
  // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.
48
- maybeRunGraph(config, syncState, callback) {
96
+ maybeRunGraph(callback) {
49
97
  if (this.isGraphRunning) {
50
98
  return;
51
99
  }
100
+ if (!this.currentSyncConfig || !this.currentSyncState) {
101
+ throw new Error(
102
+ `maybeRunGraph called without active sync session. This should not happen.`
103
+ );
104
+ }
52
105
  this.isGraphRunning = true;
53
106
  try {
54
- const { begin, commit, markReady } = config;
55
- if (this.allCollectionsReadyOrInitialCommit() && syncState.subscribedToAllCollections) {
107
+ const { begin, commit } = this.currentSyncConfig;
108
+ const syncState = this.currentSyncState;
109
+ if (this.isInErrorState) {
110
+ return;
111
+ }
112
+ if (syncState.subscribedToAllCollections) {
56
113
  while (syncState.graph.pendingWork()) {
57
114
  syncState.graph.run();
58
115
  callback?.();
@@ -60,22 +117,136 @@ class CollectionConfigBuilder {
60
117
  if (syncState.messagesCount === 0) {
61
118
  begin();
62
119
  commit();
63
- }
64
- if (this.allCollectionsReady()) {
65
- markReady();
120
+ this.updateLiveQueryStatus(this.currentSyncConfig);
66
121
  }
67
122
  }
68
123
  } finally {
69
124
  this.isGraphRunning = false;
70
125
  }
71
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
+ }
72
235
  getSyncConfig() {
73
236
  return {
74
237
  rowUpdateMode: `full`,
75
238
  sync: this.syncFn.bind(this)
76
239
  };
77
240
  }
241
+ incrementRunCount() {
242
+ this.runCount++;
243
+ }
244
+ getRunCount() {
245
+ return this.runCount;
246
+ }
78
247
  syncFn(config) {
248
+ this.liveQueryCollection = config.collection;
249
+ this.currentSyncConfig = config;
79
250
  const syncState = {
80
251
  messagesCount: 0,
81
252
  subscribedToAllCollections: false,
@@ -85,44 +256,66 @@ class CollectionConfigBuilder {
85
256
  config,
86
257
  syncState
87
258
  );
259
+ this.currentSyncState = fullSyncState;
260
+ this.unsubscribeFromSchedulerClears = transactionScopedScheduler.onClear(
261
+ (contextId) => {
262
+ this.clearPendingGraphRun(contextId);
263
+ }
264
+ );
88
265
  const loadMoreDataCallbacks = this.subscribeToAllCollections(
89
266
  config,
90
267
  fullSyncState
91
268
  );
92
- this.maybeRunGraph(config, fullSyncState, loadMoreDataCallbacks);
269
+ this.scheduleGraphRun(loadMoreDataCallbacks);
93
270
  return () => {
94
271
  syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
272
+ this.currentSyncConfig = void 0;
273
+ this.currentSyncState = void 0;
274
+ this.pendingGraphRuns.clear();
95
275
  this.graphCache = void 0;
96
276
  this.inputsCache = void 0;
97
277
  this.pipelineCache = void 0;
98
- this.collectionWhereClausesCache = void 0;
99
- this.lazyCollections.clear();
278
+ this.sourceWhereClausesCache = void 0;
279
+ this.lazySources.clear();
100
280
  this.optimizableOrderByCollections = {};
101
- this.lazyCollectionsCallbacks = {};
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;
102
288
  };
103
289
  }
290
+ /**
291
+ * Compiles the query pipeline with all declared aliases.
292
+ */
104
293
  compileBasePipeline() {
105
294
  this.graphCache = new D2();
106
295
  this.inputsCache = Object.fromEntries(
107
- Object.entries(this.collections).map(([key]) => [
108
- key,
296
+ Object.keys(this.collectionByAlias).map((alias) => [
297
+ alias,
109
298
  this.graphCache.newInput()
110
299
  ])
111
300
  );
112
- const {
113
- pipeline: pipelineCache,
114
- collectionWhereClauses: collectionWhereClausesCache
115
- } = compileQuery(
301
+ const compilation = compileQuery(
116
302
  this.query,
117
303
  this.inputsCache,
118
304
  this.collections,
119
305
  this.subscriptions,
120
- this.lazyCollectionsCallbacks,
121
- this.lazyCollections,
306
+ this.lazySourcesCallbacks,
307
+ this.lazySources,
122
308
  this.optimizableOrderByCollections
123
309
  );
124
- this.pipelineCache = pipelineCache;
125
- this.collectionWhereClausesCache = collectionWhereClausesCache;
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
+ }
126
319
  }
127
320
  maybeCompileBasePipeline() {
128
321
  if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {
@@ -188,40 +381,95 @@ class CollectionConfigBuilder {
188
381
  );
189
382
  }
190
383
  }
384
+ /**
385
+ * Handle status changes from source collections
386
+ */
387
+ handleSourceStatusChange(config, collectionId, event) {
388
+ const { status } = event;
389
+ if (status === `error`) {
390
+ this.transitionToError(
391
+ `Source collection '${collectionId}' entered error state`
392
+ );
393
+ return;
394
+ }
395
+ if (status === `cleaned-up`) {
396
+ this.transitionToError(
397
+ `Source collection '${collectionId}' was manually cleaned up while live query '${this.id}' depends on it. Live queries prevent automatic GC, so this was likely a manual cleanup() call.`
398
+ );
399
+ return;
400
+ }
401
+ this.updateLiveQueryStatus(config);
402
+ }
403
+ /**
404
+ * Update the live query status based on source collection statuses
405
+ */
406
+ updateLiveQueryStatus(config) {
407
+ const { markReady } = config;
408
+ if (this.isInErrorState) {
409
+ return;
410
+ }
411
+ if (this.allCollectionsReady()) {
412
+ markReady();
413
+ }
414
+ }
415
+ /**
416
+ * Transition the live query to error state
417
+ */
418
+ transitionToError(message) {
419
+ this.isInErrorState = true;
420
+ console.error(`[Live Query Error] ${message}`);
421
+ this.liveQueryCollection?._lifecycle.setStatus(`error`);
422
+ }
191
423
  allCollectionsReady() {
192
424
  return Object.values(this.collections).every(
193
425
  (collection) => collection.isReady()
194
426
  );
195
427
  }
196
- allCollectionsReadyOrInitialCommit() {
197
- return Object.values(this.collections).every(
198
- (collection) => collection.status === `ready` || collection.status === `initialCommit`
199
- );
200
- }
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
+ */
201
433
  subscribeToAllCollections(config, syncState) {
202
- const loaders = Object.entries(this.collections).map(
203
- ([collectionId, collection]) => {
204
- const collectionSubscriber = new CollectionSubscriber(
205
- collectionId,
206
- collection,
207
- config,
208
- syncState,
209
- this
210
- );
211
- const subscription = collectionSubscriber.subscribe();
212
- this.subscriptions[collectionId] = subscription;
213
- const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(
214
- collectionSubscriber,
215
- subscription
216
- );
217
- 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] = [];
218
448
  }
219
- );
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
+ });
220
467
  const loadMoreDataCallback = () => {
221
468
  loaders.map((loader) => loader());
222
469
  return true;
223
470
  };
224
471
  syncState.subscribedToAllCollections = true;
472
+ this.updateLiveQueryStatus(config);
225
473
  return loadMoreDataCallback;
226
474
  }
227
475
  }
@@ -271,6 +519,34 @@ function extractCollectionsFromQuery(query) {
271
519
  extractFromQuery(query);
272
520
  return collections;
273
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
+ }
274
550
  function accumulateChanges(acc, [[key, tupleData], multiplicity]) {
275
551
  const [value, orderByIndex] = tupleData;
276
552
  const changes = acc.get(key) || {