@tanstack/db 0.4.8 → 0.4.10

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 (134) hide show
  1. package/dist/cjs/collection/events.cjs +9 -51
  2. package/dist/cjs/collection/events.cjs.map +1 -1
  3. package/dist/cjs/collection/events.d.cts +18 -7
  4. package/dist/cjs/collection/index.cjs +9 -12
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +13 -14
  7. package/dist/cjs/collection/subscription.cjs +62 -6
  8. package/dist/cjs/collection/subscription.cjs.map +1 -1
  9. package/dist/cjs/collection/subscription.d.cts +16 -3
  10. package/dist/cjs/collection/sync.cjs +58 -6
  11. package/dist/cjs/collection/sync.cjs.map +1 -1
  12. package/dist/cjs/collection/sync.d.cts +18 -4
  13. package/dist/cjs/errors.cjs +59 -17
  14. package/dist/cjs/errors.cjs.map +1 -1
  15. package/dist/cjs/errors.d.cts +44 -8
  16. package/dist/cjs/event-emitter.cjs +94 -0
  17. package/dist/cjs/event-emitter.cjs.map +1 -0
  18. package/dist/cjs/event-emitter.d.cts +45 -0
  19. package/dist/cjs/index.cjs +9 -4
  20. package/dist/cjs/index.cjs.map +1 -1
  21. package/dist/cjs/local-only.cjs.map +1 -1
  22. package/dist/cjs/local-only.d.cts +2 -5
  23. package/dist/cjs/query/builder/types.d.cts +1 -1
  24. package/dist/cjs/query/compiler/index.cjs +46 -19
  25. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  26. package/dist/cjs/query/compiler/index.d.cts +35 -9
  27. package/dist/cjs/query/compiler/joins.cjs +91 -66
  28. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  29. package/dist/cjs/query/compiler/joins.d.cts +6 -3
  30. package/dist/cjs/query/compiler/order-by.cjs +20 -4
  31. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  32. package/dist/cjs/query/compiler/order-by.d.cts +3 -1
  33. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  34. package/dist/cjs/query/compiler/types.d.cts +4 -0
  35. package/dist/cjs/query/index.d.cts +1 -0
  36. package/dist/cjs/query/live/collection-config-builder.cjs +306 -46
  37. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  38. package/dist/cjs/query/live/collection-config-builder.d.cts +97 -9
  39. package/dist/cjs/query/live/collection-registry.cjs +16 -0
  40. package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
  41. package/dist/cjs/query/live/collection-registry.d.cts +26 -0
  42. package/dist/cjs/query/live/collection-subscriber.cjs +86 -58
  43. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  44. package/dist/cjs/query/live/collection-subscriber.d.cts +5 -7
  45. package/dist/cjs/query/live-query-collection.cjs +11 -5
  46. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  47. package/dist/cjs/query/live-query-collection.d.cts +12 -5
  48. package/dist/cjs/query/optimizer.cjs +44 -7
  49. package/dist/cjs/query/optimizer.cjs.map +1 -1
  50. package/dist/cjs/query/optimizer.d.cts +4 -4
  51. package/dist/cjs/scheduler.cjs +137 -0
  52. package/dist/cjs/scheduler.cjs.map +1 -0
  53. package/dist/cjs/scheduler.d.cts +56 -0
  54. package/dist/cjs/transactions.cjs +7 -1
  55. package/dist/cjs/transactions.cjs.map +1 -1
  56. package/dist/cjs/types.d.cts +82 -11
  57. package/dist/esm/collection/events.d.ts +18 -7
  58. package/dist/esm/collection/events.js +9 -51
  59. package/dist/esm/collection/events.js.map +1 -1
  60. package/dist/esm/collection/index.d.ts +13 -14
  61. package/dist/esm/collection/index.js +9 -12
  62. package/dist/esm/collection/index.js.map +1 -1
  63. package/dist/esm/collection/subscription.d.ts +16 -3
  64. package/dist/esm/collection/subscription.js +62 -6
  65. package/dist/esm/collection/subscription.js.map +1 -1
  66. package/dist/esm/collection/sync.d.ts +18 -4
  67. package/dist/esm/collection/sync.js +59 -7
  68. package/dist/esm/collection/sync.js.map +1 -1
  69. package/dist/esm/errors.d.ts +44 -8
  70. package/dist/esm/errors.js +60 -18
  71. package/dist/esm/errors.js.map +1 -1
  72. package/dist/esm/event-emitter.d.ts +45 -0
  73. package/dist/esm/event-emitter.js +94 -0
  74. package/dist/esm/event-emitter.js.map +1 -0
  75. package/dist/esm/index.js +10 -5
  76. package/dist/esm/local-only.d.ts +2 -5
  77. package/dist/esm/local-only.js.map +1 -1
  78. package/dist/esm/query/builder/types.d.ts +1 -1
  79. package/dist/esm/query/compiler/index.d.ts +35 -9
  80. package/dist/esm/query/compiler/index.js +46 -19
  81. package/dist/esm/query/compiler/index.js.map +1 -1
  82. package/dist/esm/query/compiler/joins.d.ts +6 -3
  83. package/dist/esm/query/compiler/joins.js +93 -68
  84. package/dist/esm/query/compiler/joins.js.map +1 -1
  85. package/dist/esm/query/compiler/order-by.d.ts +3 -1
  86. package/dist/esm/query/compiler/order-by.js +20 -4
  87. package/dist/esm/query/compiler/order-by.js.map +1 -1
  88. package/dist/esm/query/compiler/select.js.map +1 -1
  89. package/dist/esm/query/compiler/types.d.ts +4 -0
  90. package/dist/esm/query/index.d.ts +1 -0
  91. package/dist/esm/query/live/collection-config-builder.d.ts +97 -9
  92. package/dist/esm/query/live/collection-config-builder.js +306 -46
  93. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  94. package/dist/esm/query/live/collection-registry.d.ts +26 -0
  95. package/dist/esm/query/live/collection-registry.js +16 -0
  96. package/dist/esm/query/live/collection-registry.js.map +1 -0
  97. package/dist/esm/query/live/collection-subscriber.d.ts +5 -7
  98. package/dist/esm/query/live/collection-subscriber.js +86 -58
  99. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  100. package/dist/esm/query/live-query-collection.d.ts +12 -5
  101. package/dist/esm/query/live-query-collection.js +11 -5
  102. package/dist/esm/query/live-query-collection.js.map +1 -1
  103. package/dist/esm/query/optimizer.d.ts +4 -4
  104. package/dist/esm/query/optimizer.js +44 -7
  105. package/dist/esm/query/optimizer.js.map +1 -1
  106. package/dist/esm/scheduler.d.ts +56 -0
  107. package/dist/esm/scheduler.js +137 -0
  108. package/dist/esm/scheduler.js.map +1 -0
  109. package/dist/esm/transactions.js +7 -1
  110. package/dist/esm/transactions.js.map +1 -1
  111. package/dist/esm/types.d.ts +82 -11
  112. package/package.json +2 -2
  113. package/src/collection/events.ts +25 -74
  114. package/src/collection/index.ts +15 -19
  115. package/src/collection/subscription.ts +88 -6
  116. package/src/collection/sync.ts +81 -9
  117. package/src/errors.ts +91 -13
  118. package/src/event-emitter.ts +118 -0
  119. package/src/local-only.ts +5 -12
  120. package/src/query/builder/types.ts +1 -1
  121. package/src/query/compiler/index.ts +124 -33
  122. package/src/query/compiler/joins.ts +187 -128
  123. package/src/query/compiler/order-by.ts +30 -2
  124. package/src/query/compiler/select.ts +2 -3
  125. package/src/query/compiler/types.ts +5 -0
  126. package/src/query/index.ts +1 -0
  127. package/src/query/live/collection-config-builder.ts +501 -60
  128. package/src/query/live/collection-registry.ts +47 -0
  129. package/src/query/live/collection-subscriber.ts +137 -105
  130. package/src/query/live-query-collection.ts +47 -18
  131. package/src/query/optimizer.ts +85 -15
  132. package/src/scheduler.ts +198 -0
  133. package/src/transactions.ts +12 -1
  134. package/src/types.ts +93 -11
