dexie / Dexie.js

A Minimalistic Wrapper for IndexedDB

Home Page:https://dexie.org

Repository from Github https://github.comdexie/Dexie.jsRepository from Github https://github.comdexie/Dexie.js

Async Loading Status

ericdudley opened this issue · comments

Hello, I am using Dexie.js for the first time with my Svelte5 app, and I am trying to figure out how to show the query async status lifecycle.

	let transactions = $derived.by(() => {
		// noop just to make it reactive
		prefix;
		startDate;
		endDate;

		return liveQuery(() =>
			db.tx
				.where('yyyyMMDd')
				.between(format(startDate, 'yyyy-MM-dd'), format(endDate, 'yyyy-MM-dd'), true, true)
				.and((tx) => !!tx.label?.startsWith(prefix ?? ''))
				.sortBy('yyyyMMDd')
		);
	});

For the initial load, I can check for transactions or $transactions being undefined, but after I change the query inputs I would like to show some "loading" UI while the new query is running. Likewise, if an error occurs, I would like to be able to show that.

What is the best practice for showing these intermediate loading + error states?

liveQuery() does not emit anything until a query finish. If arguments change and you gain a new store, Svelte will keep showing the value from the previous store until the new store emits something. For most cases this is good because it could otherwise have been causing unnecessary flickering. liveQuery emits errors according to the TC39 Observable proposal but Svelte stores does not consume errors.

The following helper should give what you need (not tested):

export function svelteLiveQuery<T>(querier: () => Promise<T>): Readable<QueryResult<T>> {
  return {
    subscribe(emit) {
      let current: QueryResult<T> = {
        value: null,
        error: null,
        isLoading: true,
      };
      emit(current); // immediately emit initial value before loading
      const subscription = liveQuery(querier).subscribe(
        (value) => {
          current.isLoading = false;
          current.error = null;
          current.value = value;
          emit(current);
        },
        (error) => {
          current.isLoading = false;
          current.error = error;
          emit(current);
        }
      );
      return () => subscription.unsubscribe();
    },
  };
}

interface QueryResult<T> {
  value: T | null;
  error: any;
  isLoading: boolean;
}

interface Readable<T> {
  subscribe(this: void, subscriber: (current: T) => void): () => void;
}

To use it:

<script>
  let transactions = $derived.by(() => {
		// noop just to make it reactive
		prefix;
		startDate;
		endDate;

		return svelteLiveQuery(() =>
			db.tx
				.where('yyyyMMDd')
				.between(format(startDate, 'yyyy-MM-dd'), format(endDate, 'yyyy-MM-dd'), true, true)
				.and((tx) => !!tx.label?.startsWith(prefix ?? ''))
				.sortBy('yyyyMMDd')
		);
	});
</script>

{#if $transactions.isLoading}
   <p>Loading...</p>
{/if}
{#if $transactions.error}
   <p>Error: $transactions.error</p>
{/if}
{#if $transactions.value}
<ul>
  {#each $transactions.value as tx (tx.id)}
    <li> ... </li>
  {/each}
</ul>
{/if}

There might be some even better helper (such as a liveQuery rune or something) that would feel more native to Svelte. I'm not a Svelte expert but any contribution would be welcome.

Note: I just updated the my code snippet with type annotations and renamed subscriber to emit