UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

349 lines (348 loc) 11.7 kB
import { serializeValue, MultiSet } from "@tanstack/db-ivm"; import { normalizeExpressionPaths, normalizeOrderByPaths } from "../compiler/expressions.js"; const loadMoreCallbackSymbol = /* @__PURE__ */ Symbol.for( `@tanstack/db.collection-config-builder` ); class CollectionSubscriber { constructor(alias, collectionId, collection, collectionConfigBuilder) { this.alias = alias; this.collectionId = collectionId; this.collection = collection; this.collectionConfigBuilder = collectionConfigBuilder; this.biggest = void 0; this.subscriptionLoadingPromises = /* @__PURE__ */ new Map(); this.sentToD2Keys = /* @__PURE__ */ new Set(); } subscribe() { const whereClause = this.getWhereClauseForAlias(); if (whereClause) { const whereExpression = normalizeExpressionPaths(whereClause, this.alias); return this.subscribeToChanges(whereExpression); } return this.subscribeToChanges(); } subscribeToChanges(whereExpression) { const orderByInfo = this.getOrderByInfo(); const trackLoadResult = (result) => { if (result instanceof Promise) { this.collectionConfigBuilder.liveQueryCollection._sync.trackLoadPromise( result ); } }; const onStatusChange = (event) => { const subscription2 = event.subscription; if (event.status === `loadingSubset`) { this.ensureLoadingPromise(subscription2); } else { const deferred = this.subscriptionLoadingPromises.get(subscription2); if (deferred) { this.subscriptionLoadingPromises.delete(subscription2); deferred.resolve(); } } }; let subscription; if (orderByInfo) { subscription = this.subscribeToOrderedChanges( whereExpression, orderByInfo, onStatusChange, trackLoadResult ); } else { const includeInitialState = !this.collectionConfigBuilder.isLazyAlias( this.alias ); subscription = this.subscribeToMatchingChanges( whereExpression, includeInitialState, onStatusChange ); } if (subscription.status === `loadingSubset`) { this.ensureLoadingPromise(subscription); } const unsubscribe = () => { const deferred = this.subscriptionLoadingPromises.get(subscription); if (deferred) { this.subscriptionLoadingPromises.delete(subscription); deferred.resolve(); } subscription.unsubscribe(); }; this.collectionConfigBuilder.currentSyncState.unsubscribeCallbacks.add( unsubscribe ); return subscription; } sendChangesToPipeline(changes, callback) { const changesArray = Array.isArray(changes) ? changes : [...changes]; const filteredChanges = []; for (const change of changesArray) { if (change.type === `insert`) { if (this.sentToD2Keys.has(change.key)) { continue; } this.sentToD2Keys.add(change.key); } else if (change.type === `delete`) { this.sentToD2Keys.delete(change.key); } filteredChanges.push(change); } const input = this.collectionConfigBuilder.currentSyncState.inputs[this.alias]; const sentChanges = sendChangesToInput( input, filteredChanges, this.collection.config.getKey ); const dataLoader = sentChanges > 0 ? callback : void 0; this.collectionConfigBuilder.scheduleGraphRun(dataLoader, { alias: this.alias }); } subscribeToMatchingChanges(whereExpression, includeInitialState, onStatusChange) { const sendChanges = (changes) => { this.sendChangesToPipeline(changes); }; const { orderBy, limit, offset } = this.collectionConfigBuilder.query; const effectiveLimit = limit !== void 0 && offset !== void 0 ? limit + offset : limit; const normalizedOrderBy = orderBy ? normalizeOrderByPaths(orderBy, this.alias) : void 0; const canPassOrderBy = normalizedOrderBy?.every((clause) => { const exp = clause.expression; if (exp.type !== `ref`) { return false; } const path = exp.path; return Array.isArray(path) && path.length === 1; }) ?? false; const orderByForSubscription = canPassOrderBy ? normalizedOrderBy : void 0; const limitForSubscription = canPassOrderBy ? effectiveLimit : void 0; const onLoadSubsetResult = includeInitialState ? (result) => { if (result instanceof Promise) { this.collectionConfigBuilder.liveQueryCollection._sync.trackLoadPromise( result ); } } : void 0; const subscription = this.collection.subscribeChanges(sendChanges, { ...includeInitialState && { includeInitialState }, whereExpression, onStatusChange, orderBy: orderByForSubscription, limit: limitForSubscription, onLoadSubsetResult }); return subscription; } subscribeToOrderedChanges(whereExpression, orderByInfo, onStatusChange, onLoadSubsetResult) { const { orderBy, offset, limit, index } = orderByInfo; const handleLoadSubsetResult = (result) => { if (result instanceof Promise) { this.pendingOrderedLoadPromise = result; result.finally(() => { if (this.pendingOrderedLoadPromise === result) { this.pendingOrderedLoadPromise = void 0; } }); } onLoadSubsetResult(result); }; this.orderedLoadSubsetResult = handleLoadSubsetResult; const subscriptionHolder = {}; const sendChangesInRange = (changes) => { const changesArray = Array.isArray(changes) ? changes : [...changes]; this.trackSentValues(changesArray, orderByInfo.comparator); const splittedChanges = splitUpdates(changesArray); this.sendChangesToPipelineWithTracking( splittedChanges, subscriptionHolder.current ); }; const subscription = this.collection.subscribeChanges(sendChangesInRange, { whereExpression, onStatusChange }); subscriptionHolder.current = subscription; const truncateUnsubscribe = this.collection.on(`truncate`, () => { this.biggest = void 0; this.lastLoadRequestKey = void 0; this.pendingOrderedLoadPromise = void 0; this.sentToD2Keys.clear(); }); subscription.on(`unsubscribed`, () => { truncateUnsubscribe(); }); const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias); if (index) { subscription.setOrderByIndex(index); subscription.requestLimitedSnapshot({ limit: offset + limit, orderBy: normalizedOrderBy, trackLoadSubsetPromise: false, onLoadSubsetResult: handleLoadSubsetResult }); } else { subscription.requestSnapshot({ orderBy: normalizedOrderBy, limit: offset + limit, trackLoadSubsetPromise: false, onLoadSubsetResult: handleLoadSubsetResult }); } return subscription; } // This function is called by maybeRunGraph // after each iteration of the query pipeline // to ensure that the orderBy operator has enough data to work with loadMoreIfNeeded(subscription) { const orderByInfo = this.getOrderByInfo(); if (!orderByInfo) { return true; } const { dataNeeded } = orderByInfo; if (!dataNeeded) { return true; } if (this.pendingOrderedLoadPromise) { return true; } const n = dataNeeded(); if (n > 0) { this.loadNextItems(n, subscription); } return true; } sendChangesToPipelineWithTracking(changes, subscription) { const orderByInfo = this.getOrderByInfo(); if (!orderByInfo) { this.sendChangesToPipeline(changes); return; } const subscriptionWithLoader = subscription; subscriptionWithLoader[loadMoreCallbackSymbol] ??= this.loadMoreIfNeeded.bind(this, subscription); this.sendChangesToPipeline( changes, subscriptionWithLoader[loadMoreCallbackSymbol] ); } // Loads the next `n` items from the collection // starting from the biggest item it has sent loadNextItems(n, subscription) { const orderByInfo = this.getOrderByInfo(); if (!orderByInfo) { return; } const { orderBy, valueExtractorForRawRow, offset } = orderByInfo; const biggestSentRow = this.biggest; const extractedValues = biggestSentRow ? valueExtractorForRawRow(biggestSentRow) : void 0; let minValues; if (extractedValues !== void 0) { minValues = Array.isArray(extractedValues) ? extractedValues : [extractedValues]; } const loadRequestKey = this.getLoadRequestKey({ minValues, offset, limit: n }); if (this.lastLoadRequestKey === loadRequestKey) { return; } const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias); subscription.requestLimitedSnapshot({ orderBy: normalizedOrderBy, limit: n, minValues, // Omit offset so requestLimitedSnapshot can advance the offset based on // the number of rows already loaded (supports offset-based backends). trackLoadSubsetPromise: false, onLoadSubsetResult: this.orderedLoadSubsetResult }); this.lastLoadRequestKey = loadRequestKey; } getWhereClauseForAlias() { const sourceWhereClausesCache = this.collectionConfigBuilder.sourceWhereClausesCache; if (!sourceWhereClausesCache) { return void 0; } return sourceWhereClausesCache.get(this.alias); } getOrderByInfo() { const info = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId]; if (info && info.alias === this.alias) { return info; } return void 0; } trackSentValues(changes, comparator) { for (const change of changes) { if (change.type === `delete`) { continue; } const isNewKey = !this.sentToD2Keys.has(change.key); if (!this.biggest) { this.biggest = change.value; this.lastLoadRequestKey = void 0; } else if (comparator(this.biggest, change.value) < 0) { this.biggest = change.value; this.lastLoadRequestKey = void 0; } else if (isNewKey) { this.lastLoadRequestKey = void 0; } } } ensureLoadingPromise(subscription) { if (this.subscriptionLoadingPromises.has(subscription)) { return; } let resolve; const promise = new Promise((res) => { resolve = res; }); this.subscriptionLoadingPromises.set(subscription, { resolve }); this.collectionConfigBuilder.liveQueryCollection._sync.trackLoadPromise( promise ); } getLoadRequestKey(options) { return serializeValue({ minValues: options.minValues ?? null, offset: options.offset, limit: options.limit }); } } function sendChangesToInput(input, changes, getKey) { const multiSetArray = []; for (const change of changes) { const key = getKey(change.value); if (change.type === `insert`) { multiSetArray.push([[key, change.value], 1]); } else if (change.type === `update`) { multiSetArray.push([[key, change.previousValue], -1]); multiSetArray.push([[key, change.value], 1]); } else { multiSetArray.push([[key, change.value], -1]); } } if (multiSetArray.length !== 0) { input.sendData(new MultiSet(multiSetArray)); } return multiSetArray.length; } function* splitUpdates(changes) { for (const change of changes) { if (change.type === `update`) { yield { type: `delete`, key: change.key, value: change.previousValue }; yield { type: `insert`, key: change.key, value: change.value }; } else { yield change; } } } export { CollectionSubscriber }; //# sourceMappingURL=collection-subscriber.js.map