Introduction
This week I used Dexie.js with Svelte 5’s createSubscriber. This turned out to be a great way for me to implement my own simplified local-first solution. It did not require buying into larger frameworks, or setting up a significant amount of backend architecture.
Here’s what I learned.
Dexie.js
Dexie is a minimalistic wrapper for IndexedDB. Dexie isn’t Svelte-specific, but Svelte makes it easy to integrate any TypeScript / JavaScript library.
After defining a schema, you can read, update, and liveQuery from IndexedDB like this. For more examples, see this Dexie cheat sheet.
import { db } from '$lib/db';
// Read Once
export async function get_tasks_by_list_id(list_id: string): Promise<Task[]> {
return await db.tasks.where('tasks_list_id').equals(list_id).toArray()
}
// Upsert and Delete
await db.transaction('rw', db.tasks, async () => {
await db.tasks.bulkDelete(rowsToDelete);
await db.tasks.bulkPut(rowsToUpsert);
});
// Live Query
const subscription = liveQuery(() => get_tasks_by_list_id(list_id)).subscribe((rows) => {
tasks = rows;
});
subscription.unsubscribe(); Create Subscriber
Svelte 5’s createSubscriber integrates external event-based systems with Svelte’s reactivity. In this snippet, we manage the subscription to a Dexie.js liveQuery using createSubscriber.
This example passes listId as arg through a $derived. This allows us to recreate the liveQuery whenever the listId changes, returning reactive live queries that update with our Svelte state.
import { liveQuery } from 'dexie';
import { createSubscriber } from 'svelte/reactivity';
export class LiveQuery<T, A> {
#value: T | undefined;
#subscribe: () => void;
constructor(query: (arg: A) => T | Promise<T>, arg: A) {
const observable = liveQuery(() => query(arg));
this.#subscribe = createSubscriber((update) => {
const sub = observable.subscribe({
next: (value) => {
this.#value = value;
this.#error = undefined;
update();
},
});
return () => sub.unsubscribe();
});
}
get current() {
this.#subscribe();
return this.#value;
}
}
// Example Usage
const query = $derived(new LiveQuery(get_tasks_by_list_id, listId)); Note, calling subscribe in the getter felt weird to me at first. But, this is how the docs show it, and it enables automatic handling of our subscription.
This means the subscription will automatically start and stop whenever there is a reference to our getter. The interactive example below could also work by using query.current in an $effect.
1, listening for changes...{#if on}
{query.current}
{:else}
<!-- No getter -->
{/if} Tomatillo Updates
For Tomatillo Timer users, an update on the tasks feature:
- Tasks are potentially more performant on slow machines
- Tasks now rehydrate from the server on browser focus
- Fixed a bug where the explanation page wasn’t accessible
