UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

291 lines (290 loc) 10.1 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const errors = require("./errors.cjs"); const liveQueryAdapter = require("./live-query-adapter.cjs"); const DISABLED_SNAPSHOT = { state: void 0, data: void 0, collection: void 0, layoutRevision: 0, status: `disabled`, isLoading: false, isReady: true, isIdle: false, isError: false, isCleanedUp: false, isEnabled: false }; class LiveQueryObserverImpl { // Construction is side-effect-free: sync activation belongs to the first // subscription (attach), so building an observer — e.g. in a React render // that may be abandoned — cannot activate resources on its own. constructor(collection, wholesale) { this.snapshotDirty = true; this.cachedSnapshot = DISABLED_SNAPSHOT; this.layoutRevision = 0; this.subscriptions = /* @__PURE__ */ new Set(); this.publicationQueue = []; this.dispatching = false; this.blockDelivery = false; this.attached = false; this.collectionUnsub = null; this.disposed = false; this.collection = collection; this.wholesale = wholesale; } getSnapshot() { const collection = this.collection; if (!collection) return DISABLED_SNAPSHOT; if (!this.attached) this.refreshDetachedState(collection); if (this.snapshotDirty) { const entries = this.cachedEntries ?? this.captureEntries(collection).entries; const state = new Map(entries); const data = entries.map(([, value]) => value); const singleResult = liveQueryAdapter.isSingleResultCollection(collection); const status = this.visibleStatus ?? collection.status; const prevKeys = this.lastLayoutKeys; 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.lastLayoutKeys = entries.map(([key]) => key); this.layoutRevision++; } this.cachedSnapshot = { state, data: singleResult ? data[0] : data, collection, layoutRevision: this.layoutRevision, status, ...liveQueryAdapter.getLiveQueryStatusFlags(status), isEnabled: true }; this.snapshotDirty = false; } return this.cachedSnapshot; } getCollectionRevision(collection) { const revision = collection._stateRevision; return typeof revision === `number` ? revision : void 0; } getCollectionLayoutRevision(collection) { const revision = collection._layoutRevision; return typeof revision === `number` ? revision : void 0; } readEntries(collection) { const entries = Array.from(collection.entries()); const revision = this.getCollectionRevision(collection); return { entries, revision }; } captureEntries(collection) { const { entries, revision } = this.readEntries(collection); this.updateCachedEntries(entries, revision); return { entries, revision }; } updateCachedEntries(entries, revision) { const changed = revision !== void 0 ? this.cachedEntries === void 0 || revision !== this.cachedCollectionRevision : !this.entriesEqual(this.cachedEntries, entries); this.cachedEntries = entries; this.cachedCollectionRevision = revision; if (changed) this.snapshotDirty = true; } entriesEqual(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. */ refreshDetachedState(collection) { const status = collection.status; const revision = this.getCollectionRevision(collection); const layoutRevision = this.getCollectionLayoutRevision(collection); if (revision !== void 0) { if (this.cachedEntries === void 0 || revision !== this.cachedCollectionRevision || layoutRevision !== this.cachedCollectionLayoutRevision) { this.captureEntries(collection); this.cachedCollectionLayoutRevision = layoutRevision; this.snapshotDirty = true; } } else { const entries = Array.from(collection.entries()); this.updateCachedEntries(entries, void 0); } if (this.visibleStatus !== status) { this.visibleStatus = status; this.snapshotDirty = true; } } subscribe(listener) { if (this.disposed) throw new errors.LiveQueryObserverDisposedError(); const record = { listener, active: true }; this.subscriptions.add(record); if (this.subscriptions.size === 1) { this.attach(); } else { if (!this.wholesale) this.seed(record); } return () => { if (!record.active) return; record.active = false; this.subscriptions.delete(record); if (this.subscriptions.size === 0) this.detach(); }; } /** 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 collection.entries()) { seedChanges.push({ type: `insert`, key, value }); } if (seedChanges.length === 0) return; this.emit(seedChanges, [record]); } attach() { const collection = this.collection; if (!collection || this.disposed) return; this.refreshDetachedState(collection); this.attached = true; this.visibleStatus ??= collection.status; this.deliveredLayoutRevision = this.getCollectionLayoutRevision(collection); this.blockDelivery = this.wholesale; const notify = (changes, status = collection.status, explicitLayoutChange = false) => { if (this.disposed || this.subscriptions.size === 0) return; const layoutRevision = this.getCollectionLayoutRevision(collection); let layoutChanged = explicitLayoutChange; if (!explicitLayoutChange && changes !== void 0 && changes.length === 0) { if (layoutRevision === void 0 || layoutRevision === this.deliveredLayoutRevision) { return; } layoutChanged = true; } if (changes !== void 0 && layoutRevision !== void 0) { this.deliveredLayoutRevision = layoutRevision; } const captured = changes !== void 0 ? this.readEntries(collection) : status === `cleaned-up` ? this.readEntries(collection) : void 0; this.emit( changes, void 0, captured?.entries, status, captured?.revision, layoutRevision, layoutChanged ); }; const statusUnsub = collection.on( `status:change`, ({ status }) => notify(void 0, status) ); const subscribeLayoutChanges = collection._subscribeLayoutChanges; const layoutUnsub = typeof subscribeLayoutChanges === `function` ? subscribeLayoutChanges.call( collection, () => notify([], collection.status, true) ) : () => { }; let subscription = null; const release = () => { statusUnsub(); layoutUnsub(); subscription?.unsubscribe(); }; this.collectionUnsub = release; subscription = collection.subscribeChanges( (changes) => notify(changes), { includeInitialState: !this.wholesale } ); this.blockDelivery = false; if (this.collectionUnsub !== release) { subscription.unsubscribe(); return; } if (this.wholesale) { this.flushPublications(false); const { entries, revision } = this.readEntries(collection); this.updateCachedEntries(entries, revision); } } detach() { this.collectionUnsub?.(); this.collectionUnsub = null; this.attached = false; this.blockDelivery = false; this.publicationQueue.length = 0; } emit(changes, targets = Array.from(this.subscriptions), entries, status = this.collection?.status ?? `cleaned-up`, collectionRevision, collectionLayoutRevision, layoutChanged = false) { this.publicationQueue.push({ changes, targets, entries, status, collectionRevision, collectionLayoutRevision, layoutChanged }); if (this.dispatching || this.blockDelivery) return; this.flushPublications(); } flushPublications(deliver = true) { if (this.dispatching) return; this.dispatching = true; try { while (this.publicationQueue.length > 0) { const publication = this.publicationQueue.shift(); if (publication.entries) { this.updateCachedEntries( publication.entries, publication.collectionRevision ); } if (publication.collectionLayoutRevision !== void 0) { this.cachedCollectionLayoutRevision = publication.collectionLayoutRevision; } if (publication.layoutChanged) { this.snapshotDirty = true; } if (this.visibleStatus !== publication.status) { this.visibleStatus = publication.status; this.snapshotDirty = true; } if (deliver) { for (const subRecord of publication.targets) { if (this.disposed) return; subRecord.listener(publication.changes); } } } } finally { this.dispatching = false; } } async preload() { await this.collection?.preload(); } dispose() { if (this.disposed) return; this.disposed = true; this.detach(); for (const subRecord of this.subscriptions) subRecord.active = false; this.subscriptions.clear(); this.publicationQueue.length = 0; } } function createLiveQueryObserver(collection, options = {}) { return new LiveQueryObserverImpl( collection ?? null, options.mode === `wholesale` ); } exports.createLiveQueryObserver = createLiveQueryObserver; //# sourceMappingURL=live-query-observer.cjs.map