@tanstack/svelte-db
Version:
Svelte integration for @tanstack/db
203 lines (202 loc) • 8.34 kB
JavaScript
// 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`;
},
};
}