@tanstack/db
Version:
A reactive client store for building super fast apps on sync
291 lines (290 loc) • 10.1 kB
JavaScript
"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