UNPKG

react-inlinesvg

Version:
206 lines (205 loc) 6.28 kB
"use client"; import React, { createContext, useContext, useState } from "react"; //#region src/config.ts const CACHE_NAME = "react-inlinesvg"; const STATUS = { IDLE: "idle", LOADING: "loading", LOADED: "loaded", FAILED: "failed", READY: "ready", UNSUPPORTED: "unsupported" }; //#endregion //#region src/modules/helpers.ts function randomCharacter(character) { return character[Math.floor(Math.random() * character.length)]; } function canUseDOM() { return !!(typeof window !== "undefined" && window.document?.createElement); } function isSupportedEnvironment() { return supportsInlineSVG() && typeof window !== "undefined" && window !== null; } /** * Remove properties from an object */ function omit(input, ...filter) { const output = {}; for (const key in input) if ({}.hasOwnProperty.call(input, key) && !filter.includes(key)) output[key] = input[key]; return output; } function randomString(length) { const letters = "abcdefghijklmnopqrstuvwxyz"; const charset = `${letters}${letters.toUpperCase()}1234567890`; let R = ""; for (let index = 0; index < length; index++) R += randomCharacter(charset); return R; } async function request(url, options) { const response = await fetch(url, options); const [fileType] = (response.headers.get("content-type") ?? "").split(/ ?; ?/); if (response.status > 299) throw new Error("Not found"); if (!["image/svg+xml", "text/plain"].some((d) => fileType.includes(d))) throw new Error(`Content type isn't valid: ${fileType}`); return response.text(); } function supportsInlineSVG() { /* c8 ignore next 3 */ if (!document) return false; const div = document.createElement("div"); div.innerHTML = "<svg />"; const svg = div.firstChild; return !!svg && svg.namespaceURI === "http://www.w3.org/2000/svg"; } //#endregion //#region src/modules/cache.ts var CacheStore = class { cacheApi; cacheStore; subscribers = []; isReady = false; constructor(options = {}) { const { name = CACHE_NAME, persistent = false } = options; this.cacheStore = /* @__PURE__ */ new Map(); if (persistent && canUseDOM() && "caches" in window) caches.open(name).then((cache) => { this.cacheApi = cache; }).catch((error) => { console.error(`Failed to open cache: ${error.message}`); this.cacheApi = void 0; }).finally(() => { this.isReady = true; const callbacks = [...this.subscribers]; this.subscribers.length = 0; callbacks.forEach((callback) => { try { callback(); } catch (error) { console.error(`Error in CacheStore subscriber callback: ${error.message}`); } }); }); else this.isReady = true; } onReady(callback) { if (this.isReady) { callback(); return () => {}; } this.subscribers.push(callback); return () => { const index = this.subscribers.indexOf(callback); if (index >= 0) this.subscribers.splice(index, 1); }; } waitForReady() { if (this.isReady) return Promise.resolve(); return new Promise((resolve) => { this.onReady(resolve); }); } async get(url, fetchOptions) { await this.fetchAndCache(url, fetchOptions); return this.cacheStore.get(url)?.content ?? ""; } getContent(url) { return this.cacheStore.get(url)?.content ?? ""; } set(url, data) { this.cacheStore.set(url, data); } isCached(url) { return this.cacheStore.get(url)?.status === STATUS.LOADED; } async fetchAndCache(url, fetchOptions) { 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 || void 0, async () => { this.cacheStore.set(url, { content: "", status: STATUS.IDLE }); await this.fetchAndCache(url, fetchOptions); }); const failed = this.cacheStore.get(url); if (failed?.status === STATUS.FAILED) { if (failed.error) throw failed.error; this.cacheStore.delete(url); 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) { const isAbort = error?.name === "AbortError"; this.cacheStore.set(url, { content: "", error: isAbort ? void 0 : error, status: STATUS.FAILED }); throw error; } } async fetchFromPersistentCache(url, fetchOptions) { const data = await this.cacheApi?.match(url); if (data) return data.text(); await this.cacheApi?.add(new Request(url, fetchOptions)); return await (await this.cacheApi?.match(url))?.text() ?? ""; } async handleLoading(url, signal, callback) { for (let retryCount = 0; retryCount < 10; 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(.1); } await callback(); } keys() { return [...this.cacheStore.keys()]; } data() { return [...this.cacheStore.entries()].map(([key, value]) => ({ [key]: value })); } async delete(url) { if (this.cacheApi) await this.cacheApi.delete(url); this.cacheStore.delete(url); } 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 * 1e3); }); } //#endregion //#region src/provider.tsx const CacheContext = createContext(null); function CacheProvider({ children, name }) { const [store] = useState(() => new CacheStore({ name, persistent: true })); return /* @__PURE__ */ React.createElement(CacheContext.Provider, { value: store }, children); } function useCacheStore() { return useContext(CacheContext); } //#endregion export { isSupportedEnvironment as a, request as c, canUseDOM as i, STATUS as l, useCacheStore as n, omit as o, CacheStore as r, randomString as s, CacheProvider as t }; //# sourceMappingURL=provider-D8ZFleLH.mjs.map