@tanstack/db
Version:
A reactive client store for building super fast apps on sync
382 lines (381 loc) • 13 kB
JavaScript
import { canExpressCursorOrder, buildCursorCurrent } from "../../utils/cursor.js";
import { normalizeError } from "../../utils/error.js";
import { runAllCallbacks } from "../../utils/callbacks.js";
import { normalizeOrderByPaths } from "../compiler/expressions.js";
class OrderedSourceLoader {
constructor(info, subscription, alias, onResult = () => {
}) {
this.info = info;
this.subscription = subscription;
this.alias = alias;
this.onResult = onResult;
this.hasSettledSourceRequest = false;
this.needsFullSourceRecovery = false;
this.requesting = false;
this.fullSource = `none`;
this.settledFiniteAcquisitions = /* @__PURE__ */ new Set();
this.failedAcquisitions = /* @__PURE__ */ new Map();
this.active = true;
this.generation = 0;
this.info.isRequesting = () => this.requesting;
}
/** Derive invalidation from actual contributions, not a second cursor. */
onSourceChanges(changes, sentRows) {
let hasNewRows = false;
for (const change of changes) {
const previous = sentRows?.get(change.key);
if (change.type !== `insert` && previous !== void 0 && (change.type === `delete` || this.info.comparator(previous, change.value) !== 0)) {
this.invalidateSourceOrdering();
return;
}
if (change.type !== `delete` && previous === void 0) hasNewRows = true;
}
if (hasNewRows) this.invalidateCursor();
}
start() {
const { index, limit, offset, orderBy, requiresFullSource } = this.info;
if (index) this.subscription.setOrderByIndex(index);
if (limit === 0) return;
if (requiresFullSource) {
this.loadFullSource();
return;
}
if (!index || orderBy.length !== 1) {
this.loadPrefix(offset + limit);
return;
}
this.loadPage(offset + limit);
}
loadMore(windowOperationGeneration) {
if (!this.active || this.info.limit === 0 || this.requesting) return;
const mayRetryFailure = this.failedRequest === void 0 || windowOperationGeneration !== void 0 && windowOperationGeneration !== this.failedRequest.windowOperationGeneration;
if (!mayRetryFailure) return this.pending;
if ((this.failedRequest || this.failedAcquisitions.size > 0) && windowOperationGeneration !== void 0) {
if (this.failedRequest) {
this.failedRequest.windowOperationGeneration = windowOperationGeneration;
}
const failedAcquisitions = this.failedAcquisitions;
this.failedAcquisitions = /* @__PURE__ */ new Map();
if (failedAcquisitions.size > 0) {
this.requesting = true;
try {
runAllCallbacks(failedAcquisitions.keys());
} finally {
this.requesting = false;
}
if (!this.active) return;
}
}
if (this.fullSource === `failed`) this.fullSource = `none`;
else if (this.fullSource !== `none`) return this.pending;
if (this.needsFullSourceRecovery || this.info.requiresFullSource) {
this.loadFullSource(windowOperationGeneration);
return this.pending;
}
if (!this.info.index || this.info.orderBy.length !== 1) {
this.loadPrefix(
this.info.offset + this.info.limit,
windowOperationGeneration
);
return this.pending;
}
if (!this.info.dataNeeded || this.pending) return this.pending;
let count = Math.max(
this.info.dataNeeded(),
this.hasSettledSourceRequest ? 0 : this.info.offset + this.info.limit
);
if (windowOperationGeneration !== void 0 && this.settledSourceBoundary !== void 0) {
const needed = this.info.offset + this.info.limit;
count = Math.max(count, needed - this.countAcquiredRows());
}
if (count > 0) {
this.loadPage(count, windowOperationGeneration);
}
return this.pending;
}
loadFullSource(windowOperationGeneration) {
if (!this.active || this.fullSource !== `none`) return;
this.fullSource = `held`;
this.requestAndObserve(
(onLoadSubsetResult) => {
this.subscription.requestSnapshot({
trackLoadSubsetPromise: false,
onLoadSubsetResult
});
},
`full-source`,
windowOperationGeneration
);
}
loadPrefix(count, windowOperationGeneration) {
if (!this.active || this.pending) return;
if (this.lastPrefixCount === count) {
if ((this.info.dataNeeded?.() ?? 0) > 0) {
this.loadFullSource(windowOperationGeneration);
}
return;
}
this.requestAndObserve(
(onLoadSubsetResult) => {
this.subscription.requestSnapshot({
orderBy: normalizeOrderByPaths(this.info.orderBy, this.alias),
limit: count,
trackLoadSubsetPromise: false,
onLoadSubsetResult
});
},
`ordered`,
windowOperationGeneration
);
this.lastPrefixCount = count;
}
resetCursor() {
this.generation++;
if (this.fullSource === `complete`) this.fullSource = `held`;
this.pending = void 0;
this.lastBoundary = void 0;
this.settledSourceBoundary = void 0;
this.invalidateCursor();
}
settleFullSourceReplay() {
if (this.fullSource === `failed`) {
for (const [release, kind] of this.failedAcquisitions) {
if (kind === `full-source`) this.failedAcquisitions.delete(release);
}
this.fullSource = `held`;
}
if (this.fullSource !== `none`) {
this.fullSource = `complete`;
this.retireSettledFiniteAcquisitions();
}
}
retireSettledFiniteAcquisitions() {
if (this.fullSource !== `complete` || this.subscription.hasPendingTruncateReplacement)
return;
const generation = this.generation;
runAllCallbacks(
Array.from(this.settledFiniteAcquisitions, (release) => () => {
if (!this.active || generation !== this.generation) return;
this.settledFiniteAcquisitions.delete(release);
release();
})
);
}
invalidateCursor() {
this.lastPage = void 0;
this.lastPrefixCount = void 0;
}
invalidateSourceOrdering() {
this.invalidateCursor();
this.requireFullSourceRecovery();
}
dispose() {
this.active = false;
this.resetCursor();
this.failedAcquisitions.clear();
this.settledFiniteAcquisitions.clear();
}
countAcquiredRows() {
return this.subscription.readOrderedSnapshot({
orderBy: normalizeOrderByPaths(this.info.orderBy, this.alias),
limit: this.info.offset + this.info.limit
}).filter(
({ value }) => this.info.comparator(value, this.settledSourceBoundary) <= 0
).length;
}
loadPage(count, windowOperationGeneration) {
if (!this.active || this.pending) return;
const startsFromSourcePrefix = this.settledSourceBoundary === void 0;
const biggest = this.settledSourceBoundary;
let minValues;
if (biggest !== void 0) {
const value = this.info.valueExtractorForRawRow(biggest);
if (!canExpressCursorOrder(this.info.orderBy, [value])) {
this.loadPrefix(
this.info.offset + this.info.limit,
windowOperationGeneration
);
return;
}
minValues = [value];
}
const boundary = minValues?.[0];
if (this.lastPage?.count === count && Object.is(this.lastPage.boundary, boundary)) {
return;
}
this.lastPage = { count, boundary };
this.requestAndObserve(
(onLoadSubsetResult) => {
this.subscription.requestLimitedSnapshot({
orderBy: normalizeOrderByPaths(this.info.orderBy, this.alias),
limit: count,
minValues,
// Local rows seen before the first provider request prove neither
// a cursor nor a remote offset. Start the first acquisition at zero.
offset: startsFromSourcePrefix ? 0 : this.countAcquiredRows(),
trackLoadSubsetPromise: false,
onLoadSubsetResult
});
},
`ordered`,
windowOperationGeneration
);
}
observe(result, releaseAcquisition, kind, windowOperationGeneration, options) {
const isFullSource = kind === `full-source`;
const generation = this.generation;
const complete = () => {
if (this.pending === tracked) this.pending = void 0;
if (!this.active) return;
if (!isFullSource) {
this.settledFiniteAcquisitions.add(releaseAcquisition);
this.retireSettledFiniteAcquisitions();
}
if (generation !== this.generation) return;
if (!isFullSource && (this.failedRequest || this.fullSource !== `none`))
return;
this.failedRequest = void 0;
if (kind !== `boundary`) {
this.hasSettledSourceRequest = true;
if (options?.orderBy && !options.cursor) {
this.lastPrefixCount = options.limit;
}
if (!isFullSource && options?.orderBy) {
try {
this.settledSourceBoundary = this.subscription.readOrderedSnapshot(options).at(-1)?.value ?? this.settledSourceBoundary;
} catch (error) {
fail(error);
}
}
}
if (isFullSource) {
this.needsFullSourceRecovery = false;
this.fullSource = `complete`;
this.retireSettledFiniteAcquisitions();
}
if (kind === `ordered`) {
this.loadBoundary(windowOperationGeneration);
return;
}
this.loadMore();
};
const settlesAsync = result instanceof Promise;
const request = settlesAsync ? result : Promise.resolve();
const fail = (error) => {
this.settledFiniteAcquisitions.delete(releaseAcquisition);
if (this.pending === tracked) this.pending = void 0;
if (!this.active) return;
this.requireFullSourceRecovery();
if (generation !== this.generation) return;
if (isFullSource) this.fullSource = `failed`;
this.recordRequestFailure(windowOperationGeneration);
this.failedAcquisitions.set(releaseAcquisition, kind);
throw error;
};
const tracked = request.then(complete, fail);
this.pending = tracked;
void tracked.catch(() => {
});
this.onResult(
tracked,
settlesAsync && isFullSource && this.needsFullSourceRecovery
);
return tracked;
}
loadBoundary(windowOperationGeneration) {
const biggest = this.settledSourceBoundary;
if (biggest === void 0) return;
const value = this.info.valueExtractorForRawRow(biggest);
const orderBy = normalizeOrderByPaths(this.info.orderBy, this.alias);
if (!canExpressCursorOrder(orderBy.slice(0, 1), [value])) {
this.loadFullSource(windowOperationGeneration);
return this.pending;
}
if (Object.is(this.lastBoundary, value)) {
return this.loadMore();
}
const where = buildCursorCurrent(orderBy, [value]);
if (!where) {
this.loadFullSource(windowOperationGeneration);
return this.pending;
}
this.lastBoundary = value;
return this.requestAndObserve(
(onLoadSubsetResult) => {
this.subscription.requestSnapshot({
where,
trackLoadSubsetPromise: false,
onLoadSubsetResult
});
},
`boundary`,
windowOperationGeneration
);
}
requireFullSourceRecovery() {
this.settledSourceBoundary = void 0;
this.needsFullSourceRecovery = true;
}
failRequest(observed, error, isFullSource, windowOperationGeneration, cancelObservedSettlement = false) {
if (cancelObservedSettlement) {
this.generation++;
this.pending = void 0;
}
this.requireFullSourceRecovery();
this.recordRequestFailure(windowOperationGeneration);
if (isFullSource) this.fullSource = `none`;
try {
observed?.release({ error });
} catch {
}
return error;
}
/** A failed request blocks ordinary refinement until a new operation. */
recordRequestFailure(windowOperationGeneration) {
this.failedRequest = { windowOperationGeneration };
this.invalidateCursor();
this.lastBoundary = void 0;
}
/** Observe settlement only after all synchronous request work succeeds. */
requestAndObserve(request, kind, windowOperationGeneration) {
const isFullSource = kind === `full-source`;
let observed;
this.requesting = true;
let observing = false;
try {
try {
request((result, options, release) => {
observed = { result, options, release };
});
} finally {
this.requesting = false;
}
if (!observed) return;
observing = true;
return this.observe(
observed.result,
observed.release,
kind,
windowOperationGeneration,
observed.options
);
} catch (error) {
this.requesting = true;
try {
throw this.failRequest(
observed,
normalizeError(error),
isFullSource,
windowOperationGeneration,
observing
);
} finally {
this.requesting = false;
}
}
}
}
export {
OrderedSourceLoader
};
//# sourceMappingURL=ordered-source-loader.js.map