react-inlinesvg 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/modules/helpers.ts","../src/provider.tsx"],"sourcesContent":["import type { PlainObject } from '../types';\n\nfunction randomCharacter(character: string) {\n return character[Math.floor(Math.random() * character.length)];\n}\n\nexport function canUseDOM(): boolean {\n return !!(typeof window !== 'undefined' && window.document?.createElement);\n}\n\nexport function isSupportedEnvironment(): boolean {\n return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;\n}\n\n/**\n * Remove properties from an object\n */\nexport function omit<T extends PlainObject, K extends keyof T>(\n input: T,\n ...filter: K[]\n): Omit<T, K> {\n const output: any = {};\n\n for (const key in input) {\n if ({}.hasOwnProperty.call(input, key)) {\n if (!filter.includes(key as unknown as K)) {\n output[key] = input[key];\n }\n }\n }\n\n return output as Omit<T, K>;\n}\n\nexport function randomString(length: number): string {\n const letters = 'abcdefghijklmnopqrstuvwxyz';\n const numbers = '1234567890';\n const charset = `${letters}${letters.toUpperCase()}${numbers}`;\n\n let R = '';\n\n for (let index = 0; index < length; index++) {\n R += randomCharacter(charset);\n }\n\n return R;\n}\n\nexport async function request(url: string, options?: RequestInit) {\n const response = await fetch(url, options);\n const contentType = response.headers.get('content-type');\n const [fileType] = (contentType ?? '').split(/ ?; ?/);\n\n if (response.status > 299) {\n throw new Error('Not found');\n }\n\n if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {\n throw new Error(`Content type isn't valid: ${fileType}`);\n }\n\n return response.text();\n}\n\nexport function sleep(seconds = 1) {\n return new Promise(resolve => {\n setTimeout(resolve, seconds * 1000);\n });\n}\n\nexport function supportsInlineSVG(): boolean {\n /* c8 ignore next 3 */\n if (!document) {\n return false;\n }\n\n const div = document.createElement('div');\n\n div.innerHTML = '<svg />';\n const svg = div.firstChild as SVGSVGElement;\n\n return !!svg && svg.namespaceURI === 'http://www.w3.org/2000/svg';\n}\n","import { ReactNode } from 'react';\n\nimport { canUseDOM } from './modules/helpers';\n\ninterface Props {\n children: ReactNode;\n name?: string;\n}\n\nexport default function CacheProvider({ children, name }: Props) {\n if (canUseDOM()) {\n window.REACT_INLINESVG_CACHE_NAME = name;\n window.REACT_INLINESVG_PERSISTENT_CACHE = true;\n }\n\n return children;\n}\n"],"mappings":";;;AAMO,SAAS,YAAqB;AACnC,SAAO,CAAC,EAAE,OAAO,WAAW,eAAe,OAAO,UAAU;AAC9D;;;ACCe,SAAR,cAA+B,EAAE,UAAU,KAAK,GAAU;AAC/D,MAAI,UAAU,GAAG;AACf,WAAO,6BAA6B;AACpC,WAAO,mCAAmC;AAAA,EAC5C;AAEA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/provider.tsx","../src/config.ts","../src/modules/helpers.ts","../src/modules/cache.ts"],"sourcesContent":["import React, { createContext, ReactNode, useContext, useState } from 'react';\n\nimport CacheStore from './modules/cache';\n\nconst CacheContext = createContext<CacheStore | null>(null);\n\ninterface Props {\n children: ReactNode;\n name?: string;\n}\n\nexport default function CacheProvider({ children, name }: Props) {\n const [store] = useState(() => new CacheStore({ name, persistent: true }));\n\n return <CacheContext.Provider value={store}>{children}</CacheContext.Provider>;\n}\n\nexport function useCacheStore(): CacheStore | null {\n return useContext(CacheContext);\n}\n","export const CACHE_NAME = 'react-inlinesvg';\nexport const CACHE_MAX_RETRIES = 10;\n\nexport const STATUS = {\n IDLE: 'idle',\n LOADING: 'loading',\n LOADED: 'loaded',\n FAILED: 'failed',\n READY: 'ready',\n UNSUPPORTED: 'unsupported',\n} as const;\n","function randomCharacter(character: string) {\n return character[Math.floor(Math.random() * character.length)];\n}\n\nexport function canUseDOM(): boolean {\n return !!(typeof window !== 'undefined' && window.document?.createElement);\n}\n\nexport function isSupportedEnvironment(): boolean {\n return supportsInlineSVG() && typeof window !== 'undefined' && window !== null;\n}\n\n/**\n * Remove properties from an object\n */\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(\n input: T,\n ...filter: K[]\n): Omit<T, K> {\n const output: any = {};\n\n for (const key in input) {\n if ({}.hasOwnProperty.call(input, key)) {\n if (!filter.includes(key as unknown as K)) {\n output[key] = input[key];\n }\n }\n }\n\n return output as Omit<T, K>;\n}\n\nexport function randomString(length: number): string {\n const letters = 'abcdefghijklmnopqrstuvwxyz';\n const numbers = '1234567890';\n const charset = `${letters}${letters.toUpperCase()}${numbers}`;\n\n let R = '';\n\n for (let index = 0; index < length; index++) {\n R += randomCharacter(charset);\n }\n\n return R;\n}\n\nexport async function request(url: string, options?: RequestInit) {\n const response = await fetch(url, options);\n const contentType = response.headers.get('content-type');\n const [fileType] = (contentType ?? '').split(/ ?; ?/);\n\n if (response.status > 299) {\n throw new Error('Not found');\n }\n\n if (!['image/svg+xml', 'text/plain'].some(d => fileType.includes(d))) {\n throw new Error(`Content type isn't valid: ${fileType}`);\n }\n\n return response.text();\n}\n\nexport function supportsInlineSVG(): boolean {\n /* c8 ignore next 3 */\n if (!document) {\n return false;\n }\n\n const div = document.createElement('div');\n\n div.innerHTML = '<svg />';\n const svg = div.firstChild as SVGSVGElement;\n\n return !!svg && svg.namespaceURI === 'http://www.w3.org/2000/svg';\n}\n","import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from '../config';\nimport { StorageItem } from '../types';\n\nimport { canUseDOM, request } from './helpers';\n\nexport interface CacheStoreOptions {\n name?: string;\n persistent?: boolean;\n}\n\nexport default class CacheStore {\n private cacheApi: Cache | undefined;\n private readonly cacheStore: Map<string, StorageItem>;\n private readonly subscribers: Array<() => void> = [];\n public isReady = false;\n\n constructor(options: CacheStoreOptions = {}) {\n const { name = CACHE_NAME, persistent = false } = options;\n\n this.cacheStore = new Map<string, StorageItem>();\n\n const usePersistentCache = persistent && canUseDOM() && 'caches' in window;\n\n if (usePersistentCache) {\n // eslint-disable-next-line promise/catch-or-return\n caches\n .open(name)\n .then(cache => {\n this.cacheApi = cache;\n })\n .catch(error => {\n // eslint-disable-next-line no-console\n console.error(`Failed to open cache: ${error.message}`);\n this.cacheApi = undefined;\n })\n .finally(() => {\n this.isReady = true;\n // Copy to avoid mutation issues\n const callbacks = [...this.subscribers];\n\n // Clear array efficiently\n this.subscribers.length = 0;\n\n callbacks.forEach(callback => {\n try {\n callback();\n } catch (error: any) {\n // eslint-disable-next-line no-console\n console.error(`Error in CacheStore subscriber callback: ${error.message}`);\n }\n });\n });\n } else {\n this.isReady = true;\n }\n }\n\n public onReady(callback: () => void): () => void {\n if (this.isReady) {\n callback();\n\n return () => {};\n }\n\n this.subscribers.push(callback);\n\n return () => {\n const index = this.subscribers.indexOf(callback);\n\n if (index >= 0) {\n this.subscribers.splice(index, 1);\n }\n };\n }\n\n private waitForReady(): Promise<void> {\n if (this.isReady) {\n return Promise.resolve();\n }\n\n return new Promise(resolve => {\n this.onReady(resolve);\n });\n }\n\n public async get(url: string, fetchOptions?: RequestInit) {\n await this.fetchAndCache(url, fetchOptions);\n\n return this.cacheStore.get(url)?.content ?? '';\n }\n\n public getContent(url: string): string {\n return this.cacheStore.get(url)?.content ?? '';\n }\n\n public set(url: string, data: StorageItem) {\n this.cacheStore.set(url, data);\n }\n\n public isCached(url: string) {\n return this.cacheStore.get(url)?.status === STATUS.LOADED;\n }\n\n private async fetchAndCache(url: string, fetchOptions?: RequestInit) {\n if (!this.isReady) {\n await this.waitForReady();\n }\n\n const cache = this.cacheStore.get(url);\n\n if (cache?.status === STATUS.LOADED) {\n return;\n }\n\n if (cache?.status === STATUS.LOADING) {\n await this.handleLoading(url, fetchOptions?.signal || undefined, async () => {\n this.cacheStore.set(url, { content: '', status: STATUS.IDLE });\n await this.fetchAndCache(url, fetchOptions);\n });\n\n return;\n }\n\n this.cacheStore.set(url, { content: '', status: STATUS.LOADING });\n\n try {\n const content = this.cacheApi\n ? await this.fetchFromPersistentCache(url, fetchOptions)\n : await request(url, fetchOptions);\n\n this.cacheStore.set(url, { content, status: STATUS.LOADED });\n } catch (error: any) {\n this.cacheStore.set(url, { content: '', status: STATUS.FAILED });\n throw error;\n }\n }\n\n private async fetchFromPersistentCache(url: string, fetchOptions?: RequestInit): Promise<string> {\n const data = await this.cacheApi?.match(url);\n\n if (data) {\n return data.text();\n }\n\n await this.cacheApi?.add(new Request(url, fetchOptions));\n\n const response = await this.cacheApi?.match(url);\n\n return (await response?.text()) ?? '';\n }\n\n private async handleLoading(\n url: string,\n signal: AbortSignal | undefined,\n callback: () => Promise<void>,\n ) {\n for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) {\n if (signal?.aborted) {\n throw signal.reason instanceof Error\n ? signal.reason\n : new DOMException('The operation was aborted.', 'AbortError');\n }\n\n if (this.cacheStore.get(url)?.status !== STATUS.LOADING) {\n return;\n }\n\n await sleep(0.1);\n }\n\n await callback();\n }\n\n public keys(): Array<string> {\n return [...this.cacheStore.keys()];\n }\n\n public data(): Array<Record<string, StorageItem>> {\n return [...this.cacheStore.entries()].map(([key, value]) => ({ [key]: value }));\n }\n\n public async delete(url: string) {\n if (this.cacheApi) {\n await this.cacheApi.delete(url);\n }\n\n this.cacheStore.delete(url);\n }\n\n public async clear() {\n if (this.cacheApi) {\n const keys = await this.cacheApi.keys();\n\n await Promise.allSettled(keys.map(key => this.cacheApi!.delete(key)));\n }\n\n this.cacheStore.clear();\n }\n}\n\nfunction sleep(seconds = 1) {\n return new Promise(resolve => {\n setTimeout(resolve, seconds * 1000);\n });\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-inlinesvg",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "An SVG loader for React",
5
5
  "author": "Gil Barbara <gilbarbara@gmail.com>",
6
6
  "contributors": [
@@ -54,35 +54,37 @@
54
54
  "react-from-dom": "^0.7.5"
55
55
  },
56
56
  "devDependencies": {
57
- "@arethetypeswrong/cli": "^0.17.3",
58
- "@gilbarbara/eslint-config": "^0.8.4",
57
+ "@arethetypeswrong/cli": "^0.18.2",
58
+ "@gilbarbara/eslint-config": "^1.1.0",
59
59
  "@gilbarbara/prettier-config": "^1.0.0",
60
- "@gilbarbara/tsconfig": "^0.2.3",
61
- "@size-limit/preset-small-lib": "^11.1.6",
62
- "@testing-library/jest-dom": "^6.6.3",
63
- "@testing-library/react": "^16.2.0",
64
- "@types/node": "^22.13.4",
65
- "@types/react": "^18.3.18",
66
- "@types/react-dom": "^18.3.5",
67
- "@vitejs/plugin-react": "^4.3.4",
68
- "@vitest/coverage-v8": "^3.0.6",
60
+ "@gilbarbara/tsconfig": "^1.0.0",
61
+ "@size-limit/preset-small-lib": "^12.0.1",
62
+ "@testing-library/jest-dom": "^6.9.1",
63
+ "@testing-library/react": "^16.3.2",
64
+ "@types/node": "^24.12.0",
65
+ "@types/react": "^19.2.14",
66
+ "@types/react-dom": "^19.2.3",
67
+ "@vitejs/plugin-react": "^6.0.1",
68
+ "@vitest/coverage-v8": "^4.1.2",
69
69
  "browser-cache-mock": "^0.1.7",
70
- "del-cli": "^6.0.0",
70
+ "del-cli": "^7.0.0",
71
+ "eslint": "^9.39.4",
71
72
  "fix-tsup-cjs": "^1.2.0",
72
73
  "http-server": "^14.1.1",
73
74
  "husky": "^9.1.7",
74
- "jest-extended": "^4.0.2",
75
- "jsdom": "^26.0.0",
76
- "react": "^18.3.1",
77
- "react-dom": "^18.3.1",
75
+ "jest-extended": "^7.0.0",
76
+ "jsdom": "^29.0.1",
77
+ "prettier": "^3.8.1",
78
+ "react": "^19.2.4",
79
+ "react-dom": "^19.2.4",
78
80
  "repo-tools": "^0.3.1",
79
- "size-limit": "^11.1.6",
80
- "start-server-and-test": "^2.0.10",
81
+ "size-limit": "^12.0.1",
82
+ "start-server-and-test": "^3.0.0",
81
83
  "ts-node": "^10.9.2",
82
- "tsup": "^8.3.6",
83
- "typescript": "^5.7.3",
84
- "vitest": "^3.0.6",
85
- "vitest-fetch-mock": "^0.4.3"
84
+ "tsup": "^8.5.1",
85
+ "typescript": "^5.9.3",
86
+ "vitest": "^4.1.2",
87
+ "vitest-fetch-mock": "^0.4.5"
86
88
  },
87
89
  "scripts": {
88
90
  "build": "pnpm run clean && tsup && fix-tsup-cjs",
@@ -117,24 +119,6 @@
117
119
  "sourcemap": true,
118
120
  "splitting": false
119
121
  },
120
- "eslintConfig": {
121
- "extends": [
122
- "@gilbarbara/eslint-config"
123
- ],
124
- "overrides": [
125
- {
126
- "files": [
127
- "test/**/*.ts?(x)"
128
- ],
129
- "rules": {
130
- "@typescript-eslint/ban-ts-comment": "off",
131
- "no-console": "off",
132
- "testing-library/no-container": "off",
133
- "testing-library/no-node-access": "off"
134
- }
135
- }
136
- ]
137
- },
138
122
  "prettier": "@gilbarbara/prettier-config",
139
123
  "size-limit": [
140
124
  {
package/src/index.tsx CHANGED
@@ -1,275 +1,45 @@
1
- import React, {
2
- cloneElement,
3
- isValidElement,
4
- ReactElement,
5
- useCallback,
6
- useEffect,
7
- useReducer,
8
- useRef,
9
- useState,
10
- } from 'react';
11
- import convert from 'react-from-dom';
1
+ import { cloneElement, ReactElement, SVGProps } from 'react';
12
2
 
13
3
  import { STATUS } from './config';
14
4
  import CacheStore from './modules/cache';
15
- import { canUseDOM, isSupportedEnvironment, omit, randomString, request } from './modules/helpers';
16
- import { usePrevious } from './modules/hooks';
17
- import { getNode } from './modules/utils';
18
- import { FetchError, Props, State, Status } from './types';
5
+ import { canUseDOM, omit } from './modules/helpers';
6
+ import useInlineSVG from './modules/useInlineSVG';
7
+ import { useCacheStore } from './provider';
8
+ import { Props, Status } from './types';
19
9
 
20
- // eslint-disable-next-line import/no-mutable-exports
21
- export let cacheStore: CacheStore;
10
+ export const cacheStore = new CacheStore();
22
11
 
23
- function ReactInlineSVG(props: Props) {
24
- const {
25
- cacheRequests = true,
26
- children = null,
27
- description,
28
- fetchOptions,
29
- innerRef,
30
- loader = null,
31
- onError,
32
- onLoad,
33
- src,
34
- title,
35
- uniqueHash,
36
- } = props;
37
- const [state, setState] = useReducer(
38
- (previousState: State, nextState: Partial<State>) => ({
39
- ...previousState,
40
- ...nextState,
41
- }),
42
- {
43
- content: '',
44
- element: null,
45
-
46
- isCached: cacheRequests && cacheStore.isCached(props.src),
47
- status: STATUS.IDLE,
48
- },
49
- );
50
- const { content, element, isCached, status } = state;
51
- const previousProps = usePrevious(props);
52
- const previousState = usePrevious(state);
53
-
54
- const hash = useRef(uniqueHash ?? randomString(8));
55
- const isActive = useRef(false);
56
- const isInitialized = useRef(false);
57
-
58
- const handleError = useCallback(
59
- (error: Error | FetchError) => {
60
- if (isActive.current) {
61
- setState({
62
- status:
63
- error.message === 'Browser does not support SVG' ? STATUS.UNSUPPORTED : STATUS.FAILED,
64
- });
65
-
66
- onError?.(error);
67
- }
68
- },
69
- [onError],
70
- );
71
-
72
- const handleLoad = useCallback((loadedContent: string, hasCache = false) => {
73
- if (isActive.current) {
74
- setState({
75
- content: loadedContent,
76
- isCached: hasCache,
77
- status: STATUS.LOADED,
78
- });
79
- }
80
- }, []);
81
-
82
- const fetchContent = useCallback(async () => {
83
- const responseContent: string = await request(src, fetchOptions);
84
-
85
- handleLoad(responseContent);
86
- }, [fetchOptions, handleLoad, src]);
87
-
88
- const getElement = useCallback(() => {
89
- try {
90
- const node = getNode({ ...props, handleError, hash: hash.current, content }) as Node;
91
- const convertedElement = convert(node);
92
-
93
- if (!convertedElement || !isValidElement(convertedElement)) {
94
- throw new Error('Could not convert the src to a React element');
95
- }
96
-
97
- setState({
98
- element: convertedElement,
99
- status: STATUS.READY,
100
- });
101
- } catch (error: any) {
102
- handleError(error);
103
- }
104
- }, [content, handleError, props]);
105
-
106
- const getContent = useCallback(async () => {
107
- const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/u.exec(src);
108
- let inlineSrc;
109
-
110
- if (dataURI) {
111
- inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
112
- } else if (src.includes('<svg')) {
113
- inlineSrc = src;
114
- }
115
-
116
- if (inlineSrc) {
117
- handleLoad(inlineSrc);
118
-
119
- return;
120
- }
121
-
122
- try {
123
- if (cacheRequests) {
124
- const cachedContent = await cacheStore.get(src, fetchOptions);
125
-
126
- handleLoad(cachedContent, true);
127
- } else {
128
- await fetchContent();
129
- }
130
- } catch (error: any) {
131
- handleError(error);
132
- }
133
- }, [cacheRequests, fetchContent, fetchOptions, handleError, handleLoad, src]);
134
-
135
- const load = useCallback(async () => {
136
- if (isActive.current) {
137
- setState({
138
- content: '',
139
- element: null,
140
- isCached: false,
141
- status: STATUS.LOADING,
142
- });
143
- }
144
- }, []);
145
-
146
- // Run on mount
147
- useEffect(
148
- () => {
149
- isActive.current = true;
150
-
151
- if (!canUseDOM() || isInitialized.current) {
152
- return undefined;
153
- }
154
-
155
- try {
156
- if (status === STATUS.IDLE) {
157
- if (!isSupportedEnvironment()) {
158
- throw new Error('Browser does not support SVG');
159
- }
160
-
161
- if (!src) {
162
- throw new Error('Missing src');
163
- }
164
-
165
- load();
166
- }
167
- } catch (error: any) {
168
- handleError(error);
169
- }
170
-
171
- isInitialized.current = true;
172
-
173
- return () => {
174
- isActive.current = false;
175
- };
176
- },
177
- // eslint-disable-next-line react-hooks/exhaustive-deps
178
- [],
179
- );
180
-
181
- // Handles `src` changes
182
- useEffect(() => {
183
- if (!canUseDOM() || !previousProps) {
184
- return;
185
- }
186
-
187
- if (previousProps.src !== src) {
188
- if (!src) {
189
- handleError(new Error('Missing src'));
190
-
191
- return;
192
- }
193
-
194
- load();
195
- }
196
- }, [handleError, load, previousProps, src]);
197
-
198
- // Handles content loading
199
- useEffect(() => {
200
- if (status === STATUS.LOADED) {
201
- getElement();
202
- }
203
- }, [status, getElement]);
204
-
205
- // Handles `title` and `description` changes
206
- useEffect(() => {
207
- if (!canUseDOM() || !previousProps || previousProps.src !== src) {
208
- return;
209
- }
210
-
211
- if (previousProps.title !== title || previousProps.description !== description) {
212
- getElement();
213
- }
214
- }, [description, getElement, previousProps, src, title]);
215
-
216
- // handle state
217
- useEffect(() => {
218
- if (!previousState) {
219
- return;
220
- }
221
-
222
- switch (status) {
223
- case STATUS.LOADING: {
224
- if (previousState.status !== STATUS.LOADING) {
225
- getContent();
226
- }
227
-
228
- break;
229
- }
230
- case STATUS.LOADED: {
231
- if (previousState.status !== STATUS.LOADED) {
232
- getElement();
233
- }
234
-
235
- break;
236
- }
237
- case STATUS.READY: {
238
- if (previousState.status !== STATUS.READY) {
239
- onLoad?.(src, isCached);
240
- }
241
-
242
- break;
243
- }
244
- }
245
- }, [getContent, getElement, isCached, onLoad, previousState, src, status]);
12
+ export default function InlineSVG(props: Props) {
13
+ const { children = null, innerRef, loader = null } = props;
14
+ const contextStore = useCacheStore();
15
+ const store = contextStore ?? cacheStore;
246
16
 
247
- const elementProps = omit(
248
- props,
249
- 'baseURL',
250
- 'cacheRequests',
251
- 'children',
252
- 'description',
253
- 'fetchOptions',
254
- 'innerRef',
255
- 'loader',
256
- 'onError',
257
- 'onLoad',
258
- 'preProcessor',
259
- 'src',
260
- 'title',
261
- 'uniqueHash',
262
- 'uniquifyIDs',
263
- );
17
+ const { element, status } = useInlineSVG(props, store);
264
18
 
265
19
  if (!canUseDOM()) {
266
20
  return loader;
267
21
  }
268
22
 
269
23
  if (element) {
270
- return cloneElement(element as ReactElement, {
24
+ return cloneElement(element as ReactElement<SVGProps<SVGElement>>, {
271
25
  ref: innerRef,
272
- ...elementProps,
26
+ ...omit(
27
+ props,
28
+ 'baseURL',
29
+ 'cacheRequests',
30
+ 'children',
31
+ 'description',
32
+ 'fetchOptions',
33
+ 'innerRef',
34
+ 'loader',
35
+ 'onError',
36
+ 'onLoad',
37
+ 'preProcessor',
38
+ 'src',
39
+ 'title',
40
+ 'uniqueHash',
41
+ 'uniquifyIDs',
42
+ ),
273
43
  });
274
44
  }
275
45
 
@@ -280,29 +50,4 @@ function ReactInlineSVG(props: Props) {
280
50
  return loader;
281
51
  }
282
52
 
283
- export default function InlineSVG(props: Props) {
284
- if (!cacheStore) {
285
- cacheStore = new CacheStore();
286
- }
287
-
288
- const { loader } = props;
289
- const [isReady, setReady] = useState(cacheStore.isReady);
290
-
291
- useEffect(() => {
292
- if (isReady) {
293
- return;
294
- }
295
-
296
- cacheStore.onReady(() => {
297
- setReady(true);
298
- });
299
- }, [isReady]);
300
-
301
- if (!isReady) {
302
- return loader;
303
- }
304
-
305
- return <ReactInlineSVG {...props} />;
306
- }
307
-
308
53
  export * from './types';
@@ -1,7 +1,12 @@
1
1
  import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from '../config';
2
2
  import { StorageItem } from '../types';
3
3
 
4
- import { canUseDOM, request, sleep } from './helpers';
4
+ import { canUseDOM, request } from './helpers';
5
+
6
+ export interface CacheStoreOptions {
7
+ name?: string;
8
+ persistent?: boolean;
9
+ }
5
10
 
6
11
  export default class CacheStore {
7
12
  private cacheApi: Cache | undefined;
@@ -9,20 +14,17 @@ export default class CacheStore {
9
14
  private readonly subscribers: Array<() => void> = [];
10
15
  public isReady = false;
11
16
 
12
- constructor() {
17
+ constructor(options: CacheStoreOptions = {}) {
18
+ const { name = CACHE_NAME, persistent = false } = options;
19
+
13
20
  this.cacheStore = new Map<string, StorageItem>();
14
21
 
15
- let cacheName = CACHE_NAME;
16
- let usePersistentCache = false;
17
-
18
- if (canUseDOM()) {
19
- cacheName = window.REACT_INLINESVG_CACHE_NAME ?? CACHE_NAME;
20
- usePersistentCache = !!window.REACT_INLINESVG_PERSISTENT_CACHE && 'caches' in window;
21
- }
22
+ const usePersistentCache = persistent && canUseDOM() && 'caches' in window;
22
23
 
23
24
  if (usePersistentCache) {
25
+ // eslint-disable-next-line promise/catch-or-return
24
26
  caches
25
- .open(cacheName)
27
+ .open(name)
26
28
  .then(cache => {
27
29
  this.cacheApi = cache;
28
30
  })
@@ -53,22 +55,44 @@ export default class CacheStore {
53
55
  }
54
56
  }
55
57
 
56
- public onReady(callback: () => void) {
58
+ public onReady(callback: () => void): () => void {
57
59
  if (this.isReady) {
58
60
  callback();
59
- } else {
60
- this.subscribers.push(callback);
61
+
62
+ return () => {};
63
+ }
64
+
65
+ this.subscribers.push(callback);
66
+
67
+ return () => {
68
+ const index = this.subscribers.indexOf(callback);
69
+
70
+ if (index >= 0) {
71
+ this.subscribers.splice(index, 1);
72
+ }
73
+ };
74
+ }
75
+
76
+ private waitForReady(): Promise<void> {
77
+ if (this.isReady) {
78
+ return Promise.resolve();
61
79
  }
80
+
81
+ return new Promise(resolve => {
82
+ this.onReady(resolve);
83
+ });
62
84
  }
63
85
 
64
86
  public async get(url: string, fetchOptions?: RequestInit) {
65
- await (this.cacheApi
66
- ? this.fetchAndAddToPersistentCache(url, fetchOptions)
67
- : this.fetchAndAddToInternalCache(url, fetchOptions));
87
+ await this.fetchAndCache(url, fetchOptions);
68
88
 
69
89
  return this.cacheStore.get(url)?.content ?? '';
70
90
  }
71
91
 
92
+ public getContent(url: string): string {
93
+ return this.cacheStore.get(url)?.content ?? '';
94
+ }
95
+
72
96
  public set(url: string, data: StorageItem) {
73
97
  this.cacheStore.set(url, data);
74
98
  }
@@ -77,33 +101,11 @@ export default class CacheStore {
77
101
  return this.cacheStore.get(url)?.status === STATUS.LOADED;
78
102
  }
79
103
 
80
- private async fetchAndAddToInternalCache(url: string, fetchOptions?: RequestInit) {
81
- const cache = this.cacheStore.get(url);
82
-
83
- if (cache?.status === STATUS.LOADING) {
84
- await this.handleLoading(url, async () => {
85
- this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
86
- await this.fetchAndAddToInternalCache(url, fetchOptions);
87
- });
88
-
89
- return;
104
+ private async fetchAndCache(url: string, fetchOptions?: RequestInit) {
105
+ if (!this.isReady) {
106
+ await this.waitForReady();
90
107
  }
91
108
 
92
- if (!cache?.content) {
93
- this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
94
-
95
- try {
96
- const content = await request(url, fetchOptions);
97
-
98
- this.cacheStore.set(url, { content, status: STATUS.LOADED });
99
- } catch (error: any) {
100
- this.cacheStore.set(url, { content: '', status: STATUS.FAILED });
101
- throw error;
102
- }
103
- }
104
- }
105
-
106
- private async fetchAndAddToPersistentCache(url: string, fetchOptions?: RequestInit) {
107
109
  const cache = this.cacheStore.get(url);
108
110
 
109
111
  if (cache?.status === STATUS.LOADED) {
@@ -111,9 +113,9 @@ export default class CacheStore {
111
113
  }
112
114
 
113
115
  if (cache?.status === STATUS.LOADING) {
114
- await this.handleLoading(url, async () => {
116
+ await this.handleLoading(url, fetchOptions?.signal || undefined, async () => {
115
117
  this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
116
- await this.fetchAndAddToPersistentCache(url, fetchOptions);
118
+ await this.fetchAndCache(url, fetchOptions);
117
119
  });
118
120
 
119
121
  return;
@@ -121,21 +123,10 @@ export default class CacheStore {
121
123
 
122
124
  this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
123
125
 
124
- const data = await this.cacheApi?.match(url);
125
-
126
- if (data) {
127
- const content = await data.text();
128
-
129
- this.cacheStore.set(url, { content, status: STATUS.LOADED });
130
-
131
- return;
132
- }
133
-
134
126
  try {
135
- await this.cacheApi?.add(new Request(url, fetchOptions));
136
-
137
- const response = await this.cacheApi?.match(url);
138
- const content = (await response?.text()) ?? '';
127
+ const content = this.cacheApi
128
+ ? await this.fetchFromPersistentCache(url, fetchOptions)
129
+ : await request(url, fetchOptions);
139
130
 
140
131
  this.cacheStore.set(url, { content, status: STATUS.LOADED });
141
132
  } catch (error: any) {
@@ -144,13 +135,36 @@ export default class CacheStore {
144
135
  }
145
136
  }
146
137
 
147
- private async handleLoading(url: string, callback: () => Promise<void>) {
138
+ private async fetchFromPersistentCache(url: string, fetchOptions?: RequestInit): Promise<string> {
139
+ const data = await this.cacheApi?.match(url);
140
+
141
+ if (data) {
142
+ return data.text();
143
+ }
144
+
145
+ await this.cacheApi?.add(new Request(url, fetchOptions));
146
+
147
+ const response = await this.cacheApi?.match(url);
148
+
149
+ return (await response?.text()) ?? '';
150
+ }
151
+
152
+ private async handleLoading(
153
+ url: string,
154
+ signal: AbortSignal | undefined,
155
+ callback: () => Promise<void>,
156
+ ) {
148
157
  for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) {
158
+ if (signal?.aborted) {
159
+ throw signal.reason instanceof Error
160
+ ? signal.reason
161
+ : new DOMException('The operation was aborted.', 'AbortError');
162
+ }
163
+
149
164
  if (this.cacheStore.get(url)?.status !== STATUS.LOADING) {
150
165
  return;
151
166
  }
152
167
 
153
- // eslint-disable-next-line no-await-in-loop
154
168
  await sleep(0.1);
155
169
  }
156
170
 
@@ -183,3 +197,9 @@ export default class CacheStore {
183
197
  this.cacheStore.clear();
184
198
  }
185
199
  }
200
+
201
+ function sleep(seconds = 1) {
202
+ return new Promise(resolve => {
203
+ setTimeout(resolve, seconds * 1000);
204
+ });
205
+ }