Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / api / cache.ts
blob7103e5f763b6496828d7c2d6cae23bd69e65feb1
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();
46     }
48     return true;
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)}`);
55     return headers;
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 () => {
64     try {
65         const cache = await getCache();
66         const cacheKeys = (await cache?.keys()) ?? [];
68         const staleKeys = (
69             await Promise.all(
70                 cacheKeys.map(async (request) => {
71                     const res = await cache?.match(request).catch(noop);
72                     return res && shouldRevalidate(res) ? request : null;
73                 })
74             )
75         ).filter(truthy);
77         logger.debug(`[HttpCache] Removing ${staleKeys.length} stale cache entrie(s)`);
78         await Promise.all(staleKeys.map((req) => cache?.delete(req).catch(noop)));
79     } catch {}