UNPKG

@tanstack/svelte-db

Version:

Svelte integration for @tanstack/db

212 lines (211 loc) 8.76 kB
// eslint-disable-next-line import/no-duplicates -- See https://github.com/un-ts/eslint-plugin-import-x/issues/308 import { untrack } from 'svelte'; // eslint-disable-next-line import/no-duplicates -- See https://github.com/un-ts/eslint-plugin-import-x/issues/308 import { SvelteMap } from 'svelte/reactivity'; import { BaseQueryBuilder, UnhashableQueryIRError, createLiveQueryCollection, createLiveQueryObserver, getLiveQueryHash, getStableValueHash, isCollection, isSingleResultCollection, prepareLiveQueryValue, } from '@tanstack/db'; import { useOptionalDbClient } from './db-context.js'; function toValue(value) { if (typeof value === `function`) { return value(); } return value; } // Implementation export function useLiveQuery(configOrQueryOrCollection, deps = []) { const contextDbClient = useOptionalDbClient(); const resolved = $derived.by(() => { // First check if the original parameter might be a getter // by seeing if toValue returns something different than the original let unwrappedParam = configOrQueryOrCollection; try { const potentiallyUnwrapped = toValue(configOrQueryOrCollection); if (potentiallyUnwrapped !== configOrQueryOrCollection) { unwrappedParam = potentiallyUnwrapped; } } catch { // If toValue fails, use original parameter unwrappedParam = configOrQueryOrCollection; } // Check if it's already a collection by checking for specific collection methods const inputIsCollection = isCollection(unwrappedParam); const dbClient = inputIsCollection ? contextDbClient : (unwrappedParam?.client ?? contextDbClient); if (inputIsCollection) { // Warn when passing a collection directly with on-demand sync mode // In on-demand mode, data is only loaded when queries with predicates request it // Passing the collection directly doesn't provide any predicates, so no data loads const syncMode = unwrappedParam .config?.syncMode; if (syncMode === `on-demand`) { console.warn(`[useLiveQuery] Warning: Passing a collection with syncMode "on-demand" directly to useLiveQuery ` + `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\n\n` + `Instead, use a query builder function:\n` + ` const { data } = useLiveQuery((q) => q.from({ c: myCollection }).select(({ c }) => c))\n\n` + `Or switch to syncMode "eager" if you want all data to sync automatically.`); } // It's already a collection, ensure sync is started for Svelte helpers // Only start sync if the collection is in idle state if (unwrappedParam.status === `idle`) { unwrappedParam.startSyncImmediate(); } return { collection: unwrappedParam, client: dbClient, queryHash: getStableValueHash([`collection`, unwrappedParam.id], `queryKey`), resumeDeferredCollections: () => { }, }; } // Reference deps to make computed reactive to them const dependencyValues = deps.map((dep) => toValue(dep)); const deferredCollections = new Set(); const preparedValue = prepareLiveQueryValue(unwrappedParam, dbClient, deferredCollections); const configuredQueryKey = unwrappedParam?.queryKey; const queryKey = configuredQueryKey ? toValue(configuredQueryKey) : undefined; let queryHash; try { queryHash = deps.length > 0 && !queryKey ? getStableValueHash([`deps`, dependencyValues, getLiveQueryHash(preparedValue)], `queryKey`) : getLiveQueryHash(preparedValue, queryKey); } catch (error) { if (!(error instanceof UnhashableQueryIRError)) throw error; if (queryKey !== undefined) throw error; } let collection; if (preparedValue === undefined || preparedValue === null) { collection = null; } else if (isCollection(preparedValue)) { collection = preparedValue; } else if (preparedValue instanceof BaseQueryBuilder) { collection = createLiveQueryCollection({ query: preparedValue, startSync: true, }); } else { collection = createLiveQueryCollection({ ...preparedValue, startSync: true, }); } return { collection, client: dbClient, queryHash, resumeDeferredCollections: () => { for (const deferredCollection of deferredCollections) { deferredCollection._resumeSyncStart(); } deferredCollections.clear(); }, }; }); let currentResolved = untrack(() => resolved); let currentObserver = createLiveQueryObserver(currentResolved.collection, { client: currentResolved.client, queryHash: currentResolved.queryHash, onPreload: currentResolved.resumeDeferredCollections, }); const initialSnapshot = currentObserver.getServerSnapshot(); // Reactive state that gets updated granularly through change events const state = new SvelteMap(initialSnapshot.state ?? []); // Reactive data array that maintains sorted order let internalData = $state(Array.from(initialSnapshot.state?.values() ?? [])); // Track collection status reactively let status = $state(initialSnapshot.status); const syncFromObserver = (observer, changes) => { const snapshot = observer.getSnapshot(); status = snapshot.status; untrack(() => { if (changes && changes.length > 0) { for (const change of changes) { switch (change.type) { case `insert`: case `update`: state.set(change.key, change.value); break; case `delete`: state.delete(change.key); break; } } } else { state.clear(); for (const [key, value] of snapshot.state ?? []) { state.set(key, value); } } internalData = Array.from(snapshot.state?.values() ?? []); }); }; // Watch for collection changes and subscribe to updates $effect(() => { const nextResolved = resolved; if (nextResolved !== currentResolved) { currentObserver.dispose(); currentResolved = nextResolved; currentObserver = createLiveQueryObserver(nextResolved.collection, { client: nextResolved.client, queryHash: nextResolved.queryHash, onPreload: nextResolved.resumeDeferredCollections, }); syncFromObserver(currentObserver); } const observer = currentObserver; const unsubscribe = observer.subscribe((changes) => { syncFromObserver(observer, changes); }); currentResolved.resumeDeferredCollections(); syncFromObserver(observer); // Cleanup when effect is invalidated return () => { unsubscribe(); if (observer === currentObserver) observer.dispose(); }; }); return { get state() { return state; }, get data() { const currentCollection = resolved.collection; if (currentCollection && isSingleResultCollection(currentCollection)) { return internalData[0]; } return internalData; }, get collection() { return resolved.collection; }, get status() { return status; }, get isLoading() { return status === `loading`; }, get isReady() { return status === `ready` || status === `disabled`; }, get isIdle() { return status === `idle`; }, get isError() { return status === `error`; }, get isCleanedUp() { return status === `cleaned-up`; }, }; }