@tanstack/db
Version:
A reactive client store for building super fast apps on sync
439 lines (438 loc) • 14.1 kB
JavaScript
import { LiveQueryObserverDisposedError } from "./errors.js";
import { runAllCallbacks } from "./utils/callbacks.js";
import { isFailedStatus, isSingleResultCollection, getLiveQueryStatusFlags, getPublicCollection } from "./live-query-primitives.js";
import { describeProblem } from "./collection/lifecycle.js";
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 && isFailedStatus(this.cs ?? collection.status)) {
this.aD = true;
}
if (this.aD) {
const entries = this.cH(collection);
const data = entries.map(([, value]) => value);
const singleResult = isSingleResultCollection(collection);
const liveStatus = this.cs ?? collection.status;
const status = this.aO?.failed ? `error` : this.aO?.seed && (forServerSnapshot || !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: getPublicCollection(collection),
layoutRevision: this.layoutRevision,
status,
...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 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 = () => 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 (isFailedStatus(status) && !this.bC)
this.aO.handoff();
else if (status === `ready` || 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` || 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;
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(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
);
}
export {
createLiveQueryObserver,
toBoundaryError
};
//# sourceMappingURL=live-query-observer.js.map