@tanstack/svelte-db
Version:
Svelte integration for @tanstack/db
212 lines (211 loc) • 8.76 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, 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`;
},
};
}