UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

652 lines (651 loc) • 21.1 kB
import { LiveQueryWindowControllerDisposedError, SetWindowRequiresOrderByError } from "./errors.js"; import { isCollection, isSingleResultCollection, getLiveQueryStatusFlags } from "./live-query-adapter.js"; import { createLiveQueryObserver } from "./live-query-observer.js"; import { BaseQueryBuilder } from "./query/builder/index.js"; import { deepEquals } from "./utils.js"; const DEFAULT_PAGE_SIZE = 20; function getLiveQueryWindowInputKind(input) { if (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 BaseQueryBuilder()); if (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 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 (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, index) => dependency !== current[index]); return { changed, structurallyEqual: previous !== null && previous !== void 0 && 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 = 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 ? 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 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 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); } export { assertLiveQueryWindowManyResult, compareLiveQueryWindowDependencies, createLiveQueryWindowController, fetchNextLiveQueryWindowPage, getLiveQueryWindowCollectionWarning, getLiveQueryWindowInputKind, hasLiveQueryWindowLeases, isLiveQueryWindowCollection, normalizeLiveQueryWindowPageSize, resolveLiveQueryWindowInput, shouldPreserveLiveQueryWindowPageCount }; //# sourceMappingURL=live-query-window-controller.js.map