agmission/Development/client/docs/BROWSER_CACHE_SERVICE.md

4.6 KiB

BrowserCacheService

File: src/app/domain/services/browser-cache.service.ts

A generic, injectable Angular service that provides a typed read/write/invalidate API over the browser's Cache Storage API. Intended as a shared foundation for any feature that wants to cache HTTP responses across navigation events without a Service Worker.


Why Cache Storage?

Mechanism Survives navigation Survives page reload Configurable TTL Storage limit
Component state Memory
NgRx store ✓ (same tab) Memory
sessionStorage Manual ~5 MB
localStorage Manual ~5 MB
Cache Storage ✓ (per entry) Quota-managed

Cache Storage was chosen because it:

  • Is available in all modern browsers (Chrome 40+, Firefox 44+, Safari 11.1+)
  • Stores structured data alongside an expiry timestamp without size pressure
  • Is already used by Service Workers and the browser's native HTTP cache, so quota management is handled by the browser
  • Falls back gracefully (service becomes a no-op) when unavailable

API

@Injectable({ providedIn: 'root' })
class BrowserCacheService {

  get<T>(cacheName: string, key: string, maxAgeMs?: number): Observable<T | null>

  put<T>(cacheName: string, key: string, data: T): void

  invalidate(cacheName: string): void
}

get<T>(cacheName, key, maxAgeMs?)

Returns an Observable that emits the cached value (T) or null when:

  • The Cache Storage API is unavailable (e.g. older browser, unit test environment)
  • No entry exists for the given cacheName + key combination
  • The entry is older than maxAgeMs (default: 60 000 ms / 1 minute)

Errors from the Cache API are caught and converted to null — they never propagate to the caller.

put<T>(cacheName, key, data)

Stores data in the named cache bucket under key. A cachedAt timestamp is embedded alongside the data so staleness can be checked on the next get.

Fire-and-forget: errors are silently swallowed.

invalidate(cacheName)

Deletes the entire Cache Storage bucket for cacheName. This removes all entries for that feature in one call.

Fire-and-forget: errors are silently swallowed.


Cache key format

Internally, entries are stored under a pseudo-URL:

/browser-cache/<encodedCacheName>?<key>

This keeps entries within a single Cache Storage bucket readable via browser DevTools (Application → Cache Storage).


Adding a new feature cache

Create a typed facade service that delegates to BrowserCacheService. This keeps the cache name and TTL in one place and gives callers a clean domain API.

// src/app/domain/services/customer-cache.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { BrowserCacheService } from './browser-cache.service';
import { ICustomer } from '../../customers/models/customer.model';

const CACHE_NAME = 'agm-customer-list-v1';
const MAX_AGE_MS = 60_000; // 1 minute

@Injectable({ providedIn: 'root' })
export class CustomerCacheService {

  constructor(private readonly browserCache: BrowserCacheService) {}

  get(queryParams: string): Observable<ICustomer[] | null> {
    return this.browserCache.get<ICustomer[]>(CACHE_NAME, queryParams, MAX_AGE_MS);
  }

  put(queryParams: string, data: ICustomer[]): void {
    this.browserCache.put(CACHE_NAME, queryParams, data);
  }

  invalidate(): void {
    this.browserCache.invalidate(CACHE_NAME);
  }
}

Then, in the corresponding service:

// In CustomerService.loadCustomers():
const cacheKey = params.toString();

return this.customerCache.get(cacheKey).pipe(
  switchMap(cached => {
    if (cached !== null) return of(cached);
    return this.http.get<ICustomer[]>(this.url, { params }).pipe(
      tap(data => this.customerCache.put(cacheKey, data))
    );
  })
);

And in the effects, call this.customerCache.invalidate() after any create / update / delete action succeeds.


Existing implementations

Feature Facade Cache name TTL
Job list JobCacheService agm-jobs-list-v1 60 s

Versioning the cache name

Append a version suffix (e.g. -v1, -v2) to cacheName whenever the shape of the stored data changes. The old bucket will be orphaned in the browser until the browser's quota manager evicts it, or you can explicitly delete the old name during app initialisation.


Browser DevTools

Cached entries are visible under:

Chrome DevTools → Application tab → Cache Storage → agm-jobs-list-v1