@tanstack/db
Version:
A reactive client store for building super fast apps on sync
652 lines (651 loc) • 21.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const errors = require("./errors.cjs");
const liveQueryAdapter = require("./live-query-adapter.cjs");
const liveQueryObserver = require("./live-query-observer.cjs");
const index = require("./query/builder/index.cjs");
const utils = require("./utils.cjs");
const DEFAULT_PAGE_SIZE = 20;
function getLiveQueryWindowInputKind(input) {
if (liveQueryAdapter.isCollection(input)) return `collection`;
if (typeof input === `function`) return `query`;
throw new Error(
`useLiveInfiniteQuery: First argument must be either a pre-created live query collection or a query function. Received: ${typeof input}`
);
}
function resolveLiveQueryWindowInput(input) {
if (getLiveQueryWindowInputKind(input) === `collection`) {
return {
kind: `collection`,
collection: input
};
}
const value = input(new index.BaseQueryBuilder());
if (liveQueryAdapter.isCollection(value)) {
return { kind: `collection`, collection: value };
}
if (typeof value !== `object` || value === null || typeof value.limit !== `function` || typeof value.offset !== `function`) {
throw new Error(
`useLiveInfiniteQuery: Query function must return a query builder. Disabled null or undefined queries are not supported.`
);
}
return { kind: `query`, query: value };
}
function normalizeLiveQueryWindowPageSize(pageSize) {
if (pageSize === void 0 || !Number.isSafeInteger(pageSize) || pageSize <= 0 || pageSize >= Number.MAX_SAFE_INTEGER) {
return DEFAULT_PAGE_SIZE;
}
return pageSize;
}
class WindowCoordinator {
constructor(target) {
this.target = target;
this.cw = /* @__PURE__ */ new Map();
this.bV = /* @__PURE__ */ new Map();
this.aY = true;
this.generation = 0;
this.fI = 0;
}
request(lease, limit) {
if (this.cw.size === 0) {
const currentWindow = this.target.utils?.getWindow?.();
const retainedWindowChanged = this.b8 !== void 0 && (currentWindow?.offset !== this.b8.offset || currentWindow.limit !== this.b8.limit);
if (this.aY || retainedWindowChanged) {
this.cJ = currentWindow;
this.aY = false;
}
this.b8 = void 0;
}
const previousLimit = this.cw.get(lease);
const previousVersion = this.bV.get(lease);
const version = ++this.fI;
this.cw.set(lease, limit);
this.bV.set(lease, version);
let result;
try {
result = this.df();
} catch (error) {
this.e7(lease, version, previousLimit, previousVersion);
this.bt = void 0;
if (this.cw.size === 0) this.aY = true;
throw error;
}
if (result === true) return true;
return result.catch(async (error) => {
if (this.e7(lease, version, previousLimit, previousVersion)) {
this.generation++;
this.pending = void 0;
this.bt = void 0;
try {
if (this.cw.size === 0) {
this.d9();
} else {
const rollback = this.df();
if (rollback !== true) await rollback;
}
} catch {
}
}
throw error;
});
}
getLeaseResult(lease, minimumLimit) {
const limit = this.cw.get(lease);
if (limit === void 0 || limit < minimumLimit) return false;
const desiredLimit = this.c6();
if (this.pending && this.pending.limit === desiredLimit)
return this.pending.promise;
const currentWindow = this.target.utils?.getWindow?.();
return currentWindow === void 0 || currentWindow.offset === 0 && currentWindow.limit === desiredLimit;
}
hasLeases() {
return this.cw.size > 0;
}
release(lease, restoreWhenEmpty) {
if (!this.cw.delete(lease)) return;
this.bV.delete(lease);
this.generation++;
this.pending = void 0;
this.bt = void 0;
if (this.cw.size === 0) {
if (restoreWhenEmpty) {
this.d9();
} else {
this.b8 = this.target.utils?.getWindow?.();
}
return;
}
try {
const result = this.df();
if (result !== true) {
void result.catch(() => {
this.bt = void 0;
});
}
} catch {
this.bt = void 0;
}
}
c6() {
let desired;
for (const limit of this.cw.values()) {
desired = desired === void 0 ? limit : Math.max(desired, limit);
}
return desired;
}
e7(lease, version, previousLimit, previousVersion) {
if (this.bV.get(lease) !== version) return false;
if (previousLimit === void 0) {
this.cw.delete(lease);
this.bV.delete(lease);
} else {
this.cw.set(lease, previousLimit);
if (previousVersion === void 0) {
this.bV.delete(lease);
} else {
this.bV.set(lease, previousVersion);
}
}
return true;
}
d9() {
const setWindow = this.target.utils?.setWindow;
const baselineWindow = this.cJ;
this.b8 = void 0;
this.aY = false;
if (!baselineWindow || typeof setWindow !== `function`) {
this.aY = true;
return;
}
const generation = this.generation;
const markRestored = () => {
if (generation === this.generation && this.cw.size === 0) {
this.aY = true;
}
};
try {
const result = setWindow.call(this.target.utils, baselineWindow);
if (result === true) {
markRestored();
} else {
void result.then(markRestored, () => {
});
}
} catch {
}
}
df() {
const limit = this.c6();
if (limit === void 0) return true;
if (this.pending?.limit === limit) return this.pending.promise;
if (this.pending) {
this.generation++;
this.pending = void 0;
this.bt = void 0;
}
const currentWindow = this.target.utils?.getWindow?.();
if (limit === this.bt && currentWindow?.offset === 0 && currentWindow.limit === limit) {
return true;
}
const setWindow = this.target.utils?.setWindow;
if (typeof setWindow !== `function`) {
throw new errors.SetWindowRequiresOrderByError();
}
const generation = ++this.generation;
const result = setWindow.call(this.target.utils, { offset: 0, limit });
if (result === true) {
if (generation === this.generation && this.c6() === limit) {
this.bt = limit;
}
return true;
}
const promise = result.then(
() => {
if (generation === this.generation && this.c6() === limit) {
this.bt = limit;
}
if (this.pending?.generation === generation) {
this.pending = void 0;
}
},
(error) => {
if (this.pending?.generation === generation) {
this.pending = void 0;
}
throw error;
}
);
this.pending = { generation, limit, promise };
return promise;
}
}
const windowCoordinators = /* @__PURE__ */ new WeakMap();
function getWindowCoordinator(target) {
let coordinator = windowCoordinators.get(target);
if (!coordinator) {
coordinator = new WindowCoordinator(target);
windowCoordinators.set(target, coordinator);
}
return coordinator;
}
function hasLiveQueryWindowLeases(target) {
return windowCoordinators.get(target)?.hasLeases() ?? false;
}
function assertLiveQueryWindowManyResult(collection) {
if (liveQueryAdapter.isSingleResultCollection(collection)) {
throw new Error(
`useLiveInfiniteQuery: Infinite queries do not support single-result queries. Remove .findOne().`
);
}
}
function isLiveQueryWindowCollection(collection) {
return typeof collection.utils?.setWindow === `function` && collection.utils.getWindow?.() !== void 0;
}
function getLiveQueryWindowCollectionWarning(collection, expectedLimit) {
assertLiveQueryWindowManyResult(collection);
if (!isLiveQueryWindowCollection(collection)) {
throw new Error(
`useLiveInfiniteQuery: Pre-created live query collection must have an ORDER BY (orderBy) clause for infinite pagination to work. Please add .orderBy() to your createLiveQueryCollection query.`
);
}
const currentWindow = collection.utils.getWindow();
if (!currentWindow || hasLiveQueryWindowLeases(collection) || currentWindow.offset === 0 && currentWindow.limit === expectedLimit) {
return void 0;
}
return `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} but the hook expects {offset: 0, limit: ${expectedLimit}}. Adjusting window now.`;
}
function compareLiveQueryWindowDependencies(previous, current) {
const changed = previous === null || previous === void 0 || previous.length !== current.length || previous.some((dependency, index2) => dependency !== current[index2]);
return {
changed,
structurallyEqual: previous !== null && previous !== void 0 && utils.deepEquals(previous, current)
};
}
function shouldPreserveLiveQueryWindowPageCount(options) {
if (!options.hasPreviousController || options.previousInputKind !== options.inputKind) {
return false;
}
if (options.inputKind === `collection`) return options.sameCollection;
return options.dependenciesChanged ? options.dependenciesStructurallyEqual : options.pageShapeChanged;
}
function fetchNextLiveQueryWindowPage(controller) {
return controller.fetchNextPage().catch(() => {
});
}
class LiveQueryWindowControllerImpl {
constructor(collection, options) {
this.aX = /* @__PURE__ */ Symbol(`liveQueryWindowLease`);
this.isFetchingNextPage = false;
this.hasPaginationError = false;
this.failedHasNextPage = false;
this.Q = null;
this.bn = 0;
this.dI = false;
this.bd = 0;
this.b$ = 0;
this.aZ = false;
this.subscriptions = /* @__PURE__ */ new Set();
this.aE = [];
this.b4 = false;
this.bC = false;
this.dc = 0;
this.cP = false;
this.a0 = null;
this.bp = null;
this.ct = null;
this.disposed = false;
this.collection = collection;
this.coordinator = collection ? getWindowCoordinator(collection) : null;
this.pageSize = normalizeLiveQueryWindowPageSize(options.pageSize);
this.initialPageParam = options.initialPageParam ?? 0;
const initialPageCount = Math.floor(options.initialPageCount ?? 1);
this.committedPageCount = Number.isFinite(initialPageCount) ? Math.max(1, initialPageCount) : 1;
this.observer = liveQueryObserver.createLiveQueryObserver(collection, {
mode: `wholesale`
});
}
getSnapshot() {
const observerSnapshot = this.observer.getSnapshot();
if (this.hasPaginationError && observerSnapshot.status === `ready`) {
this.failedHasNextPage = this.bI(observerSnapshot);
}
const cached = this.bp;
if (cached && this.ct && this.ct.observerSnapshot === observerSnapshot && this.ct.committedPageCount === this.committedPageCount && this.ct.isFetchingNextPage === this.isFetchingNextPage && this.ct.hasPaginationError === this.hasPaginationError && this.ct.paginationError === this.paginationError && this.ct.failedHasNextPage === this.failedHasNextPage) {
return cached;
}
const enabled = observerSnapshot.isEnabled;
const rows = enabled && Array.isArray(observerSnapshot.data) ? observerSnapshot.data : [];
const totalRequested = this.committedPageCount * this.pageSize;
const computedHasNextPage = enabled && rows.length > totalRequested;
const hasNextPage = this.hasPaginationError ? this.failedHasNextPage : computedHasNextPage;
const pageCount = enabled ? this.committedPageCount : 0;
const pages = [];
const pageParams = [];
for (let i = 0; i < pageCount; i++) {
pages.push(rows.slice(i * this.pageSize, (i + 1) * this.pageSize));
pageParams.push(this.initialPageParam + i);
}
const status = this.hasPaginationError ? `error` : observerSnapshot.status;
const statusFlags = this.hasPaginationError ? liveQueryAdapter.getLiveQueryStatusFlags(`error`) : observerSnapshot;
this.bp = {
data: rows.slice(0, totalRequested),
pages,
pageParams,
hasNextPage,
isFetchingNextPage: this.isFetchingNextPage,
error: this.hasPaginationError ? this.paginationError : void 0,
state: observerSnapshot.state,
collection: observerSnapshot.collection,
status,
isLoading: statusFlags.isLoading,
isReady: statusFlags.isReady,
persistedStatus: observerSnapshot.persistedStatus,
isPersistedReady: observerSnapshot.isPersistedReady,
persistedError: observerSnapshot.persistedError,
isIdle: statusFlags.isIdle,
isError: statusFlags.isError,
isCleanedUp: observerSnapshot.isCleanedUp,
isEnabled: observerSnapshot.isEnabled
};
this.ct = {
observerSnapshot,
committedPageCount: this.committedPageCount,
isFetchingNextPage: this.isFetchingNextPage,
hasPaginationError: this.hasPaginationError,
paginationError: this.paginationError,
failedHasNextPage: this.failedHasNextPage
};
return this.bp;
}
subscribe(listener) {
if (this.disposed) throw new errors.LiveQueryWindowControllerDisposedError();
const record = { listener, active: true };
this.subscriptions.add(record);
if (this.subscriptions.size === 1) {
this.aZ = false;
this.bC = true;
let observerUnsub = null;
try {
const windowResult = this.eo(this.committedPageCount);
const leaseGeneration = this.bd;
observerUnsub = this.observer.subscribe(() => this.notify());
this.a0 = observerUnsub;
if (windowResult !== true) {
this.eN(windowResult, leaseGeneration);
}
} catch (error) {
observerUnsub?.();
this.a0 = null;
this.c5(true);
record.active = false;
this.subscriptions.delete(record);
throw error;
} finally {
this.bC = false;
}
}
return () => {
if (!record.active) return;
record.active = false;
this.subscriptions.delete(record);
if (this.subscriptions.size === 0) {
this.aZ = true;
this.a0?.();
this.a0 = null;
if (this.b$ === 0) this.c5(true);
}
};
}
fetchNextPage() {
if (this.disposed) return Promise.resolve();
if (this.Q) {
return this.Q;
}
const snapshot = this.getSnapshot();
const awaitingInitialLoad = snapshot.isLoading || snapshot.isIdle;
if (!snapshot.hasNextPage && !awaitingInitialLoad) return Promise.resolve();
let resolveFetch;
let rejectFetch;
const activeFetchPromise = new Promise((resolve, reject) => {
resolveFetch = resolve;
rejectFetch = reject;
});
this.Q = activeFetchPromise;
let request;
try {
const generation = this.bn;
request = awaitingInitialLoad ? this.preload().then(() => {
if (this.disposed || generation !== this.bn || !this.getSnapshot().hasNextPage) {
return;
}
return this.dP(this.committedPageCount + 1, true);
}) : this.dP(this.committedPageCount + 1, true);
} catch (error) {
this.Q = null;
rejectFetch(error);
return activeFetchPromise;
}
void request.then(
() => {
if (this.Q === activeFetchPromise) {
this.Q = null;
}
resolveFetch();
},
(error) => {
if (this.Q === activeFetchPromise) {
this.Q = null;
}
rejectFetch(error);
}
);
return activeFetchPromise;
}
reset() {
if (this.disposed) return Promise.resolve();
if (this.committedPageCount === 1 && !this.hasPaginationError && !this.isFetchingNextPage && !this.Q && this.aM === void 0) {
return Promise.resolve();
}
this.Q = null;
return this.dP(1, false);
}
async preload() {
if (this.disposed) throw new errors.LiveQueryWindowControllerDisposedError();
const hadPaginationError = this.hasPaginationError;
this.hasPaginationError = false;
this.paginationError = void 0;
this.dZ();
try {
const result = this.eo(this.committedPageCount);
if (result !== true) await result;
await this.observer.preload();
this.failedHasNextPage = false;
if (hadPaginationError) this.notify();
} catch (error) {
this.hasPaginationError = true;
this.paginationError = error;
this.failedHasNextPage = this.bI();
this.notify();
throw error;
} finally {
this.b0();
}
}
dispose() {
if (this.disposed) return;
this.disposed = true;
this.bn++;
this.aM = void 0;
this.a0?.();
this.a0 = null;
this.c5(true);
this.observer.dispose();
for (const record of this.subscriptions) record.active = false;
this.subscriptions.clear();
this.aE.length = 0;
}
dP(requestedPageCount, fetchingNextPage) {
const generation = ++this.bn;
const previousHasNextPage = this.getSnapshot().hasNextPage;
this.aM = void 0;
this.dZ();
this.c2();
this.isFetchingNextPage = fetchingNextPage;
this.hasPaginationError = false;
this.paginationError = void 0;
if (fetchingNextPage) this.notify();
let result;
try {
result = this.e0(requestedPageCount);
} catch (error) {
this.isFetchingNextPage = false;
this.hasPaginationError = true;
this.paginationError = error;
this.failedHasNextPage = previousHasNextPage;
this.notify();
this.cq();
this.b0();
return Promise.reject(error);
}
if (result === true) {
if (!this.disposed && generation === this.bn) {
this.committedPageCount = requestedPageCount;
this.isFetchingNextPage = false;
this.notify();
}
this.cq();
this.b0();
return Promise.resolve();
}
this.aM = generation;
this.cq();
return result.then(
() => {
if (this.disposed || generation !== this.bn) return;
this.c2();
this.aM = void 0;
this.committedPageCount = requestedPageCount;
this.isFetchingNextPage = false;
this.failedHasNextPage = this.bI();
this.notify();
this.cq();
},
(error) => {
if (!this.disposed && generation === this.bn) {
this.c2();
this.aM = void 0;
this.isFetchingNextPage = false;
this.hasPaginationError = true;
this.paginationError = error;
this.failedHasNextPage = previousHasNextPage;
this.notify();
this.cq();
}
throw error;
}
).finally(() => {
this.b0();
});
}
dZ() {
this.b$++;
}
b0() {
this.b$--;
if (this.b$ === 0 && this.subscriptions.size === 0) {
this.c5(this.aZ);
}
}
e0(pageCount) {
this.bd++;
if (!this.coordinator || !this.collection) return true;
this.dI = true;
return this.coordinator.request(this.aX, pageCount * this.pageSize + 1);
}
eo(pageCount) {
const minimumLimit = pageCount * this.pageSize + 1;
if (this.dI) {
const result = this.coordinator?.getLeaseResult(this.aX, minimumLimit);
if (result) return result;
}
return this.e0(pageCount);
}
c5(restoreWhenEmpty = false) {
if (!this.dI || !this.coordinator) return;
this.bd++;
this.dI = false;
this.aZ = false;
this.coordinator.release(this.aX, restoreWhenEmpty);
}
eN(result, leaseGeneration) {
void result.catch((error) => {
if (this.disposed || !this.dI || leaseGeneration !== this.bd) {
return;
}
this.c2();
this.hasPaginationError = true;
this.paginationError = error;
this.failedHasNextPage = this.bI();
this.notify();
this.cq();
});
}
bI(snapshot = this.observer.getSnapshot()) {
return snapshot.isEnabled && Array.isArray(snapshot.data) && snapshot.data.length > this.committedPageCount * this.pageSize;
}
c2() {
this.dc++;
}
cq() {
this.dc--;
if (this.dc === 0 && this.cP) {
this.cP = false;
this.publish();
}
}
notify() {
if (this.dc > 0) {
this.cP = true;
return;
}
this.publish();
}
publish() {
if (this.disposed || this.bC || this.subscriptions.size === 0) {
return;
}
this.aE.push({ targets: [...this.subscriptions] });
if (this.b4) return;
this.b4 = true;
try {
while (this.aE.length > 0) {
const publication = this.aE.shift();
for (const record of publication.targets) {
if (this.fy()) return;
if (!record.active) continue;
record.listener();
}
}
} finally {
this.b4 = false;
}
}
fy() {
return this.disposed;
}
}
function createLiveQueryWindowController(collection, options = {}) {
return new LiveQueryWindowControllerImpl(collection ?? null, options);
}
exports.assertLiveQueryWindowManyResult = assertLiveQueryWindowManyResult;
exports.compareLiveQueryWindowDependencies = compareLiveQueryWindowDependencies;
exports.createLiveQueryWindowController = createLiveQueryWindowController;
exports.fetchNextLiveQueryWindowPage = fetchNextLiveQueryWindowPage;
exports.getLiveQueryWindowCollectionWarning = getLiveQueryWindowCollectionWarning;
exports.getLiveQueryWindowInputKind = getLiveQueryWindowInputKind;
exports.hasLiveQueryWindowLeases = hasLiveQueryWindowLeases;
exports.isLiveQueryWindowCollection = isLiveQueryWindowCollection;
exports.normalizeLiveQueryWindowPageSize = normalizeLiveQueryWindowPageSize;
exports.resolveLiveQueryWindowInput = resolveLiveQueryWindowInput;
exports.shouldPreserveLiveQueryWindowPageCount = shouldPreserveLiveQueryWindowPageCount;
//# sourceMappingURL=live-query-window-controller.cjs.map