@@ -8,3 +8,7 @@ export type QueryCache = WeakMap<QueryIR, CompilationResult>;
8
8
  * Mapping from optimized queries back to their original queries for caching
9
9
  */
10
10
  export type QueryMapping = WeakMap<QueryIR, QueryIR>;
11
+ export type WindowOptions = {
12
+ offset?: number;
13
+ limit?: number;
14
+ };
@@ -4,3 +4,4 @@ export type { Ref } from './builder/types.js';
4
4
  export { compileQuery } from './compiler/index.js';
5
5
  export { createLiveQueryCollection, liveQueryCollectionOptions, } from './live-query-collection.js';
6
6
  export { type LiveQueryCollectionConfig } from './live/types.js';
7
+ export { type LiveQueryCollectionUtils } from './live/collection-config-builder.js';
@@ -1,35 +1,119 @@
1
+ import { WindowOptions } from '../compiler/index.js';
2
+ import { SchedulerContextId } from '../../scheduler.js';
1
3
  import { CollectionSubscription } from '../../collection/subscription.js';
2
4
  import { OrderByOptimizationInfo } from '../compiler/order-by.js';
3
- import { CollectionConfigSingleRowOption, SyncConfig } from '../../types.js';
5
+ import { Collection } from '../../collection/index.js';
6
+ import { CollectionConfigSingleRowOption, SyncConfig, UtilsRecord } from '../../types.js';
4
7
  import { Context, GetResult } from '../builder/types.js';
