UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

481 lines (480 loc) 16.7 kB
import { LiveQueryWindowControllerDisposedError, SetWindowRequiresOrderByError } from "./errors.js"; import { getLiveQueryStatusFlags } from "./live-query-adapter.js"; import { createLiveQueryObserver } from "./live-query-observer.js"; const DEFAULT_PAGE_SIZE = 20; class WindowCoordinator { constructor(target) { this.target = target; this.leases = /* @__PURE__ */ new Map(); this.leaseVersions = /* @__PURE__ */ new Map(); this.generation = 0; this.leaseVersion = 0; this.initialWindow = target.utils?.getWindow?.(); } request(lease, limit) { const previousLimit = this.leases.get(lease); const previousVersion = this.leaseVersions.get(lease); const version = ++this.leaseVersion; this.leases.set(lease, limit); this.leaseVersions.set(lease, version); let result; try { result = this.applyDesiredWindow(); } catch (error) { this.rollbackLease(lease, version, previousLimit, previousVersion); this.appliedLimit = void 0; throw error; } if (result === true) return true; return result.catch(async (error) => { if (this.rollbackLease(lease, version, previousLimit, previousVersion)) { this.generation++; this.pending = void 0; this.appliedLimit = void 0; try { if (this.leases.size === 0) { this.restoreInitialWindow(); } else { const rollback = this.applyDesiredWindow(); if (rollback !== true) await rollback; } } catch { } } throw error; }); } isLeaseSatisfied(lease, minimumLimit) { const limit = this.leases.get(lease); if (limit === void 0 || limit < minimumLimit) return false; const desiredLimit = this.getDesiredLimit(); const currentWindow = this.target.utils?.getWindow?.(); return currentWindow === void 0 || currentWindow.offset === 0 && currentWindow.limit === desiredLimit; } release(lease) { if (!this.leases.delete(lease)) return; this.leaseVersions.delete(lease); this.generation++; this.pending = void 0; this.appliedLimit = void 0; if (this.leases.size === 0) { return; } try { const result = this.applyDesiredWindow(); if (result !== true) { void result.catch(() => { this.appliedLimit = void 0; }); } } catch { this.appliedLimit = void 0; } } getDesiredLimit() { let desired; for (const limit of this.leases.values()) { desired = desired === void 0 ? limit : Math.max(desired, limit); } return desired; } rollbackLease(lease, version, previousLimit, previousVersion) { if (this.leaseVersions.get(lease) !== version) return false; if (previousLimit === void 0) { this.leases.delete(lease); this.leaseVersions.delete(lease); } else { this.leases.set(lease, previousLimit); if (previousVersion === void 0) { this.leaseVersions.delete(lease); } else { this.leaseVersions.set(lease, previousVersion); } } return true; } restoreInitialWindow() { const setWindow = this.target.utils?.setWindow; if (!this.initialWindow || typeof setWindow !== `function`) return; try { const result = setWindow.call(this.target.utils, this.initialWindow); if (result !== true) void result.catch(() => { }); } catch { } } applyDesiredWindow() { const limit = this.getDesiredLimit(); 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.appliedLimit = void 0; } const currentWindow = this.target.utils?.getWindow?.(); if (limit === this.appliedLimit && 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.getDesiredLimit() === limit) { this.appliedLimit = limit; } return true; } const promise = result.then( () => { if (generation === this.generation && this.getDesiredLimit() === limit) { this.appliedLimit = 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; } class LiveQueryWindowControllerImpl { constructor(collection, options) { this.lease = /* @__PURE__ */ Symbol(`liveQueryWindowLease`); this.isFetchingNextPage = false; this.hasPaginationError = false; this.failedHasNextPage = false; this.windowGeneration = 0; this.leaseActive = false; this.leaseGeneration = 0; this.inFlightLeaseHolders = 0; this.subscriptions = /* @__PURE__ */ new Set(); this.publicationQueue = []; this.dispatching = false; this.blockDelivery = false; this.transitionDepth = 0; this.transitionNeedsNotify = false; this.observerUnsub = null; this.cachedSnapshot = null; this.cachedFrom = null; this.disposed = false; this.collection = collection; this.coordinator = collection ? getWindowCoordinator(collection) : null; this.pageSize = options.pageSize !== void 0 && options.pageSize > 0 ? options.pageSize : DEFAULT_PAGE_SIZE; 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(); const cached = this.cachedSnapshot; if (cached && this.cachedFrom && this.cachedFrom.observerSnapshot === observerSnapshot && this.cachedFrom.committedPageCount === this.committedPageCount && this.cachedFrom.isFetchingNextPage === this.isFetchingNextPage && this.cachedFrom.hasPaginationError === this.hasPaginationError && this.cachedFrom.paginationError === this.paginationError && this.cachedFrom.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.cachedSnapshot = { 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, isIdle: statusFlags.isIdle, isError: statusFlags.isError, isCleanedUp: observerSnapshot.isCleanedUp, isEnabled: observerSnapshot.isEnabled }; this.cachedFrom = { observerSnapshot, committedPageCount: this.committedPageCount, isFetchingNextPage: this.isFetchingNextPage, hasPaginationError: this.hasPaginationError, paginationError: this.paginationError, failedHasNextPage: this.failedHasNextPage }; return this.cachedSnapshot; } subscribe(listener) { if (this.disposed) throw new LiveQueryWindowControllerDisposedError(); const record = { listener, active: true }; this.subscriptions.add(record); if (this.subscriptions.size === 1) { this.blockDelivery = true; let observerUnsub = null; try { const windowResult = this.ensureLeaseActive(this.committedPageCount); const leaseGeneration = this.leaseGeneration; observerUnsub = this.observer.subscribe(() => this.onObserverNotify()); this.observerUnsub = observerUnsub; if (windowResult !== true) { this.trackAttachmentFailure(windowResult, leaseGeneration); } } catch (error) { observerUnsub?.(); this.observerUnsub = null; this.deactivateLease(); record.active = false; this.subscriptions.delete(record); throw error; } finally { this.blockDelivery = false; } } return () => { if (!record.active) return; record.active = false; this.subscriptions.delete(record); if (this.subscriptions.size === 0) { this.observerUnsub?.(); this.observerUnsub = null; if (this.inFlightLeaseHolders === 0) this.deactivateLease(); } }; } fetchNextPage() { if (this.disposed || this.isFetchingNextPage) return Promise.resolve(); if (!this.getSnapshot().hasNextPage) return Promise.resolve(); return this.requestPageCount(this.committedPageCount + 1, true); } reset() { if (this.disposed) return Promise.resolve(); if (this.committedPageCount === 1 && !this.hasPaginationError && !this.isFetchingNextPage && this.pendingWindowGeneration === void 0) { return Promise.resolve(); } return this.requestPageCount(1, false); } async preload() { if (this.disposed) throw new LiveQueryWindowControllerDisposedError(); const hadPaginationError = this.hasPaginationError; this.hasPaginationError = false; this.paginationError = void 0; this.acquireInFlightLease(); try { const result = this.ensureLeaseActive(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.getComputedHasNextPage(); this.notify(); throw error; } finally { this.releaseInFlightLease(); } } dispose() { if (this.disposed) return; this.disposed = true; this.windowGeneration++; this.pendingWindowGeneration = void 0; this.observerUnsub?.(); this.observerUnsub = null; this.deactivateLease(); this.observer.dispose(); for (const record of this.subscriptions) record.active = false; this.subscriptions.clear(); this.publicationQueue.length = 0; } requestPageCount(requestedPageCount, fetchingNextPage) { const generation = ++this.windowGeneration; const previousHasNextPage = this.getSnapshot().hasNextPage; this.pendingWindowGeneration = void 0; this.acquireInFlightLease(); this.beginTransition(); this.isFetchingNextPage = fetchingNextPage; this.hasPaginationError = false; this.paginationError = void 0; if (fetchingNextPage) this.notify(); let result; try { result = this.activateLease(requestedPageCount); } catch (error) { this.isFetchingNextPage = false; this.hasPaginationError = true; this.paginationError = error; this.failedHasNextPage = previousHasNextPage; this.notify(); this.endTransition(); this.releaseInFlightLease(); return Promise.reject(error); } if (result === true) { if (!this.disposed && generation === this.windowGeneration) { this.committedPageCount = requestedPageCount; this.isFetchingNextPage = false; this.failedHasNextPage = false; this.notify(); } this.endTransition(); this.releaseInFlightLease(); return Promise.resolve(); } this.pendingWindowGeneration = generation; this.endTransition(); return result.then( () => { if (this.disposed || generation !== this.windowGeneration) return; this.beginTransition(); this.pendingWindowGeneration = void 0; this.committedPageCount = requestedPageCount; this.isFetchingNextPage = false; this.failedHasNextPage = false; this.notify(); this.endTransition(); }, (error) => { if (!this.disposed && generation === this.windowGeneration) { this.beginTransition(); this.pendingWindowGeneration = void 0; this.isFetchingNextPage = false; this.hasPaginationError = true; this.paginationError = error; this.failedHasNextPage = previousHasNextPage; this.notify(); this.endTransition(); } throw error; } ).finally(() => { this.releaseInFlightLease(); }); } acquireInFlightLease() { this.inFlightLeaseHolders++; } releaseInFlightLease() { this.inFlightLeaseHolders--; if (this.inFlightLeaseHolders === 0 && this.subscriptions.size === 0) { this.deactivateLease(); } } activateLease(pageCount) { this.leaseGeneration++; if (!this.coordinator || !this.collection) return true; this.leaseActive = true; return this.coordinator.request(this.lease, pageCount * this.pageSize + 1); } ensureLeaseActive(pageCount) { const minimumLimit = pageCount * this.pageSize + 1; if (this.leaseActive && this.coordinator?.isLeaseSatisfied(this.lease, minimumLimit)) { return true; } return this.activateLease(pageCount); } deactivateLease() { if (!this.leaseActive || !this.coordinator) return; this.leaseGeneration++; this.leaseActive = false; this.coordinator.release(this.lease); } trackAttachmentFailure(result, leaseGeneration) { void result.catch((error) => { if (this.disposed || !this.leaseActive || leaseGeneration !== this.leaseGeneration) { return; } this.beginTransition(); this.hasPaginationError = true; this.paginationError = error; this.failedHasNextPage = this.getComputedHasNextPage(); this.notify(); this.endTransition(); }); } getComputedHasNextPage() { const snapshot = this.observer.getSnapshot(); return snapshot.isEnabled && Array.isArray(snapshot.data) && snapshot.data.length > this.committedPageCount * this.pageSize; } onObserverNotify() { this.notify(); } beginTransition() { this.transitionDepth++; } endTransition() { this.transitionDepth--; if (this.transitionDepth === 0 && this.transitionNeedsNotify) { this.transitionNeedsNotify = false; this.publish(); } } notify() { if (this.transitionDepth > 0) { this.transitionNeedsNotify = true; return; } this.publish(); } publish() { if (this.disposed || this.blockDelivery || this.subscriptions.size === 0) { return; } this.publicationQueue.push({ targets: [...this.subscriptions] }); if (this.dispatching) return; this.dispatching = true; try { while (this.publicationQueue.length > 0) { const publication = this.publicationQueue.shift(); for (const record of publication.targets) { if (this.hasBeenDisposed()) return; if (!record.active) continue; record.listener(); } } } finally { this.dispatching = false; } } hasBeenDisposed() { return this.disposed; } } function createLiveQueryWindowController(collection, options = {}) { return new LiveQueryWindowControllerImpl(collection ?? null, options); } export { createLiveQueryWindowController }; //# sourceMappingURL=live-query-window-controller.js.map