2024-04-13 01:17:43 +00:00
|
|
|
import isElectron from "is-electron";
|
|
|
|
|
2024-04-12 09:11:33 +00:00
|
|
|
const cacheNames = [
|
|
|
|
"thumbs",
|
|
|
|
"face-crops",
|
2024-04-11 15:24:53 +00:00
|
|
|
// Desktop app only
|
2024-04-12 09:11:33 +00:00
|
|
|
"files",
|
|
|
|
] as const;
|
|
|
|
|
2024-04-12 14:54:48 +00:00
|
|
|
/**
|
|
|
|
* Namespaces into which our caches data is divided
|
|
|
|
*
|
|
|
|
* Note that namespaces are just arbitrary (but predefined) strings to split the
|
|
|
|
* cached data into "folders", so to speak.
|
|
|
|
* */
|
2024-04-12 09:11:33 +00:00
|
|
|
export type CacheName = (typeof cacheNames)[number];
|
2024-04-11 15:24:53 +00:00
|
|
|
|
2024-04-12 14:40:14 +00:00
|
|
|
/**
|
|
|
|
* A namespaced cache.
|
|
|
|
*
|
2024-04-12 14:54:48 +00:00
|
|
|
* This cache is suitable for storing large amounts of data (entire files).
|
|
|
|
*
|
2024-04-12 14:40:14 +00:00
|
|
|
* To obtain a cache for a given namespace, use {@link openCache}. To clear all
|
|
|
|
* cached data (e.g. during logout), use {@link clearCaches}.
|
|
|
|
*
|
2024-04-12 14:54:48 +00:00
|
|
|
* [Note: Caching files]
|
|
|
|
*
|
2024-04-12 14:40:14 +00:00
|
|
|
* The underlying implementation of the cache is different depending on the
|
|
|
|
* runtime environment.
|
|
|
|
*
|
|
|
|
* * The preferred implementation, and the one that is used when we're running
|
|
|
|
* in a browser, is to use the standard [Web
|
|
|
|
* Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
|
|
|
|
*
|
|
|
|
* * However when running under Electron (when this code runs as part of our
|
|
|
|
* desktop app), a custom OPFS based cache is used instead. This is because
|
|
|
|
* Electron currently doesn't support using standard Web Cache API for data
|
|
|
|
* served by a custom protocol handler (See this
|
|
|
|
* [issue](https://github.com/electron/electron/issues/35033), and the
|
|
|
|
* underlying restriction that comes from
|
|
|
|
* [Chromium](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/cache_storage/cache.cc;l=83-87?q=%22Request%20scheme%20%27%22&ss=chromium))
|
|
|
|
*
|
|
|
|
* [OPFS](https://web.dev/articles/origin-private-file-system) stands for Origin
|
|
|
|
* Private File System. It is a recent API that allows a web site to store
|
|
|
|
* reasonably large amounts of data. One option (that may still become possible
|
|
|
|
* in the future) was to always use OPFS for caching instead of this dual
|
|
|
|
* implementation, however currently [Safari does not support writing to OPFS
|
|
|
|
* outside of web
|
|
|
|
* workers](https://webkit.org/blog/12257/the-file-system-access-api-with-origin-private-file-system/)
|
|
|
|
* ([the WebKit bug](https://bugs.webkit.org/show_bug.cgi?id=231706)), so it's
|
|
|
|
* not trivial to use this as a full on replacement of the Web Cache in the
|
|
|
|
* browser. So for now we go with this split implementation.
|
2024-04-12 14:54:48 +00:00
|
|
|
*
|
|
|
|
* See also: [Note: Increased disk cache for the desktop app].
|
2024-04-12 14:40:14 +00:00
|
|
|
*/
|
2024-04-12 14:54:48 +00:00
|
|
|
export interface EnteCache {
|
|
|
|
/**
|
|
|
|
* Get the data corresponding to {@link key} (if found) from the cache.
|
|
|
|
*/
|
2024-04-13 01:49:11 +00:00
|
|
|
get: (key: string) => Promise<Uint8Array | undefined>;
|
|
|
|
match: (key: string) => Promise<Response | undefined>;
|
2024-04-12 14:54:48 +00:00
|
|
|
/**
|
|
|
|
* Add the given {@link key}-value ({@link data}) pair to the cache.
|
|
|
|
*/
|
2024-04-11 15:24:53 +00:00
|
|
|
put: (key: string, data: Response) => Promise<void>;
|
2024-04-12 14:54:48 +00:00
|
|
|
/**
|
|
|
|
* Delete the data corresponding to the given {@link key}.
|
|
|
|
*
|
|
|
|
* The returned promise resolves to `true` if a cache entry was found,
|
|
|
|
* otherwise it resolves to `false`.
|
|
|
|
* */
|
2024-04-11 15:24:53 +00:00
|
|
|
delete: (key: string) => Promise<boolean>;
|
|
|
|
}
|
|
|
|
|
2024-04-12 14:54:48 +00:00
|
|
|
/**
|
|
|
|
* Return the {@link EnteCache} corresponding to the given {@link name}.
|
|
|
|
*
|
|
|
|
* @param name One of the arbitrary but predefined namespaces of type
|
|
|
|
* {@link CacheName} which group related data and allow us to use the same key
|
|
|
|
* across namespaces.
|
|
|
|
*/
|
2024-04-13 01:49:11 +00:00
|
|
|
export const openCache = async (name: CacheName): Promise<EnteCache> =>
|
2024-04-13 01:17:43 +00:00
|
|
|
isElectron() ? openOPFSCacheWeb(name) : openWebCache(name);
|
2024-04-12 15:22:33 +00:00
|
|
|
|
|
|
|
/** An implementation of {@link EnteCache} using Web Cache APIs */
|
|
|
|
const openWebCache = async (name: CacheName) => {
|
|
|
|
const cache = await caches.open(name);
|
|
|
|
return {
|
2024-04-13 01:49:11 +00:00
|
|
|
get: async (key: string) => {
|
|
|
|
// return cache.match(key);
|
|
|
|
const _ = await cache.match(key);
|
|
|
|
return undefined;
|
|
|
|
},
|
2024-04-12 15:24:48 +00:00
|
|
|
match: (key: string) => {
|
2024-04-12 15:22:33 +00:00
|
|
|
return cache.match(key);
|
|
|
|
},
|
|
|
|
put: (key: string, data: Response) => {
|
|
|
|
return cache.put(key, data);
|
|
|
|
},
|
|
|
|
delete: (key: string) => {
|
|
|
|
return cache.delete(key);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
/** An implementation of {@link EnteCache} using OPFS */
|
|
|
|
const openOPFSCacheWeb = async (name: CacheName) => {
|
2024-04-12 15:42:37 +00:00
|
|
|
// While all major browsers support OPFS now, their implementations still
|
|
|
|
// have various quirks. However, we don't need to handle all possible cases
|
|
|
|
// and can just instead use the APIs and guarantees Chromium provides since
|
|
|
|
// this code will only run in our Electron app (which'll use Chromium as the
|
|
|
|
// renderer).
|
|
|
|
//
|
2024-04-13 01:17:43 +00:00
|
|
|
// So for our purpose, these can serve as the doc for what's available:
|
2024-04-12 15:42:37 +00:00
|
|
|
// https://web.dev/articles/origin-private-file-system
|
2024-04-12 09:15:03 +00:00
|
|
|
const cache = await caches.open(name);
|
2024-04-12 15:42:37 +00:00
|
|
|
|
|
|
|
const root = await navigator.storage.getDirectory();
|
|
|
|
const _caches = await root.getDirectoryHandle("cache", { create: true });
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
const _cache = await _caches.getDirectoryHandle(name, { create: true });
|
|
|
|
|
2024-04-12 09:15:03 +00:00
|
|
|
return {
|
2024-04-13 01:49:11 +00:00
|
|
|
get: async (key: string) => {
|
|
|
|
const _ = await cache.match(key);
|
|
|
|
return undefined;
|
|
|
|
},
|
2024-04-12 15:24:48 +00:00
|
|
|
match: (key: string) => {
|
2024-04-12 15:53:22 +00:00
|
|
|
// try {
|
|
|
|
// const fileHandle = _cache.getFileHandle(key);
|
|
|
|
// const file = await fileHandle.getFile();
|
|
|
|
// } catch (e) {
|
|
|
|
// if (e instanceof DOMException && e.name == "NotFoundError")
|
|
|
|
// return undefined;
|
|
|
|
// throw e;
|
|
|
|
// }
|
2024-04-12 09:15:03 +00:00
|
|
|
return cache.match(key);
|
|
|
|
},
|
2024-04-12 15:53:22 +00:00
|
|
|
put: async (key: string, data: Response) => {
|
|
|
|
// const fileHandle = await _cache.getFileHandle(key, { create: true })
|
|
|
|
// await fileHandle.write(data);
|
|
|
|
// await fileHandle.close();
|
|
|
|
await cache.put(key, data);
|
2024-04-12 14:54:48 +00:00
|
|
|
},
|
|
|
|
delete: (key: string) => {
|
2024-04-12 15:53:22 +00:00
|
|
|
// try {
|
|
|
|
// await _cache.removeEntry(key);
|
|
|
|
// return true;
|
|
|
|
// } catch (e) {
|
|
|
|
// if (e instanceof DOMException && e.name == "NotFoundError")
|
|
|
|
// return false;
|
|
|
|
// throw e;
|
|
|
|
// }
|
2024-04-12 14:54:48 +00:00
|
|
|
return cache.delete(key);
|
|
|
|
},
|
2024-04-12 09:15:03 +00:00
|
|
|
};
|
|
|
|
};
|
2024-04-11 15:24:53 +00:00
|
|
|
|
|
|
|
export async function cached(
|
2024-04-12 09:15:03 +00:00
|
|
|
cacheName: CacheName,
|
2024-04-11 15:24:53 +00:00
|
|
|
id: string,
|
|
|
|
get: () => Promise<Blob>,
|
|
|
|
): Promise<Blob> {
|
2024-04-12 15:28:00 +00:00
|
|
|
const cache = await openCache(cacheName);
|
2024-04-11 15:24:53 +00:00
|
|
|
const cacheResponse = await cache.match(id);
|
|
|
|
|
|
|
|
let result: Blob;
|
|
|
|
if (cacheResponse) {
|
|
|
|
result = await cacheResponse.blob();
|
|
|
|
} else {
|
|
|
|
result = await get();
|
|
|
|
|
|
|
|
try {
|
|
|
|
await cache.put(id, new Response(result));
|
|
|
|
} catch (e) {
|
|
|
|
// TODO: handle storage full exception.
|
|
|
|
console.error("Error while storing file to cache: ", id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete all cached data.
|
|
|
|
*
|
|
|
|
* Meant for use during logout, to reset the state of the user's account.
|
|
|
|
*/
|
2024-04-12 15:42:37 +00:00
|
|
|
export const clearCaches = async () =>
|
2024-04-13 01:17:43 +00:00
|
|
|
isElectron() ? clearOPFSCaches() : clearWebCaches();
|
2024-04-12 15:42:37 +00:00
|
|
|
|
|
|
|
export const clearWebCaches = async () => {
|
2024-04-12 09:11:33 +00:00
|
|
|
await Promise.all(cacheNames.map((name) => caches.delete(name)));
|
2024-04-11 15:24:53 +00:00
|
|
|
};
|
2024-04-12 15:42:37 +00:00
|
|
|
|
|
|
|
export const clearOPFSCaches = async () => {
|
|
|
|
const root = await navigator.storage.getDirectory();
|
|
|
|
await root.removeEntry("cache", { recursive: true });
|
|
|
|
};
|