@tanstack/db
Version:
A reactive client store for building super fast apps on sync
481 lines (480 loc) • 16.9 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 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 errors.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 = liveQueryObserver.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 ? liveQueryAdapter.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 errors.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 errors.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);
}
exports.createLiveQueryWindowController = createLiveQueryWindowController;
//# sourceMappingURL=live-query-window-controller.cjs.map