IndexedDB Storage Layer
The $lib/idb.ts module provides a lightweight, promise-based wrapper around the browser's IndexedDB API. It replaces localStorage for stores that may accumulate significant data over time โ wishlists with hundreds of saves, notification preferences, recently viewed history, and uChat unread timestamps.
Why IndexedDB?
| Aspect | localStorage | IndexedDB |
|---|---|---|
| Size limit | ~5 MB per origin | ~50+ MB (browser-dependent) |
| API | Synchronous (blocks main thread) | Asynchronous (non-blocking) |
| Data model | String key-value only | Structured cloning (objects, arrays, blobs) |
| Transactions | None | Full ACID transactions |
| Querying | Manual JSON.parse on every read | Native indexing and cursors |
For stores holding a few small values (auth tokens, theme preference, org ID), localStorage remains appropriate. For stores that may grow with user activity, IndexedDB is the correct choice.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Svelte Stores (reactive) โ
โ โ
โ favorites.ts recentlyViewed.ts uchatStore.ts โ
โ wishlistStore.ts โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ $lib/idb.ts โ
โ idbGet / idbSet / idbDelete โ
โ idbClear / migrateFromLS โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ IndexedDB โ
โ "mallnline_store"โ
โ โ
โ โโ favorites โ
โ โโ recently_viewedโ
โ โโ notifications โ
โ โโ navigation โ
โ โโ uchat_unread โ
โโโโโโโโโโโโโโโโโโโโ
API Reference
`idbGet(store, key?)`
Read a value from IndexedDB.
import { idbGet, IDB_STORES } from '$lib/idb';
const items = await idbGet<string[]>(IDB_STORES.FAVORITES);
// โ ['luminara-crafts', 'pixel-forge'] or undefined
Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
store |
StoreName |
โ | Object store name (from IDB_STORES) |
key |
string |
'data' |
Key within the store |
Returns T | undefined. Resolves to undefined if the key doesn't exist or IndexedDB is unavailable.
`idbSet(store, value, key?)`
Write a value to IndexedDB (upsert semantics).
import { idbSet, IDB_STORES } from '$lib/idb';
await idbSet(IDB_STORES.FAVORITES, ['luminara-crafts', 'pixel-forge']);
Fire-and-forget pattern โ the calling store updates its reactive state synchronously, then persists asynchronously.
`idbDelete(store, key?)`
Delete a single key from an object store.
import { idbDelete, IDB_STORES } from '$lib/idb';
await idbDelete(IDB_STORES.RECENTLY_VIEWED, 'data');
`idbClear(store)`
Clear all entries in an object store.
import { idbClear, IDB_STORES } from '$lib/idb';
await idbClear(IDB_STORES.RECENTLY_VIEWED);
`migrateFromLocalStorage(lsKey, store, idbKey?, parser?)`
One-time migration utility. Reads from localStorage, writes to IndexedDB, then removes the localStorage key. Idempotent โ subsequent calls are no-ops.
import { migrateFromLocalStorage, IDB_STORES } from '$lib/idb';
const migrated = await migrateFromLocalStorage<string[]>(
'malet_favorites', // localStorage key
IDB_STORES.FAVORITES // target IDB store
);
// If migration occurred: returns parsed data
// If no localStorage key: returns undefined
IMPORTANT
The localStorage key is only removed after a successful IndexedDB write. If IndexedDB is unavailable, the migration fails silently and localStorage data is preserved.
Object Store Registry
export const IDB_STORES = {
FAVORITES: 'favorites', // Wishlist items + malet follows
RECENTLY_VIEWED: 'recently_viewed', // Recent malet visit history
NOTIFICATIONS: 'notifications', // Notification preferences (future)
NAVIGATION: 'navigation', // Sidebar state (future)
UCHAT_UNREAD: 'uchat_unread' // Last-read timestamps per conversation
} as const;
All stores are created in a single onupgradeneeded handler when the database is first opened (version 1).
Migrated Stores
`favorites.ts`
Before: localStorage.getItem('malet_favorites')
After: IDB_STORES.FAVORITES / key 'data'
The favorites store now uses idbGet for hydration at load and idbSet for persistence on follow/unfollow. On first load, migrateFromLocalStorage checks for existing localStorage data and moves it to IndexedDB.
`recentlyViewed.ts`
Before: localStorage.getItem('recently_viewed_malets')
After: IDB_STORES.RECENTLY_VIEWED / key 'data'
Same migration pattern. The store loads asynchronously from IndexedDB on import (browser only), with a loadFromIDB() function that handles migration.
`uchatStore.svelte.ts`
Before: localStorage.getItem('uchat_last_read')
After: IDB_STORES.UCHAT_UNREAD / key 'last_read'
The unread tracking timestamps (Record<conversationId, isoTimestamp>) are migrated to IndexedDB. The store initializes the lastReadTimestamps state from IDB in an async IIFE on module load.
What Stays on localStorage
| Key | Store | Rationale |
|---|---|---|
auth |
auth.ts |
Synchronous read needed on page load for auth checks |
theme |
themeStore.ts |
Synchronous read to prevent FOUC (flash of unstyled content) |
guestId |
auth.ts |
Tiny string, read synchronously for anonymous tracking |
orgId |
orgs.ts |
Tiny string, read synchronously for header injection |
These values are small (< 100 bytes each) and require synchronous access โ making localStorage the correct choice.
Design Principles
- Graceful degradation โ If IndexedDB is unavailable (e.g., private browsing in some engines), all operations return
undefinedor resolve silently. No errors thrown. - Single database โ One database (
mallnline_store) with multiple object stores. Avoids connection overhead. - Cached connection โ The
openDB()promise is memoized. Only one connection per page lifecycle. - Fire-and-forget writes โ UI updates synchronously via Svelte stores. IndexedDB syncs asynchronously. Users never wait for persistence.
- Automatic migration โ Existing localStorage data is transparently migrated on first access. Users don't notice the transition.
Related
- Wishlists & Collections โ Primary consumer of IndexedDB storage
- uChat Client SDK โ Uses IndexedDB for unread timestamp persistence
- Privacy & Security APIs โ Data export considerations for IndexedDB-stored data