The async function to cache
Cache configuration; see TtlCacheOptions
A drop-in replacement for fn that serves from cache when possible
const getCidsByCas = withTtlCache(getCidsByCasUncached, { namespace: "cidsByCas" });
await getCidsByCas("15681-89-7"); // fetches, then caches for 3 days
await getCidsByCas("15681-89-7"); // served from cache, no network call
export function withTtlCache<Args extends unknown[], Result>(
fn: (...args: Args) => Promise<Result>,
options: TtlCacheOptions<Args>,
): (...args: Args) => Promise<Result> {
const { namespace, ttlMs = THREE_DAYS_MS } = options;
const keyFromArgs = options.keyFromArgs ?? ((...args: Args) => JSON.stringify(args));
return async (...args: Args): Promise<Result> => {
const storageKey = `${CACHE_KEY_PREFIX}:${namespace}:${keyFromArgs(...args)}`;
// Try the cache first; any storage failure falls through to the source.
try {
const stored = await cstorage.local.get(storageKey);
const entry = stored[storageKey];
if (isCacheEntry<Result>(entry) && Date.now() - entry.cachedAt < ttlMs) {
return entry.value;
}
} catch (error) {
console.debug(`Cache read failed for "${storageKey}"; querying source`, error);
}
const result = await fn(...args);
// Skip caching undefined so failed/empty lookups aren't negatively cached for the whole TTL.
if (result !== undefined) {
try {
await cstorage.local.set({ [storageKey]: { cachedAt: Date.now(), value: result } });
} catch (error) {
console.debug(`Cache write failed for "${storageKey}"`, error);
}
}
return result;
};
}
Wraps an async function with a persistent, time-to-live cache backed by
chrome.storage.local. The returned function checks the cache first and only invokesfnon a miss or when the cached entry has expired. Only defined results are cached, so a failed or empty lookup (undefined) is never negatively cached. Any storage error is swallowed and the call falls through tofn, so the wrapper can never break a caller that runs withoutchrome.storage.