UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

300 lines (299 loc) 10.1 kB
import { normalizeExpressionPaths, normalizeOrderByPaths } from "../compiler/expressions.js"; import { filterDuplicateInserts, sendChangesToInput, computeSubscriptionOrderByHints, computeOrderedLoadCursor, trackBiggestSentValue, splitUpdates } from "./utils.js"; import { SubsetDemandController } from "./subset-demand-controller.js"; const loadMoreCallbackSymbol = /* @__PURE__ */ Symbol.for( `@tanstack/db.collection-config-builder` ); class CollectionSubscriber { constructor(sourceId, alias, collection, collectionConfigBuilder) { this.sourceId = sourceId; this.alias = alias; this.collection = collection; this.collectionConfigBuilder = collectionConfigBuilder; this.biggest = void 0; this.subscriptionLoadingPromises = /* @__PURE__ */ new Map(); this.sentToD2Keys = /* @__PURE__ */ new Set(); this.demand = new SubsetDemandController(); } subscribe() { const whereClause = this.getWhereClause(); 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) => { if (this.collectionConfigBuilder.isLazySource(this.sourceId)) return; 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.isLazySource( this.sourceId ); subscription = this.subscribeToMatchingChanges( whereExpression, includeInitialState, onStatusChange ); } if (!this.collectionConfigBuilder.isLazySource(this.sourceId) && subscription.status === `loadingSubset`) { this.ensureLoadingPromise(subscription); } const unsubscribe = () => { const deferred = this.subscriptionLoadingPromises.get(subscription); if (deferred) { this.subscriptionLoadingPromises.delete(subscription); deferred.resolve(); } this.demand.clear(); subscription.unsubscribe(); }; this.collectionConfigBuilder.currentSyncState.unsubscribeCallbacks.add( unsubscribe ); return subscription; } setDemand(subscription, plan, keys) { const update = this.demand.setDemand(subscription, plan, keys); if (!update.changed) return; if (update.empty) { this.collectionConfigBuilder.retireDemand(plan.id); return; } const generation = this.collectionConfigBuilder.beginDemand(plan.id); if (update.ready instanceof Promise) { void update.ready.then( () => this.collectionConfigBuilder.settleDemand(plan.id, generation), (error) => this.collectionConfigBuilder.failDemand(plan.id, generation, error) ); } else { this.collectionConfigBuilder.settleDemand(plan.id, generation); } } sendChangesToPipeline(changes, callback) { const changesArray = Array.isArray(changes) ? changes : [...changes]; const filteredChanges = filterDuplicateInserts( changesArray, this.sentToD2Keys ); const input = this.collectionConfigBuilder.currentSyncState.inputs[this.sourceId]; const sentChanges = sendChangesToInput(input, filteredChanges); const dataLoader = sentChanges > 0 ? callback : void 0; this.collectionConfigBuilder.scheduleGraphRun(dataLoader, { sourceId: this.sourceId }); } subscribeToMatchingChanges(whereExpression, includeInitialState, onStatusChange) { const sendChanges = (changes) => { this.sendChangesToPipeline(changes); }; const hints = computeSubscriptionOrderByHints( this.collectionConfigBuilder.query, this.alias ); 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: hints.orderBy, limit: hints.limit, onLoadSubsetResult }); return subscription; } subscribeToOrderedChanges(whereExpression, orderByInfo, onStatusChange, onLoadSubsetResult) { const { orderBy, offset, limit, index } = orderByInfo; const handleLoadSubsetResult = (result) => { if (result instanceof Promise) { this.pendingOrderedLoadPromise = result; const finish = () => { if (this.pendingOrderedLoadPromise === result) { this.pendingOrderedLoadPromise = void 0; } }; void result.then(finish, finish); } 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, index } = orderByInfo; if (!dataNeeded || !index) { 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 cursor = computeOrderedLoadCursor( orderByInfo, this.biggest, this.lastLoadRequestKey, this.alias, n ); if (!cursor) return; this.lastLoadRequestKey = cursor.loadRequestKey; subscription.requestLimitedSnapshot({ orderBy: cursor.normalizedOrderBy, limit: n, minValues: cursor.minValues, trackLoadSubsetPromise: false, onLoadSubsetResult: this.orderedLoadSubsetResult }); } getWhereClause() { const sourceWhereClausesCache = this.collectionConfigBuilder.sourceWhereClausesCache; if (!sourceWhereClausesCache) { return void 0; } return sourceWhereClausesCache.get(this.sourceId); } getOrderByInfo() { const info = this.collectionConfigBuilder.optimizableOrderByCollections[this.sourceId]; if (info?.sourceId === this.sourceId) { return info; } return void 0; } trackSentValues(changes, comparator) { const result = trackBiggestSentValue( changes, this.biggest, this.sentToD2Keys, comparator ); this.biggest = result.biggest; if (result.shouldResetLoadKey) { 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 ); } } export { CollectionSubscriber }; //# sourceMappingURL=collection-subscriber.js.map