react-inlinesvg
Version:
An SVG loader for React
206 lines (205 loc) • 6.28 kB
JavaScript
"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