UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

291 lines (290 loc) 10.7 kB
import { normalizeExpressionPaths } from "../compiler/expressions.js"; import { OrderedSourceLoader } from "./ordered-source-loader.js"; import { reconcileChangesForD2, sendChangesToInput, computeSubscriptionOrderByHints, 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.subscriptionLoadingPromises = /* @__PURE__ */ new Map(); this.sentToD2Rows = /* @__PURE__ */ new Map(); 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) { const trackedResult = result.catch(async (error) => { await Promise.resolve(); throw error; }); this.collectionConfigBuilder.trackSubsetLoadPromise(trackedResult); } }; 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(); } } }; const onLoadSubsetError = (event) => { this.collectionConfigBuilder.recordSubsetError( event.error, // Lazy demand owns its fatal-error path. For eager sources, one // successful page does not finish initial ordered refinement. !this.collectionConfigBuilder.isLazySource(this.sourceId) && this.collectionConfigBuilder.liveQueryCollection?.status === `loading` ); }; let subscription; if (orderByInfo) { subscription = this.subscribeToOrderedChanges( whereExpression, orderByInfo, onStatusChange, trackLoadResult, onLoadSubsetError ); } else { const includeInitialState = (this.collection.config.syncMode !== `on-demand` || this.collectionConfigBuilder.query.limit !== 0) && !this.collectionConfigBuilder.isLazySource(this.sourceId); subscription = this.subscribeToMatchingChanges( whereExpression, includeInitialState, onStatusChange, trackLoadResult, onLoadSubsetError ); this.registerSubscriptionCleanup(subscription); } if (!this.collectionConfigBuilder.isLazySource(this.sourceId) && subscription.status === `loadingSubset`) { this.ensureLoadingPromise(subscription); } return subscription; } registerSubscriptionCleanup(subscription) { const unsubscribe = () => { const deferred = this.subscriptionLoadingPromises.get(subscription); if (deferred) { this.subscriptionLoadingPromises.delete(subscription); deferred.resolve(); } try { this.demand.clear(); } finally { subscription.unsubscribe(); } }; this.collectionConfigBuilder.currentSyncState.unsubscribeCallbacks.add( unsubscribe ); } setDemand(subscription, plan, keys) { let update; try { update = this.demand.setDemand(subscription, plan, keys); } catch (error) { if (!Object.is(subscription.lastError, error)) throw error; const isInitialSync = this.collectionConfigBuilder.liveQueryCollection?.status === `loading`; const generation2 = this.collectionConfigBuilder.beginDemand(plan.id); this.collectionConfigBuilder.failDemand(plan.id, generation2, error); if (isInitialSync) throw error; return; } 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) { this.collectionConfigBuilder.trackSubsetLoadOperationPromise(update.ready); 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 reconciledChanges = reconcileChangesForD2( changesArray, this.sentToD2Rows ); const input = this.collectionConfigBuilder.currentSyncState.inputs[this.sourceId]; const sentChanges = sendChangesToInput(input, reconciledChanges); const dataLoader = sentChanges > 0 ? callback : void 0; this.collectionConfigBuilder.scheduleGraphRun(dataLoader); } subscribeToMatchingChanges(whereExpression, includeInitialState, onStatusChange, onLoadSubsetResult, onLoadSubsetError) { const sendChanges = (changes) => { this.sendChangesToPipeline(changes); }; const hints = computeSubscriptionOrderByHints( this.collectionConfigBuilder.query, this.alias ); const subscription = this.collection.subscribeChanges(sendChanges, { ...includeInitialState && { includeInitialState }, whereExpression, onStatusChange, onLoadSubsetError, truncateReplayPublication: this.truncateReplayPublicationControl(), orderBy: hints.orderBy, limit: hints.limit, onLoadSubsetResult: includeInitialState ? onLoadSubsetResult : void 0 }); return subscription; } subscribeToOrderedChanges(whereExpression, orderByInfo, onStatusChange, onLoadSubsetResult, onLoadSubsetError) { const subscriptionHolder = {}; const sendChangesInRange = (changes) => { const subscription2 = subscriptionHolder.current; if (!subscription2) return; const changesArray = Array.isArray(changes) ? changes : [...changes]; this.orderedLoader?.onSourceChanges(changesArray, this.sentToD2Rows); const splittedChanges = splitUpdates(changesArray); this.sendChangesToPipelineWithTracking(splittedChanges, subscription2); }; const subscription = this.collection.subscribeChanges(sendChangesInRange, { whereExpression, onStatusChange, onLoadSubsetError, truncateReplayPublication: this.truncateReplayPublicationControl(() => { const loader = this.orderedLoader; this.collectionConfigBuilder.trackOrderedLoadPromise( Promise.resolve().then(() => loader?.loadFullSource()), true ); }) }); subscriptionHolder.current = subscription; this.registerSubscriptionCleanup(subscription); const truncateUnsubscribe = this.collection.on(`truncate`, () => { this.orderedLoader?.resetCursor(); }); subscription.on(`unsubscribed`, () => { truncateUnsubscribe(); subscriptionHolder.current = void 0; this.orderedLoader?.dispose(); this.orderedLoader = void 0; }); this.orderedLoader = new OrderedSourceLoader( orderByInfo, subscription, this.alias, (result, holdPublication) => { if (result instanceof Promise) { this.collectionConfigBuilder.trackOrderedLoadPromise( result, holdPublication && !subscription.hasPendingTruncateReplacement ); } onLoadSubsetResult(result); } ); this.orderedLoader.start(); return subscription; } truncateReplayPublicationControl(onStart) { const syncSession = this.collectionConfigBuilder.getSyncSession(); return { start: () => { onStart?.(); }, succeed: () => { if (syncSession !== this.collectionConfigBuilder.getSyncSession()) { return; } this.orderedLoader?.settleFullSourceReplay(); this.collectionConfigBuilder.scheduleGraphRunForSession(syncSession); } }; } // 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) { if (subscription.hasPendingTruncateReplacement && !this.collectionConfigBuilder.hasActiveWindowOperation()) { return; } const orderByInfo = this.getOrderByInfo(); if (!orderByInfo) { return; } try { const pending = this.orderedLoader?.loadMore( this.collectionConfigBuilder.getActiveWindowOperationGeneration() ); if (pending) { this.collectionConfigBuilder.trackSubsetLoadOperationPromise(pending); } } catch (error) { if (!Object.is(subscription.lastError, error)) throw error; } } 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] ); } 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; } ensureLoadingPromise(subscription) { if (this.subscriptionLoadingPromises.has(subscription)) { return; } let resolve; const promise = new Promise((res) => { resolve = res; }); this.subscriptionLoadingPromises.set(subscription, { resolve }); this.collectionConfigBuilder.trackSubsetLoadPromise(promise); } } export { CollectionSubscriber }; //# sourceMappingURL=collection-subscriber.js.map