@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.
Files changed (34) hide show
  1. package/dist/cjs/index.cjs +2 -0
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +1 -0
  4. package/dist/cjs/query/effect.cjs +602 -0
  5. package/dist/cjs/query/effect.cjs.map +1 -0
  6. package/dist/cjs/query/effect.d.cts +94 -0
  7. package/dist/cjs/query/live/collection-config-builder.cjs +5 -74
  8. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  9. package/dist/cjs/query/live/collection-subscriber.cjs +33 -100
  10. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  11. package/dist/cjs/query/live/collection-subscriber.d.cts +0 -1
  12. package/dist/cjs/query/live/utils.cjs +179 -0
  13. package/dist/cjs/query/live/utils.cjs.map +1 -0
  14. package/dist/cjs/query/live/utils.d.cts +109 -0
  15. package/dist/esm/index.d.ts +1 -0
  16. package/dist/esm/index.js +2 -0
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/query/effect.d.ts +94 -0
  19. package/dist/esm/query/effect.js +602 -0
  20. package/dist/esm/query/effect.js.map +1 -0
  21. package/dist/esm/query/live/collection-config-builder.js +1 -70
  22. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  23. package/dist/esm/query/live/collection-subscriber.d.ts +0 -1
  24. package/dist/esm/query/live/collection-subscriber.js +31 -98
  25. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  26. package/dist/esm/query/live/utils.d.ts +109 -0
  27. package/dist/esm/query/live/utils.js +179 -0
  28. package/dist/esm/query/live/utils.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/index.ts +11 -0
  31. package/src/query/effect.ts +1119 -0
  32. package/src/query/live/collection-config-builder.ts +6 -132
  33. package/src/query/live/collection-subscriber.ts +40 -156
  34. package/src/query/live/utils.ts +356 -0
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -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