@tanstack/db
Version:
A reactive client store for building super fast apps on sync
349 lines (348 loc) • 11.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const dbIvm = require("@tanstack/db-ivm");
const expressions = require("../compiler/expressions.cjs");
const loadMoreCallbackSymbol = /* @__PURE__ */ Symbol.for(
`/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 = expressions.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 ? expressions.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 = expressions.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 = expressions.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 dbIvm.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 dbIvm.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;
}
}
}
exports.CollectionSubscriber = CollectionSubscriber;
//# sourceMappingURL=collection-subscriber.cjs.map