@tanstack/db
Version:
A reactive client store for building super fast apps on sync
349 lines (348 loc) • 11.4 kB
JavaScript
"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