Skip to content

Commit

Permalink
Add cache module
Browse files Browse the repository at this point in the history
  • Loading branch information
gilbarbara committed Aug 7, 2023
1 parent d07dba8 commit 9404feb
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 169 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -126,7 +126,7 @@ Create unique IDs for each icon.
description="The React logo"
loader={<span>Loading...</span>}
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"
Expand Down
77 changes: 77 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -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<string, StorageItem>;

constructor() {
this.cacheStore = new Map<string, StorageItem>();
}

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<string> {
return [...this.cacheStore.keys()];
}

public delete(url: string) {
this.cacheStore.delete(url);
}

public clear() {
this.cacheStore.clear();
}
}

export default new CacheStore();
10 changes: 10 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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;
31 changes: 22 additions & 9 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down
110 changes: 32 additions & 78 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Props, State> {
private readonly hash: string;
Expand All @@ -22,7 +22,7 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
this.state = {
content: '',
element: null,
hasCache: !!props.cacheRequests && !!cacheStore[props.src],
isCached: !!props.cacheRequests && cacheStore.isCached(props.src),
status: STATUS.IDLE,
};

Expand Down Expand Up @@ -66,13 +66,13 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
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);
}
}

Expand All @@ -91,6 +91,14 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
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;
Expand Down Expand Up @@ -178,7 +186,7 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
this.setState(
{
content,
hasCache,
isCached: hasCache,
status: STATUS.LOADED,
},
this.getElement,
Expand All @@ -193,18 +201,11 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
{
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;
Expand All @@ -221,7 +222,17 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
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);
}
},
);
}
Expand All @@ -238,65 +249,6 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
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'];
Expand Down Expand Up @@ -378,3 +330,5 @@ export default class InlineSVG extends React.PureComponent<Props, State> {
}

export * from './types';

export { default as cacheStore } from './cache';
5 changes: 3 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,12 +22,13 @@ export interface Props extends Omit<React.SVGProps<SVGElement>, '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;
}

Expand Down
Loading

0 comments on commit 9404feb

Please sign in to comment.