UNPKG

react-inlinesvg

Version:
206 lines (159 loc) 5.19 kB
import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from '../config'; import { StorageItem } from '../types'; import { canUseDOM, request } from './helpers'; export interface CacheStoreOptions { name?: string; persistent?: boolean; } export default class CacheStore { private cacheApi: Cache | undefined; private readonly cacheStore: Map<string, StorageItem>; private readonly subscribers: Array<() => void> = []; public isReady = false; constructor(options: CacheStoreOptions = {}) { const { name = CACHE_NAME, persistent = false } = options; this.cacheStore = new Map<string, StorageItem>(); const usePersistentCache = persistent && canUseDOM() && 'caches' in window; if (usePersistentCache) { // eslint-disable-next-line promise/catch-or-return caches .open(name) .then(cache => { this.cacheApi = cache; }) .catch(error => { // eslint-disable-next-line no-console console.error(`Failed to open cache: ${error.message}`); this.cacheApi = undefined; }) .finally(() => { this.isReady = true; // Copy to avoid mutation issues const callbacks = [...this.subscribers]; // Clear array efficiently this.subscribers.length = 0; callbacks.forEach(callback => { try { callback(); } catch (error: any) { // eslint-disable-next-line no-console console.error(`Error in CacheStore subscriber callback: ${error.message}`); } }); }); } else { this.isReady = true; } } public onReady(callback: () => void): () => void { if (this.isReady) { callback(); return () => {}; } this.subscribers.push(callback); return () => { const index = this.subscribers.indexOf(callback); if (index >= 0) { this.subscribers.splice(index, 1); } }; } private waitForReady(): Promise<void> { if (this.isReady) { return Promise.resolve(); } return new Promise(resolve => { this.onReady(resolve); }); } public async get(url: string, fetchOptions?: RequestInit) { await this.fetchAndCache(url, fetchOptions); return this.cacheStore.get(url)?.content ?? ''; } public getContent(url: string): string { return this.cacheStore.get(url)?.content ?? ''; } public set(url: string, data: StorageItem) { this.cacheStore.set(url, data); } public isCached(url: string) { return this.cacheStore.get(url)?.status === STATUS.LOADED; } private async fetchAndCache(url: string, fetchOptions?: RequestInit) { if (!this.isReady) { await this.waitForReady(); } const cache = this.cacheStore.get(url); if (cache?.status === STATUS.LOADED) { return; } if (cache?.status === STATUS.LOADING) { await this.handleLoading(url, fetchOptions?.signal || undefined, async () => { this.cacheStore.set(url, { content: '', status: STATUS.IDLE }); await this.fetchAndCache(url, fetchOptions); }); return; } this.cacheStore.set(url, { content: '', status: STATUS.LOADING }); try { const content = this.cacheApi ? await this.fetchFromPersistentCache(url, fetchOptions) : await request(url, fetchOptions); this.cacheStore.set(url, { content, status: STATUS.LOADED }); } catch (error: any) { this.cacheStore.set(url, { content: '', status: STATUS.FAILED }); throw error; } } private async fetchFromPersistentCache(url: string, fetchOptions?: RequestInit): Promise<string> { const data = await this.cacheApi?.match(url); if (data) { return data.text(); } await this.cacheApi?.add(new Request(url, fetchOptions)); const response = await this.cacheApi?.match(url); return (await response?.text()) ?? ''; } private async handleLoading( url: string, signal: AbortSignal | undefined, callback: () => Promise<void>, ) { for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) { if (signal?.aborted) { throw signal.reason instanceof Error ? signal.reason : new DOMException('The operation was aborted.', 'AbortError'); } if (this.cacheStore.get(url)?.status !== STATUS.LOADING) { return; } await sleep(0.1); } await callback(); } public keys(): Array<string> { return [...this.cacheStore.keys()]; } public data(): Array<Record<string, StorageItem>> { return [...this.cacheStore.entries()].map(([key, value]) => ({ [key]: value })); } public async delete(url: string) { if (this.cacheApi) { await this.cacheApi.delete(url); } this.cacheStore.delete(url); } public async clear() { if (this.cacheApi) { const keys = await this.cacheApi.keys(); await Promise.allSettled(keys.map(key => this.cacheApi!.delete(key))); } this.cacheStore.clear(); } } function sleep(seconds = 1) { return new Promise(resolve => { setTimeout(resolve, seconds * 1000); }); }