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