diff --git a/README.md b/README.md
index c1ecacf..1e66b6b 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,7 @@ This will receive a single argument with:
**onLoad** {function}.
A callback to be invoked upon successful load.
-This will receive 2 arguments: the `src` prop and a `hasCache` boolean
+This will receive 2 arguments: the `src` prop and a `isCached` boolean
**preProcessor** {function} ▶︎ `string`
A function to process the contents of the SVG text before parsing.
@@ -126,7 +126,7 @@ Create unique IDs for each icon.
description="The React logo"
loader={Loading...}
onError={(error) => console.log(error.message)}
- onLoad={(src, hasCache) => console.log(src, hasCache)}
+ onLoad={(src, isCached) => console.log(src, isCached)}
preProcessor={(code) => code.replace(/fill=".*?"/g, 'fill="currentColor"')}
src="https://cdn.svgporn.com/logos/react.svg"
title="React"
diff --git a/src/cache.ts b/src/cache.ts
new file mode 100644
index 0000000..a7ec042
--- /dev/null
+++ b/src/cache.ts
@@ -0,0 +1,77 @@
+import { CACHE_MAX_RETRIES, STATUS } from './config';
+import { request, sleep } from './helpers';
+import { StorageItem } from './types';
+
+export class CacheStore {
+ private readonly cacheStore: Map;
+
+ constructor() {
+ this.cacheStore = new Map();
+ }
+
+ public async get(url: string, fetchOptions?: RequestInit) {
+ await this.fetchAndAddToInternalCache(url, fetchOptions);
+
+ 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 fetchAndAddToInternalCache(url: string, fetchOptions?: RequestInit) {
+ const cache = this.cacheStore.get(url);
+
+ if (cache?.status === STATUS.LOADING) {
+ let retryCount = 0;
+
+ // eslint-disable-next-line no-await-in-loop
+ while (
+ this.cacheStore.get(url)?.status === STATUS.LOADING &&
+ retryCount < CACHE_MAX_RETRIES
+ ) {
+ // eslint-disable-next-line no-await-in-loop
+ await sleep(0.1);
+ retryCount += 1;
+ }
+
+ if (retryCount >= CACHE_MAX_RETRIES) {
+ this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
+ await this.fetchAndAddToInternalCache(url, fetchOptions);
+ }
+
+ return;
+ }
+
+ if (!cache?.content) {
+ this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
+
+ try {
+ const content = 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;
+ }
+ }
+ }
+
+ public keys(): Array {
+ return [...this.cacheStore.keys()];
+ }
+
+ public delete(url: string) {
+ this.cacheStore.delete(url);
+ }
+
+ public clear() {
+ this.cacheStore.clear();
+ }
+}
+
+export default new CacheStore();
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..0c32644
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,10 @@
+export const CACHE_MAX_RETRIES = 10;
+
+export const STATUS = {
+ IDLE: 'idle',
+ LOADING: 'loading',
+ LOADED: 'loaded',
+ FAILED: 'failed',
+ READY: 'ready',
+ UNSUPPORTED: 'unsupported',
+} as const;
diff --git a/src/helpers.ts b/src/helpers.ts
index 1e12268..7a25691 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -2,15 +2,6 @@ import { canUseDOM as canUseDOMFlag } from 'exenv';
import type { PlainObject } from './types';
-export const STATUS = {
- IDLE: 'idle',
- LOADING: 'loading',
- LOADED: 'loaded',
- FAILED: 'failed',
- READY: 'ready',
- UNSUPPORTED: 'unsupported',
-} as const;
-
export function canUseDOM(): boolean {
return canUseDOMFlag;
}
@@ -19,6 +10,28 @@ export function isSupportedEnvironment(): boolean {
return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;
}
+export async function request(url: string, options?: RequestInit) {
+ const response = await fetch(url, options);
+ const contentType = response.headers.get('content-type');
+ const [fileType] = (contentType || '').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();
+}
+
+export function sleep(seconds = 1) {
+ return new Promise(resolve => {
+ setTimeout(resolve, seconds * 1000);
+ });
+}
+
export function supportsInlineSVG(): boolean {
/* istanbul ignore next */
if (!document) {
diff --git a/src/index.tsx b/src/index.tsx
index 880dec9..b5df742 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,10 +1,10 @@
import * as React from 'react';
import convert from 'react-from-dom';
-import { canUseDOM, isSupportedEnvironment, omit, randomString, STATUS } from './helpers';
-import { FetchError, Props, State, Status, StorageItem } from './types';
-
-export const cacheStore: { [key: string]: StorageItem } = Object.create(null);
+import cacheStore from './cache';
+import { STATUS } from './config';
+import { canUseDOM, isSupportedEnvironment, omit, randomString, request } from './helpers';
+import { FetchError, Props, State, Status } from './types';
export default class InlineSVG extends React.PureComponent {
private readonly hash: string;
@@ -22,7 +22,7 @@ export default class InlineSVG extends React.PureComponent {
this.state = {
content: '',
element: null,
- hasCache: !!props.cacheRequests && !!cacheStore[props.src],
+ isCached: !!props.cacheRequests && cacheStore.isCached(props.src),
status: STATUS.IDLE,
};
@@ -66,13 +66,13 @@ export default class InlineSVG extends React.PureComponent {
return;
}
- const { hasCache, status } = this.state;
+ const { isCached, status } = this.state;
const { onLoad, src } = this.props;
if (previousState.status !== STATUS.READY && status === STATUS.READY) {
/* istanbul ignore else */
if (onLoad) {
- onLoad(src, hasCache);
+ onLoad(src, isCached);
}
}
@@ -91,6 +91,14 @@ export default class InlineSVG extends React.PureComponent {
this.isActive = false;
}
+ private fetchContent = async () => {
+ const { fetchOptions, src } = this.props;
+
+ const content: string = await request(src, fetchOptions);
+
+ this.handleLoad(content);
+ };
+
private getElement() {
try {
const node = this.getNode() as Node;
@@ -178,7 +186,7 @@ export default class InlineSVG extends React.PureComponent {
this.setState(
{
content,
- hasCache,
+ isCached: hasCache,
status: STATUS.LOADED,
},
this.getElement,
@@ -193,18 +201,11 @@ export default class InlineSVG extends React.PureComponent {
{
content: '',
element: null,
- hasCache: false,
+ isCached: false,
status: STATUS.LOADING,
},
- () => {
- const { cacheRequests, src } = this.props;
- const cache = cacheRequests && cacheStore[src];
-
- if (cache && cache.status === STATUS.LOADED) {
- this.handleLoad(cache.content, true);
-
- return;
- }
+ async () => {
+ const { cacheRequests, fetchOptions, src } = this.props;
const dataURI = src.match(/^data:image\/svg[^,]*?(;base64)?,(.*)/u);
let inlineSrc;
@@ -221,7 +222,17 @@ export default class InlineSVG extends React.PureComponent {
return;
}
- this.request();
+ try {
+ if (cacheRequests) {
+ const content = await cacheStore.get(src, fetchOptions);
+
+ this.handleLoad(content, true);
+ } else {
+ await this.fetchContent();
+ }
+ } catch (error: any) {
+ this.handleError(error);
+ }
},
);
}
@@ -238,65 +249,6 @@ export default class InlineSVG extends React.PureComponent {
return content;
}
- private request = async () => {
- const { cacheRequests, fetchOptions, src } = this.props;
-
- if (cacheRequests) {
- cacheStore[src] = { content: '', status: STATUS.LOADING };
- }
-
- try {
- const response = await fetch(src, fetchOptions);
- const contentType = response.headers.get('content-type');
- const [fileType] = (contentType || '').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}`);
- }
-
- const content: string = await response.text();
- const { src: currentSrc } = this.props;
-
- // the current src don't match the previous one, skipping...
- if (src !== currentSrc) {
- if (cacheStore[src].status === STATUS.LOADING) {
- delete cacheStore[src];
- }
-
- return;
- }
-
- this.handleLoad(content);
-
- /* istanbul ignore else */
- if (cacheRequests) {
- const cache = cacheStore[src];
-
- /* istanbul ignore else */
- if (cache) {
- cache.content = content;
- cache.status = STATUS.LOADED;
- }
- }
- } catch (error: any) {
- this.handleError(error);
-
- /* istanbul ignore else */
- if (cacheRequests) {
- const cache = cacheStore[src];
-
- /* istanbul ignore else */
- if (cache) {
- delete cacheStore[src];
- }
- }
- }
- };
-
private updateSVGAttributes(node: SVGSVGElement): SVGSVGElement {
const { baseURL = '', uniquifyIDs } = this.props;
const replaceableAttributes = ['id', 'href', 'xlink:href', 'xlink:role', 'xlink:arcrole'];
@@ -378,3 +330,5 @@ export default class InlineSVG extends React.PureComponent {
}
export * from './types';
+
+export { default as cacheStore } from './cache';
diff --git a/src/types.ts b/src/types.ts
index d573cb2..ecd8524 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,6 @@
import * as React from 'react';
-import { STATUS } from './helpers';
+import { STATUS } from './config';
export type ErrorCallback = (error: Error | FetchError) => void;
export type LoadCallback = (src: string, isCached: boolean) => void;
@@ -22,12 +22,13 @@ export interface Props extends Omit, 'onLoad' | 'onEr
title?: string | null;
uniqueHash?: string;
uniquifyIDs?: boolean;
+ usePersistentCache?: boolean;
}
export interface State {
content: string;
element: React.ReactNode;
- hasCache: boolean;
+ isCached: boolean;
status: Status;
}
diff --git a/test/__snapshots__/index.spec.tsx.snap b/test/__snapshots__/index.spec.tsx.snap
index 8879abf..58e3124 100644
--- a/test/__snapshots__/index.spec.tsx.snap
+++ b/test/__snapshots__/index.spec.tsx.snap
@@ -2422,38 +2422,6 @@ exports[`react-inlinesvg basic functionality should uniquify ids with a custom u
`;
-exports[`react-inlinesvg cached requests should handle cached entries with loading status 1`] = `
-{
- "http://127.0.0.1:1337/react.svg": {
- "content": ,
- "status": "loaded",
- },
-}
-`;
-
-exports[`react-inlinesvg cached requests should request an SVG only once with cacheRequests prop 1`] = `
-{
- "https://cdn.svgporn.com/logos/react.svg": {
- "content": ,
- "status": "loaded",
- },
-}
-`;
-
-exports[`react-inlinesvg cached requests should skip the cache if \`cacheRequest\` is false 1`] = `{}`;
-
exports[`react-inlinesvg integration should handle pre-cached entries in the cacheStore 1`] = `