5
8
  import { BasicExpression, QueryIR } from '../ir.js';
6
9
  import { LazyCollectionCallbacks } from '../compiler/joins.js';
7
10
  import { FullSyncState, LiveQueryCollectionConfig } from './types.js';
8
- type SyncMethods<TResult extends object> = Parameters<SyncConfig<TResult>[`sync`]>[0];
11
+ export type LiveQueryCollectionUtils = UtilsRecord & {
12
+ getRunCount: () => number;
13
+ getBuilder: () => CollectionConfigBuilder<any, any>;
14
+ /**
15
+ * Sets the offset and limit of an ordered query.
16
+ * Is a no-op if the query is not ordered.
17
+ *
18
+ * @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded
19
+ */
20
+ setWindow: (options: WindowOptions) => true | Promise<void>;
21
+ };
9
22
  export declare class CollectionConfigBuilder<TContext extends Context, TResult extends object = GetResult<TContext>> {
10
23
  private readonly config;
11
24
  private readonly id;
12
25
  readonly query: QueryIR;
13
26
  private readonly collections;
27
+ private readonly collectionByAlias;
28
+ private compiledAliasToCollectionId;
14
29
  private readonly resultKeys;
15
30
  private readonly orderByIndices;
16
31
  private readonly compare?;
17
32
  private isGraphRunning;
33
+ private runCount;
34
+ currentSyncConfig: Parameters<SyncConfig<TResult>[`sync`]>[0] | undefined;
35
+ currentSyncState: FullSyncState | undefined;
18
36
  private isInErrorState;
19
- private liveQueryCollection?;
37
+ liveQueryCollection?: Collection<TResult, any, any>;
38
+ private windowFn;
39
+ private maybeRunGraphFn;
40
+ private readonly aliasDependencies;
41
+ private readonly builderDependencies;
42
+ private readonly pendingGraphRuns;
43
+ private unsubscribeFromSchedulerClears?;
20
44
  private graphCache;
21
45
  private inputsCache;
22
46
  private pipelineCache;
23
- collectionWhereClausesCache: Map<string, BasicExpression<boolean>> | undefined;
47
+ sourceWhereClausesCache: Map<string, BasicExpression<boolean>> | undefined;
24
48
  readonly subscriptions: Record<string, CollectionSubscription>;
25
- lazyCollectionsCallbacks: Record<string, LazyCollectionCallbacks>;
26
- readonly lazyCollections: Set<string>;
49
+ lazySourcesCallbacks: Record<string, LazyCollectionCallbacks>;
50
+ readonly lazySources: Set<string>;
27
51
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>;
28
52
  constructor(config: LiveQueryCollectionConfig<TContext, TResult>);
29
- getConfig(): CollectionConfigSingleRowOption<TResult>;
30
- maybeRunGraph(config: SyncMethods<TResult>, syncState: FullSyncState, callback?: () => boolean): void;
53
+ getConfig(): CollectionConfigSingleRowOption<TResult> & {
54
+ utils: LiveQueryCollectionUtils;
55
+ };
56
+ setWindow(options: WindowOptions): true | Promise<void>;
57
+ /**
58
+ * Resolves a collection alias to its collection ID.
59
+ *
60
+ * Uses a two-tier lookup strategy:
61
+ * 1. First checks compiled aliases (includes subquery inner aliases)
62
+ * 2. Falls back to declared aliases from the query's from/join clauses
63
+ *
64
+ * @param alias - The alias to resolve (e.g., "employee", "manager")
65
+ * @returns The collection ID that the alias references
66
+ * @throws {Error} If the alias is not found in either lookup
67
+ */
68
+ getCollectionIdForAlias(alias: string): string;
69
+ isLazyAlias(alias: string): boolean;
70
+ maybeRunGraph(callback?: () => boolean): void;
71
+ /**
72
+ * Schedules a graph run with the transaction-scoped scheduler.
73
+ * Ensures each builder runs at most once per transaction, with automatic dependency tracking
74
+ * to run parent queries before child queries. Outside a transaction, runs immediately.
75
+ *
76
+ * Multiple calls during a transaction are coalesced into a single execution.
77
+ * Dependencies are auto-discovered from subscribed live queries, or can be overridden.
78
+ * Load callbacks are combined when entries merge.
79
+ *
80
+ * Uses the current sync session's config and syncState from instance properties.
81
+ *
82
+ * @param callback - Optional callback to load more data if needed (returns true when done)
83
+ * @param options - Optional scheduling configuration
84
+ * @param options.contextId - Transaction ID to group work; defaults to active transaction
85
+ * @param options.jobId - Unique identifier for this job; defaults to this builder instance
86
+ * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies
87
+ * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies
88
+ */
89
+ scheduleGraphRun(callback?: () => boolean, options?: {
90
+ contextId?: SchedulerContextId;
91
+ jobId?: unknown;
92
+ alias?: string;
93
+ dependencies?: Array<CollectionConfigBuilder<any, any>>;
94
+ }): void;
95
+ /**
96
+ * Clears pending graph run state for a specific context.
97
+ * Called when the scheduler clears a context (e.g., transaction rollback/abort).
98
+ */
99
+ clearPendingGraphRun(contextId: SchedulerContextId): void;
100
+ /**
101
+ * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.
102
+ * Clears the pending state BEFORE execution so that any re-schedules during the run
103
+ * create fresh state and don't interfere with the current execution.
104
+ * Uses instance sync state - if sync has ended, gracefully returns without executing.
105
+ *
106
+ * @param contextId - Optional context ID to look up pending state
107
+ * @param pendingParam - For immediate execution (no context), pending state is passed directly
108
+ */
109
+ private executeGraphRun;
31
110
  private getSyncConfig;
111
+ incrementRunCount(): void;
112
+ getRunCount(): number;
32
113
  private syncFn;
114
+ /**
115
+ * Compiles the query pipeline with all declared aliases.
116
+ */
33
117
  private compileBasePipeline;
34
118
  private maybeCompileBasePipeline;
35
119
  private extendPipelineWithChangeProcessing;
@@ -47,6 +131,10 @@ export declare class CollectionConfigBuilder<TContext extends Context, TResult e
47
131
  */
48
132
  private transitionToError;
49
133
  private allCollectionsReady;
134
+ /**
135
+ * Creates per-alias subscriptions enabling self-join support.
136
+ * Each alias gets its own subscription with independent filters, even for the same collection.
137
+ * Example: `{ employee: col, manager: col }` creates two separate subscriptions.
138
+ */
50
139
  private subscribeToAllCollections;
51
140
  }
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 { SetWindowRequiresOrderByError, 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.lazyCollectionsCallbacks = {};
15
- this.lazyCollections = /* @__PURE__ */ new Set();
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,60 @@ 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
+ setWindow: this.setWindow.bind(this)
61
+ }
39
62
  };
