Building a Persistent Cache Service in Angular
Caching is a crucial optimization technique for web applications, reducing server load and improving user experience by storing frequently accessed data locally. This challenge asks you to create a persistent cache service in Angular that utilizes the browser's localStorage to store cached data, ensuring data persists across browser sessions. This service should allow you to cache data, retrieve it, and invalidate specific cache entries.
Problem Description
You need to develop an Angular service called PersistentCacheService that provides the following functionalities:
set(key: string, data: any, expirySeconds?: number): Stores data in the cache using the provided key. Optionally, an expiry time in seconds can be specified. If an expiry time is provided, the cached data should be automatically removed after the specified duration.get(key: string): Observable<any>: Retrieves data from the cache using the provided key. Returns an Observable that emits the cached data if found and hasn't expired. If the data is not found or has expired, the Observable should emitnull.delete(key: string): Removes a specific entry from the cache using the provided key.clear(): Removes all entries from the cache.
The service should use localStorage for persistence. The expiry mechanism should use setTimeout to remove expired entries. The get method should return an Observable to handle asynchronous expiry checks and potential data retrieval failures gracefully.
Key Requirements:
- The service must be injectable into Angular components.
- Data should be serialized and deserialized correctly when storing and retrieving from
localStorage. UseJSON.stringifyandJSON.parse. - Expiry times should be handled accurately.
- Error handling should be considered (e.g.,
localStorageunavailable). - The service should be thread-safe (consider potential race conditions if multiple components access the cache simultaneously). While perfect thread safety is difficult to guarantee in JavaScript, strive to minimize potential issues.
Expected Behavior:
- Data stored using
setshould be available in subsequent browser sessions. - Data with an expiry time should be automatically removed after the specified duration.
getshould return cached data immediately if available and not expired.deleteshould remove the specified entry.clearshould remove all entries.- The service should handle cases where
localStorageis unavailable (e.g., in private browsing mode).
Edge Cases to Consider:
localStorageis full. (While you don't need to implement complex eviction strategies, be aware of this limitation.)- Invalid keys (e.g., null, undefined, empty string).
- Data that cannot be serialized/deserialized by
JSON. - Expiry times that are zero or negative.
Examples
Example 1:
Input:
cacheService.set('user', { id: 1, name: 'John Doe' }, 60); // Cache user data for 60 seconds
cacheService.get('user').subscribe(userData => { console.log(userData); }); // Should log {id: 1, name: 'John Doe'} immediately
setTimeout(() => { cacheService.get('user').subscribe(userData => { console.log(userData); }); }, 61000); // After 61 seconds, should log null
Output:
{id: 1, name: 'John Doe'}
null
Explanation: The user data is cached and retrieved immediately. After 61 seconds (slightly more than the expiry time), the data is automatically removed, and get returns null.
Example 2:
Input:
cacheService.set('settings', { theme: 'dark' });
cacheService.get('settings').subscribe(settings => { console.log(settings); }); // Should log {theme: 'dark'}
cacheService.delete('settings');
cacheService.get('settings').subscribe(settings => { console.log(settings); }); // Should log null
Output:
{theme: 'dark'}
null
Explanation: The settings are cached, retrieved, and then deleted. A subsequent get call returns null.
Example 3:
Input:
cacheService.set('data', 'some data');
localStorage.clear(); // Simulate localStorage being cleared
cacheService.get('data').subscribe(data => { console.log(data); }); // Should log null
Output:
null
Explanation: Clearing localStorage removes the cached data, and get returns null.
Constraints
- The service must be written in TypeScript.
- The service must use
localStoragefor persistence. - Expiry times are in seconds.
- The
getmethod must return anObservable<any>. - The service should be reasonably performant. Avoid unnecessary computations or operations.
- The maximum size of data stored in
localStorageis limited by the browser. While you don't need to implement a complex eviction strategy, be mindful of this limitation.
Notes
- Consider using RxJS operators like
takeUntilto manage the lifecycle of the Observables returned byget. - Think about how to handle potential errors when accessing
localStorage. - The expiry mechanism should be implemented using
setTimeout. - Focus on clarity, readability, and maintainability of the code.
- While perfect thread safety is difficult to achieve in JavaScript, consider potential race conditions and try to minimize them. Using
localStoragedirectly is inherently not thread-safe, but the service's internal logic can be designed to reduce the likelihood of issues.