@tanstack/db 0.5.32 → 0.5.33
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/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/query/effect.cjs +602 -0
- package/dist/cjs/query/effect.cjs.map +1 -0
- package/dist/cjs/query/effect.d.cts +94 -0
- package/dist/cjs/query/live/collection-config-builder.cjs +5 -74
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.cjs +33 -100
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +0 -1
- package/dist/cjs/query/live/utils.cjs +179 -0
- package/dist/cjs/query/live/utils.cjs.map +1 -0
- package/dist/cjs/query/live/utils.d.cts +109 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/query/effect.d.ts +94 -0
- package/dist/esm/query/effect.js +602 -0
- package/dist/esm/query/effect.js.map +1 -0
- package/dist/esm/query/live/collection-config-builder.js +1 -70
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +0 -1
- package/dist/esm/query/live/collection-subscriber.js +31 -98
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live/utils.d.ts +109 -0
- package/dist/esm/query/live/utils.js +179 -0
- package/dist/esm/query/live/utils.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +11 -0
- package/src/query/effect.ts +1119 -0
- package/src/query/live/collection-config-builder.ts +6 -132
- package/src/query/live/collection-subscriber.ts +40 -156
- package/src/query/live/utils.ts +356 -0
package/dist/cjs/index.cjs
CHANGED
|
@@ -15,6 +15,7 @@ const baseIndex = require("./indexes/base-index.cjs");
|
|
|
15
15
|
const btreeIndex = require("./indexes/btree-index.cjs");
|
|
16
16
|
const lazyIndex = require("./indexes/lazy-index.cjs");
|
|
17
17
|
const expressionHelpers = require("./query/expression-helpers.cjs");
|
|
18
|
+
const effect = require("./query/effect.cjs");
|
|
18
19
|
const functions = require("./query/builder/functions.cjs");
|
|
19
20
|
const index = require("./query/builder/index.cjs");
|
|
20
21
|
const subsetDedupe = require("./query/subset-dedupe.cjs");
|
|
@@ -138,6 +139,7 @@ exports.parseLoadSubsetOptions = expressionHelpers.parseLoadSubsetOptions;
|
|
|
138
139
|
exports.parseOrderByExpression = expressionHelpers.parseOrderByExpression;
|
|
139
140
|
exports.parseWhereExpression = expressionHelpers.parseWhereExpression;
|
|
140
141
|
exports.walkExpression = expressionHelpers.walkExpression;
|
|
142
|
+
exports.createEffect = effect.createEffect;
|
|
141
143
|
exports.add = functions.add;
|
|
142
144
|
exports.and = functions.and;
|
|
143
145
|
exports.avg = functions.avg;
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -17,6 +17,7 @@ export * from './indexes/btree-index.js';
|
|
|
17
17
|
export * from './indexes/lazy-index.js';
|
|
18
18
|
export { type IndexOptions } from './indexes/index-options.js';
|
|
19
19
|
export * from './query/expression-helpers.js';
|
|
20
|
+
export { createEffect, type DeltaEvent, type DeltaType, type EffectConfig, type EffectContext, type Effect, type EffectQueryInput, } from './query/effect.js';
|
|
20
21
|
export type { Collection } from './collection/index.js';
|
|
21
22
|
export { IR };
|
|
22
23
|
export { operators, type OperatorName } from './query/builder/functions.js';
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const dbIvm = require("@tanstack/db-ivm");
|
|
4
|
+
const scheduler = require("../scheduler.cjs");
|
|
5
|
+
const transactions = require("../transactions.cjs");
|
|
6
|
+
const index = require("./compiler/index.cjs");
|
|
7
|
+
const expressions = require("./compiler/expressions.cjs");
|
|
8
|
+
const collectionRegistry = require("./live/collection-registry.cjs");
|
|
9
|
+
const utils = require("./live/utils.cjs");
|
|
10
|
+
let effectCounter = 0;
|
|
11
|
+
function createEffect(config) {
|
|
12
|
+
const id = config.id ?? `live-query-effect-${++effectCounter}`;
|
|
13
|
+
const abortController = new AbortController();
|
|
14
|
+
const ctx = {
|
|
15
|
+
effectId: id,
|
|
16
|
+
signal: abortController.signal
|
|
17
|
+
};
|
|
18
|
+
const inFlightHandlers = /* @__PURE__ */ new Set();
|
|
19
|
+
let disposed = false;
|
|
20
|
+
const onBatchProcessed = (events) => {
|
|
21
|
+
if (disposed) return;
|
|
22
|
+
if (events.length === 0) return;
|
|
23
|
+
if (config.onBatch) {
|
|
24
|
+
try {
|
|
25
|
+
const result = config.onBatch(events, ctx);
|
|
26
|
+
if (result instanceof Promise) {
|
|
27
|
+
const tracked = result.catch((error) => {
|
|
28
|
+
reportError(error, events[0], config.onError);
|
|
29
|
+
});
|
|
30
|
+
trackPromise(tracked, inFlightHandlers);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
reportError(error, events[0], config.onError);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const event of events) {
|
|
37
|
+
if (abortController.signal.aborted) break;
|
|
38
|
+
const handler = getHandlerForEvent(event, config);
|
|
39
|
+
if (!handler) continue;
|
|
40
|
+
try {
|
|
41
|
+
const result = handler(event, ctx);
|
|
42
|
+
if (result instanceof Promise) {
|
|
43
|
+
const tracked = result.catch((error) => {
|
|
44
|
+
reportError(error, event, config.onError);
|
|
45
|
+
});
|
|
46
|
+
trackPromise(tracked, inFlightHandlers);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
reportError(error, event, config.onError);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const dispose = async () => {
|
|
54
|
+
if (disposed) return;
|
|
55
|
+
disposed = true;
|
|
56
|
+
abortController.abort();
|
|
57
|
+
runner.dispose();
|
|
58
|
+
if (inFlightHandlers.size > 0) {
|
|
59
|
+
await Promise.allSettled([...inFlightHandlers]);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const runner = new EffectPipelineRunner({
|
|
63
|
+
query: config.query,
|
|
64
|
+
skipInitial: config.skipInitial ?? false,
|
|
65
|
+
onBatchProcessed,
|
|
66
|
+
onSourceError: (error) => {
|
|
67
|
+
if (disposed) return;
|
|
68
|
+
if (config.onSourceError) {
|
|
69
|
+
try {
|
|
70
|
+
config.onSourceError(error);
|
|
71
|
+
} catch (callbackError) {
|
|
72
|
+
console.error(
|
|
73
|
+
`[Effect '${id}'] onSourceError callback threw:`,
|
|
74
|
+
callbackError
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.error(`[Effect '${id}'] ${error.message}. Disposing effect.`);
|
|
79
|
+
}
|
|
80
|
+
dispose();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
runner.start();
|
|
84
|
+
return {
|
|
85
|
+
dispose,
|
|
86
|
+
get disposed() {
|
|
87
|
+
return disposed;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
class EffectPipelineRunner {
|
|
92
|
+
constructor(config) {
|
|
93
|
+
this.compiledAliasToCollectionId = {};
|
|
94
|
+
this.subscriptions = {};
|
|
95
|
+
this.lazySourcesCallbacks = {};
|
|
96
|
+
this.lazySources = /* @__PURE__ */ new Set();
|
|
97
|
+
this.optimizableOrderByCollections = {};
|
|
98
|
+
this.biggestSentValue = /* @__PURE__ */ new Map();
|
|
99
|
+
this.lastLoadRequestKey = /* @__PURE__ */ new Map();
|
|
100
|
+
this.unsubscribeCallbacks = /* @__PURE__ */ new Set();
|
|
101
|
+
this.sentToD2KeysByAlias = /* @__PURE__ */ new Map();
|
|
102
|
+
this.pendingChanges = /* @__PURE__ */ new Map();
|
|
103
|
+
this.initialLoadComplete = false;
|
|
104
|
+
this.subscribedToAllCollections = false;
|
|
105
|
+
this.builderDependencies = /* @__PURE__ */ new Set();
|
|
106
|
+
this.aliasDependencies = {};
|
|
107
|
+
this.isGraphRunning = false;
|
|
108
|
+
this.disposed = false;
|
|
109
|
+
this.deferredCleanup = false;
|
|
110
|
+
this.skipInitial = config.skipInitial;
|
|
111
|
+
this.onBatchProcessed = config.onBatchProcessed;
|
|
112
|
+
this.onSourceError = config.onSourceError;
|
|
113
|
+
this.query = utils.buildQueryFromConfig({ query: config.query });
|
|
114
|
+
this.collections = utils.extractCollectionsFromQuery(this.query);
|
|
115
|
+
const aliasesById = utils.extractCollectionAliases(this.query);
|
|
116
|
+
this.collectionByAlias = {};
|
|
117
|
+
for (const [collectionId, aliases] of aliasesById.entries()) {
|
|
118
|
+
const collection = this.collections[collectionId];
|
|
119
|
+
if (!collection) continue;
|
|
120
|
+
for (const alias of aliases) {
|
|
121
|
+
this.collectionByAlias[alias] = collection;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
this.compilePipeline();
|
|
125
|
+
}
|
|
126
|
+
/** Compile the D2 graph and query pipeline */
|
|
127
|
+
compilePipeline() {
|
|
128
|
+
this.graph = new dbIvm.D2();
|
|
129
|
+
this.inputs = Object.fromEntries(
|
|
130
|
+
Object.keys(this.collectionByAlias).map((alias) => [
|
|
131
|
+
alias,
|
|
132
|
+
this.graph.newInput()
|
|
133
|
+
])
|
|
134
|
+
);
|
|
135
|
+
const compilation = index.compileQuery(
|
|
136
|
+
this.query,
|
|
137
|
+
this.inputs,
|
|
138
|
+
this.collections,
|
|
139
|
+
// These mutable objects are captured by reference. The join compiler
|
|
140
|
+
// reads them later when the graph runs, so they must be populated
|
|
141
|
+
// (in start()) before the first graph run.
|
|
142
|
+
this.subscriptions,
|
|
143
|
+
this.lazySourcesCallbacks,
|
|
144
|
+
this.lazySources,
|
|
145
|
+
this.optimizableOrderByCollections,
|
|
146
|
+
() => {
|
|
147
|
+
}
|
|
148
|
+
// setWindowFn (no-op — effects don't paginate)
|
|
149
|
+
);
|
|
150
|
+
this.pipeline = compilation.pipeline;
|
|
151
|
+
this.sourceWhereClauses = compilation.sourceWhereClauses;
|
|
152
|
+
this.compiledAliasToCollectionId = compilation.aliasToCollectionId;
|
|
153
|
+
this.pipeline.pipe(
|
|
154
|
+
dbIvm.output((data) => {
|
|
155
|
+
const messages = data.getInner();
|
|
156
|
+
messages.reduce(accumulateEffectChanges, this.pendingChanges);
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
this.graph.finalize();
|
|
160
|
+
}
|
|
161
|
+
/** Subscribe to source collections and start processing */
|
|
162
|
+
start() {
|
|
163
|
+
const compiledAliases = Object.entries(this.compiledAliasToCollectionId);
|
|
164
|
+
if (compiledAliases.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!this.skipInitial) {
|
|
168
|
+
this.initialLoadComplete = true;
|
|
169
|
+
}
|
|
170
|
+
const pendingBuffers = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const [alias, collectionId] of compiledAliases) {
|
|
172
|
+
const collection = this.collectionByAlias[alias] ?? this.collections[collectionId];
|
|
173
|
+
this.sentToD2KeysByAlias.set(alias, /* @__PURE__ */ new Set());
|
|
174
|
+
const dependencyBuilder = collectionRegistry.getCollectionBuilder(collection);
|
|
175
|
+
if (dependencyBuilder) {
|
|
176
|
+
this.aliasDependencies[alias] = [dependencyBuilder];
|
|
177
|
+
this.builderDependencies.add(dependencyBuilder);
|
|
178
|
+
} else {
|
|
179
|
+
this.aliasDependencies[alias] = [];
|
|
180
|
+
}
|
|
181
|
+
const whereClause = this.sourceWhereClauses?.get(alias);
|
|
182
|
+
const whereExpression = whereClause ? expressions.normalizeExpressionPaths(whereClause, alias) : void 0;
|
|
183
|
+
const buffer = [];
|
|
184
|
+
pendingBuffers.set(alias, buffer);
|
|
185
|
+
const isLazy = this.lazySources.has(alias);
|
|
186
|
+
const orderByInfo = this.getOrderByInfoForAlias(alias);
|
|
187
|
+
const changeCallback = orderByInfo ? (changes) => {
|
|
188
|
+
if (pendingBuffers.has(alias)) {
|
|
189
|
+
pendingBuffers.get(alias).push(changes);
|
|
190
|
+
} else {
|
|
191
|
+
this.trackSentValues(alias, changes, orderByInfo.comparator);
|
|
192
|
+
const split = [...utils.splitUpdates(changes)];
|
|
193
|
+
this.handleSourceChanges(alias, split);
|
|
194
|
+
}
|
|
195
|
+
} : (changes) => {
|
|
196
|
+
if (pendingBuffers.has(alias)) {
|
|
197
|
+
pendingBuffers.get(alias).push(changes);
|
|
198
|
+
} else {
|
|
199
|
+
this.handleSourceChanges(alias, changes);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const subscriptionOptions = this.buildSubscriptionOptions(
|
|
203
|
+
alias,
|
|
204
|
+
isLazy,
|
|
205
|
+
orderByInfo,
|
|
206
|
+
whereExpression
|
|
207
|
+
);
|
|
208
|
+
const subscription = collection.subscribeChanges(
|
|
209
|
+
changeCallback,
|
|
210
|
+
subscriptionOptions
|
|
211
|
+
);
|
|
212
|
+
this.subscriptions[alias] = subscription;
|
|
213
|
+
if (orderByInfo) {
|
|
214
|
+
this.requestInitialOrderedSnapshot(alias, orderByInfo, subscription);
|
|
215
|
+
}
|
|
216
|
+
this.unsubscribeCallbacks.add(() => {
|
|
217
|
+
subscription.unsubscribe();
|
|
218
|
+
delete this.subscriptions[alias];
|
|
219
|
+
});
|
|
220
|
+
const statusUnsubscribe = collection.on(`status:change`, (event) => {
|
|
221
|
+
if (this.disposed) return;
|
|
222
|
+
const { status } = event;
|
|
223
|
+
if (status === `error`) {
|
|
224
|
+
this.onSourceError(
|
|
225
|
+
new Error(
|
|
226
|
+
`Source collection '${collectionId}' entered error state`
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (status === `cleaned-up`) {
|
|
232
|
+
this.onSourceError(
|
|
233
|
+
new Error(
|
|
234
|
+
`Source collection '${collectionId}' was cleaned up while effect depends on it`
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (this.skipInitial && !this.initialLoadComplete && this.checkAllCollectionsReady()) {
|
|
240
|
+
this.initialLoadComplete = true;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
this.unsubscribeCallbacks.add(statusUnsubscribe);
|
|
244
|
+
}
|
|
245
|
+
this.subscribedToAllCollections = true;
|
|
246
|
+
for (const [alias] of pendingBuffers) {
|
|
247
|
+
const buffer = pendingBuffers.get(alias);
|
|
248
|
+
pendingBuffers.delete(alias);
|
|
249
|
+
const orderByInfo = this.getOrderByInfoForAlias(alias);
|
|
250
|
+
for (const changes of buffer) {
|
|
251
|
+
if (orderByInfo) {
|
|
252
|
+
this.trackSentValues(alias, changes, orderByInfo.comparator);
|
|
253
|
+
const split = [...utils.splitUpdates(changes)];
|
|
254
|
+
this.sendChangesToD2(alias, split);
|
|
255
|
+
} else {
|
|
256
|
+
this.sendChangesToD2(alias, changes);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this.runGraph();
|
|
261
|
+
if (this.skipInitial && !this.initialLoadComplete) {
|
|
262
|
+
if (this.checkAllCollectionsReady()) {
|
|
263
|
+
this.initialLoadComplete = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Handle incoming changes from a source collection */
|
|
268
|
+
handleSourceChanges(alias, changes) {
|
|
269
|
+
this.sendChangesToD2(alias, changes);
|
|
270
|
+
this.scheduleGraphRun(alias);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Schedule a graph run via the transaction-scoped scheduler.
|
|
274
|
+
*
|
|
275
|
+
* When called within a transaction, the run is deferred until the
|
|
276
|
+
* transaction flushes, coalescing multiple changes into a single graph
|
|
277
|
+
* execution. Without a transaction, the graph runs immediately.
|
|
278
|
+
*
|
|
279
|
+
* Dependencies are discovered from source collections that are themselves
|
|
280
|
+
* live query collections, ensuring parent queries run before effects.
|
|
281
|
+
*/
|
|
282
|
+
scheduleGraphRun(alias) {
|
|
283
|
+
const contextId = transactions.getActiveTransaction()?.id;
|
|
284
|
+
const deps = new Set(this.builderDependencies);
|
|
285
|
+
if (alias) {
|
|
286
|
+
const aliasDeps = this.aliasDependencies[alias];
|
|
287
|
+
if (aliasDeps) {
|
|
288
|
+
for (const dep of aliasDeps) {
|
|
289
|
+
deps.add(dep);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (contextId) {
|
|
294
|
+
for (const dep of deps) {
|
|
295
|
+
if (typeof dep === `object` && dep !== null && `scheduleGraphRun` in dep && typeof dep.scheduleGraphRun === `function`) {
|
|
296
|
+
dep.scheduleGraphRun(void 0, { contextId });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
scheduler.transactionScopedScheduler.schedule({
|
|
301
|
+
contextId,
|
|
302
|
+
jobId: this,
|
|
303
|
+
dependencies: deps,
|
|
304
|
+
run: () => this.executeScheduledGraphRun()
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Called by the scheduler when dependencies are satisfied.
|
|
309
|
+
* Checks that the effect is still active before running.
|
|
310
|
+
*/
|
|
311
|
+
executeScheduledGraphRun() {
|
|
312
|
+
if (this.disposed || !this.subscribedToAllCollections) return;
|
|
313
|
+
this.runGraph();
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Send changes to the D2 input for the given alias.
|
|
317
|
+
* Returns the number of multiset entries sent.
|
|
318
|
+
*/
|
|
319
|
+
sendChangesToD2(alias, changes) {
|
|
320
|
+
if (this.disposed || !this.inputs || !this.graph) return 0;
|
|
321
|
+
const input = this.inputs[alias];
|
|
322
|
+
if (!input) return 0;
|
|
323
|
+
const collection = this.collectionByAlias[alias];
|
|
324
|
+
if (!collection) return 0;
|
|
325
|
+
const sentKeys = this.sentToD2KeysByAlias.get(alias);
|
|
326
|
+
const filtered = utils.filterDuplicateInserts(changes, sentKeys);
|
|
327
|
+
return utils.sendChangesToInput(input, filtered, collection.config.getKey);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Run the D2 graph until quiescence, then emit accumulated events once.
|
|
331
|
+
*
|
|
332
|
+
* All output across the entire while-loop is accumulated into a single
|
|
333
|
+
* batch so that users see one `onBatchProcessed` invocation per scheduler
|
|
334
|
+
* run, even when ordered loading causes multiple graph steps.
|
|
335
|
+
*/
|
|
336
|
+
runGraph() {
|
|
337
|
+
if (this.isGraphRunning || this.disposed || !this.graph) return;
|
|
338
|
+
this.isGraphRunning = true;
|
|
339
|
+
try {
|
|
340
|
+
while (this.graph.pendingWork()) {
|
|
341
|
+
this.graph.run();
|
|
342
|
+
if (this.disposed) break;
|
|
343
|
+
this.loadMoreIfNeeded();
|
|
344
|
+
}
|
|
345
|
+
this.flushPendingChanges();
|
|
346
|
+
} finally {
|
|
347
|
+
this.isGraphRunning = false;
|
|
348
|
+
if (this.deferredCleanup) {
|
|
349
|
+
this.deferredCleanup = false;
|
|
350
|
+
this.finalCleanup();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** Classify accumulated changes into DeltaEvents and invoke the callback */
|
|
355
|
+
flushPendingChanges() {
|
|
356
|
+
if (this.pendingChanges.size === 0) return;
|
|
357
|
+
if (this.skipInitial && !this.initialLoadComplete) {
|
|
358
|
+
this.pendingChanges = /* @__PURE__ */ new Map();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const events = [];
|
|
362
|
+
for (const [key, changes] of this.pendingChanges) {
|
|
363
|
+
const event = classifyDelta(key, changes);
|
|
364
|
+
if (event) {
|
|
365
|
+
events.push(event);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
this.pendingChanges = /* @__PURE__ */ new Map();
|
|
369
|
+
if (events.length > 0) {
|
|
370
|
+
this.onBatchProcessed(events);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/** Check if all source collections are in the ready state */
|
|
374
|
+
checkAllCollectionsReady() {
|
|
375
|
+
return Object.values(this.collections).every(
|
|
376
|
+
(collection) => collection.isReady()
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Build subscription options for an alias based on whether it uses ordered
|
|
381
|
+
* loading, is lazy, or should pass orderBy/limit hints.
|
|
382
|
+
*/
|
|
383
|
+
buildSubscriptionOptions(alias, isLazy, orderByInfo, whereExpression) {
|
|
384
|
+
if (orderByInfo) {
|
|
385
|
+
return { includeInitialState: false, whereExpression };
|
|
386
|
+
}
|
|
387
|
+
const includeInitialState = !isLazy;
|
|
388
|
+
const hints = utils.computeSubscriptionOrderByHints(this.query, alias);
|
|
389
|
+
return {
|
|
390
|
+
includeInitialState,
|
|
391
|
+
whereExpression,
|
|
392
|
+
...hints.orderBy ? { orderBy: hints.orderBy } : {},
|
|
393
|
+
...hints.limit !== void 0 ? { limit: hints.limit } : {}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Request the initial ordered snapshot for an alias.
|
|
398
|
+
* Uses requestLimitedSnapshot (index-based cursor) or requestSnapshot
|
|
399
|
+
* (full load with limit) depending on whether an index is available.
|
|
400
|
+
*/
|
|
401
|
+
requestInitialOrderedSnapshot(alias, orderByInfo, subscription) {
|
|
402
|
+
const { orderBy, offset, limit, index: index2 } = orderByInfo;
|
|
403
|
+
const normalizedOrderBy = expressions.normalizeOrderByPaths(orderBy, alias);
|
|
404
|
+
if (index2) {
|
|
405
|
+
subscription.setOrderByIndex(index2);
|
|
406
|
+
subscription.requestLimitedSnapshot({
|
|
407
|
+
limit: offset + limit,
|
|
408
|
+
orderBy: normalizedOrderBy,
|
|
409
|
+
trackLoadSubsetPromise: false
|
|
410
|
+
});
|
|
411
|
+
} else {
|
|
412
|
+
subscription.requestSnapshot({
|
|
413
|
+
orderBy: normalizedOrderBy,
|
|
414
|
+
limit: offset + limit,
|
|
415
|
+
trackLoadSubsetPromise: false
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Get orderBy optimization info for a given alias.
|
|
421
|
+
* Returns undefined if no optimization exists for this alias.
|
|
422
|
+
*/
|
|
423
|
+
getOrderByInfoForAlias(alias) {
|
|
424
|
+
const collectionId = this.compiledAliasToCollectionId[alias];
|
|
425
|
+
if (!collectionId) return void 0;
|
|
426
|
+
const info = this.optimizableOrderByCollections[collectionId];
|
|
427
|
+
if (info && info.alias === alias) {
|
|
428
|
+
return info;
|
|
429
|
+
}
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* After each graph run step, check if any ordered query's topK operator
|
|
434
|
+
* needs more data. If so, load more rows via requestLimitedSnapshot.
|
|
435
|
+
*/
|
|
436
|
+
loadMoreIfNeeded() {
|
|
437
|
+
for (const [, orderByInfo] of Object.entries(
|
|
438
|
+
this.optimizableOrderByCollections
|
|
439
|
+
)) {
|
|
440
|
+
if (!orderByInfo.dataNeeded) continue;
|
|
441
|
+
if (this.pendingOrderedLoadPromise) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const n = orderByInfo.dataNeeded();
|
|
445
|
+
if (n > 0) {
|
|
446
|
+
this.loadNextItems(orderByInfo, n);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Load n more items from the source collection, starting from the cursor
|
|
452
|
+
* position (the biggest value sent so far).
|
|
453
|
+
*/
|
|
454
|
+
loadNextItems(orderByInfo, n) {
|
|
455
|
+
const { alias } = orderByInfo;
|
|
456
|
+
const subscription = this.subscriptions[alias];
|
|
457
|
+
if (!subscription) return;
|
|
458
|
+
const cursor = utils.computeOrderedLoadCursor(
|
|
459
|
+
orderByInfo,
|
|
460
|
+
this.biggestSentValue.get(alias),
|
|
461
|
+
this.lastLoadRequestKey.get(alias),
|
|
462
|
+
alias,
|
|
463
|
+
n
|
|
464
|
+
);
|
|
465
|
+
if (!cursor) return;
|
|
466
|
+
this.lastLoadRequestKey.set(alias, cursor.loadRequestKey);
|
|
467
|
+
subscription.requestLimitedSnapshot({
|
|
468
|
+
orderBy: cursor.normalizedOrderBy,
|
|
469
|
+
limit: n,
|
|
470
|
+
minValues: cursor.minValues,
|
|
471
|
+
trackLoadSubsetPromise: false,
|
|
472
|
+
onLoadSubsetResult: (loadResult) => {
|
|
473
|
+
if (loadResult instanceof Promise) {
|
|
474
|
+
this.pendingOrderedLoadPromise = loadResult;
|
|
475
|
+
loadResult.finally(() => {
|
|
476
|
+
if (this.pendingOrderedLoadPromise === loadResult) {
|
|
477
|
+
this.pendingOrderedLoadPromise = void 0;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Track the biggest value sent for a given ordered alias.
|
|
486
|
+
* Used for cursor-based pagination in loadNextItems.
|
|
487
|
+
*/
|
|
488
|
+
trackSentValues(alias, changes, comparator) {
|
|
489
|
+
const sentKeys = this.sentToD2KeysByAlias.get(alias) ?? /* @__PURE__ */ new Set();
|
|
490
|
+
const result = utils.trackBiggestSentValue(
|
|
491
|
+
changes,
|
|
492
|
+
this.biggestSentValue.get(alias),
|
|
493
|
+
sentKeys,
|
|
494
|
+
comparator
|
|
495
|
+
);
|
|
496
|
+
this.biggestSentValue.set(alias, result.biggest);
|
|
497
|
+
if (result.shouldResetLoadKey) {
|
|
498
|
+
this.lastLoadRequestKey.delete(alias);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/** Tear down subscriptions and clear state */
|
|
502
|
+
dispose() {
|
|
503
|
+
if (this.disposed) return;
|
|
504
|
+
this.disposed = true;
|
|
505
|
+
this.subscribedToAllCollections = false;
|
|
506
|
+
this.unsubscribeCallbacks.forEach((fn) => fn());
|
|
507
|
+
this.unsubscribeCallbacks.clear();
|
|
508
|
+
this.sentToD2KeysByAlias.clear();
|
|
509
|
+
this.pendingChanges.clear();
|
|
510
|
+
this.lazySources.clear();
|
|
511
|
+
this.builderDependencies.clear();
|
|
512
|
+
this.biggestSentValue.clear();
|
|
513
|
+
this.lastLoadRequestKey.clear();
|
|
514
|
+
this.pendingOrderedLoadPromise = void 0;
|
|
515
|
+
for (const key of Object.keys(this.lazySourcesCallbacks)) {
|
|
516
|
+
delete this.lazySourcesCallbacks[key];
|
|
517
|
+
}
|
|
518
|
+
for (const key of Object.keys(this.aliasDependencies)) {
|
|
519
|
+
delete this.aliasDependencies[key];
|
|
520
|
+
}
|
|
521
|
+
for (const key of Object.keys(this.optimizableOrderByCollections)) {
|
|
522
|
+
delete this.optimizableOrderByCollections[key];
|
|
523
|
+
}
|
|
524
|
+
if (this.isGraphRunning) {
|
|
525
|
+
this.deferredCleanup = true;
|
|
526
|
+
} else {
|
|
527
|
+
this.finalCleanup();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/** Clear graph references — called after graph run completes or immediately from dispose */
|
|
531
|
+
finalCleanup() {
|
|
532
|
+
this.graph = void 0;
|
|
533
|
+
this.inputs = void 0;
|
|
534
|
+
this.pipeline = void 0;
|
|
535
|
+
this.sourceWhereClauses = void 0;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function getHandlerForEvent(event, config) {
|
|
539
|
+
switch (event.type) {
|
|
540
|
+
case `enter`:
|
|
541
|
+
return config.onEnter;
|
|
542
|
+
case `exit`:
|
|
543
|
+
return config.onExit;
|
|
544
|
+
case `update`:
|
|
545
|
+
return config.onUpdate;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function accumulateEffectChanges(acc, [[key, tupleData], multiplicity]) {
|
|
549
|
+
const [value] = tupleData;
|
|
550
|
+
const changes = acc.get(key) || {
|
|
551
|
+
deletes: 0,
|
|
552
|
+
inserts: 0
|
|
553
|
+
};
|
|
554
|
+
if (multiplicity < 0) {
|
|
555
|
+
changes.deletes += Math.abs(multiplicity);
|
|
556
|
+
changes.deleteValue ??= value;
|
|
557
|
+
} else if (multiplicity > 0) {
|
|
558
|
+
changes.inserts += multiplicity;
|
|
559
|
+
changes.insertValue = value;
|
|
560
|
+
}
|
|
561
|
+
acc.set(key, changes);
|
|
562
|
+
return acc;
|
|
563
|
+
}
|
|
564
|
+
function classifyDelta(key, changes) {
|
|
565
|
+
const { inserts, deletes, insertValue, deleteValue } = changes;
|
|
566
|
+
if (inserts > 0 && deletes === 0) {
|
|
567
|
+
return { type: `enter`, key, value: insertValue };
|
|
568
|
+
}
|
|
569
|
+
if (deletes > 0 && inserts === 0) {
|
|
570
|
+
return { type: `exit`, key, value: deleteValue };
|
|
571
|
+
}
|
|
572
|
+
if (inserts > 0 && deletes > 0) {
|
|
573
|
+
return {
|
|
574
|
+
type: `update`,
|
|
575
|
+
key,
|
|
576
|
+
value: insertValue,
|
|
577
|
+
previousValue: deleteValue
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
582
|
+
function trackPromise(promise, inFlightHandlers) {
|
|
583
|
+
inFlightHandlers.add(promise);
|
|
584
|
+
promise.finally(() => {
|
|
585
|
+
inFlightHandlers.delete(promise);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function reportError(error, event, onError) {
|
|
589
|
+
const normalised = error instanceof Error ? error : new Error(String(error));
|
|
590
|
+
if (onError) {
|
|
591
|
+
try {
|
|
592
|
+
onError(normalised, event);
|
|
593
|
+
} catch (onErrorError) {
|
|
594
|
+
console.error(`[Effect] Error in onError handler:`, onErrorError);
|
|
595
|
+
console.error(`[Effect] Original error:`, normalised);
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
console.error(`[Effect] Unhandled error in handler:`, normalised);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
exports.createEffect = createEffect;
|
|
602
|
+
//# sourceMappingURL=effect.cjs.map
|