@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
@@ -1,5 +1,5 @@
1
- import { serializeValue, MultiSet } from "@tanstack/db-ivm";
2
1
  import { normalizeExpressionPaths, normalizeOrderByPaths } from "../compiler/expressions.js";
2
+ import { filterDuplicateInserts, sendChangesToInput, computeSubscriptionOrderByHints, computeOrderedLoadCursor, trackBiggestSentValue, splitUpdates } from "./utils.js";
3
3
  const loadMoreCallbackSymbol = /* @__PURE__ */ Symbol.for(
4
4
  `@tanstack/db.collection-config-builder`
5
5
  );
@@ -78,18 +78,10 @@ class CollectionSubscriber {
78
78
  }
79
79
  sendChangesToPipeline(changes, callback) {
80
80
  const changesArray = Array.isArray(changes) ? changes : [...changes];
81
- const filteredChanges = [];
82
- for (const change of changesArray) {
83
- if (change.type === `insert`) {
84
- if (this.sentToD2Keys.has(change.key)) {
85
- continue;
86
- }
87
- this.sentToD2Keys.add(change.key);
88
- } else if (change.type === `delete`) {
89
- this.sentToD2Keys.delete(change.key);
90
- }
91
- filteredChanges.push(change);
92
- }
81
+ const filteredChanges = filterDuplicateInserts(
82
+ changesArray,
83
+ this.sentToD2Keys
84
+ );
93
85
  const input = this.collectionConfigBuilder.currentSyncState.inputs[this.alias];
94
86
  const sentChanges = sendChangesToInput(
95
87
  input,
@@ -105,19 +97,10 @@ class CollectionSubscriber {
105
97
  const sendChanges = (changes) => {
106
98
  this.sendChangesToPipeline(changes);
107
99
  };
108
- const { orderBy, limit, offset } = this.collectionConfigBuilder.query;
109
- const effectiveLimit = limit !== void 0 && offset !== void 0 ? limit + offset : limit;
110
- const normalizedOrderBy = orderBy ? normalizeOrderByPaths(orderBy, this.alias) : void 0;
111
- const canPassOrderBy = normalizedOrderBy?.every((clause) => {
112
- const exp = clause.expression;
113
- if (exp.type !== `ref`) {
114
- return false;
115
- }
116
- const path = exp.path;
117
- return Array.isArray(path) && path.length === 1;
118
- }) ?? false;
119
- const orderByForSubscription = canPassOrderBy ? normalizedOrderBy : void 0;
120
- const limitForSubscription = canPassOrderBy ? effectiveLimit : void 0;
100
+ const hints = computeSubscriptionOrderByHints(
101
+ this.collectionConfigBuilder.query,
102
+ this.alias
103
+ );
121
104
  const onLoadSubsetResult = includeInitialState ? (result) => {
122
105
  if (result instanceof Promise) {
123
106
  this.collectionConfigBuilder.liveQueryCollection._sync.trackLoadPromise(
@@ -129,8 +112,8 @@ class CollectionSubscriber {
129
112
  ...includeInitialState && { includeInitialState },
130
113
  whereExpression,
131
114
  onStatusChange,
132
- orderBy: orderByForSubscription,
133
- limit: limitForSubscription,
115
+ orderBy: hints.orderBy,
116
+ limit: hints.limit,
134
117
  onLoadSubsetResult
135
118
  });
136
119
  return subscription;
@@ -233,32 +216,22 @@ class CollectionSubscriber {
233
216
  if (!orderByInfo) {
234
217
  return;
235
218
  }
236
- const { orderBy, valueExtractorForRawRow, offset } = orderByInfo;
237
- const biggestSentRow = this.biggest;
238
- const extractedValues = biggestSentRow ? valueExtractorForRawRow(biggestSentRow) : void 0;
239
- let minValues;
240
- if (extractedValues !== void 0) {
241
- minValues = Array.isArray(extractedValues) ? extractedValues : [extractedValues];
242
- }
243
- const loadRequestKey = this.getLoadRequestKey({
244
- minValues,
245
- offset,
246
- limit: n
247
- });
248
- if (this.lastLoadRequestKey === loadRequestKey) {
249
- return;
250
- }
251
- const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias);
219
+ const cursor = computeOrderedLoadCursor(
220
+ orderByInfo,
221
+ this.biggest,
222
+ this.lastLoadRequestKey,
223
+ this.alias,
224
+ n
225
+ );
226
+ if (!cursor) return;
227
+ this.lastLoadRequestKey = cursor.loadRequestKey;
252
228
  subscription.requestLimitedSnapshot({
253
- orderBy: normalizedOrderBy,
229
+ orderBy: cursor.normalizedOrderBy,
254
230
  limit: n,
255
- minValues,
256
- // Omit offset so requestLimitedSnapshot can advance the offset based on
257
- // the number of rows already loaded (supports offset-based backends).
231
+ minValues: cursor.minValues,
258
232
  trackLoadSubsetPromise: false,
259
233
  onLoadSubsetResult: this.orderedLoadSubsetResult
260
234
  });
261
- this.lastLoadRequestKey = loadRequestKey;
262
235
  }
263
236
  getWhereClauseForAlias() {
264
237
  const sourceWhereClausesCache = this.collectionConfigBuilder.sourceWhereClausesCache;
@@ -275,20 +248,15 @@ class CollectionSubscriber {
275
248
  return void 0;
276
249
  }
277
250
  trackSentValues(changes, comparator) {
278
- for (const change of changes) {
279
- if (change.type === `delete`) {
280
- continue;
281
- }
282
- const isNewKey = !this.sentToD2Keys.has(change.key);
283
- if (!this.biggest) {
284
- this.biggest = change.value;
285
- this.lastLoadRequestKey = void 0;
286
- } else if (comparator(this.biggest, change.value) < 0) {
287
- this.biggest = change.value;
288
- this.lastLoadRequestKey = void 0;
289
- } else if (isNewKey) {
290
- this.lastLoadRequestKey = void 0;
291
- }
251
+ const result = trackBiggestSentValue(
252
+ changes,
253
+ this.biggest,
254
+ this.sentToD2Keys,
255
+ comparator
256
+ );
257
+ this.biggest = result.biggest;
258
+ if (result.shouldResetLoadKey) {
259
+ this.lastLoadRequestKey = void 0;
292
260
  }
293
261
  }
294
262
  ensureLoadingPromise(subscription) {
@@ -306,41 +274,6 @@ class CollectionSubscriber {
306
274
  promise
307
275
  );
308
276
  }
309
- getLoadRequestKey(options) {
310
- return serializeValue({
311
- minValues: options.minValues ?? null,
312
- offset: options.offset,
313
- limit: options.limit
314
- });
315
- }
316
- }
317
- function sendChangesToInput(input, changes, getKey) {
318
- const multiSetArray = [];
319
- for (const change of changes) {
320
- const key = getKey(change.value);
321
- if (change.type === `insert`) {
322
- multiSetArray.push([[key, change.value], 1]);
323
- } else if (change.type === `update`) {
324
- multiSetArray.push([[key, change.previousValue], -1]);
325
- multiSetArray.push([[key, change.value], 1]);
326
- } else {
327
- multiSetArray.push([[key, change.value], -1]);
328
- }
329
- }
330
- if (multiSetArray.length !== 0) {
331
- input.sendData(new MultiSet(multiSetArray));
332
- }
333
- return multiSetArray.length;
334
- }
335
- function* splitUpdates(changes) {
336
- for (const change of changes) {
337
- if (change.type === `update`) {
338
- yield { type: `delete`, key: change.key, value: change.previousValue };
339
- yield { type: `insert`, key: change.key, value: change.value };
340
- } else {
341
- yield change;
342
- }
343
- }
344
277
  }
345
278
  export {
346
279
  CollectionSubscriber
@@ -1 +1 @@
1
- {"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet, serializeValue } from '@tanstack/db-ivm'\nimport {\n normalizeExpressionPaths,\n normalizeOrderByPaths,\n} from '../compiler/expressions.js'\nimport type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n ChangeMessage,\n SubscriptionStatusChangeEvent,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { CollectionConfigBuilder } from './collection-config-builder.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`,\n)\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n // Track the most recent ordered load request key (cursor + window).\n // This avoids infinite loops from cached data re-writes while still allowing\n // window moves or new keys at the same cursor value to trigger new requests.\n private lastLoadRequestKey: string | undefined\n\n // Track deferred promises for subscription loading states\n private subscriptionLoadingPromises = new Map<\n CollectionSubscription,\n { resolve: () => void }\n >()\n\n // Track keys that have been sent to the D2 pipeline to prevent duplicate inserts\n // This is necessary because different code paths (initial load, change events)\n // can potentially send the same item to D2 multiple times.\n private sentToD2Keys = new Set<string | number>()\n\n // Direct load tracking callback for ordered path (set during subscribeToOrderedChanges,\n // used by loadNextItems for subsequent requestLimitedSnapshot calls)\n private orderedLoadSubsetResult?: (result: Promise<void> | true) => void\n private pendingOrderedLoadPromise: Promise<void> | undefined\n\n constructor(\n private alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = normalizeExpressionPaths(whereClause, this.alias)\n return this.subscribeToChanges(whereExpression)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n const orderByInfo = this.getOrderByInfo()\n\n // Direct load promise tracking: pipes loadSubset results straight to the\n // live query collection, avoiding the multi-hop deferred promise chain that\n // can break under microtask timing (e.g., queueMicrotask in TanStack Query).\n const trackLoadResult = (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n result,\n )\n }\n }\n\n // Status change handler - passed to subscribeChanges so it's registered\n // BEFORE any snapshot is requested, preventing race conditions.\n // Used as a fallback for status transitions not covered by direct tracking\n // (e.g., truncate-triggered reloads that call trackLoadSubsetPromise directly).\n const onStatusChange = (event: SubscriptionStatusChangeEvent) => {\n const subscription = event.subscription as CollectionSubscription\n if (event.status === `loadingSubset`) {\n this.ensureLoadingPromise(subscription)\n } else {\n // status is 'ready'\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n }\n }\n\n // Create subscription with onStatusChange - listener is registered before any async work\n let subscription: CollectionSubscription\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\n onStatusChange,\n trackLoadResult,\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias,\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState,\n onStatusChange,\n )\n }\n\n // Check current status after subscribing - if status is 'loadingSubset', track it.\n // The onStatusChange listener will catch the transition to 'ready'.\n if (subscription.status === `loadingSubset`) {\n this.ensureLoadingPromise(subscription)\n }\n\n const unsubscribe = () => {\n // If subscription has a pending promise, resolve it before unsubscribing\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe,\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean,\n ) {\n // Filter changes to prevent duplicate inserts to D2 pipeline.\n // This ensures D2 multiplicity stays at 1 for visible items, so deletes\n // properly reduce multiplicity to 0 (triggering DELETE output).\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n const filteredChanges: Array<ChangeMessage<any, string | number>> = []\n for (const change of changesArray) {\n if (change.type === `insert`) {\n if (this.sentToD2Keys.has(change.key)) {\n // Skip duplicate insert - already sent to D2\n continue\n }\n this.sentToD2Keys.add(change.key)\n } else if (change.type === `delete`) {\n // Remove from tracking so future re-inserts are allowed\n this.sentToD2Keys.delete(change.key)\n }\n // Updates are handled as delete+insert by splitUpdates, so no special handling needed\n filteredChanges.push(change)\n }\n\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\n const sentChanges = sendChangesToInput(\n input,\n filteredChanges,\n this.collection.config.getKey,\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to schedule a graph run even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\n })\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Get the query's orderBy and limit to pass to loadSubset.\n // Only include orderBy when it is scoped to this alias and uses simple refs,\n // to avoid leaking cross-collection paths into backend-specific compilers.\n const { orderBy, limit, offset } = this.collectionConfigBuilder.query\n const effectiveLimit =\n limit !== undefined && offset !== undefined ? limit + offset : limit\n const normalizedOrderBy = orderBy\n ? normalizeOrderByPaths(orderBy, this.alias)\n : undefined\n const canPassOrderBy =\n normalizedOrderBy?.every((clause) => {\n const exp = clause.expression\n if (exp.type !== `ref`) {\n return false\n }\n const path = exp.path\n return Array.isArray(path) && path.length === 1\n }) ?? false\n const orderByForSubscription = canPassOrderBy\n ? normalizedOrderBy\n : undefined\n const limitForSubscription = canPassOrderBy ? effectiveLimit : undefined\n\n // Track loading via the loadSubset promise directly.\n // requestSnapshot uses trackLoadSubsetPromise: false (needed for truncate handling),\n // so we use onLoadSubsetResult to get the promise and track it ourselves.\n const onLoadSubsetResult = includeInitialState\n ? (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n result,\n )\n }\n }\n : undefined\n\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n onStatusChange,\n orderBy: orderByForSubscription,\n limit: limitForSubscription,\n onLoadSubsetResult,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n onLoadSubsetResult: (result: Promise<void> | true) => void,\n ): CollectionSubscription {\n const { orderBy, offset, limit, index } = orderByInfo\n\n // Store the callback so loadNextItems can also use direct tracking.\n // Track in-flight ordered loads to avoid issuing redundant requests while\n // a previous snapshot is still pending.\n const handleLoadSubsetResult = (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.pendingOrderedLoadPromise = result\n result.finally(() => {\n if (this.pendingOrderedLoadPromise === result) {\n this.pendingOrderedLoadPromise = undefined\n }\n })\n }\n onLoadSubsetResult(result)\n }\n\n this.orderedLoadSubsetResult = handleLoadSubsetResult\n\n // Use a holder to forward-reference subscription in the callback\n const subscriptionHolder: { current?: CollectionSubscription } = {}\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>,\n ) => {\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n\n this.trackSentValues(changesArray, orderByInfo.comparator)\n\n // Split live updates into a delete of the old value and an insert of the new value\n const splittedChanges = splitUpdates(changesArray)\n this.sendChangesToPipelineWithTracking(\n splittedChanges,\n subscriptionHolder.current!,\n )\n }\n\n // Subscribe to changes with onStatusChange - listener is registered before any snapshot\n // values bigger than what we've sent don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n onStatusChange,\n })\n subscriptionHolder.current = subscription\n\n // Listen for truncate events to reset cursor tracking state and sentToD2Keys\n // This ensures that after a must-refetch/truncate, we don't use stale cursor data\n // and allow re-inserts of previously sent keys\n const truncateUnsubscribe = this.collection.on(`truncate`, () => {\n this.biggest = undefined\n this.lastLoadRequestKey = undefined\n this.pendingOrderedLoadPromise = undefined\n this.sentToD2Keys.clear()\n })\n\n // Clean up truncate listener when subscription is unsubscribed\n subscription.on(`unsubscribed`, () => {\n truncateUnsubscribe()\n })\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Trigger the snapshot request — use direct load tracking (trackLoadSubsetPromise: false)\n // to pipe the loadSubset result straight to the live query collection. This bypasses\n // the subscription status → onStatusChange → deferred promise chain which is fragile\n // under microtask timing (e.g., queueMicrotask delays in TanStack Query observers).\n if (index) {\n // We have an index on the first orderBy column - use lazy loading optimization\n subscription.setOrderByIndex(index)\n\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: handleLoadSubsetResult,\n })\n } else {\n // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset\n subscription.requestSnapshot({\n orderBy: normalizedOrderBy,\n limit: offset + limit,\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: handleLoadSubsetResult,\n })\n }\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // dataNeeded is not set when there's no index (e.g., non-ref expression).\n // In this case, we've already loaded all data via requestSnapshot\n // and don't need to lazily load more.\n return true\n }\n\n if (this.pendingOrderedLoadPromise) {\n // Wait for in-flight ordered loads to resolve before issuing another request.\n return true\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription,\n ) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n changes,\n subscriptionWithLoader[loadMoreCallbackSymbol],\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n const { orderBy, valueExtractorForRawRow, offset } = orderByInfo\n const biggestSentRow = this.biggest\n\n // Extract all orderBy column values from the biggest sent row\n // For single-column: returns single value, for multi-column: returns array\n const extractedValues = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : undefined\n\n // Normalize to array format for minValues\n let minValues: Array<unknown> | undefined\n if (extractedValues !== undefined) {\n minValues = Array.isArray(extractedValues)\n ? extractedValues\n : [extractedValues]\n }\n\n const loadRequestKey = this.getLoadRequestKey({\n minValues,\n offset,\n limit: n,\n })\n\n // Skip if we already requested a load for this cursor+window.\n // This prevents infinite loops from cached data re-writes while still allowing\n // window moves (offset/limit changes) to trigger new requests.\n if (this.lastLoadRequestKey === loadRequestKey) {\n return\n }\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Take the `n` items after the biggest sent value\n // Pass the current window offset to ensure proper deduplication\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\n limit: n,\n minValues,\n // Omit offset so requestLimitedSnapshot can advance the offset based on\n // the number of rows already loaded (supports offset-based backends).\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: this.orderedLoadSubsetResult,\n })\n\n this.lastLoadRequestKey = loadRequestKey\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\n }\n return undefined\n }\n\n private trackSentValues(\n changes: Array<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number,\n ): void {\n for (const change of changes) {\n if (change.type === `delete`) {\n continue\n }\n\n const isNewKey = !this.sentToD2Keys.has(change.key)\n\n // Only track inserts/updates for cursor positioning, not deletes\n if (!this.biggest) {\n this.biggest = change.value\n this.lastLoadRequestKey = undefined\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n this.lastLoadRequestKey = undefined\n } else if (isNewKey) {\n // New key with same orderBy value - allow another load if needed\n this.lastLoadRequestKey = undefined\n }\n }\n }\n\n private ensureLoadingPromise(subscription: CollectionSubscription) {\n if (this.subscriptionLoadingPromises.has(subscription)) {\n return\n }\n\n let resolve: () => void\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n\n this.subscriptionLoadingPromises.set(subscription, {\n resolve: resolve!,\n })\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n promise,\n )\n }\n\n private getLoadRequestKey(options: {\n minValues: Array<unknown> | undefined\n offset: number\n limit: number\n }): string {\n return serializeValue({\n minValues: options.minValues ?? null,\n offset: options.offset,\n limit: options.limit,\n })\n }\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any,\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n"],"names":["subscription"],"mappings":"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"}
1
+ {"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import {\n normalizeExpressionPaths,\n normalizeOrderByPaths,\n} from '../compiler/expressions.js'\nimport {\n computeOrderedLoadCursor,\n computeSubscriptionOrderByHints,\n filterDuplicateInserts,\n sendChangesToInput,\n splitUpdates,\n trackBiggestSentValue,\n} from './utils.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n ChangeMessage,\n SubscriptionStatusChangeEvent,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { CollectionConfigBuilder } from './collection-config-builder.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`,\n)\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n // Track the most recent ordered load request key (cursor + window).\n // This avoids infinite loops from cached data re-writes while still allowing\n // window moves or new keys at the same cursor value to trigger new requests.\n private lastLoadRequestKey: string | undefined\n\n // Track deferred promises for subscription loading states\n private subscriptionLoadingPromises = new Map<\n CollectionSubscription,\n { resolve: () => void }\n >()\n\n // Track keys that have been sent to the D2 pipeline to prevent duplicate inserts\n // This is necessary because different code paths (initial load, change events)\n // can potentially send the same item to D2 multiple times.\n private sentToD2Keys = new Set<string | number>()\n\n // Direct load tracking callback for ordered path (set during subscribeToOrderedChanges,\n // used by loadNextItems for subsequent requestLimitedSnapshot calls)\n private orderedLoadSubsetResult?: (result: Promise<void> | true) => void\n private pendingOrderedLoadPromise: Promise<void> | undefined\n\n constructor(\n private alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = normalizeExpressionPaths(whereClause, this.alias)\n return this.subscribeToChanges(whereExpression)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n const orderByInfo = this.getOrderByInfo()\n\n // Direct load promise tracking: pipes loadSubset results straight to the\n // live query collection, avoiding the multi-hop deferred promise chain that\n // can break under microtask timing (e.g., queueMicrotask in TanStack Query).\n const trackLoadResult = (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n result,\n )\n }\n }\n\n // Status change handler - passed to subscribeChanges so it's registered\n // BEFORE any snapshot is requested, preventing race conditions.\n // Used as a fallback for status transitions not covered by direct tracking\n // (e.g., truncate-triggered reloads that call trackLoadSubsetPromise directly).\n const onStatusChange = (event: SubscriptionStatusChangeEvent) => {\n const subscription = event.subscription as CollectionSubscription\n if (event.status === `loadingSubset`) {\n this.ensureLoadingPromise(subscription)\n } else {\n // status is 'ready'\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n }\n }\n\n // Create subscription with onStatusChange - listener is registered before any async work\n let subscription: CollectionSubscription\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\n onStatusChange,\n trackLoadResult,\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias,\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState,\n onStatusChange,\n )\n }\n\n // Check current status after subscribing - if status is 'loadingSubset', track it.\n // The onStatusChange listener will catch the transition to 'ready'.\n if (subscription.status === `loadingSubset`) {\n this.ensureLoadingPromise(subscription)\n }\n\n const unsubscribe = () => {\n // If subscription has a pending promise, resolve it before unsubscribing\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe,\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean,\n ) {\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n const filteredChanges = filterDuplicateInserts(\n changesArray,\n this.sentToD2Keys,\n )\n\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\n const sentChanges = sendChangesToInput(\n input,\n filteredChanges,\n this.collection.config.getKey,\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to schedule a graph run even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\n })\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Get the query's orderBy and limit to pass to loadSubset.\n const hints = computeSubscriptionOrderByHints(\n this.collectionConfigBuilder.query,\n this.alias,\n )\n\n // Track loading via the loadSubset promise directly.\n // requestSnapshot uses trackLoadSubsetPromise: false (needed for truncate handling),\n // so we use onLoadSubsetResult to get the promise and track it ourselves.\n const onLoadSubsetResult = includeInitialState\n ? (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n result,\n )\n }\n }\n : undefined\n\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n onStatusChange,\n orderBy: hints.orderBy,\n limit: hints.limit,\n onLoadSubsetResult,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n onLoadSubsetResult: (result: Promise<void> | true) => void,\n ): CollectionSubscription {\n const { orderBy, offset, limit, index } = orderByInfo\n\n // Store the callback so loadNextItems can also use direct tracking.\n // Track in-flight ordered loads to avoid issuing redundant requests while\n // a previous snapshot is still pending.\n const handleLoadSubsetResult = (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.pendingOrderedLoadPromise = result\n result.finally(() => {\n if (this.pendingOrderedLoadPromise === result) {\n this.pendingOrderedLoadPromise = undefined\n }\n })\n }\n onLoadSubsetResult(result)\n }\n\n this.orderedLoadSubsetResult = handleLoadSubsetResult\n\n // Use a holder to forward-reference subscription in the callback\n const subscriptionHolder: { current?: CollectionSubscription } = {}\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>,\n ) => {\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n\n this.trackSentValues(changesArray, orderByInfo.comparator)\n\n // Split live updates into a delete of the old value and an insert of the new value\n const splittedChanges = splitUpdates(changesArray)\n this.sendChangesToPipelineWithTracking(\n splittedChanges,\n subscriptionHolder.current!,\n )\n }\n\n // Subscribe to changes with onStatusChange - listener is registered before any snapshot\n // values bigger than what we've sent don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n onStatusChange,\n })\n subscriptionHolder.current = subscription\n\n // Listen for truncate events to reset cursor tracking state and sentToD2Keys\n // This ensures that after a must-refetch/truncate, we don't use stale cursor data\n // and allow re-inserts of previously sent keys\n const truncateUnsubscribe = this.collection.on(`truncate`, () => {\n this.biggest = undefined\n this.lastLoadRequestKey = undefined\n this.pendingOrderedLoadPromise = undefined\n this.sentToD2Keys.clear()\n })\n\n // Clean up truncate listener when subscription is unsubscribed\n subscription.on(`unsubscribed`, () => {\n truncateUnsubscribe()\n })\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Trigger the snapshot request — use direct load tracking (trackLoadSubsetPromise: false)\n // to pipe the loadSubset result straight to the live query collection. This bypasses\n // the subscription status → onStatusChange → deferred promise chain which is fragile\n // under microtask timing (e.g., queueMicrotask delays in TanStack Query observers).\n if (index) {\n // We have an index on the first orderBy column - use lazy loading optimization\n subscription.setOrderByIndex(index)\n\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: handleLoadSubsetResult,\n })\n } else {\n // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset\n subscription.requestSnapshot({\n orderBy: normalizedOrderBy,\n limit: offset + limit,\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: handleLoadSubsetResult,\n })\n }\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // dataNeeded is not set when there's no index (e.g., non-ref expression).\n // In this case, we've already loaded all data via requestSnapshot\n // and don't need to lazily load more.\n return true\n }\n\n if (this.pendingOrderedLoadPromise) {\n // Wait for in-flight ordered loads to resolve before issuing another request.\n return true\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription,\n ) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n changes,\n subscriptionWithLoader[loadMoreCallbackSymbol],\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n\n const cursor = computeOrderedLoadCursor(\n orderByInfo,\n this.biggest,\n this.lastLoadRequestKey,\n this.alias,\n n,\n )\n if (!cursor) return // Duplicate request — skip\n\n this.lastLoadRequestKey = cursor.loadRequestKey\n\n // Take the `n` items after the biggest sent value\n // Omit offset so requestLimitedSnapshot can advance based on\n // the number of rows already loaded (supports offset-based backends).\n subscription.requestLimitedSnapshot({\n orderBy: cursor.normalizedOrderBy,\n limit: n,\n minValues: cursor.minValues,\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: this.orderedLoadSubsetResult,\n })\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\n }\n return undefined\n }\n\n private trackSentValues(\n changes: Array<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number,\n ): void {\n const result = trackBiggestSentValue(\n changes,\n this.biggest,\n this.sentToD2Keys,\n comparator,\n )\n this.biggest = result.biggest\n if (result.shouldResetLoadKey) {\n this.lastLoadRequestKey = undefined\n }\n }\n\n private ensureLoadingPromise(subscription: CollectionSubscription) {\n if (this.subscriptionLoadingPromises.has(subscription)) {\n return\n }\n\n let resolve: () => void\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n\n this.subscriptionLoadingPromises.set(subscription, {\n resolve: resolve!,\n })\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n promise,\n )\n }\n}\n"],"names":["subscription"],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ import { RootStreamBuilder } from '@tanstack/db-ivm';
2
+ import { Collection } from '../../collection/index.js';
3
+ import { ChangeMessage } from '../../types.js';
4
+ import { InitialQueryBuilder, QueryBuilder } from '../builder/index.js';
5
+ import { Context } from '../builder/types.js';
6
+ import { OrderBy, QueryIR } from '../ir.js';
7
+ import { OrderByOptimizationInfo } from '../compiler/order-by.js';
8
+ /**
9
+ * Helper function to extract collections from a compiled query.
10
+ * Traverses the query IR to find all collection references.
11
+ * Maps collections by their ID (not alias) as expected by the compiler.
12
+ */
13
+ export declare function extractCollectionsFromQuery(query: any): Record<string, Collection<any, any, any>>;
14
+ /**
15
+ * Helper function to extract the collection that is referenced in the query's FROM clause.
16
+ * The FROM clause may refer directly to a collection or indirectly to a subquery.
17
+ */
18
+ export declare function extractCollectionFromSource(query: any): Collection<any, any, any>;
19
+ /**
20
+ * Extracts all aliases used for each collection across the entire query tree.
21
+ *
22
+ * Traverses the QueryIR recursively to build a map from collection ID to all aliases
23
+ * that reference that collection. This is essential for self-join support, where the
24
+ * same collection may be referenced multiple times with different aliases.
25
+ *
26
+ * For example, given a query like:
27
+ * ```ts
28
+ * q.from({ employee: employeesCollection })
29
+ * .join({ manager: employeesCollection }, ({ employee, manager }) =>
30
+ * eq(employee.managerId, manager.id)
31
+ * )
32
+ * ```
33
+ *
34
+ * This function would return:
35
+ * ```
36
+ * Map { "employees" => Set { "employee", "manager" } }
37
+ * ```
38
+ *
39
+ * @param query - The query IR to extract aliases from
40
+ * @returns A map from collection ID to the set of all aliases referencing that collection
41
+ */
42
+ export declare function extractCollectionAliases(query: QueryIR): Map<string, Set<string>>;
43
+ /**
44
+ * Builds a query IR from a config object that contains either a query builder
45
+ * function or a QueryBuilder instance.
46
+ */
47
+ export declare function buildQueryFromConfig<TContext extends Context>(config: {
48
+ query: ((q: InitialQueryBuilder) => QueryBuilder<TContext>) | QueryBuilder<TContext>;
49
+ }): QueryIR;
50
+ /**
51
+ * Helper function to send changes to a D2 input stream.
52
+ * Converts ChangeMessages to D2 MultiSet data and sends to the input.
53
+ *
54
+ * @returns The number of multiset entries sent
55
+ */
56
+ export declare function sendChangesToInput(input: RootStreamBuilder<unknown>, changes: Iterable<ChangeMessage>, getKey: (item: ChangeMessage[`value`]) => any): number;
57
+ /** Splits updates into a delete of the old value and an insert of the new value */
58
+ export declare function splitUpdates<T extends object = Record<string, unknown>, TKey extends string | number = string | number>(changes: Iterable<ChangeMessage<T, TKey>>): Generator<ChangeMessage<T, TKey>>;
59
+ /**
60
+ * Filter changes to prevent duplicate inserts to a D2 pipeline.
61
+ * Maintains D2 multiplicity at 1 for visible items so that deletes
62
+ * properly reduce multiplicity to 0.
63
+ *
64
+ * Mutates `sentKeys` in place: adds keys on insert, removes on delete.
65
+ */
66
+ export declare function filterDuplicateInserts(changes: Array<ChangeMessage<any, string | number>>, sentKeys: Set<string | number>): Array<ChangeMessage<any, string | number>>;
67
+ /**
68
+ * Track the biggest value seen in a stream of changes, used for cursor-based
69
+ * pagination in ordered subscriptions. Returns whether the load request key
70
+ * should be reset (allowing another load).
71
+ *
72
+ * @param changes - changes to process (deletes are skipped)
73
+ * @param current - the current biggest value (or undefined if none)
74
+ * @param sentKeys - set of keys already sent to D2 (for new-key detection)
75
+ * @param comparator - orderBy comparator
76
+ * @returns `{ biggest, shouldResetLoadKey }` — the new biggest value and
77
+ * whether the caller should clear its last-load-request-key
78
+ */
79
+ export declare function trackBiggestSentValue(changes: Array<ChangeMessage<any, string | number>>, current: unknown | undefined, sentKeys: Set<string | number>, comparator: (a: any, b: any) => number): {
80
+ biggest: unknown;
81
+ shouldResetLoadKey: boolean;
82
+ };
83
+ /**
84
+ * Compute orderBy/limit subscription hints for an alias.
85
+ * Returns normalised orderBy and effective limit suitable for passing to
86
+ * `subscribeChanges`, or `undefined` values when the query's orderBy cannot
87
+ * be scoped to the given alias (e.g. cross-collection refs or aggregates).
88
+ */
89
+ export declare function computeSubscriptionOrderByHints(query: {
90
+ orderBy?: OrderBy;
91
+ limit?: number;
92
+ offset?: number;
93
+ }, alias: string): {
94
+ orderBy: OrderBy | undefined;
95
+ limit: number | undefined;
96
+ };
97
+ /**
98
+ * Compute the cursor for loading the next batch of ordered data.
99
+ * Extracts values from the biggest sent row and builds the `minValues`
100
+ * array and a deduplication key.
101
+ *
102
+ * @returns `undefined` if the load should be skipped (duplicate request),
103
+ * otherwise `{ minValues, normalizedOrderBy, loadRequestKey }`.
104
+ */
105
+ export declare function computeOrderedLoadCursor(orderByInfo: Pick<OrderByOptimizationInfo, 'orderBy' | 'valueExtractorForRawRow' | 'offset'>, biggestSentRow: unknown | undefined, lastLoadRequestKey: string | undefined, alias: string, limit: number): {
106
+ minValues: Array<unknown> | undefined;
107
+ normalizedOrderBy: OrderBy;
108
+ loadRequestKey: string;
109
+ } | undefined;
@@ -0,0 +1,179 @@
1
+ import { MultiSet, serializeValue } from "@tanstack/db-ivm";
2
+ import { normalizeOrderByPaths } from "../compiler/expressions.js";
3
+ import { buildQuery, getQueryIR } from "../builder/index.js";
4
+ function extractCollectionsFromQuery(query) {
5
+ const collections = {};
6
+ function extractFromSource(source) {
7
+ if (source.type === `collectionRef`) {
8
+ collections[source.collection.id] = source.collection;
9
+ } else if (source.type === `queryRef`) {
10
+ extractFromQuery(source.query);
11
+ }
12
+ }
13
+ function extractFromQuery(q) {
14
+ if (q.from) {
15
+ extractFromSource(q.from);
16
+ }
17
+ if (q.join && Array.isArray(q.join)) {
18
+ for (const joinClause of q.join) {
19
+ if (joinClause.from) {
20
+ extractFromSource(joinClause.from);
21
+ }
22
+ }
23
+ }
24
+ }
25
+ extractFromQuery(query);
26
+ return collections;
27
+ }
28
+ function extractCollectionFromSource(query) {
29
+ const from = query.from;
30
+ if (from.type === `collectionRef`) {
31
+ return from.collection;
32
+ } else if (from.type === `queryRef`) {
33
+ return extractCollectionFromSource(from.query);
34
+ }
35
+ throw new Error(
36
+ `Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`
37
+ );
38
+ }
39
+ function extractCollectionAliases(query) {
40
+ const aliasesById = /* @__PURE__ */ new Map();
41
+ function recordAlias(source) {
42
+ if (!source) return;
43
+ if (source.type === `collectionRef`) {
44
+ const { id } = source.collection;
45
+ const existing = aliasesById.get(id);
46
+ if (existing) {
47
+ existing.add(source.alias);
48
+ } else {
49
+ aliasesById.set(id, /* @__PURE__ */ new Set([source.alias]));
50
+ }
51
+ } else if (source.type === `queryRef`) {
52
+ traverse(source.query);
53
+ }
54
+ }
55
+ function traverse(q) {
56
+ if (!q) return;
57
+ recordAlias(q.from);
58
+ if (q.join) {
59
+ for (const joinClause of q.join) {
60
+ recordAlias(joinClause.from);
61
+ }
62
+ }
63
+ }
64
+ traverse(query);
65
+ return aliasesById;
66
+ }
67
+ function buildQueryFromConfig(config) {
68
+ if (typeof config.query === `function`) {
69
+ return buildQuery(config.query);
70
+ }
71
+ return getQueryIR(config.query);
72
+ }
73
+ function sendChangesToInput(input, changes, getKey) {
74
+ const multiSetArray = [];
75
+ for (const change of changes) {
76
+ const key = getKey(change.value);
77
+ if (change.type === `insert`) {
78
+ multiSetArray.push([[key, change.value], 1]);
79
+ } else if (change.type === `update`) {
80
+ multiSetArray.push([[key, change.previousValue], -1]);
81
+ multiSetArray.push([[key, change.value], 1]);
82
+ } else {
83
+ multiSetArray.push([[key, change.value], -1]);
84
+ }
85
+ }
86
+ if (multiSetArray.length !== 0) {
87
+ input.sendData(new MultiSet(multiSetArray));
88
+ }
89
+ return multiSetArray.length;
90
+ }
91
+ function* splitUpdates(changes) {
92
+ for (const change of changes) {
93
+ if (change.type === `update`) {
94
+ yield { type: `delete`, key: change.key, value: change.previousValue };
95
+ yield { type: `insert`, key: change.key, value: change.value };
96
+ } else {
97
+ yield change;
98
+ }
99
+ }
100
+ }
101
+ function filterDuplicateInserts(changes, sentKeys) {
102
+ const filtered = [];
103
+ for (const change of changes) {
104
+ if (change.type === `insert`) {
105
+ if (sentKeys.has(change.key)) {
106
+ continue;
107
+ }
108
+ sentKeys.add(change.key);
109
+ } else if (change.type === `delete`) {
110
+ sentKeys.delete(change.key);
111
+ }
112
+ filtered.push(change);
113
+ }
114
+ return filtered;
115
+ }
116
+ function trackBiggestSentValue(changes, current, sentKeys, comparator) {
117
+ let biggest = current;
118
+ let shouldResetLoadKey = false;
119
+ for (const change of changes) {
120
+ if (change.type === `delete`) continue;
121
+ const isNewKey = !sentKeys.has(change.key);
122
+ if (biggest === void 0) {
123
+ biggest = change.value;
124
+ shouldResetLoadKey = true;
125
+ } else if (comparator(biggest, change.value) < 0) {
126
+ biggest = change.value;
127
+ shouldResetLoadKey = true;
128
+ } else if (isNewKey) {
129
+ shouldResetLoadKey = true;
130
+ }
131
+ }
132
+ return { biggest, shouldResetLoadKey };
133
+ }
134
+ function computeSubscriptionOrderByHints(query, alias) {
135
+ const { orderBy, limit, offset } = query;
136
+ const effectiveLimit = limit !== void 0 && offset !== void 0 ? limit + offset : limit;
137
+ const normalizedOrderBy = orderBy ? normalizeOrderByPaths(orderBy, alias) : void 0;
138
+ const canPassOrderBy = normalizedOrderBy?.every((clause) => {
139
+ const exp = clause.expression;
140
+ if (exp.type !== `ref`) return false;
141
+ const path = exp.path;
142
+ return Array.isArray(path) && path.length === 1;
143
+ }) ?? false;
144
+ return {
145
+ orderBy: canPassOrderBy ? normalizedOrderBy : void 0,
146
+ limit: canPassOrderBy ? effectiveLimit : void 0
147
+ };
148
+ }
149
+ function computeOrderedLoadCursor(orderByInfo, biggestSentRow, lastLoadRequestKey, alias, limit) {
150
+ const { orderBy, valueExtractorForRawRow, offset } = orderByInfo;
151
+ const extractedValues = biggestSentRow ? valueExtractorForRawRow(biggestSentRow) : void 0;
152
+ let minValues;
153
+ if (extractedValues !== void 0) {
154
+ minValues = Array.isArray(extractedValues) ? extractedValues : [extractedValues];
155
+ }
156
+ const loadRequestKey = serializeValue({
157
+ minValues: minValues ?? null,
158
+ offset,
159
+ limit
160
+ });
161
+ if (lastLoadRequestKey === loadRequestKey) {
162
+ return void 0;
163
+ }
164
+ const normalizedOrderBy = normalizeOrderByPaths(orderBy, alias);
165
+ return { minValues, normalizedOrderBy, loadRequestKey };
166
+ }
167
+ export {
168
+ buildQueryFromConfig,
169
+ computeOrderedLoadCursor,
170
+ computeSubscriptionOrderByHints,
171
+ extractCollectionAliases,
172
+ extractCollectionFromSource,
173
+ extractCollectionsFromQuery,
174
+ filterDuplicateInserts,
175
+ sendChangesToInput,
176
+ splitUpdates,
177
+ trackBiggestSentValue
178
+ };
179
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sources":["../../../../src/query/live/utils.ts"],"sourcesContent":["import { MultiSet, serializeValue } from '@tanstack/db-ivm'\nimport { normalizeOrderByPaths } from '../compiler/expressions.js'\nimport { buildQuery, getQueryIR } from '../builder/index.js'\nimport type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type { ChangeMessage } from '../../types.js'\nimport type { InitialQueryBuilder, QueryBuilder } from '../builder/index.js'\nimport type { Context } from '../builder/types.js'\nimport type { OrderBy, QueryIR } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\n\n/**\n * Helper function to extract collections from a compiled query.\n * Traverses the query IR to find all collection references.\n * Maps collections by their ID (not alias) as expected by the compiler.\n */\nexport function extractCollectionsFromQuery(\n query: any,\n): Record<string, Collection<any, any, any>> {\n const collections: Record<string, any> = {}\n\n // Helper function to recursively extract collections from a query or source\n function extractFromSource(source: any) {\n if (source.type === `collectionRef`) {\n collections[source.collection.id] = source.collection\n } else if (source.type === `queryRef`) {\n // Recursively extract from subquery\n extractFromQuery(source.query)\n }\n }\n\n // Helper function to recursively extract collections from a query\n function extractFromQuery(q: any) {\n // Extract from FROM clause\n if (q.from) {\n extractFromSource(q.from)\n }\n\n // Extract from JOIN clauses\n if (q.join && Array.isArray(q.join)) {\n for (const joinClause of q.join) {\n if (joinClause.from) {\n extractFromSource(joinClause.from)\n }\n }\n }\n }\n\n // Start extraction from the root query\n extractFromQuery(query)\n\n return collections\n}\n\n/**\n * Helper function to extract the collection that is referenced in the query's FROM clause.\n * The FROM clause may refer directly to a collection or indirectly to a subquery.\n */\nexport function extractCollectionFromSource(\n query: any,\n): Collection<any, any, any> {\n const from = query.from\n\n if (from.type === `collectionRef`) {\n return from.collection\n } else if (from.type === `queryRef`) {\n // Recursively extract from subquery\n return extractCollectionFromSource(from.query)\n }\n\n throw new Error(\n `Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`,\n )\n}\n\n/**\n * Extracts all aliases used for each collection across the entire query tree.\n *\n * Traverses the QueryIR recursively to build a map from collection ID to all aliases\n * that reference that collection. This is essential for self-join support, where the\n * same collection may be referenced multiple times with different aliases.\n *\n * For example, given a query like:\n * ```ts\n * q.from({ employee: employeesCollection })\n * .join({ manager: employeesCollection }, ({ employee, manager }) =>\n * eq(employee.managerId, manager.id)\n * )\n * ```\n *\n * This function would return:\n * ```\n * Map { \"employees\" => Set { \"employee\", \"manager\" } }\n * ```\n *\n * @param query - The query IR to extract aliases from\n * @returns A map from collection ID to the set of all aliases referencing that collection\n */\nexport function extractCollectionAliases(\n query: QueryIR,\n): Map<string, Set<string>> {\n const aliasesById = new Map<string, Set<string>>()\n\n function recordAlias(source: any) {\n if (!source) return\n\n if (source.type === `collectionRef`) {\n const { id } = source.collection\n const existing = aliasesById.get(id)\n if (existing) {\n existing.add(source.alias)\n } else {\n aliasesById.set(id, new Set([source.alias]))\n }\n } else if (source.type === `queryRef`) {\n traverse(source.query)\n }\n }\n\n function traverse(q?: QueryIR) {\n if (!q) return\n\n recordAlias(q.from)\n\n if (q.join) {\n for (const joinClause of q.join) {\n recordAlias(joinClause.from)\n }\n }\n }\n\n traverse(query)\n\n return aliasesById\n}\n\n/**\n * Builds a query IR from a config object that contains either a query builder\n * function or a QueryBuilder instance.\n */\nexport function buildQueryFromConfig<TContext extends Context>(config: {\n query:\n | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)\n | QueryBuilder<TContext>\n}): QueryIR {\n // Build the query using the provided query builder function or instance\n if (typeof config.query === `function`) {\n return buildQuery<TContext>(config.query)\n }\n return getQueryIR(config.query)\n}\n\n/**\n * Helper function to send changes to a D2 input stream.\n * Converts ChangeMessages to D2 MultiSet data and sends to the input.\n *\n * @returns The number of multiset entries sent\n */\nexport function sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any,\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nexport function* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n\n/**\n * Filter changes to prevent duplicate inserts to a D2 pipeline.\n * Maintains D2 multiplicity at 1 for visible items so that deletes\n * properly reduce multiplicity to 0.\n *\n * Mutates `sentKeys` in place: adds keys on insert, removes on delete.\n */\nexport function filterDuplicateInserts(\n changes: Array<ChangeMessage<any, string | number>>,\n sentKeys: Set<string | number>,\n): Array<ChangeMessage<any, string | number>> {\n const filtered: Array<ChangeMessage<any, string | number>> = []\n for (const change of changes) {\n if (change.type === `insert`) {\n if (sentKeys.has(change.key)) {\n continue // Skip duplicate\n }\n sentKeys.add(change.key)\n } else if (change.type === `delete`) {\n sentKeys.delete(change.key)\n }\n filtered.push(change)\n }\n return filtered\n}\n\n/**\n * Track the biggest value seen in a stream of changes, used for cursor-based\n * pagination in ordered subscriptions. Returns whether the load request key\n * should be reset (allowing another load).\n *\n * @param changes - changes to process (deletes are skipped)\n * @param current - the current biggest value (or undefined if none)\n * @param sentKeys - set of keys already sent to D2 (for new-key detection)\n * @param comparator - orderBy comparator\n * @returns `{ biggest, shouldResetLoadKey }` — the new biggest value and\n * whether the caller should clear its last-load-request-key\n */\nexport function trackBiggestSentValue(\n changes: Array<ChangeMessage<any, string | number>>,\n current: unknown | undefined,\n sentKeys: Set<string | number>,\n comparator: (a: any, b: any) => number,\n): { biggest: unknown; shouldResetLoadKey: boolean } {\n let biggest = current\n let shouldResetLoadKey = false\n\n for (const change of changes) {\n if (change.type === `delete`) continue\n\n const isNewKey = !sentKeys.has(change.key)\n\n if (biggest === undefined) {\n biggest = change.value\n shouldResetLoadKey = true\n } else if (comparator(biggest, change.value) < 0) {\n biggest = change.value\n shouldResetLoadKey = true\n } else if (isNewKey) {\n // New key at same sort position — allow another load if needed\n shouldResetLoadKey = true\n }\n }\n\n return { biggest, shouldResetLoadKey }\n}\n\n/**\n * Compute orderBy/limit subscription hints for an alias.\n * Returns normalised orderBy and effective limit suitable for passing to\n * `subscribeChanges`, or `undefined` values when the query's orderBy cannot\n * be scoped to the given alias (e.g. cross-collection refs or aggregates).\n */\nexport function computeSubscriptionOrderByHints(\n query: { orderBy?: OrderBy; limit?: number; offset?: number },\n alias: string,\n): { orderBy: OrderBy | undefined; limit: number | undefined } {\n const { orderBy, limit, offset } = query\n const effectiveLimit =\n limit !== undefined && offset !== undefined ? limit + offset : limit\n\n const normalizedOrderBy = orderBy\n ? normalizeOrderByPaths(orderBy, alias)\n : undefined\n\n // Only pass orderBy when it is scoped to this alias and uses simple refs,\n // to avoid leaking cross-collection paths into backend-specific compilers.\n const canPassOrderBy =\n normalizedOrderBy?.every((clause) => {\n const exp = clause.expression\n if (exp.type !== `ref`) return false\n const path = exp.path\n return Array.isArray(path) && path.length === 1\n }) ?? false\n\n return {\n orderBy: canPassOrderBy ? normalizedOrderBy : undefined,\n limit: canPassOrderBy ? effectiveLimit : undefined,\n }\n}\n\n/**\n * Compute the cursor for loading the next batch of ordered data.\n * Extracts values from the biggest sent row and builds the `minValues`\n * array and a deduplication key.\n *\n * @returns `undefined` if the load should be skipped (duplicate request),\n * otherwise `{ minValues, normalizedOrderBy, loadRequestKey }`.\n */\nexport function computeOrderedLoadCursor(\n orderByInfo: Pick<\n OrderByOptimizationInfo,\n 'orderBy' | 'valueExtractorForRawRow' | 'offset'\n >,\n biggestSentRow: unknown | undefined,\n lastLoadRequestKey: string | undefined,\n alias: string,\n limit: number,\n):\n | {\n minValues: Array<unknown> | undefined\n normalizedOrderBy: OrderBy\n loadRequestKey: string\n }\n | undefined {\n const { orderBy, valueExtractorForRawRow, offset } = orderByInfo\n\n // Extract all orderBy column values from the biggest sent row\n // For single-column: returns single value, for multi-column: returns array\n const extractedValues = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow as Record<string, unknown>)\n : undefined\n\n // Normalize to array format for minValues\n let minValues: Array<unknown> | undefined\n if (extractedValues !== undefined) {\n minValues = Array.isArray(extractedValues)\n ? extractedValues\n : [extractedValues]\n }\n\n // Deduplicate: skip if we already issued an identical load request\n const loadRequestKey = serializeValue({\n minValues: minValues ?? null,\n offset,\n limit,\n })\n if (lastLoadRequestKey === loadRequestKey) {\n return undefined\n }\n\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, alias)\n\n return { minValues, normalizedOrderBy, loadRequestKey }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
- "version": "0.5.32",
3
+ "version": "0.5.33",
4
4
  "description": "A reactive client store for building super fast apps on sync",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -26,6 +26,17 @@ export { type IndexOptions } from './indexes/index-options.js'
26
26
  // Expression helpers
27
27
  export * from './query/expression-helpers.js'
28
28
 
29
+ // Reactive effects
30
+ export {
31
+ createEffect,
32
+ type DeltaEvent,
33
+ type DeltaType,
34
+ type EffectConfig,
35
+ type EffectContext,
36
+ type Effect,
37
+ type EffectQueryInput,
38
+ } from './query/effect.js'
39
+
29
40
  // Re-export some stuff explicitly to ensure the type & value is exported
30
41
  export type { Collection } from './collection/index.js'
31
42
  export { IR }