How TCG Pocket Collection Tracker Caches Card Collections Locally Using localStorage: Keys, Validation, and Invalidation Strategies

The TCG Pocket Collection Tracker persists card collection data to the browser's localStorage using versioned, email-specific keys, validating freshness via timestamp comparison while automatically invalidating entries on data mutations, parsing errors, or storage quota violations.

The open-source project marcelpanse/tcg-pocket-collection-tracker eliminates unnecessary network round-trips to Supabase by implementing a robust client-side caching layer in TypeScript. By storing a user's card collection locally and comparing timestamps between the cache and the remote database, the application delivers instant UI updates while maintaining data consistency. This article examines the implementation details found in the collection service, including cache key generation, read/write mechanisms, and five distinct invalidation strategies.

Cache Key Structure and Storage Format

The service defines two constants in frontend/src/services/collection/collectionService.ts (lines 4–6) to generate unique keys per user:

  • tcg_collection_cache_v2_<email> – Stores the serialized collection data.
  • tcg_collection_timestamp_v2_<email> – Stores the ISO 8601 timestamp string representing when the cache was last updated.

Using email-specific keys ensures that multiple users on the same browser do not share cached data, while the _v2 prefix allows for future schema migrations without conflicts.

Reading from the Cache

The getCollectionFromCache(email) function retrieves and parses the stored data (lines 68–80). It first checks for localStorage availability, then uses JSON.parse() to deserialize the collection. Because JSON.stringify strips Date objects, the function converts ISO date strings back to native JavaScript Date objects during parsing.

If parsing fails due to corruption, the catch block (lines 81–90) invokes removeLocalCacheItems(email) to purge both keys, preventing the application from crashing on malformed data.

Cache Validation Logic

The primary entry point getCollection(email, collectionLastUpdatedRaw?) implements the validation strategy (lines 27–40). This function receives the remote collection's last-updated timestamp from the Supabase account record and performs the following check:

  1. Reads the cached timestamp from tcg_collection_timestamp_v2_<email>.
  2. Compares the cached timestamp against the remote collectionLastUpdatedRaw.
  3. If the cached timestamp is newer or equal and the data exists, returns the cached Map<internal_id, CollectionRow> immediately.
  4. Otherwise, falls back to fetchCollectionFromAPI(email), then writes the fresh data to the cache.

This timestamp comparison ensures the UI never displays stale data when another client has modified the collection.

Writing and Updating the Cache

The updateCollectionCache(collection, email, timestamp) function (lines 96–114) handles persistence. It guards against missing localStorage, serializes the collection to JSON, and writes both the data and the ISO timestamp string to their respective keys.

The service calls this function in three critical scenarios:

  • After a successful API fetch in getCollection (lines 50–53).
  • After a bulk update via updateCards (lines 55–56).
  • After a card deletion via deleteCard (lines 10–12).

To handle browser storage limits, the function includes a QuotaExceededError handler (lines 121–128). When the browser throws this DOMException, the service removes the existing cache entry for that email to free space, then retries the write operation.

Cache Invalidation Strategies

The tracker employs five distinct invalidation mechanisms to ensure consistency between localStorage and the remote Supabase database:

Post-Mutation Updates – After any successful write operation (add, update, or delete), the service immediately calls updateCollectionCache with the new server timestamp. This refreshes the in-memory latestFromCache map and updates both localStorage keys, ensuring subsequent reads return the modified data without an API call.

Mutation Error Recovery – If updateCards encounters an error during the database transaction, the catch block (lines 22–24) calls removeLocalCacheItems(email). This forces the next getCollection invocation to bypass the cache and fetch fresh data from the API, preventing the UI from displaying orphaned cache entries that lack corresponding database rows.

Corruption Detection – When getCollectionFromCache catches a SyntaxError during JSON parsing (lines 84–88), it automatically invokes the removal helper to delete both keys. This self-healing behavior prevents persistent crashes from malformed localStorage entries caused by browser crashes or manual tampering.

Storage Quota Management – As mentioned in the write logic, encountering a QuotaExceededError triggers the removal of the stale cache entry before retrying. This aggressive eviction policy prioritizes application functionality over cache longevity when disk space is constrained.

Explicit Programmatic Clearing – The exported helper removeLocalCacheItems(email) (lines 8–12) provides a public API for forcibly clearing the cache. Developers can invoke this function to implement "Refresh" buttons or logout cleanup routines that guarantee a clean slate on the next data fetch.

Summary

  • Versioned, email-scoped keys (tcg_collection_cache_v2_<email>) isolate user data and support schema evolution.
  • Timestamp comparison between cached data and the remote collectionLastUpdated field prevents stale reads without unnecessary API calls.
  • Defensive error handling automatically purges corrupted or quota-blocked cache entries via removeLocalCacheItems.
  • Eager cache updates after mutations ensure the UI reflects changes immediately while maintaining server consistency.
  • Graceful degradation falls back to API fetches whenever the cache is invalidated, missing, or newer than the server state.

Frequently Asked Questions

How does the tracker handle corrupted cache data?

When getCollectionFromCache attempts to parse the JSON stored in localStorage, any SyntaxError triggers an immediate call to removeLocalCacheItems(email), which deletes both the data and timestamp keys (lines 84–88). This ensures the application recovers gracefully from malformed entries by forcing a fresh API fetch on the next request.

What triggers a cache refresh from the API?

The getCollection function compares the remote collectionLastUpdatedRaw timestamp (from the Supabase account row) against the value stored under tcg_collection_timestamp_v2_<email>. If the remote timestamp is newer, or if no cached data exists, the service bypasses the cache and calls fetchCollectionFromAPI(email), subsequently writing the fresh data to localStorage (lines 27–40).

How is the storage quota exceeded error handled?

The updateCollectionCache function includes a specific catch block for DOMException with the name QuotaExceededError (lines 121–128). When triggered, the service removes the existing cache entry for that user to free space, then retries the write operation. If the retry fails, the error propagates to the caller.

Can developers force a cache invalidation programmatically?

Yes, the service exports removeLocalCacheItems(email), which synchronously removes both tcg_collection_cache_v2_<email> and tcg_collection_timestamp_v2_<email> from localStorage (lines 8–12). Invoking this function ensures the next call to getCollection will fetch fresh data from Supabase regardless of timestamp comparisons.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →