UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

439 lines (438 loc) • 14.4 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const errors = require("./errors.cjs"); const callbacks = require("./utils/callbacks.cjs"); const liveQueryPrimitives = require("./live-query-primitives.cjs"); const lifecycle = require("./collection/lifecycle.cjs"); const DISABLED_SNAPSHOT = { state: void 0, data: void 0, collection: void 0, layoutRevision: 0, status: `disabled`, isLoading: false, isReady: true, isIdle: false, isLoadingError: false, isCrashed: false, isCleanedUp: false, isEnabled: false, isSyncError: false, health: void 0 }; class LiveQueryObserverImpl { // Sync activation belongs to the first subscription (attach), so building // an observer cannot activate collection resources on its own. Server // request clients still record ownership here because React may render an // observer without ever subscribing to it. constructor(collection, wholesale, client, queryHash, onPreload, suspendUntilReady) { this.aD = true; this.eN = false; this.bp = DISABLED_SNAPSHOT; this.layoutRevision = 0; this.subscriptions = /* @__PURE__ */ new Set(); this.aE = []; this.b4 = false; this.bC = false; this.fV = false; this.cu = null; this.disposed = false; this.collection = collection; this.wholesale = wholesale; this.onPreload = onPreload; this.aO = client?._bindLiveQueryObserver( collection, queryHash, suspendUntilReady, { observer: this, read: (server) => this.getSnapshot(server), entries: () => collection ? this.cH(collection) : [], isAttached: () => this.fV, isDisposed: () => this.disposed, dirty: () => { this.aD = true; }, publish: (changes, entries) => { const revision = entries && collection ? this.dR(collection) : void 0; if (entries) { this.bF(entries, revision); this.aD = true; } this.emit( this.wholesale ? void 0 : changes, void 0, entries, collection?.status, revision, entries && collection ? this.bc(collection) : void 0, entries !== void 0 ); }, preload: () => this.bs() } ); this.aO?.registerResource(); } getSnapshot(forServerSnapshot = false) { const collection = this.collection; if (!collection) return DISABLED_SNAPSHOT; this.aO?.sync(forServerSnapshot); if (!this.fV) this.d6(collection); if (this.eN !== forServerSnapshot && this.aO?.seed && liveQueryPrimitives.isFailedStatus(this.cs ?? collection.status)) { this.aD = true; } if (this.aD) { const entries = this.cH(collection); const data = entries.map(([, value]) => value); const singleResult = liveQueryPrimitives.isSingleResultCollection(collection); const liveStatus = this.cs ?? collection.status; const status = this.aO?.failed ? `error` : this.aO?.seed && (forServerSnapshot || !liveQueryPrimitives.isFailedStatus(liveStatus)) ? this.aO.seed.snapshot.status ?? `ready` : liveStatus; this.eN = forServerSnapshot; const prevKeys = this.eU; let layoutChanged = prevKeys === void 0 || prevKeys.length !== entries.length; if (!layoutChanged) { for (let i = 0; i < entries.length; i++) { if (prevKeys[i] !== entries[i][0]) { layoutChanged = true; break; } } } if (layoutChanged) { this.eU = entries.map(([key]) => key); this.layoutRevision++; } const health = this.aU ? this.aU.tree : collection.health; let state; this.bp = { get state() { return state ??= new Map(entries); }, data: singleResult ? data[0] : data, collection: liveQueryPrimitives.getPublicCollection(collection), layoutRevision: this.layoutRevision, status, ...liveQueryPrimitives.getLiveQueryStatusFlags(status, health), isEnabled: true }; this.aD = false; } return this.bp; } getServerSnapshot(preloadRequested = false) { return this.aO?.getServerSnapshot(preloadRequested) ?? this.getSnapshot(); } getError() { this.aO?.sync(); return this.aO?.error; } dehydrate() { const collection = this.collection; if (!collection) return { rows: [] }; const entries = this.aO?.seed && this.aO.seed.snapshot.status === void 0 ? this.aO.seed.entries : this.cZ(collection).entries; return { rows: entries.map(([key, value]) => ({ key, value })) }; } cH(collection) { if (this.aO?.seed) return this.aO.seed.entries; return this.cT ?? this.eQ(collection).entries; } dR(collection) { const revision = collection._stateRevision; return typeof revision === `number` ? revision : void 0; } bc(collection) { const revision = collection._layoutRevision; return typeof revision === `number` ? revision : void 0; } cZ(collection) { const entries = Array.from(collection.entries()); const revision = this.dR(collection); return { entries, revision }; } eQ(collection) { const { entries, revision } = this.cZ(collection); this.bF(entries, revision); return { entries, revision }; } bF(entries, revision) { const changed = revision !== void 0 ? this.cT === void 0 || revision !== this.ca : !this.fH(this.cT, entries); this.cT = entries; this.ca = revision; if (changed) this.aD = true; } fH(left, right) { if (!left || left.length !== right.length) return false; return left.every( ([key, value], index) => right[index][0] === key && right[index][1] === value ); } /** * While detached there is no delivered-publication clock, so fall back to * the collection revision. Compatible cross-copy collections that predate * `_stateRevision` are compared structurally instead. */ d6(collection) { const status = collection.status; const revision = this.dR(collection); const layoutRevision = this.bc(collection); if (revision !== void 0) { if (this.cT === void 0 || revision !== this.ca || layoutRevision !== this.bu) { this.eQ(collection); this.bu = layoutRevision; this.aD = true; } } else { const entries = Array.from(collection.entries()); this.bF(entries, void 0); } if (this.cs !== status || this.bp.health !== collection.health) { this.cs = status; this.aD = true; } } subscribe(listener) { if (this.disposed) throw new errors.LiveQueryObserverDisposedError(); if (this.subscriptions.size === 0) this.aO?.assertSources(); const record = { listener, active: true }; const release = () => { if (!record.active) return; record.active = false; this.subscriptions.delete(record); if (this.subscriptions.size === 0) this.gC(); }; this.subscriptions.add(record); const attaching = !this.fV; try { if (attaching) { this.gF(); } else { if (!this.wholesale) this.seed(record); } } catch (error) { if (attaching) this.gC(); release(); throw error; } return release; } /** Deliver the collection's current rows to one late subscription as inserts. */ seed(record) { const collection = this.collection; if (!collection) return; const seedChanges = []; for (const [key, value] of this.cH(collection)) { seedChanges.push({ type: `insert`, key, value }); } if (seedChanges.length === 0) return; this.emit(seedChanges, [record]); } gF() { const collection = this.collection; if (!collection || this.disposed) return; const releases = []; const release = () => callbacks.runAllCallbacks(releases.splice(0)); this.cu = release; const ownRelease = (unsubscribe) => { if (this.cu === release) releases.push(unsubscribe); else unsubscribe(); }; this.aO?.registerResource(); this.aO?.sync(); this.d6(collection); this.fV = true; this.cs ??= collection.status; this.cm = this.bc(collection); const attachedWithHydrationSeed = !!this.aO?.seed; this.bC = this.wholesale || attachedWithHydrationSeed; const notify = (changes, status = collection.status, explicitLayoutChange = false) => { if (this.disposed || this.subscriptions.size === 0) return; if (this.aO?.seed) { if (liveQueryPrimitives.isFailedStatus(status) && !this.bC) this.aO.handoff(); else if (status === `ready` || liveQueryPrimitives.isFailedStatus(status)) this.aO.scheduleHandoff(); return; } if (status === `ready` && !collection.isLoadingSubset) { this.aO?.markAuthoritative(); } const layoutRevision = this.bc(collection); let layoutChanged = explicitLayoutChange; if (!explicitLayoutChange && changes !== void 0 && changes.length === 0) { if (layoutRevision === void 0 || layoutRevision === this.cm) { return; } layoutChanged = true; } if (changes !== void 0 && layoutRevision !== void 0) { this.cm = layoutRevision; } const captured = changes !== void 0 ? this.cZ(collection) : status === `cleaned-up` ? this.cZ(collection) : void 0; this.emit( changes, void 0, captured?.entries, status, captured?.revision, layoutRevision, layoutChanged ); }; const healthOf = () => collection.health; this.aU = { tree: healthOf() }; ownRelease(() => { this.aU = void 0; }); ownRelease( collection.on(`status:change`, ({ status }) => { this.aU = { tree: healthOf() }; notify(void 0, status); }) ); ownRelease( collection.on(`health:change`, () => { const health = healthOf(); if (health === this.aU?.tree) return; this.aU = { tree: health }; this.aD = true; notify(void 0, collection.status); }) ); const subscribeLayoutChanges = collection._subscribeLayoutChanges; if (typeof subscribeLayoutChanges === `function`) { ownRelease( subscribeLayoutChanges.call( collection, () => notify([], collection.status, true) ) ); } if (this.aO) ownRelease(this.aO.subscribe()); const subscription = collection.subscribeChanges( (changes) => notify(changes), { includeInitialState: !this.wholesale && !attachedWithHydrationSeed } ); ownRelease(() => subscription.unsubscribe()); this.bC = false; if (this.cu !== release) return; if (this.wholesale || attachedWithHydrationSeed) { this.er(!this.wholesale); const revision = this.dR(collection); const layoutRevision = this.bc(collection); if (revision === void 0 || this.cT === void 0 || revision !== this.ca || layoutRevision !== this.bu) { const { entries } = this.cZ(collection); this.bF(entries, revision); this.bu = layoutRevision; } } if (this.aO?.seed) { if (!this.wholesale) this.seed(Array.from(this.subscriptions)[0]); if (collection.status === `ready` || liveQueryPrimitives.isFailedStatus(collection.status)) { this.aO.scheduleHandoff(); } } } gC() { const release = this.cu; const unregister = this.aO?.takeResourceRelease(); this.cu = null; this.fV = false; this.bC = false; this.aE.length = 0; callbacks.runAllCallbacks([() => release?.(), () => unregister?.()]); } emit(changes, targets = Array.from(this.subscriptions), entries, status = this.collection?.status ?? `cleaned-up`, collectionRevision, collectionLayoutRevision, layoutChanged = false) { this.aE.push({ changes, targets, entries, status, collectionRevision, collectionLayoutRevision, layoutChanged }); if (this.b4 || this.bC) return; this.er(); } er(deliver = true) { if (this.b4) return; let failure; this.b4 = true; try { while (this.aE.length > 0) { const publication = this.aE.shift(); if (publication.entries) { this.bF( publication.entries, publication.collectionRevision ); } if (publication.collectionLayoutRevision !== void 0) { this.bu = publication.collectionLayoutRevision; } if (publication.layoutChanged) { this.aD = true; } if (this.cs !== publication.status) { this.cs = publication.status; this.aD = true; } if (deliver) { for (const subRecord of publication.targets) { if (this.disposed) break; try { subRecord.listener(publication.changes); } catch (error) { failure ??= { error }; } } } } } finally { this.b4 = false; } if (failure) throw failure.error; } preload() { return this.aO?.preload() ?? this.bs(); } bs() { if (this.preloadPromise) return this.preloadPromise; this.onPreload?.(); const promise = this.collection?.preload() ?? Promise.resolve(); this.preloadPromise = promise; const clear = () => { if (this.preloadPromise === promise) this.preloadPromise = void 0; }; void promise.then(clear, clear); return promise; } dispose() { if (this.disposed) return; this.disposed = true; this.gC(); for (const subRecord of this.subscriptions) subRecord.active = false; this.subscriptions.clear(); this.aE.length = 0; } } const toBoundaryError = (cause) => cause instanceof Error ? cause : new Error(lifecycle.describeProblem(cause).message, { cause }); function createLiveQueryObserver(collection, options = {}) { return new LiveQueryObserverImpl( collection ?? null, options.mode === `wholesale`, options.client, options.queryHash, options.onPreload, options.suspendUntilReady === true ); } exports.createLiveQueryObserver = createLiveQueryObserver; exports.toBoundaryError = toBoundaryError; //# sourceMappingURL=live-query-observer.cjs.map