1 import type { Maybe, MaybeNull } from '@proton/pass/types';
2 import { truthy } from '@proton/pass/utils/fp/predicates';
3 import { logger } from '@proton/pass/utils/logger';
4 import noop from '@proton/utils/noop';
5 import randomIntFromInterval from '@proton/utils/randomIntFromInterval';
7 export const CACHE_KEY = 'Pass::Http::Cache';
8 export const CACHED_IMAGE_DEFAULT_MAX_AGE = 1_209_600; /* 14 days */
9 export const CACHED_IMAGE_FALLBACK_MAX_AGE = 86_400; /* 1 day */
11 /** Returns the Cache Storage API if available.
12 * Will not be defined on:
13 * - Non-secure contexts (non-HTTPS/localhost)
14 * - Safari Private/Lockdown mode
15 * - Browsers without Cache API support */
16 export const getCacheStorage = (): Maybe<CacheStorage> => globalThis?.caches;
18 export const getResponseMaxAge = (response: Response): MaybeNull<number> => {
19 const cacheControlHeader = response.headers.get('Cache-Control');
20 const maxAge = cacheControlHeader?.match(/max-age=(\d+)/)?.[1];
21 return maxAge ? parseInt(maxAge, 10) : null;
24 export const getResponseDate = (response: Response): MaybeNull<Date> => {
25 const dateHeader = response.headers.get('Date');
26 return dateHeader ? new Date(dateHeader) : null;
29 /** stale-while-revalidate approach :
30 * Should account for stale-while-revalidate window
31 * when backend supports this cache header directive.
32 * Leveraging standard Cache-Control directives:
33 * - `max-age: <seconds>`
34 * - `stale-while-revalidate: <seconds>` <- FIXME
35 * would allow us not to query the remote API on every request,
36 * as long as the cache response is "fresh", and only perform the
37 * request when the cache response is "stale" */
38 export const shouldRevalidate = (response: Response): boolean => {
39 const maxAge = getResponseMaxAge(response);
40 const date = getResponseDate(response);
42 if (maxAge !== null && date) {
43 const now = new Date();
44 date.setSeconds(date.getSeconds() + maxAge);
45 return date.getTime() < now.getTime();
51 export const withMaxAgeHeaders = (res: Response, maxAge: number): Headers => {
52 const headers = new Headers(res.headers);
53 headers.set('Date', res.headers.get('Date') ?? new Date().toUTCString());
54 headers.set('Cache-Control', `max-age=${maxAge + randomIntFromInterval(0, 3_600)}`);
58 export const getCache = async (): Promise<Maybe<Cache>> => getCacheStorage()?.open(CACHE_KEY).catch(noop);
59 export const clearCache = async (): Promise<Maybe<boolean>> => getCacheStorage()?.delete(CACHE_KEY).catch(noop);
61 /** Opens the http cache and wipes every stale
62 * entries. This allows triggering revalidation */
63 export const cleanCache = async () => {
65 const cache = await getCache();
66 const cacheKeys = (await cache?.keys()) ?? [];
70 cacheKeys.map(async (request) => {
71 const res = await cache?.match(request).catch(noop);
72 return res && shouldRevalidate(res) ? request : null;
77 logger.debug(`[HttpCache] Removing ${staleKeys.length} stale cache entrie(s)`);
78 await Promise.all(staleKeys.map((req) => cache?.delete(req).catch(noop)));