UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

261 lines (260 loc) • 10.3 kB
import { BaseQueryBuilder } from "./query/builder/index.js"; import { collectSourceRefs, requireCollectionSource } from "./query/ir.js"; import { isCollection, isFailedStatus } from "./live-query-primitives.js"; import { DEFAULT_GC_TIME_MS, UNSUBSCRIBED_GC_FLOOR_MS } from "./collection/lifecycle.js"; import { createLiveQueryCollection } from "./query/live-query-collection.js"; import { LIVE_QUERY_INTERNAL } from "./query/live/internal.js"; import { getBuilderFromConfig } from "./query/live/collection-registry.js"; import { getStableValueHash, getStableQueryBuilderHash, getQueryIdentity } from "./query/ir-stable-identity.js"; import { getStringCollationIdentity } from "./query/runtime-reference-identity.js"; import { devBuild, codedMessage } from "./error-message.js"; import { InvalidPoolOptionError, UnhashableQueryIRError } from "./errors.js"; function configPool(config) { const { pool } = config; if (pool !== void 0 && typeof pool?.createPooledLiveQuery !== `function`) throw new InvalidPoolOptionError(); return pool; } function rejectLiveQueryInput(collections, rejection) { try { if (collections) resumeDeferredLiveQueryCollections(collections); } catch { } throw rejection; } function releaseDeferredOnReclaim(liveQuery, deferredCollections) { if (deferredCollections.size === 0) return void 0; const pending = new Set(deferredCollections); const release = () => { try { resumeDeferredLiveQueryCollections(pending); } catch { } }; const gcTime = liveQuery.config.gcTime ?? DEFAULT_GC_TIME_MS; if (gcTime > 0 && Number.isFinite(gcTime) && liveQuery.status !== `idle`) { return liveQuery.once(`status:cleaned-up`, release); } const timer = setTimeout(release, UNSUBSCRIBED_GC_FLOOR_MS); return () => clearTimeout(timer); } function resumeDeferredLiveQueryCollections(collections, reportSourceErrors = false) { const pending = Array.from(collections); collections.clear(); let firstFailure; for (const collection of pending) { try { collection._resumeSyncStart(); if (reportSourceErrors && isFailedStatus(collection.status)) { throw collection._lifecycle.getSyncError(); } } catch (error) { firstFailure ??= { error }; } } if (firstFailure) throw firstFailure.error; } function prepareLiveQueryValue(value, dbClient, deferredCollections) { if (typeof value === `function`) { return prepareLiveQueryValue( value(new BaseQueryBuilder()), dbClient, deferredCollections ); } if (value instanceof BaseQueryBuilder) { return value._bindCollectionSources( dbClient ? (options) => { const config = options; const collection = deferredCollections ? dbClient._materializeCollectionForRender(config) : dbClient.collection(config); if (deferredCollections && collection._deferSyncStart()) deferredCollections.add(collection); return collection; } : void 0 ); } if (value && typeof value === `object` && !isCollection(value) && `query` in value) { const pool = configPool(value); const { query, queryKey: _queryKey, client: _client, pool: _pool, ...config } = value; const preparedQuery = prepareLiveQueryValue( query, dbClient, deferredCollections ); if (preparedQuery === void 0 || preparedQuery === null) { return preparedQuery; } return { ...config, // A DbClient query never pools, so its identity must not depend on // pool: a server preload and a client hook must hash alike. ...pool && !dbClient ? { pool } : {}, query: preparedQuery }; } return value; } function getPreparedLiveQuerySources(value, onUnboundDescriptor) { if (isCollection(value)) return [value]; const query = queryConfig(value)?.query ?? value; if (query instanceof BaseQueryBuilder) { return collectSourceRefs(query._getQuery()).map( (source) => source.type === `descriptorRef` && onUnboundDescriptor ? onUnboundDescriptor(source.alias) : requireCollectionSource(source) ); } return isCollection(query) ? [query] : []; } function getLiveQueryRowOptions(value) { if (isCollection(value)) { const { config: config2 } = value; const internal = config2.utils?.[LIVE_QUERY_INTERNAL]; return [ internal?.hasCustomGetKey ? config2.getKey : void 0, config2.schema ]; } const config = value; return [config?.getKey, config?.schema]; } function getPreparedLiveQueryIdentity(value, pool) { if (isCollection(value)) return [`collection`, value.id]; if (value instanceof BaseQueryBuilder) { const pooled = pool?.getPooledQueryIdentity(value); return pooled === void 0 ? [`query`, getStableQueryBuilderHash(value)] : [`pooled`, pooled]; } if (value && typeof value === `object` && `query` in value) { const config = value; return [ // getKey and schema bind for this query identity. Inline options must // not turn a normal framework render into a new Collection. `config`, getPreparedLiveQueryIdentity(config.query, configPool(config)), [`singleResult`, config.singleResult === true], [ `defaultStringCollation`, getStringCollationIdentity(config.defaultStringCollation) ] ]; } if (value === void 0 || value === null) return [`disabled`]; return [`value`, value]; } function getLiveQueryHash(preparedValue, queryKey) { if (queryKey?.length) { const query = queryConfig(preparedValue)?.query; const ir = isCollection(preparedValue) ? getBuilderFromConfig(preparedValue.config)?.getWindow() : query instanceof BaseQueryBuilder ? query._getQuery() : void 0; const identity2 = [`queryKey`, queryKey]; if (ir && (ir.limit !== void 0 || (ir.offset ?? 0) !== 0)) identity2.push({ limit: ir.limit, offset: ir.offset ?? 0 }); return getStableValueHash(identity2, `queryKey`); } const identity = isCollection(preparedValue) ? [`collection`, preparedValue.id] : [`derived`, getPreparedLiveQueryIdentity(preparedValue)]; return getStableValueHash(identity, `queryKey`); } const resolvedQueryValues = /* @__PURE__ */ new WeakMap(); function queryConfig(value) { return value instanceof BaseQueryBuilder ? { query: value } : value && typeof value === `object` ? value : void 0; } function moveExistingWindow(after, previous, owner) { if (!previous || previous.status !== `ready` && previous.status !== `loading`) return; const retained = owner && resolvedQueryValues.get(owner); if (!retained || retained.collection !== previous) return; const before = retained.config; if (!(before.query instanceof BaseQueryBuilder) || !(after?.query instanceof BaseQueryBuilder)) return; const oldQuery = before.query._getQuery(); const query = after.query._getQuery(); const limit = query.limit; const offset = query.offset ?? 0; if (after.singleResult === true || query.singleResult || !query.orderBy?.length || oldQuery.limit === void 0 || limit === void 0 || !Number.isSafeInteger(limit) || limit < 0 || !Number.isSafeInteger(offset) || offset < 0) return; const keys = Object.keys(before); if (keys.length !== Object.keys(after).length || keys.some((key) => key !== `query` && before[key] !== after[key])) return; const sources = getPreparedLiveQuerySources(before); const nextSources = getPreparedLiveQuerySources(after); if (sources.length !== nextSources.length || sources.some((source, index) => source !== nextSources[index])) return; try { if (getQueryIdentity({ ...oldQuery, limit: void 0, offset: void 0 }) !== getQueryIdentity({ ...query, limit: void 0, offset: void 0 })) return; } catch (error) { if (error instanceof UnhashableQueryIRError) return; throw error; } const builder = getBuilderFromConfig(previous.config); if (!builder) return; return () => { queueMicrotask(() => { if (resolvedQueryValues.get(owner)?.collection !== previous || previous.subscriberCount === 0) return; const result = builder.setWindow({ offset, limit }); if (result !== true) void result.catch(() => { }); }); }; } function resolveLiveQueryValue(value, { gcTime, pool, startSync = true, previousCollection, windowOwner, onWindowChange } = {}) { if (previousCollection !== void 0) value = prepareLiveQueryValue(value, void 0); const input = queryConfig(value); const move = moveExistingWindow(input, previousCollection, windowOwner); if (move) { if (onWindowChange) onWindowChange(move); else move(); return previousCollection; } if (value === void 0 || value === null) return null; if (isCollection(value)) { value.startSyncImmediate(); return value; } if (input) { const requested = configPool(input); const { pool: _pool, ...given } = input; const pooling = pool === false ? void 0 : requested ?? pool; const built = pooling && typeof given.query === `function` ? given.query(new BaseQueryBuilder()) : void 0; const config = built instanceof BaseQueryBuilder ? { ...given, query: built } : given; const pooled = pooling && config.query instanceof BaseQueryBuilder && Object.keys(config).every((key) => key === `query` || key === `gcTime`) ? pooling.createPooledLiveQuery(config.query, { gcTime: config.gcTime ?? gcTime }) : void 0; const collection = pooled ?? createLiveQueryCollection({ ...config, startSync: config.startSync ?? startSync, gcTime: config.gcTime ?? gcTime }); if (previousCollection !== void 0 && windowOwner) resolvedQueryValues.set(windowOwner, { collection, config: input }); return collection; } throw new Error( devBuild() && process.env.NODE_ENV !== `production` ? `A live query must be a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof value}` : codedMessage(102, { type: typeof value }) ); } export { configPool, getLiveQueryHash, getLiveQueryRowOptions, getPreparedLiveQueryIdentity, getPreparedLiveQuerySources, prepareLiveQueryValue, rejectLiveQueryInput, releaseDeferredOnReclaim, resolveLiveQueryValue, resumeDeferredLiveQueryCollections }; //# sourceMappingURL=live-query-options.js.map