Skip to content

Persistence & Caching

Dice Chess Trainer uses localStorage to improve the user experience by persisting preferences and caching data for instant loading. We follow the Stale-While-Revalidate pattern for dynamic data.

We use localStorage for two main purposes:

  1. User Preferences: Storing settings that should survive page reloads and app restarts.
  2. State Persistence: Storing UI states like active filters to maintain context across sessions.

The PreferencesStore (src/lib/preferencesStore.svelte.ts) manages global settings:

  • preferredMode: The default view when opening the app (‘view’, ‘train’, ‘bookmarks’).
  • gamesPerPage: Pagination limit for game lists.
  • archiveOnFinish: Whether to automatically archive games after training.

In the GameListStore (src/lib/gameListStore.svelte.ts), current filters are saved whenever they are applied. This ensures that if a user navigates away or refreshes the page, their search criteria are preserved.

To overcome latency and provide a “snappy” UI, we implement caching for expensive data fetches.

The Stale-While-Revalidate pattern allows us to show data immediately while keeping it fresh:

  1. Initial Load: On store initialization, we check localStorage for a cached version of the data.
  2. Instant Render: If cache exists, we populate the UI immediately with this “stale” data.
  3. Background Refresh: Simultaneously, we trigger an async API request to fetch the latest data (“revalidate”).
  4. UI Update: Once the API response arrives, we update the store state and overwrite the cache.

We apply this pattern to the first page of the game lists with an additional layer of verification called Smart Cache:

  • We cache the first page (offset 0) for each tab: active, archived, and favorites.
  • Stored Payload: The cache includes both the items and the filters used to fetch them.
  • Verification: On restoration, the store compares the current filters with the cached filters. Data is only displayed instantly if they match exactly.
  • Cache Keys: gameListCache_active, gameListCache_archived, gameListCache_favorites.

This approach ensures that if a user frequently views a specific subset of games (e.g., “Expensive Games”), that view loads instantly upon app restart, while also preventing the “flash” of incorrect data if filters have changed.

When implementing persistence, use the helper methods provided in src/lib/preferencesStore.svelte.ts to handle localStorage safely (especially for Server-Side Rendering or environments where localStorage might be restricted):

function getStoredValue(key: string): string | null {
try {
return typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null;
} catch {
return null;
}
}
  1. Be Selective: Only cache data that significantly improves the perceived performance (e.g., the first page of a list). Avoid caching large amounts of data to prevent localStorage bloat (limit is typically 5MB).
  2. Handle Errors: Always wrap JSON.parse and localStorage access in try-catch blocks.
  3. Clear on Logout: (Planned) Ensure sensitive or user-specific cache is cleared when the user logs out.
  4. Test Coverage: Write unit tests to verify that data is correctly read from and written to a mocked localStorage.