@tanstack/react-db
Version:
React integration for @tanstack/db
208 lines (207 loc) • 8.09 kB
JavaScript
"use client";
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const react = require("react");
const db = require("@tanstack/db");
const useLiveQuery = require("./useLiveQuery.cjs");
const sourceIdBindings = require("./source-id-bindings.cjs");
const unscopedClient = {};
const suspenseCollectionsByClient = /* @__PURE__ */ new WeakMap();
function getSourceQualifiedSuspenseKey(preparedValue, queryHash, client) {
const sourceIds = sourceIdBindings.getPreparedSources(
preparedValue,
client,
`useLiveSuspenseQuery`
).map(db.getSourceObjectToken);
const {
query: _query,
pool: _pool,
...options
} = preparedValue?.query ? preparedValue : {};
options.autoIndex ??= `off`;
for (const key in options) {
const value = options[key];
if (value === void 0) delete options[key];
else if (value && (typeof value === `object` || typeof value === `function`))
options[key] = db.getSourceObjectToken(value);
}
return db.getStableValueHash([sourceIds, options, queryHash]);
}
class SuspenseBinding {
constructor() {
this.ready = false;
}
get needsOwnCollection() {
return this.entry !== void 0 && this.entry.owner !== this;
}
resolve(input, hash, client, compile) {
const scope = client ?? unscopedClient;
let cache = suspenseCollectionsByClient.get(scope);
if (!cache) {
cache = /* @__PURE__ */ new Map();
suspenseCollectionsByClient.set(scope, cache);
}
const owned = this.entry?.owner === this;
const key = owned ? void 0 : getSourceQualifiedSuspenseKey(input, hash, client);
const cached = key === void 0 ? void 0 : cache.get(key);
if (cached) return (this.entry = cached).collection;
const windowOwner = owned ? this.entry.windowOwner : {};
const collection = compile(owned, windowOwner);
if (owned && collection === this.entry.collection) return collection;
this.seed = void 0;
if (collection && this.entry?.collection !== collection) {
const entryKey = key ?? getSourceQualifiedSuspenseKey(input, hash, client);
const release = () => {
if (cache.get(entryKey) === entry) cache.delete(entryKey);
removeStatusListener();
};
const entry = { collection, windowOwner, release };
this.entry = entry;
cache.set(entryKey, entry);
const removeStatusListener = collection.on(
`status:change`,
({ status }) => {
if (status === `cleaned-up`) release();
else if (status === `error` || status === `crashed`)
setTimeout(release, 0);
}
);
}
return collection;
}
// The first successful candidate may keep the preload. Other candidates get
// private outputs seeded from its completed snapshot. Keep the retry entry
// available until commit, since React can discard a successful candidate.
forkReady(observer, compile) {
if (!this.entry || this.entry.owner === this) return;
const resource = this.entry.observer ??= observer;
const snapshot = typeof window === `undefined` ? resource.getServerSnapshot(true) : resource.getSnapshot();
if (!snapshot.isReady) return;
if (!this.entry.owner) {
this.entry.owner = this;
return;
}
const windowOwner = {};
const collection = compile(false, windowOwner);
if (!collection) return;
this.seed = { ...snapshot, collection };
this.entry = {
collection,
windowOwner,
owner: this,
release: this.entry.release
};
return collection;
}
read(snapshot) {
if (snapshot.isReady || snapshot.isLoadingError || snapshot.isCrashed) {
if (this.seed && snapshot.isLoadingError) this.ready = false;
this.seed = void 0;
}
if (this.seed && snapshot.health !== this.seed.health) {
this.seed = {
...this.seed,
...db.getLiveQueryStatusFlags(`ready`, snapshot.health)
};
}
return this.seed ?? snapshot;
}
claim() {
return this.entry?.release;
}
}
const initialRenderErrors = /* @__PURE__ */ new WeakMap();
function clearInitialRenderError(collection, client) {
initialRenderErrors.get(collection)?.delete(client ?? unscopedClient);
}
function rememberInitialRenderError(collection, client, error, clientQuery) {
if (collection.status === `cleaned-up`) return;
let entry = initialRenderErrors.get(collection);
if (!entry) {
entry = /* @__PURE__ */ new WeakMap();
collection.once(`status:cleaned-up`, () => {
initialRenderErrors.delete(collection);
});
initialRenderErrors.set(collection, entry);
}
entry.set(client ?? unscopedClient, { error, clientQuery });
}
function useLiveSuspenseQuery(configOrQueryOrCollection) {
const bindingRef = react.useRef(null);
const binding = bindingRef.current ??= new SuspenseBinding();
const { result, client, queryHash, observer, deferredCollections } = useLiveQuery.useLiveQueryImpl(configOrQueryOrCollection, binding);
if (!result.isEnabled) {
throw new Error(
`useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). The Suspense pattern requires data to always be defined (T, not T | undefined). Solutions: 1) Use conditional rendering - don't render the component until the condition is met. 2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`
);
}
const collection = result.collection;
try {
if (binding.collection !== collection) {
binding.promise = void 0;
binding.collection = collection;
binding.ready = false;
}
const collectionStatus = collection.status;
if (result.isReady) {
binding.ready = true;
binding.promise = void 0;
clearInitialRenderError(collection, client);
}
const observerError = observer.getError();
const clientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
if (!binding.ready && (observerError !== void 0 || clientQuery?.status === `error`)) {
throw observerError === void 0 ? clientQuery?.error : observerError;
}
const initialRenderError = initialRenderErrors.get(collection)?.get(client ?? unscopedClient);
if (initialRenderError?.clientQuery !== void 0 && initialRenderError.clientQuery !== clientQuery) {
clearInitialRenderError(collection, client);
} else if (initialRenderError && !binding.ready) {
throw db.toBoundaryError(initialRenderError.error);
}
if (collectionStatus === `crashed` || collectionStatus === `error` && !binding.ready) {
throw db.toBoundaryError(collection.health.error?.cause);
}
if (!binding.ready && (result.isLoading || result.isIdle || result.isLoadingError || result.isCrashed)) {
if (!binding.promise) {
let active = true;
const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => {
active = false;
});
let preload;
try {
preload = observer.preload();
} catch (error) {
stopWatchingCleanup();
throw error;
}
const preloadClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
binding.promise = preload.catch((error) => {
const latestClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
if (active && latestClientQuery === preloadClientQuery)
rememberInitialRenderError(
collection,
client,
error,
preloadClientQuery
);
throw error;
}).finally(stopWatchingCleanup);
}
throw binding.promise;
}
return {
state: result.state,
data: result.data,
collection,
health: result.health
};
} catch (error) {
if (error instanceof Promise) throw error;
binding.promise = void 0;
return db.rejectLiveQueryInput(deferredCollections, error);
}
}
exports.SuspenseBinding = SuspenseBinding;
exports.useLiveSuspenseQuery = useLiveSuspenseQuery;
//# sourceMappingURL=useLiveSuspenseQuery.cjs.map