UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

349 lines (348 loc) • 11.4 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const expressions = require("../compiler/expressions.cjs"); const errors = require("../../errors.cjs"); const orderedSourceLoader = require("./ordered-source-loader.cjs"); const utils = require("./utils.cjs"); const subsetDemandController = require("./subset-demand-controller.cjs"); class CollectionSubscriber { constructor(sourceId, alias, collection, collectionConfigBuilder) { this.sourceId = sourceId; this.alias = alias; this.collection = collection; this.fQ = collectionConfigBuilder; this.gX = 0; this.cI = 0; this.az = /* @__PURE__ */ new Map(); this.fh = /* @__PURE__ */ new Map(); this.demand = new subsetDemandController.SubsetDemandController((planId, key, error) => { const request = `${planId}:${key}`; if (error) this.fQ.reportFailedRequest(request, [error]); else this.fQ.clearFailedRequest(request); }); } /** The live query this subscription reads for, as `subscribers` lists it. */ owner() { return this.fQ.dependent(); } subscribe() { const whereClause = this.fQ.sourceWhereClausesCache?.get(this.sourceId); return this.fi( whereClause ? expressions.normalizeExpressionPaths(whereClause, this.alias) : void 0 ); } // A live-query Collection starts no provider work on its own: it defers // acquisition until it has a subscriber or a preload. c_() { return !this.fQ.hasSubscriberOrPreload(); } fi(whereExpression) { const orderByInfo = this.fQ.optimizableOrderByCollections[this.sourceId]; const trackLoadResult = (result, options) => { const order = ++this.gX; const decides = () => !options?.signal?.aborted && !options?.subscription?.pendingTruncateReplacement; void Promise.resolve(result).then( () => { if (order > this.cI && decides()) this.fQ.settleRootLoad(this.sourceId); }, () => { if (decides()) this.cI = this.gX; } ); if (result instanceof Promise) { const trackedResult = result.catch(async (error) => { await Promise.resolve(); throw error; }); this.fQ.trackSubsetLoadPromise(trackedResult); } }; const onStatusChange = (event) => { if (this.fQ.isLazySource(this.sourceId)) { if (event.status === `ready`) { this.fQ.scheduleGraphRun(); } return; } const subscription2 = event.subscription; if (event.status === `loadingSubset`) { this.d$(subscription2); } else { const deferred = this.az.get(subscription2); if (deferred) { this.az.delete(subscription2); deferred.resolve(); } } }; const onLoadSubsetError = (event) => { const error = this.fQ.recordSubsetError( event.error, // Lazy demand owns its fatal-error path. For eager sources, one // successful page does not finish initial ordered refinement. !this.fQ.isLazySource(this.sourceId) && this.fQ.liveQueryCollection?._lifecycle.inFirstLoad === true ); if (!this.fQ.isLazySource(this.sourceId) && !event.subscription.pendingTruncateReplacement) { this.fQ.failRootLoad(this.sourceId, error); } }; let subscription; if (orderByInfo) { subscription = this.da( whereExpression, orderByInfo, onStatusChange, trackLoadResult, onLoadSubsetError ); } else { const includeInitialState = (this.collection.config.syncMode !== `on-demand` || this.fQ.query.limit !== 0) && !this.fQ.isLazySource(this.sourceId); subscription = this.ek( whereExpression, includeInitialState, onStatusChange, trackLoadResult, onLoadSubsetError ); this.cQ(subscription); } if (!this.fQ.isLazySource(this.sourceId) && subscription.status === `loadingSubset`) { this.d$(subscription); } return subscription; } cQ(subscription) { const unsubscribe = () => { const deferred = this.az.get(subscription); if (deferred) { this.az.delete(subscription); deferred.resolve(); } try { this.demand.clear(); } finally { subscription.unsubscribe(); } }; this.fQ.currentSyncState.unsubscribeCallbacks.add( unsubscribe ); } setDemand(subscription, plan, keys) { const update = this.demand.setDemand(subscription, plan, keys); if (!update.changed) return; if (update.empty) { this.fQ.retireDemand(plan.id); return; } const generation = this.fQ.beginDemand( plan.id, update.ready instanceof Promise ); const settle = () => this.fQ.settleDemand(plan.id, generation); if (update.ready instanceof Promise) { void update.ready.then( settle, (error) => this.fQ.failDemand( subscription, this.collection, plan.id, generation, error ) ); } else { settle(); } } dV(changes) { const changesArray = Array.isArray(changes) ? changes : [...changes]; const reconciledChanges = utils.reconcileChangesForD2( changesArray, this.fh ); const input = this.fQ.currentSyncState.inputs[this.sourceId]; if (utils.sendChangesToInput(input, reconciledChanges) > 0) { this.fQ.advanceGraphInputRevision(); } this.fQ.scheduleGraphRun(); } ek(whereExpression, includeInitialState, onStatusChange, onLoadSubsetResult, onLoadSubsetError) { const sendChanges = (changes) => { this.dV(changes); }; const hints = utils.computeSubscriptionOrderByHints( this.fQ.query, this.alias, this.fQ.queryCompareOptions ); const subscription = this.collection.subscribeChanges(sendChanges, { ...includeInitialState && { includeInitialState }, whereExpression, onStatusChange, onLoadSubsetError, deferAcquisition: this.c_(), owner: this.owner(), truncateReplayPublication: this.ci(), orderBy: hints.orderBy, limit: hints.limit, onLoadSubsetResult: includeInitialState ? onLoadSubsetResult : void 0 }); return subscription; } da(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.bk?.onSourceChanges( changesArray, this.fh, (key) => this.collection._hasOptimisticOverlay(key) ); const splittedChanges = utils.splitUpdates(changesArray); this.dV(splittedChanges); }; const subscription = this.collection.subscribeChanges(sendChangesInRange, { whereExpression, onStatusChange, onLoadSubsetError, deferAcquisition: this.c_(), owner: this.owner(), truncateReplayPublication: this.ci( (retired) => { const loader = this.bk; if (retired) loader?.resetCursor(); const replay = subscriptionHolder.current?.pendingTruncateReplacement; if (replay) { const decided = replay.catch((error) => { if (!(error instanceof errors.LoadSubsetOperationAbortedError)) throw error; }); void decided.catch(() => void 0); this.fQ.trackSubsetLoadOperationPromise( decided ); } this.fQ.trackOrderedLoadPromise( Promise.resolve().then(() => loader?.loadFullSource()), true ); } ) }); subscriptionHolder.current = subscription; this.cQ(subscription); const truncateUnsubscribe = this.collection.on(`truncate`, () => { this.bk?.resetCursor(); }); const revalidationUnsubscribe = this.collection.on( `loadedSubsets:invalidated`, () => { const loader = this.bk; if (!loader) return; loader.revalidateLoadedCoverage(); this.fQ.scheduleGraphRunIfSyncRunCurrent( this.fQ.getSyncRunGeneration() ); } ); subscription.on(`unsubscribed`, () => { truncateUnsubscribe(); revalidationUnsubscribe(); subscriptionHolder.current = void 0; this.bk?.dispose(); this.bk = void 0; }); this.bk = new orderedSourceLoader.OrderedSourceLoader( orderByInfo, subscription, this.alias, (result, holdPublication, settlesAsync, options) => { if (result instanceof Promise) { this.fQ.trackOrderedLoadPromise( result, holdPublication && settlesAsync && !subscription.hasPendingTruncateReplacement ); } onLoadSubsetResult(result, options); }, () => this.fQ.liveQueryCollection?.status === `ready` && !this.fQ.hasActiveWindowOperation(), () => this.fQ.getGraphInputRevision(), () => orderByInfo.joinedFilterSourceId !== void 0 && this.fQ.hasPendingJoinedWork( orderByInfo.joinedFilterSourceId ) ); this.bk.start(); return subscription; } ci(onStart) { const syncRunGeneration = this.fQ.getSyncRunGeneration(); const isCurrent = () => syncRunGeneration === this.fQ.getSyncRunGeneration(); const decide = (errors2 = []) => { if (errors2.length > 0) this.fQ.failReplay(this.sourceId, errors2); else this.fQ.settleReplayedRequests(this.sourceId); this.fQ.settleRootLoad(this.sourceId); this.demand.clearRequestErrors(); }; return { start: (retired) => onStart?.(retired), siblingReplays: () => this.fQ.siblingReplays( this.collection, this.sourceId ), succeed: () => { if (!isCurrent()) return; this.bk?.settleFullSourceReplay(); decide(); this.fQ.scheduleGraphRunIfSyncRunCurrent( syncRunGeneration ); }, fail: (errors2) => { if (isCurrent()) decide(errors2); } }; } // 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.fQ.hasActiveWindowOperation()) { return; } const orderByInfo = this.fQ.optimizableOrderByCollections[this.sourceId]; if (!orderByInfo) { return; } try { const pending = this.bk?.loadMore( this.fQ.getActiveWindowOperationGeneration() ); if (pending) { this.fQ.trackSubsetLoadOperationPromise(pending); } } catch (error) { if (!Object.is(subscription.lastError, error)) throw error; } } d$(subscription) { if (this.az.has(subscription)) { return; } let resolve; const promise = new Promise((res) => { resolve = res; }); this.az.set(subscription, { resolve }); this.fQ.trackSubsetLoadPromise(promise); } } exports.CollectionSubscriber = CollectionSubscriber; //# sourceMappingURL=collection-subscriber.cjs.map