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