UNPKG

@tanstack/svelte-db

Version:

Svelte integration for @tanstack/db

203 lines (202 loc) 8.34 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, createLiveQueryCollection } from '@tanstack/db'; function toValue(value) { if (typeof value === `function`) { return value(); } return value; } // Implementation export function useLiveQuery(configOrQueryOrCollection, deps = []) { const collection = $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 isCollection = unwrappedParam && typeof unwrappedParam === `object` && typeof unwrappedParam.subscribeChanges === `function` && typeof unwrappedParam.startSyncImmediate === `function` && typeof unwrappedParam.id === `string`; if (isCollection) { // 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 unwrappedParam; } // Reference deps to make computed reactive to them deps.forEach((dep) => toValue(dep)); // Ensure we always start sync for Svelte helpers if (typeof unwrappedParam === `function`) { // Check if query function returns null/undefined (disabled query) const queryBuilder = new BaseQueryBuilder(); const result = unwrappedParam(queryBuilder); if (result === undefined || result === null) { // Disabled query - return null return null; } return createLiveQueryCollection({ query: unwrappedParam, startSync: true, }); } else { return createLiveQueryCollection({ ...unwrappedParam, startSync: true, }); } }); // Reactive state that gets updated granularly through change events const state = new SvelteMap(); // Reactive data array that maintains sorted order let internalData = $state([]); // Track collection status reactively let status = $state(collection ? collection.status : `disabled`); // Helper to sync data array from collection in correct order const syncDataFromCollection = (currentCollection) => { untrack(() => { internalData = []; internalData.push(...Array.from(currentCollection.values())); }); }; // Track current unsubscribe function let currentUnsubscribe = null; // Watch for collection changes and subscribe to updates $effect(() => { const currentCollection = collection; // Handle null collection (disabled query) if (!currentCollection) { status = `disabled`; untrack(() => { state.clear(); internalData = []; }); if (currentUnsubscribe) { currentUnsubscribe(); currentUnsubscribe = null; } return; } // Update status state whenever the effect runs status = currentCollection.status; // Clean up previous subscription if (currentUnsubscribe) { currentUnsubscribe(); } // Initialize state with current collection data untrack(() => { state.clear(); for (const [key, value] of currentCollection.entries()) { state.set(key, value); } }); // Initialize data array in correct order syncDataFromCollection(currentCollection); // Listen for the first ready event to catch status transitions // that might not trigger change events (fixes async status transition bug) currentCollection.onFirstReady(() => { // Update status directly - Svelte's reactivity system handles the update automatically // Note: We cannot use flushSync here as it's disallowed inside effects in async mode status = currentCollection.status; }); // Subscribe to collection changes with granular updates const subscription = currentCollection.subscribeChanges((changes) => { // Apply each change individually to the reactive state untrack(() => { 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; } } }); // Update the data array to maintain sorted order syncDataFromCollection(currentCollection); // Update status state on every change status = currentCollection.status; }, { includeInitialState: true, }); currentUnsubscribe = subscription.unsubscribe.bind(subscription); // Preload collection data if not already started if (currentCollection.status === `idle`) { currentCollection.preload().catch(console.error); } // Cleanup when effect is invalidated return () => { if (currentUnsubscribe) { currentUnsubscribe(); currentUnsubscribe = null; } }; }); return { get state() { return state; }, get data() { const currentCollection = collection; if (currentCollection) { const config = currentCollection.config; if (config.singleResult) { return internalData[0]; } } return internalData; }, get collection() { return 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`; }, }; }