Hone logo
Hone
Problems

Angular Persistent Cache Service

Modern web applications often benefit from caching data locally to improve performance and provide offline capabilities. This challenge focuses on creating a robust and persistent cache service in Angular that can store and retrieve data across browser sessions.

Problem Description

Your task is to design and implement an Angular service that acts as a persistent cache. This service should be able to store various types of data (e.g., strings, numbers, objects, arrays) associated with unique keys. Crucially, the cached data must persist even when the user closes and reopens their browser, leveraging the browser's localStorage or sessionStorage for persistence.

Key Requirements:

  1. Service Creation: Create an Angular service (e.g., PersistentCacheService).
  2. Storage Mechanisms: Implement methods to store data using localStorage (for session-independent persistence) and potentially sessionStorage (for session-dependent persistence, though localStorage is the primary focus).
  3. CRUD Operations:
    • setItem(key: string, value: any): void: Stores a value with a given key.
    • getItem(key: string): any | null: Retrieves a value associated with a key. Returns null if the key doesn't exist or the data is invalid.
    • removeItem(key: string): void: Removes a value associated with a key.
    • clear(): void: Clears all cached items.
  4. Data Serialization/Deserialization: Ensure that complex data types (objects, arrays) can be correctly serialized (e.g., using JSON.stringify) before storing and deserialized (e.g., using JSON.parse) upon retrieval.
  5. Error Handling: Implement graceful error handling for potential issues like exceeding storage limits or invalid data formats during retrieval.
  6. Type Safety: Use TypeScript to provide type safety for keys and values where appropriate.

Expected Behavior:

  • Data stored using setItem should be retrievable using getItem after the browser is closed and reopened.
  • getItem should return null for keys that have not been set or have been removed.
  • removeItem should effectively delete the item from the cache.
  • clear should empty the entire cache.
  • If localStorage is not available, the service should still function, but without persistence.

Edge Cases:

  • Storage Limits: localStorage has a limited size (typically 5-10MB). The service should handle cases where attempting to store data might exceed this limit.
  • Invalid Data: If data stored in localStorage becomes corrupted or is not valid JSON (if it was intended to be), getItem should return null or handle it gracefully without crashing the application.
  • Browser Support: Consider how the service behaves in browsers that might not support localStorage or have it disabled.

Examples

Example 1:

// Assume PersistentCacheService is injected into a component or another service
const cacheService: PersistentCacheService = // ... injected service

// Storing data
cacheService.setItem('userName', 'Alice');
cacheService.setItem('userSettings', { theme: 'dark', notifications: true });
cacheService.setItem('lastLoginTimestamp', Date.now());

// Retrieving data
const userName = cacheService.getItem('userName'); // Expected: 'Alice'
const userSettings = cacheService.getItem('userSettings'); // Expected: { theme: 'dark', notifications: true }
const nonExistent = cacheService.getItem('nonExistentKey'); // Expected: null

// Removing data
cacheService.removeItem('userName');
const userNameAfterRemoval = cacheService.getItem('userName'); // Expected: null

// Clearing all data
cacheService.clear();
const allItemsAfterClear = cacheService.getItem('userSettings'); // Expected: null

Example 2:

Scenario: Persistence Check

  1. In the browser:
    // In your Angular app
    cacheService.setItem('persistentData', { id: 123, message: 'Hello Persistence!' });
    console.log('Data stored.');
    
  2. Close the browser tab/window.
  3. Reopen the browser and navigate to the same application URL.
  4. In the browser console:
    // In your Angular app after reload
    const retrievedData = cacheService.getItem('persistentData');
    console.log('Retrieved data:', retrievedData);
    

Expected Output in Console (after reload):

Retrieved data: { id: 123, message: 'Hello Persistence!' }

Example 3: Handling Storage Limits (Conceptual)

If the localStorage is nearly full and an attempt is made to store a very large object:

const largeData = Array(1000000).fill('some string data'); // Potentially very large
try {
    cacheService.setItem('veryLargeData', largeData);
} catch (error) {
    console.error('Failed to store large data:', error); // Expected to catch an error related to storage quota exceeded
}

The setItem method should ideally throw an error or return a boolean indicating failure, which the calling code can then handle. The getItem method for this key should return null.

Constraints

  • The primary storage mechanism must be localStorage.
  • The service must be designed as an Angular injectable service.
  • All core operations (setItem, getItem, removeItem, clear) must be implemented.
  • The solution should gracefully handle the absence of localStorage in the browser environment, falling back to in-memory storage without persistence.
  • Performance for typical small to medium-sized data operations should be near-instantaneous. Operations involving larger data should not block the main thread for extended periods.

Notes

  • Consider using a unique prefix for your cache keys to avoid potential conflicts with other localStorage entries.
  • When storing objects or arrays, JSON.stringify and JSON.parse are your friends. Be mindful of what types JSON.stringify can handle (it cannot serialize functions or undefined directly within objects).
  • For handling storage limits, you might want to check the current size of localStorage before attempting to set a new item, or rely on the browser's QuotaExceededError exception.
  • Think about how to handle null or undefined values explicitly stored in the cache.
  • A possible strategy for handling localStorage unavailability is to wrap all localStorage operations in try...catch blocks and fall back to an in-memory array or Map if localStorage is not available or throws an error.
Loading editor...
typescript