40
63
  }
64
+ setWindow(options) {
65
+ if (!this.windowFn) {
66
+ throw new SetWindowRequiresOrderByError();
67
+ }
68
+ this.windowFn(options);
69
+ this.maybeRunGraphFn?.();
70
+ if (this.liveQueryCollection?.isLoadingSubset) {
71
+ return new Promise((resolve) => {
72
+ const unsubscribe = this.liveQueryCollection.on(
73
+ `loadingSubset:change`,
74
+ (event) => {
75
+ if (!event.isLoadingSubset) {
76
+ unsubscribe();
77
+ resolve();
78
+ }
79
+ }
80
+ );
81
+ });
82
+ }
83
+ return true;
84
+ }
85
+ /**
86
+ * Resolves a collection alias to its collection ID.
87
+ *
88
+ * Uses a two-tier lookup strategy:
89
+ * 1. First checks compiled aliases (includes subquery inner aliases)
90
+ * 2. Falls back to declared aliases from the query's from/join clauses
91
+ *
92
+ * @param alias - The alias to resolve (e.g., "employee", "manager")
93
+ * @returns The collection ID that the alias references
94
+ * @throws {Error} If the alias is not found in either lookup
95
+ */
96
+ getCollectionIdForAlias(alias) {
97
+ const compiled = this.compiledAliasToCollectionId[alias];
98
+ if (compiled) {
99
+ return compiled;
100
+ }
101
+ const collection = this.collectionByAlias[alias];
102
+ if (collection) {
103
+ return collection.id;
104
+ }
105
+ throw new Error(`Unknown source alias "${alias}"`);
106
+ }
107
+ isLazyAlias(alias) {
108
+ return this.lazySources.has(alias);
109
+ }
41
110
  // The callback function is called after the graph has run.
42
111
  // This gives the callback a chance to load more data if needed,
43
112
  // that's used to optimize orderBy operators that set a limit,
@@ -45,14 +114,20 @@ class CollectionConfigBuilder {
45
114
  // That can happen because even though we load N rows, the pipeline might filter some of these rows out
46
115
  // causing the orderBy operator to receive less than N rows or even no rows at all.
47
116
  // 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(config, syncState, callback) {
117
+ // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.
118
+ maybeRunGraph(callback) {
50
119
  if (this.isGraphRunning) {
51
120
  return;
52
121
  }
122
+ if (!this.currentSyncConfig || !this.currentSyncState) {
123
+ throw new Error(
124
+ `maybeRunGraph called without active sync session. This should not happen.`
125
+ );
126
+ }
53
127
  this.isGraphRunning = true;
54
128
  try {
55
- const { begin, commit } = config;
129
+ const { begin, commit } = this.currentSyncConfig;
130
+ const syncState = this.currentSyncState;
56
131
  if (this.isInErrorState) {
57
132
  return;
58
133
  }
@@ -64,21 +139,136 @@ class CollectionConfigBuilder {
64
139
  if (syncState.messagesCount === 0) {
65
140
  begin();
66
141
  commit();
67
- this.updateLiveQueryStatus(config);
142
+ this.updateLiveQueryStatus(this.currentSyncConfig);
68
143
  }
69
144
  }
70
145
  } finally {
71
146
  this.isGraphRunning = false;
72
147
  }
73
148
  }
149
+ /**
150
+ * Schedules a graph run with the transaction-scoped scheduler.
151
+ * Ensures each builder runs at most once per transaction, with automatic dependency tracking
152
+ * to run parent queries before child queries. Outside a transaction, runs immediately.
153
+ *
154
+ * Multiple calls during a transaction are coalesced into a single execution.
155
+ * Dependencies are auto-discovered from subscribed live queries, or can be overridden.
156
+ * Load callbacks are combined when entries merge.
157
+ *
158
+ * Uses the current sync session's config and syncState from instance properties.
159
+ *
160
+ * @param callback - Optional callback to load more data if needed (returns true when done)
161
+ * @param options - Optional scheduling configuration
162
+ * @param options.contextId - Transaction ID to group work; defaults to active transaction
163
+ * @param options.jobId - Unique identifier for this job; defaults to this builder instance
164
+ * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies
165
+ * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies
166
+ */
167
+ scheduleGraphRun(callback, options) {
168
+ const contextId = options?.contextId ?? getActiveTransaction()?.id;
169
+ const jobId = options?.jobId ?? this;
170
+ const dependentBuilders = (() => {
171
+ if (options?.dependencies) {
172
+ return options.dependencies;
173
+ }
174
+ const deps = new Set(this.builderDependencies);
175
+ if (options?.alias) {
176
+ const aliasDeps = this.aliasDependencies[options.alias];
177
+ if (aliasDeps) {
178
+ for (const dep of aliasDeps) {
179
+ deps.add(dep);
180
+ }
181
+ }
182
+ }
183
+ deps.delete(this);
184
+ return Array.from(deps);
185
+ })();
186
+ if (!this.currentSyncConfig || !this.currentSyncState) {
187
+ throw new Error(
188
+ `scheduleGraphRun called without active sync session. This should not happen.`
189
+ );
190
+ }
191
+ let pending = contextId ? this.pendingGraphRuns.get(contextId) : void 0;
192
+ if (!pending) {
193
+ pending = {
194
+ loadCallbacks: /* @__PURE__ */ new Set()
195
+ };
196
+ if (contextId) {
197
+ this.pendingGraphRuns.set(contextId, pending);
198
+ }
199
+ }
200
+ if (callback) {
201
+ pending.loadCallbacks.add(callback);
202
+ }
203
+ const pendingToPass = contextId ? void 0 : pending;
204
+ transactionScopedScheduler.schedule({
205
+ contextId,
206
+ jobId,
207
+ dependencies: dependentBuilders,
208
+ run: () => this.executeGraphRun(contextId, pendingToPass)
209
+ });
210
+ }
211
+ /**
212
+ * Clears pending graph run state for a specific context.
213
+ * Called when the scheduler clears a context (e.g., transaction rollback/abort).
214
+ */
215
+ clearPendingGraphRun(contextId) {
216
+ this.pendingGraphRuns.delete(contextId);
217
+ }
218
+ /**
219
+ * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.
220
+ * Clears the pending state BEFORE execution so that any re-schedules during the run
221
+ * create fresh state and don't interfere with the current execution.
222
+ * Uses instance sync state - if sync has ended, gracefully returns without executing.
223
+ *
224
+ * @param contextId - Optional context ID to look up pending state
225
+ * @param pendingParam - For immediate execution (no context), pending state is passed directly
226
+ */
227
+ executeGraphRun(contextId, pendingParam) {
228
+ const pending = pendingParam ?? (contextId ? this.pendingGraphRuns.get(contextId) : void 0);
229
+ if (contextId) {
230
+ this.pendingGraphRuns.delete(contextId);
231
+ }
232
+ if (!pending) {
233
+ return;
234
+ }
235
+ if (!this.currentSyncConfig || !this.currentSyncState) {
236
+ return;
237
+ }
238
+ this.incrementRunCount();
239
+ const combinedLoader = () => {
240
+ let allDone = true;
241
+ let firstError;
242
+ pending.loadCallbacks.forEach((loader) => {
243
+ try {
244
+ allDone = loader() && allDone;
245
+ } catch (error) {
246
+ allDone = false;
247
+ firstError ??= error;
248
+ }
249
+ });
250
+ if (firstError) {
251
+ throw firstError;
252
+ }
253
+ return allDone;
254
+ };
255
+ this.maybeRunGraph(combinedLoader);
256
+ }
74
257
  getSyncConfig() {
75
258
  return {
76
259
  rowUpdateMode: `full`,
77
260
  sync: this.syncFn.bind(this)
78
261
  };
79
262
  }
263
+ incrementRunCount() {
264
+ this.runCount++;
265
+ }
266
+ getRunCount() {
267
+ return this.runCount;
268
+ }
80
269
  syncFn(config) {
81
270
  this.liveQueryCollection = config.collection;
271
+ this.currentSyncConfig = config;
82
272
  const syncState = {
83
273
  messagesCount: 0,
84
274
  subscribedToAllCollections: false,
@@ -88,44 +278,70 @@ class CollectionConfigBuilder {
88
278
  config,
89
279
  syncState
90
280
  );
91
- const loadMoreDataCallbacks = this.subscribeToAllCollections(
281
+ this.currentSyncState = fullSyncState;
282
+ this.unsubscribeFromSchedulerClears = transactionScopedScheduler.onClear(
283
+ (contextId) => {
284
+ this.clearPendingGraphRun(contextId);
285
+ }
286
+ );
287
+ const loadSubsetDataCallbacks = this.subscribeToAllCollections(
92
288
  config,
93
289
  fullSyncState
94
290
  );
95
- this.maybeRunGraph(config, fullSyncState, loadMoreDataCallbacks);
291
+ this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks);
292
+ this.scheduleGraphRun(loadSubsetDataCallbacks);
96
293
  return () => {
97
294
  syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
295
+ this.currentSyncConfig = void 0;
296
+ this.currentSyncState = void 0;
297
+ this.pendingGraphRuns.clear();
98
298
  this.graphCache = void 0;
99
299
  this.inputsCache = void 0;
100
300
  this.pipelineCache = void 0;
101
- this.collectionWhereClausesCache = void 0;
102
- this.lazyCollections.clear();
301
+ this.sourceWhereClausesCache = void 0;
302
+ this.lazySources.clear();
103
303
  this.optimizableOrderByCollections = {};
104
- this.lazyCollectionsCallbacks = {};
304
+ this.lazySourcesCallbacks = {};
305
+ Object.keys(this.subscriptions).forEach(
306
+ (key) => delete this.subscriptions[key]
307
+ );
308
+ this.compiledAliasToCollectionId = {};
309
+ this.unsubscribeFromSchedulerClears?.();
310
+ this.unsubscribeFromSchedulerClears = void 0;
105
311
  };
106
312
  }
313
+ /**
314
+ * Compiles the query pipeline with all declared aliases.
315
+ */
107
316
  compileBasePipeline() {
108
317
  this.graphCache = new D2();
109
318
  this.inputsCache = Object.fromEntries(
110
- Object.entries(this.collections).map(([key]) => [
111
- key,
319
+ Object.keys(this.collectionByAlias).map((alias) => [
320
+ alias,
112
321
  this.graphCache.newInput()
113
322
  ])
114
323
  );
115
- const {
116
- pipeline: pipelineCache,
117
- collectionWhereClauses: collectionWhereClausesCache
118
- } = compileQuery(
324
+ const compilation = compileQuery(
119
325
  this.query,
120
326
  this.inputsCache,
121
327
  this.collections,
122
328
  this.subscriptions,
123
- this.lazyCollectionsCallbacks,
124
- this.lazyCollections,
125
- this.optimizableOrderByCollections
329
+ this.lazySourcesCallbacks,
330
+ this.lazySources,
331
+ this.optimizableOrderByCollections,
332
+ (windowFn) => {
333
+ this.windowFn = windowFn;
334
+ }
126
335
  );
127
- this.pipelineCache = pipelineCache;
128
- this.collectionWhereClausesCache = collectionWhereClausesCache;
336
+ this.pipelineCache = compilation.pipeline;
337
+ this.sourceWhereClausesCache = compilation.sourceWhereClauses;
338
+ this.compiledAliasToCollectionId = compilation.aliasToCollectionId;
339
+ const missingAliases = Object.keys(this.compiledAliasToCollectionId).filter(
340
+ (alias) => !Object.hasOwn(this.inputsCache, alias)
341
+ );
342
+ if (missingAliases.length > 0) {
343
+ throw new MissingAliasInputsError(missingAliases);
344
+ }
129
345
  }
130
346
  maybeCompileBasePipeline() {
131
347
  if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {
@@ -235,36 +451,52 @@ class CollectionConfigBuilder {
235
451
  (collection) => collection.isReady()
236
452
  );
237
453
  }
454
+ /**
455
+ * Creates per-alias subscriptions enabling self-join support.
456
+ * Each alias gets its own subscription with independent filters, even for the same collection.
457
+ * Example: `{ employee: col, manager: col }` creates two separate subscriptions.
458
+ */
238
459
  subscribeToAllCollections(config, syncState) {
239
- const loaders = Object.entries(this.collections).map(
240
- ([collectionId, collection]) => {
241
- const collectionSubscriber = new CollectionSubscriber(
242
- collectionId,
243
- collection,
244
- config,
245
- syncState,
246
- this
247
- );
248
- const subscription = collectionSubscriber.subscribe();
249
- this.subscriptions[collectionId] = subscription;
250
- const statusUnsubscribe = collection.on(`status:change`, (event) => {
251
- this.handleSourceStatusChange(config, collectionId, event);
252
- });
253
- syncState.unsubscribeCallbacks.add(statusUnsubscribe);
254
- const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(
255
- collectionSubscriber,
256
- subscription
257
- );
258
- return loadMore;
460
+ const compiledAliases = Object.entries(this.compiledAliasToCollectionId);
461
+ if (compiledAliases.length === 0) {
462
+ throw new Error(
463
+ `Compiler returned no alias metadata for query '${this.id}'. This should not happen; please report.`
464
+ );
465
+ }
466
+ const loaders = compiledAliases.map(([alias, collectionId]) => {
467
+ const collection = this.collectionByAlias[alias] ?? this.collections[collectionId];
468
+ const dependencyBuilder = getCollectionBuilder(collection);
469
+ if (dependencyBuilder && dependencyBuilder !== this) {
470
+ this.aliasDependencies[alias] = [dependencyBuilder];
471
+ this.builderDependencies.add(dependencyBuilder);
472
+ } else {
473
+ this.aliasDependencies[alias] = [];
259
474
  }
260
- );
261
- const loadMoreDataCallback = () => {
475
+ const collectionSubscriber = new CollectionSubscriber(
476
+ alias,
477
+ collectionId,
478
+ collection,
479
+ this
480
+ );
481
+ const statusUnsubscribe = collection.on(`status:change`, (event) => {
482
+ this.handleSourceStatusChange(config, collectionId, event);
483
+ });
484
+ syncState.unsubscribeCallbacks.add(statusUnsubscribe);
485
+ const subscription = collectionSubscriber.subscribe();
486
+ this.subscriptions[alias] = subscription;
487
+ const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(
488
+ collectionSubscriber,
489
+ subscription
490
+ );
491
+ return loadMore;
492
+ });
493
+ const loadSubsetDataCallbacks = () => {
262
494
  loaders.map((loader) => loader());
263
495
  return true;
264
496
  };
265
497
  syncState.subscribedToAllCollections = true;
266
498
  this.updateLiveQueryStatus(config);
267
- return loadMoreDataCallback;
499
+ return loadSubsetDataCallbacks;
268
500
  }
269
501
  }
270
502
  function buildQueryFromConfig(config) {
@@ -313,6 +545,34 @@ function extractCollectionsFromQuery(query) {
313
545
  extractFromQuery(query);
314
546
  return collections;
315
547
  }
548
+ function extractCollectionAliases(query) {
549
+ const aliasesById = /* @__PURE__ */ new Map();
550
+ function recordAlias(source) {
551
+ if (!source) return;
552
+ if (source.type === `collectionRef`) {
553
+ const { id } = source.collection;
554
+ const existing = aliasesById.get(id);
555
+ if (existing) {
556
+ existing.add(source.alias);
557
+ } else {
558
+ aliasesById.set(id, /* @__PURE__ */ new Set([source.alias]));
559
+ }
560
+ } else if (source.type === `queryRef`) {
561
+ traverse(source.query);
562
+ }
563
+ }
564
+ function traverse(q) {
565
+ if (!q) return;
566
+ recordAlias(q.from);
567
+ if (q.join) {
568
+ for (const joinClause of q.join) {
569
+ recordAlias(joinClause.from);
570
+ }
571
+ }
572
+ }
573
+ traverse(query);
574
+ return aliasesById;
575
+ }
316
576
  function accumulateChanges(acc, [[key, tupleData], multiplicity]) {
317
577
  const [value, orderByIndex] = tupleData;
318
578
  const changes = acc.get(key) || {