From 648e76833a9ee520e36307e7d4f6dd59c8093e6e Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sun, 8 Mar 2020 21:39:09 +0100 Subject: [PATCH 01/24] Add missing eslint plugin jest formatting package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e2cc308b..576b31f7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "url": "https://github.com/alex-cory/use-http.git" }, "dependencies": { + "eslint-plugin-jest-formatting": "^1.2.0", "use-ssr": "^1.0.22" }, "peerDependencies": { From ac04affd7649a48f296ccfe6600fc7734767166d Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sun, 8 Mar 2020 21:42:01 +0100 Subject: [PATCH 02/24] Implement persist option. --- package.json | 1 + src/__tests__/useFetch.test.tsx | 69 +++++++++++++++++++++++++++++++++ src/persistFetch.ts | 28 +++++++++++++ src/types.ts | 1 + src/useFetch.ts | 60 ++++++++++++++++------------ src/useFetchArgs.ts | 4 ++ yarn.lock | 10 +++++ 7 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 src/persistFetch.ts diff --git a/package.json b/package.json index 576b31f7..78ecd7a2 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-watch": "^5.1.2", "jest": "^24.7.1", "jest-fetch-mock": "^3.0.1", + "mockdate": "^2.0.5", "react": "^16.8.6", "react-dom": "^16.8.6", "react-hooks-testing-library": "^0.6.0", diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx index 7314f9ef..ca0febcc 100644 --- a/src/__tests__/useFetch.test.tsx +++ b/src/__tests__/useFetch.test.tsx @@ -10,6 +10,8 @@ import { toCamel } from 'convert-keys' import { renderHook, act } from '@testing-library/react-hooks' import { emptyCustomResponse } from '../utils' +import * as mockdate from 'mockdate' + const fetch = global.fetch as FetchMock const { NO_CACHE } = CachePolicies @@ -803,3 +805,70 @@ describe('useFetch - BROWSER - errors', (): void => { expect(result.current.error).toEqual(expectedError) }) }) + +describe('useFetch - BROWSER - persistence', (): void => { + const expected = { + name: 'Alex Cory', + age: 29 + } + + const wrapper = ({ children }: { children?: ReactNode }): ReactElement => {children} + + afterAll((): void => { + cleanup() + fetch.resetMocks() + mockdate.reset() + }) + + beforeAll((): void => { + fetch.mockResponseOnce( + JSON.stringify(expected) + ) + mockdate.set('2020-01-01') + }) + + afterEach((): void => { + fetch.resetMocks() + }) + + it('should fetch once', async (): Promise< + void + > => { + const { waitForNextUpdate } = renderHook( + () => useFetch({ persist: true }, []), // onMount === true + { wrapper: wrapper as React.ComponentType } + ) + + await waitForNextUpdate() + + expect(fetch).toHaveBeenCalledTimes(1) + }) + + it('should not fetch again', async (): Promise< + void + > => { + const { waitForNextUpdate } = renderHook( + () => useFetch({ persist: true }, []), // onMount === true + { wrapper: wrapper as React.ComponentType } + ) + + await waitForNextUpdate() + + expect(fetch).toHaveBeenCalledTimes(0) + }) + + it('should fetch again after 24h', async (): Promise< + void + > => { + mockdate.set('2020-01-02 01:00') + + const { waitForNextUpdate } = renderHook( + () => useFetch({ persist: true }, []), // onMount === true + { wrapper: wrapper as React.ComponentType } + ) + + await waitForNextUpdate() + + expect(fetch).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/persistFetch.ts b/src/persistFetch.ts new file mode 100644 index 00000000..2a1f73a9 --- /dev/null +++ b/src/persistFetch.ts @@ -0,0 +1,28 @@ +const cacheName = 'useHTTPcache' + +const hasPersistentData = (url: string): boolean => { + const urlCache = JSON.parse(localStorage[cacheName] || '{}') + return urlCache[url] && urlCache[url].timestamp > Date.now() - urlCache[url].ttl +} + +const getPersistentData = (url: string): any => { + const urlCache = JSON.parse(localStorage[cacheName] || '{}') + return urlCache[url].data +} + +const setPersistentData = (url: string, data: any, ttl = 24 * 3600000): void => { + const urlCache = JSON.parse(localStorage[cacheName] || '{}') + urlCache[url] = { + url, + data, + timestamp: Date.now(), + ttl + } + localStorage.setItem(cacheName, JSON.stringify(urlCache)) +} + +export { + hasPersistentData, + getPersistentData, + setPersistentData +} diff --git a/src/types.ts b/src/types.ts index 51d4fd77..751c1a98 100644 --- a/src/types.ts +++ b/src/types.ts @@ -162,6 +162,7 @@ export type Interceptors = { export interface CustomOptions { retries?: number + persist?: boolean timeout?: number path?: string url?: string diff --git a/src/useFetch.ts b/src/useFetch.ts index b15c2268..2acb71c4 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -17,6 +17,7 @@ import { import useFetchArgs from './useFetchArgs' import doFetchArgs from './doFetchArgs' import { invariant, tryGetData, responseKeys, responseMethods, responseFields } from './utils' +import { getPersistentData, hasPersistentData, setPersistentData } from './persistFetch' const { CACHE_FIRST } = CachePolicies @@ -28,6 +29,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { url: initialURL, path, interceptors, + persist, timeout, retries, onTimeout, @@ -106,33 +108,41 @@ function useFetch(...args: UseFetchArgs): UseFetch { let newData let newRes - try { - newRes = await fetch(url, options) - res.current = newRes.clone() + if (persist && hasPersistentData(url)) { + data.current = getPersistentData(url) as TData + } else { + try { + newRes = await fetch(url, options) + res.current = newRes.clone() - if (cachePolicy === CACHE_FIRST) { - cache.set(response.id, newRes.clone()) - if (cacheLife > 0) cache.set(response.ageID, Date.now()) - } + if (cachePolicy === CACHE_FIRST) { + cache.set(response.id, newRes.clone()) + if (cacheLife > 0) cache.set(response.ageID, Date.now()) + } + + newData = await tryGetData(newRes, defaults.data) + res.current.data = onNewData(data.current, newData) + + res.current = interceptors.response ? interceptors.response(res.current) : res.current + invariant('data' in res.current, 'You must have `data` field on the Response returned from your `interceptors.response`') + data.current = res.current.data as TData - newData = await tryGetData(newRes, defaults.data) - res.current.data = onNewData(data.current, newData) - - res.current = interceptors.response ? interceptors.response(res.current) : res.current - invariant('data' in res.current, 'You must have `data` field on the Response returned from your `interceptors.response`') - data.current = res.current.data as TData - - if (Array.isArray(data.current) && !!(data.current.length % perPage)) hasMore.current = false - } catch (err) { - if (attempts.current > 0) return doFetch(routeOrBody, body) - if (attempts.current < 1 && timedout.current) error.current = { name: 'AbortError', message: 'Timeout Error' } - if (err.name !== 'AbortError') error.current = err - } finally { - if (newRes && !newRes.ok && !error.current) error.current = { name: newRes.status, message: newRes.statusText } - if (attempts.current > 0) attempts.current -= 1 - timedout.current = false - if (timer) clearTimeout(timer) - controller.current = undefined + if (persist) { + setPersistentData(url, data.current) + } + + if (Array.isArray(data.current) && !!(data.current.length % perPage)) hasMore.current = false + } catch (err) { + if (attempts.current > 0) return doFetch(routeOrBody, body) + if (attempts.current < 1 && timedout.current) error.current = { name: 'AbortError', message: 'Timeout Error' } + if (err.name !== 'AbortError') error.current = err + } finally { + if (newRes && !newRes.ok && !error.current) error.current = { name: newRes.status, message: newRes.statusText } + if (attempts.current > 0) attempts.current -= 1 + timedout.current = false + if (timer) clearTimeout(timer) + controller.current = undefined + } } setLoading(false) diff --git a/src/useFetchArgs.ts b/src/useFetchArgs.ts index 4302e24a..91538130 100644 --- a/src/useFetchArgs.ts +++ b/src/useFetchArgs.ts @@ -6,6 +6,7 @@ import FetchContext from './FetchContext' type UseFetchArgsReturn = { customOptions: { retries: number + persist: boolean timeout: number path: string url: string @@ -28,6 +29,7 @@ type UseFetchArgsReturn = { export const useFetchArgsDefaults = { customOptions: { retries: 0, + persist: false, timeout: 30000, // 30 seconds path: '', url: '', @@ -113,6 +115,7 @@ export default function useFetchArgs( const path = useField('path', urlOrOptions, optionsNoURLs) const timeout = useField('timeout', urlOrOptions, optionsNoURLs) const retries = useField('retries', urlOrOptions, optionsNoURLs) + const persist = useField('persist', urlOrOptions, optionsNoURLs) const onAbort = useField<() => void>('onAbort', urlOrOptions, optionsNoURLs) const onTimeout = useField<() => void>('onTimeout', urlOrOptions, optionsNoURLs) const onNewData = useField<() => void>('onNewData', urlOrOptions, optionsNoURLs) @@ -168,6 +171,7 @@ export default function useFetchArgs( interceptors, timeout, retries, + persist, onAbort, onTimeout, onNewData, diff --git a/yarn.lock b/yarn.lock index 53733233..4806de7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1427,6 +1427,11 @@ eslint-plugin-import@^2.20.1: read-pkg-up "^2.0.0" resolve "^1.12.0" +eslint-plugin-jest-formatting@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-1.2.0.tgz#a9eef225520d53e63665d0f2fcf4d3eda894a18e" + integrity sha512-EqsbDByAtdQa5vEhJFUFMqTW7fghN0Qhb8oulM7R3j9+9xRuMsQKCPjWvCIxpWhl3SJJmlxBC25o1pUXiBHaAw== + eslint-plugin-jest@23.2.0: version "23.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.2.0.tgz#72d9ac0421b9b6ef774bcf4783329c016ed7cd6a" @@ -3099,6 +3104,11 @@ mkdirp@0.x, mkdirp@^0.5.1: dependencies: minimist "0.0.8" +mockdate@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-2.0.5.tgz#70c6abf9ed4b2dae65c81dfc170dd1a5cec53620" + integrity sha512-ST0PnThzWKcgSLyc+ugLVql45PvESt3Ul/wrdV/OPc/6Pr8dbLAIJsN1cIp41FLzbN+srVTNIRn+5Cju0nyV6A== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" From d42b6882582418670bda7fdb46d4764d03b40f06 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sun, 8 Mar 2020 21:50:32 +0100 Subject: [PATCH 03/24] Add persist to dependencies of makeFetch. --- src/useFetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useFetch.ts b/src/useFetch.ts index 2acb71c4..adc0f623 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -151,7 +151,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { } return doFetch - }, [isServer, onAbort, requestInit, initialURL, path, interceptors, cachePolicy, perPage, timeout, cacheLife, onTimeout, defaults.data, onNewData]) + }, [isServer, onAbort, requestInit, initialURL, path, interceptors, cachePolicy, perPage, timeout, persist, cacheLife, onTimeout, defaults.data, onNewData]) const post = useCallback(makeFetch(HTTPMethod.POST), [makeFetch]) const del = useCallback(makeFetch(HTTPMethod.DELETE), [makeFetch]) From 455494eaadd0910230fad94d1e1af1ca78d9bd72 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Wed, 11 Mar 2020 23:13:23 +0100 Subject: [PATCH 04/24] Move eslint plugin to devdeps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78ecd7a2..6cd8e3e3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "url": "https://github.com/alex-cory/use-http.git" }, "dependencies": { - "eslint-plugin-jest-formatting": "^1.2.0", "use-ssr": "^1.0.22" }, "peerDependencies": { @@ -31,6 +30,7 @@ "eslint-config-standard": "^14.1.0", "eslint-plugin-import": "^2.20.1", "eslint-plugin-jest": "23.2.0", + "eslint-plugin-jest-formatting": "^1.2.0", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-node": "^11.0.0", "eslint-plugin-promise": "^4.2.1", From f7cca5481bab6bb5e1ea6f6809a4179e96ef43ff Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Wed, 11 Mar 2020 23:14:18 +0100 Subject: [PATCH 05/24] Move persistence check to doFetchArgs. --- src/__tests__/doFetchArgs.test.tsx | 13 ++++ src/__tests__/useFetch.test.tsx | 46 ++++++------- src/doFetchArgs.ts | 7 +- src/{persistFetch.ts => persistentStorage.ts} | 2 +- src/types.ts | 2 + src/useFetch.ts | 68 ++++++++++--------- 6 files changed, 80 insertions(+), 58 deletions(-) rename src/{persistFetch.ts => persistentStorage.ts} (94%) diff --git a/src/__tests__/doFetchArgs.test.tsx b/src/__tests__/doFetchArgs.test.tsx index 5ed4f683..c9937d8c 100644 --- a/src/__tests__/doFetchArgs.test.tsx +++ b/src/__tests__/doFetchArgs.test.tsx @@ -2,6 +2,7 @@ import doFetchArgs from '../doFetchArgs' import { HTTPMethod, CachePolicies } from '../types' const defaultCachePolicy = CachePolicies.CACHE_FIRST +const defaultCacheLife = 24 * 3600000 describe('doFetchArgs: general usages', (): void => { it('should be defined', (): void => { @@ -20,6 +21,8 @@ describe('doFetchArgs: general usages', (): void => { controller, defaultCachePolicy, cache, + false, + defaultCacheLife, expectedRoute, {} ) @@ -45,6 +48,8 @@ describe('doFetchArgs: general usages', (): void => { controller, defaultCachePolicy, cache, + false, + defaultCacheLife, '/test', [] ) @@ -70,6 +75,8 @@ describe('doFetchArgs: general usages', (): void => { controller, defaultCachePolicy, cache, + false, + defaultCacheLife, '/route', {} ) @@ -93,6 +100,8 @@ describe('doFetchArgs: general usages', (): void => { controller, defaultCachePolicy, cache, + false, + defaultCacheLife, '/test', {}, interceptors.request @@ -140,6 +149,8 @@ describe('doFetchArgs: Errors', (): void => { controller, defaultCachePolicy, cache, + false, + defaultCacheLife, {}, {} ) @@ -178,6 +189,8 @@ describe('doFetchArgs: Errors', (): void => { controller, defaultCachePolicy, cache, + false, + defaultCacheLife, [], [] ) diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx index ca0febcc..87ea0e33 100644 --- a/src/__tests__/useFetch.test.tsx +++ b/src/__tests__/useFetch.test.tsx @@ -811,32 +811,33 @@ describe('useFetch - BROWSER - persistence', (): void => { name: 'Alex Cory', age: 29 } - - const wrapper = ({ children }: { children?: ReactNode }): ReactElement => {children} + const unexpected = { + name: 'Mattias Rost', + age: 37 + } afterAll((): void => { - cleanup() - fetch.resetMocks() mockdate.reset() }) beforeAll((): void => { - fetch.mockResponseOnce( - JSON.stringify(expected) - ) mockdate.set('2020-01-01') }) - afterEach((): void => { + afterEach(() => { + cleanup() fetch.resetMocks() }) - it('should fetch once', async (): Promise< - void - > => { + beforeEach((): void => { + fetch.mockResponse( + JSON.stringify(expected) + ) + }) + + it('should fetch once', async (): Promise => { const { waitForNextUpdate } = renderHook( - () => useFetch({ persist: true }, []), // onMount === true - { wrapper: wrapper as React.ComponentType } + () => useFetch({ url: 'https://example.com', persist: true }, []) ) await waitForNextUpdate() @@ -844,27 +845,24 @@ describe('useFetch - BROWSER - persistence', (): void => { expect(fetch).toHaveBeenCalledTimes(1) }) - it('should not fetch again', async (): Promise< - void - > => { - const { waitForNextUpdate } = renderHook( - () => useFetch({ persist: true }, []), // onMount === true - { wrapper: wrapper as React.ComponentType } + it('should not fetch again', async (): Promise => { + fetch.mockResponse(JSON.stringify(unexpected)) + + const { result, waitForNextUpdate } = renderHook( + () => useFetch({ url: 'https://example.com', persist: true, cachePolicy: NO_CACHE }, []) ) await waitForNextUpdate() expect(fetch).toHaveBeenCalledTimes(0) + expect(result.current.data).toEqual(expected) }) - it('should fetch again after 24h', async (): Promise< - void - > => { + it('should fetch again after 24h', async (): Promise => { mockdate.set('2020-01-02 01:00') const { waitForNextUpdate } = renderHook( - () => useFetch({ persist: true }, []), // onMount === true - { wrapper: wrapper as React.ComponentType } + () => useFetch({ url: 'https://example.com', persist: true, cachePolicy: NO_CACHE }, []) ) await waitForNextUpdate() diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts index 937972e1..69930594 100644 --- a/src/doFetchArgs.ts +++ b/src/doFetchArgs.ts @@ -1,5 +1,6 @@ import { HTTPMethod, Interceptors, ValueOf, DoFetchArgs, CachePolicies, Res } from './types' import { invariant, isServer, isString, isBodyObject } from './utils' +import { hasPersistentData, getPersistentData } from './persistentStorage' const { GET } = HTTPMethod @@ -11,6 +12,8 @@ export default async function doFetchArgs( controller: AbortController, cachePolicy: CachePolicies, cache: Map | number>, + persist: boolean, + cacheLife: number, routeOrBody?: string | BodyInit | object, bodyAs2ndParam?: BodyInit | object, requestInterceptor?: ValueOf> @@ -102,7 +105,9 @@ export default async function doFetchArgs( id: responseID, cached: cache.get(responseID) as Response | undefined, ageID: responseAgeID, - age: (cache.get(responseAgeID) || 0) as number + age: (cache.get(responseAgeID) || 0) as number, + isPersisted: persist && hasPersistentData(responseID), + persisted: persist ? getPersistentData(responseID) : undefined } } } diff --git a/src/persistFetch.ts b/src/persistentStorage.ts similarity index 94% rename from src/persistFetch.ts rename to src/persistentStorage.ts index 2a1f73a9..a9ffdb40 100644 --- a/src/persistFetch.ts +++ b/src/persistentStorage.ts @@ -7,7 +7,7 @@ const hasPersistentData = (url: string): boolean => { const getPersistentData = (url: string): any => { const urlCache = JSON.parse(localStorage[cacheName] || '{}') - return urlCache[url].data + return urlCache[url] && urlCache[url].data } const setPersistentData = (url: string, data: any, ttl = 24 * 3600000): void => { diff --git a/src/types.ts b/src/types.ts index 751c1a98..dc614e3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,8 @@ export interface DoFetchArgs { cached?: Response ageID: string age: number + isPersisted: boolean + persisted?: any } } diff --git a/src/useFetch.ts b/src/useFetch.ts index adc0f623..1003cfa7 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -17,7 +17,7 @@ import { import useFetchArgs from './useFetchArgs' import doFetchArgs from './doFetchArgs' import { invariant, tryGetData, responseKeys, responseMethods, responseFields } from './utils' -import { getPersistentData, hasPersistentData, setPersistentData } from './persistFetch' +import { setPersistentData } from './persistentStorage' const { CACHE_FIRST } = CachePolicies @@ -70,12 +70,19 @@ function useFetch(...args: UseFetchArgs): UseFetch { theController, cachePolicy, cache, + persist, + cacheLife, routeOrBody, body, interceptors.request ) - if (response.isCached && cachePolicy === CACHE_FIRST) { + if (response.isPersisted) { + res.current.data = response.persisted + data.current = res.current.data as TData + setLoading(false) + return data.current + } else if (response.isCached && cachePolicy === CACHE_FIRST) { setLoading(true) if (cacheLife > 0 && response.age > cacheLife) { cache.delete(response.id) @@ -108,41 +115,38 @@ function useFetch(...args: UseFetchArgs): UseFetch { let newData let newRes - if (persist && hasPersistentData(url)) { - data.current = getPersistentData(url) as TData - } else { - try { - newRes = await fetch(url, options) - res.current = newRes.clone() + try { + newRes = await fetch(url, options) + res.current = newRes.clone() - if (cachePolicy === CACHE_FIRST) { - cache.set(response.id, newRes.clone()) - if (cacheLife > 0) cache.set(response.ageID, Date.now()) - } + if (cachePolicy === CACHE_FIRST) { + cache.set(response.id, newRes.clone()) + if (cacheLife > 0) cache.set(response.ageID, Date.now()) + } - newData = await tryGetData(newRes, defaults.data) - res.current.data = onNewData(data.current, newData) + newData = await tryGetData(newRes, defaults.data) + res.current.data = onNewData(data.current, newData) - res.current = interceptors.response ? interceptors.response(res.current) : res.current - invariant('data' in res.current, 'You must have `data` field on the Response returned from your `interceptors.response`') - data.current = res.current.data as TData + res.current = interceptors.response ? interceptors.response(res.current) : res.current + invariant('data' in res.current, 'You must have `data` field on the Response returned from your `interceptors.response`') + data.current = res.current.data as TData - if (persist) { - setPersistentData(url, data.current) - } - - if (Array.isArray(data.current) && !!(data.current.length % perPage)) hasMore.current = false - } catch (err) { - if (attempts.current > 0) return doFetch(routeOrBody, body) - if (attempts.current < 1 && timedout.current) error.current = { name: 'AbortError', message: 'Timeout Error' } - if (err.name !== 'AbortError') error.current = err - } finally { - if (newRes && !newRes.ok && !error.current) error.current = { name: newRes.status, message: newRes.statusText } - if (attempts.current > 0) attempts.current -= 1 - timedout.current = false - if (timer) clearTimeout(timer) - controller.current = undefined + if (persist) { + const defaultPersistenceLength = 24 * 3600000 + setPersistentData(response.id, data.current, cacheLife || defaultPersistenceLength) } + + if (Array.isArray(data.current) && !!(data.current.length % perPage)) hasMore.current = false + } catch (err) { + if (attempts.current > 0) return doFetch(routeOrBody, body) + if (attempts.current < 1 && timedout.current) error.current = { name: 'AbortError', message: 'Timeout Error' } + if (err.name !== 'AbortError') error.current = err + } finally { + if (newRes && !newRes.ok && !error.current) error.current = { name: newRes.status, message: newRes.statusText } + if (attempts.current > 0) attempts.current -= 1 + timedout.current = false + if (timer) clearTimeout(timer) + controller.current = undefined } setLoading(false) From 0e886582f30c781c2d97ad25b5329c1b52f19b14 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Wed, 11 Mar 2020 23:21:39 +0100 Subject: [PATCH 06/24] Update README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8b9b2337..2e1afdc1 100644 --- a/README.md +++ b/README.md @@ -688,6 +688,7 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr | `loading` | Allows you to set default value for `loading` | `false` unless the last argument of `useFetch` is `[]` | | `interceptors.request` | Allows you to do something before an http request is sent out. Useful for authentication if you need to refresh tokens a lot. | `undefined` | | `interceptors.response` | Allows you to do something after an http response is recieved. Useful for something like camelCasing the keys of the response. | `undefined` | +| `persist` | Persists data for the duration of cacheLife. If cacheLife is not set it defaults to 24h. Only available in Browser. | `false` | ```jsx const options = { @@ -700,6 +701,9 @@ const options = { // The time in milliseconds that cache data remains fresh. cacheLife: 0, + // Sets wether to persist data ot not after page refresh. Only available on Browser. + persist: false, + // used to be `baseUrl`. You can set your URL this way instead of as the 1st argument url: 'https://example.com', @@ -824,8 +828,6 @@ Todos const request = useFetch({ // enabled React Suspense mode suspense: false, - // allows caching to persist after page refresh - persist: true, // false by default // Allows you to pass in your own cache to useFetch // This is controversial though because `cache` is an option in the requestInit // and it's value is a string. See: https://developer.mozilla.org/en-US/docs/Web/API/Request/cache From e38c4b9ab657e7847fe3140b07b6ca6cd42c48d0 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Wed, 11 Mar 2020 23:37:03 +0100 Subject: [PATCH 07/24] Add to docs/README --- docs/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/README.md b/docs/README.md index 6ab98cc8..ca1c494f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -651,6 +651,7 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr | `loading` | Allows you to set default value for `loading` | `false` unless the last argument of `useFetch` is `[]` | | `interceptors.request` | Allows you to do something before an http request is sent out. Useful for authentication if you need to refresh tokens a lot. | `undefined` | | `interceptors.response` | Allows you to do something after an http response is recieved. Useful for something like camelCasing the keys of the response. | `undefined` | +| `persist` | Persists data for the duration of cacheLife. If cacheLife is not set it defaults to 24h. Only available in Browser. | `false` | ```jsx const options = { @@ -663,6 +664,9 @@ const options = { // The time in milliseconds that cache data remains fresh. cacheLife: 0, + // Sets wether to persist data ot not after page refresh. Only available on Browser. + persist: false, + // used to be `baseUrl`. You can set your URL this way instead of as the 1st argument url: 'https://example.com', From ba3f1b1b0843de2374f421e7a1054a66a9bd712e Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Thu, 12 Mar 2020 18:54:18 +0100 Subject: [PATCH 08/24] Remove age. --- src/doFetchArgs.ts | 7 ++++--- src/types.ts | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts index cf8fd890..6e0f6d68 100644 --- a/src/doFetchArgs.ts +++ b/src/doFetchArgs.ts @@ -99,6 +99,8 @@ export default async function doFetchArgs( const responseAge = Date.now() - ((cache.get(responseAgeID) || 0) as number) + const isPersisted = persist && hasPersistentData(responseID) + return { url, options, @@ -108,9 +110,8 @@ export default async function doFetchArgs( id: responseID, cached: cache.get(responseID) as Response | undefined, ageID: responseAgeID, - age: (cache.get(responseAgeID) || 0) as number, - isPersisted: persist && hasPersistentData(responseID), - persisted: persist ? getPersistentData(responseID) : undefined + isPersisted, + persisted: isPersisted ? getPersistentData(responseID) : undefined } } } diff --git a/src/types.ts b/src/types.ts index 15c5925f..9e40a09e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,7 +71,6 @@ export interface DoFetchArgs { id: string cached?: Response ageID: string - age: number isPersisted: boolean persisted?: any } From efb30c1839f84207dc2b40a184a22c3aa3908272 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Thu, 12 Mar 2020 18:54:43 +0100 Subject: [PATCH 09/24] Add error handling on persistentStorage. --- src/persistentStorage.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/persistentStorage.ts b/src/persistentStorage.ts index a9ffdb40..00a2d167 100644 --- a/src/persistentStorage.ts +++ b/src/persistentStorage.ts @@ -1,17 +1,26 @@ const cacheName = 'useHTTPcache' +const getCache = () => { + try { + return JSON.parse(localStorage[cacheName] || '{}') + } catch (err) { + localStorage.removeItem(cacheName) + return {} + } +} + const hasPersistentData = (url: string): boolean => { - const urlCache = JSON.parse(localStorage[cacheName] || '{}') + const urlCache = getCache() return urlCache[url] && urlCache[url].timestamp > Date.now() - urlCache[url].ttl } const getPersistentData = (url: string): any => { - const urlCache = JSON.parse(localStorage[cacheName] || '{}') + const urlCache = getCache() return urlCache[url] && urlCache[url].data } const setPersistentData = (url: string, data: any, ttl = 24 * 3600000): void => { - const urlCache = JSON.parse(localStorage[cacheName] || '{}') + const urlCache = getCache() urlCache[url] = { url, data, From 0829cdc092ffda67fe6a7dc3a47b4ac87bee5aed Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Thu, 12 Mar 2020 19:03:50 +0100 Subject: [PATCH 10/24] Invariant check on browser and persist. --- src/useFetch.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/useFetch.ts b/src/useFetch.ts index 627dfed2..fc55f1ba 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -40,7 +40,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { cacheLife } = customOptions - const { isServer } = useSSR() + const { isBrowser, isServer } = useSSR() const controller = useRef() const res = useRef>({} as Res) @@ -52,6 +52,8 @@ function useFetch(...args: UseFetchArgs): UseFetch { const [loading, setLoading] = useState(defaults.loading) + invariant(!persist || isBrowser, 'Persistence is only supported on browsers') + const makeFetch = useCallback((method: HTTPMethod): FetchData => { const doFetch = async ( routeOrBody?: string | BodyInit | object, From 92ac2157c795bf3a8c8230e0e471661da852f8ba Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Thu, 12 Mar 2020 19:13:03 +0100 Subject: [PATCH 11/24] Rename persistantStorage functions --- src/doFetchArgs.ts | 6 +++--- src/persistentStorage.ts | 14 +++++++------- src/useFetch.ts | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts index 6e0f6d68..375e3572 100644 --- a/src/doFetchArgs.ts +++ b/src/doFetchArgs.ts @@ -1,6 +1,6 @@ import { HTTPMethod, Interceptors, ValueOf, DoFetchArgs, CachePolicies, Res } from './types' import { invariant, isServer, isString, isBodyObject } from './utils' -import { hasPersistentData, getPersistentData } from './persistentStorage' +import persistentStorage from './persistentStorage' const { GET } = HTTPMethod @@ -99,7 +99,7 @@ export default async function doFetchArgs( const responseAge = Date.now() - ((cache.get(responseAgeID) || 0) as number) - const isPersisted = persist && hasPersistentData(responseID) + const isPersisted = persist && persistentStorage.hasItem(responseID) return { url, @@ -111,7 +111,7 @@ export default async function doFetchArgs( cached: cache.get(responseID) as Response | undefined, ageID: responseAgeID, isPersisted, - persisted: isPersisted ? getPersistentData(responseID) : undefined + persisted: isPersisted ? persistentStorage.getItem(responseID) : undefined } } } diff --git a/src/persistentStorage.ts b/src/persistentStorage.ts index 00a2d167..d809aafc 100644 --- a/src/persistentStorage.ts +++ b/src/persistentStorage.ts @@ -9,17 +9,17 @@ const getCache = () => { } } -const hasPersistentData = (url: string): boolean => { +const hasItem = (url: string): boolean => { const urlCache = getCache() return urlCache[url] && urlCache[url].timestamp > Date.now() - urlCache[url].ttl } -const getPersistentData = (url: string): any => { +const getItem = (url: string): any => { const urlCache = getCache() return urlCache[url] && urlCache[url].data } -const setPersistentData = (url: string, data: any, ttl = 24 * 3600000): void => { +const setItem = (url: string, data: any, ttl = 24 * 3600000): void => { const urlCache = getCache() urlCache[url] = { url, @@ -30,8 +30,8 @@ const setPersistentData = (url: string, data: any, ttl = 24 * 3600000): void => localStorage.setItem(cacheName, JSON.stringify(urlCache)) } -export { - hasPersistentData, - getPersistentData, - setPersistentData +export default { + hasItem, + getItem, + setItem } diff --git a/src/useFetch.ts b/src/useFetch.ts index fc55f1ba..b8bb9694 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -17,7 +17,7 @@ import { import useFetchArgs from './useFetchArgs' import doFetchArgs from './doFetchArgs' import { invariant, tryGetData, responseKeys, responseMethods, responseFields } from './utils' -import { setPersistentData } from './persistentStorage' +import persistentStorage from './persistentStorage' const { CACHE_FIRST } = CachePolicies @@ -135,7 +135,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { if (persist) { const defaultPersistenceLength = 24 * 3600000 - setPersistentData(response.id, data.current, cacheLife || defaultPersistenceLength) + persistentStorage.setItem(response.id, data.current, cacheLife || defaultPersistenceLength) } if (Array.isArray(data.current) && !!(data.current.length % perPage)) hasMore.current = false From a3c88e371a94426c00e71f5e5d4c47f859676643 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Thu, 12 Mar 2020 19:19:23 +0100 Subject: [PATCH 12/24] Make persistentStorage promise based --- src/doFetchArgs.ts | 4 ++-- src/persistentStorage.ts | 11 ++++++----- src/useFetch.ts | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts index 375e3572..7d33bae8 100644 --- a/src/doFetchArgs.ts +++ b/src/doFetchArgs.ts @@ -99,7 +99,7 @@ export default async function doFetchArgs( const responseAge = Date.now() - ((cache.get(responseAgeID) || 0) as number) - const isPersisted = persist && persistentStorage.hasItem(responseID) + const isPersisted = persist && await persistentStorage.hasItem(responseID) return { url, @@ -111,7 +111,7 @@ export default async function doFetchArgs( cached: cache.get(responseID) as Response | undefined, ageID: responseAgeID, isPersisted, - persisted: isPersisted ? persistentStorage.getItem(responseID) : undefined + persisted: isPersisted ? await persistentStorage.getItem(responseID) : undefined } } } diff --git a/src/persistentStorage.ts b/src/persistentStorage.ts index d809aafc..02381e9e 100644 --- a/src/persistentStorage.ts +++ b/src/persistentStorage.ts @@ -9,17 +9,17 @@ const getCache = () => { } } -const hasItem = (url: string): boolean => { +const hasItem = (url: string): Promise => { const urlCache = getCache() - return urlCache[url] && urlCache[url].timestamp > Date.now() - urlCache[url].ttl + return Promise.resolve(urlCache[url] && urlCache[url].timestamp > Date.now() - urlCache[url].ttl) } -const getItem = (url: string): any => { +const getItem = (url: string): Promise => { const urlCache = getCache() - return urlCache[url] && urlCache[url].data + return Promise.resolve(urlCache[url] && urlCache[url].data) } -const setItem = (url: string, data: any, ttl = 24 * 3600000): void => { +const setItem = (url: string, data: any, ttl = 24 * 3600000): Promise => { const urlCache = getCache() urlCache[url] = { url, @@ -28,6 +28,7 @@ const setItem = (url: string, data: any, ttl = 24 * 3600000): void => { ttl } localStorage.setItem(cacheName, JSON.stringify(urlCache)) + return Promise.resolve() } export default { diff --git a/src/useFetch.ts b/src/useFetch.ts index b8bb9694..0ee2b0bd 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -135,7 +135,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { if (persist) { const defaultPersistenceLength = 24 * 3600000 - persistentStorage.setItem(response.id, data.current, cacheLife || defaultPersistenceLength) + await persistentStorage.setItem(response.id, data.current, cacheLife || defaultPersistenceLength) } if (Array.isArray(data.current) && !!(data.current.length % perPage)) hasMore.current = false From ee6338af3601279924ee10781f1b5b1956fa391c Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Thu, 12 Mar 2020 19:27:31 +0100 Subject: [PATCH 13/24] Add persist test for doFetchArgs --- src/__tests__/doFetchArgs.test.tsx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/__tests__/doFetchArgs.test.tsx b/src/__tests__/doFetchArgs.test.tsx index 98dcc56d..fd29d04a 100644 --- a/src/__tests__/doFetchArgs.test.tsx +++ b/src/__tests__/doFetchArgs.test.tsx @@ -1,6 +1,9 @@ import doFetchArgs from '../doFetchArgs' import { HTTPMethod } from '../types' import { defaults } from '../useFetchArgs' +import persistentStorage from '../persistentStorage' + +jest.mock('../persistentStorage') describe('doFetchArgs: general usages', (): void => { it('should be defined', (): void => { @@ -115,6 +118,33 @@ describe('doFetchArgs: general usages', (): void => { signal: controller.signal }) }) + + it('should return persistent data', async (): Promise => { + const persistedData = {} + const getItemMock = persistentStorage.getItem as jest.MockedFunction + const hasItemMock = persistentStorage.hasItem as jest.MockedFunction + getItemMock.mockResolvedValue(persistedData) + hasItemMock.mockResolvedValue(true) + + const controller = new AbortController() + const expectedRoute = '/test' + const cache = new Map() + const { response: { isPersisted, persisted } } = await doFetchArgs( + {}, + '', + '', + HTTPMethod.POST, + controller, + defaults.cachePolicy, + defaults.cacheLife, + cache, + true, + expectedRoute, + {} + ) + expect(isPersisted).toBeTruthy() + expect(persisted).toBe(persistedData) + }) }) describe('doFetchArgs: Errors', (): void => { From a2c573b239710cb2f424dfc3ead3e91abd95e4c0 Mon Sep 17 00:00:00 2001 From: Alex Cory Date: Thu, 12 Mar 2020 16:11:03 -0700 Subject: [PATCH 14/24] general idea --- src/doFetchArgs.ts | 8 ++--- src/persistentStorage.ts | 38 ---------------------- src/types.ts | 1 - src/useCache.ts | 70 ++++++++++++++++++++++++++++++++++++++++ src/useFetch.ts | 37 ++++----------------- src/utils.ts | 22 ++++++++++--- 6 files changed, 96 insertions(+), 80 deletions(-) delete mode 100644 src/persistentStorage.ts create mode 100644 src/useCache.ts diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts index 7d33bae8..036c370d 100644 --- a/src/doFetchArgs.ts +++ b/src/doFetchArgs.ts @@ -97,21 +97,17 @@ export default async function doFetchArgs( .map(([key, value]) => `${key}:${value}`).join('||') const responseAgeID = `${responseID}:ts` - const responseAge = Date.now() - ((cache.get(responseAgeID) || 0) as number) - - const isPersisted = persist && await persistentStorage.hasItem(responseID) + const responseAge = Date.now() - ((await cache.get(responseAgeID) || 0) as number) return { url, options, response: { - isCached: cache.has(responseID), + isCached: await cache.has(responseID), isExpired: cacheLife > 0 && responseAge > cacheLife, id: responseID, cached: cache.get(responseID) as Response | undefined, ageID: responseAgeID, - isPersisted, - persisted: isPersisted ? await persistentStorage.getItem(responseID) : undefined } } } diff --git a/src/persistentStorage.ts b/src/persistentStorage.ts deleted file mode 100644 index 02381e9e..00000000 --- a/src/persistentStorage.ts +++ /dev/null @@ -1,38 +0,0 @@ -const cacheName = 'useHTTPcache' - -const getCache = () => { - try { - return JSON.parse(localStorage[cacheName] || '{}') - } catch (err) { - localStorage.removeItem(cacheName) - return {} - } -} - -const hasItem = (url: string): Promise => { - const urlCache = getCache() - return Promise.resolve(urlCache[url] && urlCache[url].timestamp > Date.now() - urlCache[url].ttl) -} - -const getItem = (url: string): Promise => { - const urlCache = getCache() - return Promise.resolve(urlCache[url] && urlCache[url].data) -} - -const setItem = (url: string, data: any, ttl = 24 * 3600000): Promise => { - const urlCache = getCache() - urlCache[url] = { - url, - data, - timestamp: Date.now(), - ttl - } - localStorage.setItem(cacheName, JSON.stringify(urlCache)) - return Promise.resolve() -} - -export default { - hasItem, - getItem, - setItem -} diff --git a/src/types.ts b/src/types.ts index 9e40a09e..36536d4b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,7 +71,6 @@ export interface DoFetchArgs { id: string cached?: Response ageID: string - isPersisted: boolean persisted?: any } } diff --git a/src/useCache.ts b/src/useCache.ts new file mode 100644 index 00000000..6af3afcb --- /dev/null +++ b/src/useCache.ts @@ -0,0 +1,70 @@ +import useSSR from 'use-ssr' +import { invariant, toResponseObject } from "./utils" + +const cacheName = 'useHTTPcache' + +const getCache = () => { + try { + return JSON.parse(localStorage[cacheName] || '{}') + } catch (err) { + localStorage.removeItem(cacheName) + return {} + } +} + + +/** + * Eventually, this will be replaced by use-react-storage, so + * having this as a hook allows us to have minimal changes in + * the future when switching over. + */ +type UseCacheArgs = { persist: boolean, cacheLife: number } +const inMemoryCache = new Map() +const useCache = ({ persist, cacheLife }: UseCacheArgs) => { + const { isNative, isServer, isBrowser } = useSSR() + invariant(!(isServer && persist), 'There is no persistant storage on the Server currently! 🙅‍♂️') + invariant(!(isNative && !isServer && !isBrowser), 'React Native support is not yet implemented!') + + // right now we're not worrying about react-native + if (persist) return useLocalStorage({ cacheLife }) + return inMemoryCache +} + +const useLocalStorage = ({ cacheLife }: { cacheLife: number }) => { + // there isn't state here now, but will be eventually + + const has = async (responseID: string): Promise => { + const cache = getCache() + return !!(cache[responseID] && cache[responseID].response) + } + + const get = async (responseID: string): Promise => { + const cache = getCache() + const { body, headers, status, statusText } = (cache[responseID] ? cache[responseID].response : {}) as any + return new Response(body, { + status, + statusText, + headers: new Headers(headers || {}) + }) + } + + const set = async (responseID: string, response: Response): Promise => { + const cache = getCache() + cache[responseID] = { + response: toResponseObject(response), + timestamp: Date.now(), + ttl: cacheLife || 24 * 3600000 + } + localStorage.setItem(cacheName, JSON.stringify(cache)) + } + + const remove = async (...responseIDs: string[]) => { + const cache = getCache() + responseIDs.forEach(id => delete cache[id]) + localStorage.setItem(cacheName, cache) + } + + return { get, set, has, delete: remove } +} + +export default useCache \ No newline at end of file diff --git a/src/useFetch.ts b/src/useFetch.ts index 0ee2b0bd..87eaef53 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -1,5 +1,4 @@ import { useEffect, useState, useCallback, useRef } from 'react' -import { FunctionKeys, NonFunctionKeys } from 'utility-types' import useSSR from 'use-ssr' import { HTTPMethod, @@ -16,12 +15,11 @@ import { } from './types' import useFetchArgs from './useFetchArgs' import doFetchArgs from './doFetchArgs' -import { invariant, tryGetData, responseKeys, responseMethods, responseFields } from './utils' -import persistentStorage from './persistentStorage' +import { invariant, tryGetData, toResponseObject } from './utils' +import useCache from './useCache' const { CACHE_FIRST } = CachePolicies -const cache = new Map() function useFetch(...args: UseFetchArgs): UseFetch { const { customOptions, requestInit, defaults, dependencies } = useFetchArgs(...args) @@ -40,6 +38,8 @@ function useFetch(...args: UseFetchArgs): UseFetch { cacheLife } = customOptions + const cache = useCache({ persist, cacheLife }) + const { isBrowser, isServer } = useSSR() const controller = useRef() @@ -79,12 +79,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { interceptors.request ) - if (response.isPersisted) { - res.current.data = response.persisted - data.current = res.current.data as TData - setLoading(false) - return data.current - } else if (response.isCached && cachePolicy === CACHE_FIRST) { + if (response.isCached && cachePolicy === CACHE_FIRST) { setLoading(true) if (response.isExpired) { cache.delete(response.id) @@ -177,27 +172,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { data: data.current } - const response = Object.defineProperties({}, responseKeys.reduce((acc: any, field: keyof Res) => { - if (responseFields.includes(field as any)) { - acc[field] = { - get: () => { - if (field === 'data') return data.current - const clonedResponse = ('clone' in res.current ? res.current.clone() : {}) as Res - return clonedResponse[field as (NonFunctionKeys> | 'data')] - }, - enumerable: true - } - } else if (responseMethods.includes(field as any)) { - acc[field] = { - value: () => { - const clonedResponse = ('clone' in res.current ? res.current.clone() : {}) as Res - return clonedResponse[field as Exclude>, 'data'>]() - }, - enumerable: true - } - } - return acc - }, {})) + const response = toResponseObject(res, data) // onMount/onUpdate useEffect((): any => { diff --git a/src/utils.ts b/src/utils.ts index bfde343c..4b93792b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { useMemo, useEffect } from 'react' +import { useMemo, useEffect, MutableRefObject } from 'react' import useSSR from 'use-ssr' import { RequestInitJSON, OptionsMaybeURL, Res } from './types' import { FunctionKeys, NonFunctionKeys } from 'utility-types' @@ -173,17 +173,31 @@ export const responseMethods: ResponseMethods[] = ['clone', 'arrayBuffer', 'blob // const responseFields = [...Object.getOwnPropertyNames(Object.getPrototypeOf(new Response())), 'data'].filter(p => p !== 'constructor') type ResponseKeys = (keyof Res) export const responseKeys: ResponseKeys[] = [...responseFields, ...responseMethods] -export const emptyCustomResponse = Object.defineProperties({}, responseKeys.reduce((acc: any, field: ResponseKeys) => { +export const toResponseObject = (res?: Response | MutableRefObject, data?: any) => Object.defineProperties( + {}, + responseKeys.reduce((acc: any, field: ResponseKeys) => { if (responseFields.includes(field as any)) { acc[field] = { - get: () => { /* undefined */ }, + get: () => { + const response = res instanceof Response ? res : res && res.current + if (!response) return + if (field === 'data') return data.current + const clonedResponse = ('clone' in response ? response.clone() : {}) as Res + return clonedResponse[field as (NonFunctionKeys> | 'data')] + }, enumerable: true } } else if (responseMethods.includes(field as any)) { acc[field] = { - value: () => { /* undefined */ }, + value: () => { + const response = res instanceof Response ? res : res && res.current + if (!response) return + const clonedResponse = ('clone' in response ? response.clone() : {}) as Res + return clonedResponse[field as Exclude>, 'data'>]() + }, enumerable: true } } return acc }, {})) +export const emptyCustomResponse = toResponseObject() From ef5252ca659fcd5100d6c1e0139d4faf340be3f9 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sat, 14 Mar 2020 07:44:20 +0100 Subject: [PATCH 15/24] Fix Cache implementations --- src/__tests__/doFetchArgs.test.tsx | 73 ++++++++--------------- src/__tests__/useFetch.test.tsx | 2 +- src/doFetchArgs.ts | 14 +---- src/types.ts | 10 +++- src/useCache.ts | 96 +++++++++++++++++++++--------- src/useFetch.ts | 36 ++++------- src/utils.ts | 48 ++++++++------- 7 files changed, 139 insertions(+), 140 deletions(-) diff --git a/src/__tests__/doFetchArgs.test.tsx b/src/__tests__/doFetchArgs.test.tsx index fd29d04a..073168ca 100644 --- a/src/__tests__/doFetchArgs.test.tsx +++ b/src/__tests__/doFetchArgs.test.tsx @@ -1,9 +1,7 @@ import doFetchArgs from '../doFetchArgs' import { HTTPMethod } from '../types' import { defaults } from '../useFetchArgs' -import persistentStorage from '../persistentStorage' - -jest.mock('../persistentStorage') +import useCache from '../useCache' describe('doFetchArgs: general usages', (): void => { it('should be defined', (): void => { @@ -13,17 +11,18 @@ describe('doFetchArgs: general usages', (): void => { it('should form the correct URL', async (): Promise => { const controller = new AbortController() const expectedRoute = '/test' - const cache = new Map() + const cache = useCache({ + persist: false, + cacheLife: defaults.cacheLife + }) const { url, options } = await doFetchArgs( {}, '', '', HTTPMethod.POST, controller, - defaults.cachePolicy, defaults.cacheLife, cache, - false, expectedRoute, {} ) @@ -40,17 +39,18 @@ describe('doFetchArgs: general usages', (): void => { it('should accept an array for the body of a request', async (): Promise => { const controller = new AbortController() - const cache = new Map() + const cache = useCache({ + persist: false, + cacheLife: defaults.cacheLife + }) const { options, url } = await doFetchArgs( {}, 'https://example.com', '', HTTPMethod.POST, controller, - defaults.cachePolicy, defaults.cacheLife, cache, - false, '/test', [] ) @@ -67,17 +67,18 @@ describe('doFetchArgs: general usages', (): void => { it('should correctly add `path` and `route` to the URL', async (): Promise => { const controller = new AbortController() - const cache = new Map() + const cache = useCache({ + persist: false, + cacheLife: defaults.cacheLife + }) const { url } = await doFetchArgs( {}, 'https://example.com', '/path', HTTPMethod.POST, controller, - defaults.cachePolicy, defaults.cacheLife, cache, - false, '/route', {} ) @@ -86,7 +87,10 @@ describe('doFetchArgs: general usages', (): void => { it('should correctly modify the options with the request interceptor', async (): Promise => { const controller = new AbortController() - const cache = new Map() + const cache = useCache({ + persist: false, + cacheLife: defaults.cacheLife + }) const interceptors = { request(options: any) { options.headers.Authorization = 'Bearer test' @@ -99,10 +103,8 @@ describe('doFetchArgs: general usages', (): void => { '', HTTPMethod.POST, controller, - defaults.cachePolicy, defaults.cacheLife, cache, - false, '/test', {}, interceptors.request @@ -118,39 +120,15 @@ describe('doFetchArgs: general usages', (): void => { signal: controller.signal }) }) - - it('should return persistent data', async (): Promise => { - const persistedData = {} - const getItemMock = persistentStorage.getItem as jest.MockedFunction - const hasItemMock = persistentStorage.hasItem as jest.MockedFunction - getItemMock.mockResolvedValue(persistedData) - hasItemMock.mockResolvedValue(true) - - const controller = new AbortController() - const expectedRoute = '/test' - const cache = new Map() - const { response: { isPersisted, persisted } } = await doFetchArgs( - {}, - '', - '', - HTTPMethod.POST, - controller, - defaults.cachePolicy, - defaults.cacheLife, - cache, - true, - expectedRoute, - {} - ) - expect(isPersisted).toBeTruthy() - expect(persisted).toBe(persistedData) - }) }) describe('doFetchArgs: Errors', (): void => { it('should error if 1st and 2nd arg of doFetch are both objects', async (): Promise => { const controller = new AbortController() - const cache = new Map() + const cache = useCache({ + persist: false, + cacheLife: defaults.cacheLife + }) // AKA, the last 2 arguments of doFetchArgs are both objects // try { // await doFetchArgs( @@ -175,10 +153,8 @@ describe('doFetchArgs: Errors', (): void => { '', HTTPMethod.GET, controller, - defaults.cachePolicy, defaults.cacheLife, cache, - false, {}, {} ) @@ -190,7 +166,10 @@ describe('doFetchArgs: Errors', (): void => { it('should error if 1st and 2nd arg of doFetch are both arrays', async (): Promise => { const controller = new AbortController() - const cache = new Map() + const cache = useCache({ + persist: false, + cacheLife: defaults.cacheLife + }) // AKA, the last 2 arguments of doFetchArgs are both arrays // try { // await doFetchArgs( @@ -215,10 +194,8 @@ describe('doFetchArgs: Errors', (): void => { '', HTTPMethod.GET, controller, - defaults.cachePolicy, defaults.cacheLife, cache, - false, [], [] ) diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx index 645e14f4..92c13f93 100644 --- a/src/__tests__/useFetch.test.tsx +++ b/src/__tests__/useFetch.test.tsx @@ -859,7 +859,7 @@ describe('useFetch - BROWSER - persistence', (): void => { }) it('should fetch again after 24h', async (): Promise => { - mockdate.set('2020-01-02 01:00') + mockdate.set('2020-01-02 02:00:00') const { waitForNextUpdate } = renderHook( () => useFetch({ url: 'https://persist.com', persist: true, cachePolicy: NO_CACHE }, []) diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts index 036c370d..77ace3cb 100644 --- a/src/doFetchArgs.ts +++ b/src/doFetchArgs.ts @@ -1,6 +1,5 @@ -import { HTTPMethod, Interceptors, ValueOf, DoFetchArgs, CachePolicies, Res } from './types' +import { HTTPMethod, Interceptors, ValueOf, DoFetchArgs, Cache } from './types' import { invariant, isServer, isString, isBodyObject } from './utils' -import persistentStorage from './persistentStorage' const { GET } = HTTPMethod @@ -10,10 +9,8 @@ export default async function doFetchArgs( path: string, method: HTTPMethod, controller: AbortController, - cachePolicy: CachePolicies, cacheLife: number, - cache: Map | number>, - persist: boolean, + cache: Cache, routeOrBody?: string | BodyInit | object, bodyAs2ndParam?: BodyInit | object, requestInterceptor?: ValueOf> @@ -95,19 +92,14 @@ export default async function doFetchArgs( // used to tell if a request has already been made const responseID = Object.entries({ url, method, body: options.body || '' }) .map(([key, value]) => `${key}:${value}`).join('||') - const responseAgeID = `${responseID}:ts` - - const responseAge = Date.now() - ((await cache.get(responseAgeID) || 0) as number) return { url, options, response: { isCached: await cache.has(responseID), - isExpired: cacheLife > 0 && responseAge > cacheLife, id: responseID, - cached: cache.get(responseID) as Response | undefined, - ageID: responseAgeID, + cached: await cache.get(responseID) as Response | undefined } } } diff --git a/src/types.ts b/src/types.ts index 36536d4b..523114e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,11 +67,8 @@ export interface DoFetchArgs { options: RequestInit response: { isCached: boolean - isExpired: boolean id: string cached?: Response - ageID: string - persisted?: any } } @@ -161,6 +158,13 @@ export type Interceptors = { response?: (response: Res) => Res } +export type Cache = { + get: (name: string) => Promise + set: (name: string, data: Response) => Promise + has: (name: string) => Promise + delete: (...names: string[]) => Promise +} + export interface CustomOptions { retries?: number persist?: boolean diff --git a/src/useCache.ts b/src/useCache.ts index 6af3afcb..4cfd71ba 100644 --- a/src/useCache.ts +++ b/src/useCache.ts @@ -1,5 +1,7 @@ import useSSR from 'use-ssr' -import { invariant, toResponseObject } from "./utils" +import { invariant, toResponseObject, tryGetData } from './utils' +import { Cache } from './types' +import { defaults } from './useFetchArgs' const cacheName = 'useHTTPcache' @@ -12,35 +14,61 @@ const getCache = () => { } } +const inMemoryStorage = new Map() +const getMemoryStorage = ({ cacheLife }: { cacheLife: number }): Cache => ({ + async get(name: string) { + const item = inMemoryStorage.get(name) as Response | undefined + if (!item) return -/** - * Eventually, this will be replaced by use-react-storage, so - * having this as a hook allows us to have minimal changes in - * the future when switching over. - */ -type UseCacheArgs = { persist: boolean, cacheLife: number } -const inMemoryCache = new Map() -const useCache = ({ persist, cacheLife }: UseCacheArgs) => { - const { isNative, isServer, isBrowser } = useSSR() - invariant(!(isServer && persist), 'There is no persistant storage on the Server currently! 🙅‍♂️') - invariant(!(isNative && !isServer && !isBrowser), 'React Native support is not yet implemented!') + const expiration = inMemoryStorage.get(`${name}:ts`) + if (expiration && expiration > 0 && expiration < Date.now()) { + inMemoryStorage.delete(name) + inMemoryStorage.delete(`${name}:ts`) + return + } - // right now we're not worrying about react-native - if (persist) return useLocalStorage({ cacheLife }) - return inMemoryCache -} + return item + }, + async set(name: string, data: Response) { + inMemoryStorage.set(name, data) + inMemoryStorage.set(`${name}:ts`, cacheLife > 0 ? Date.now() + cacheLife : 0) + }, + async has(name: string) { + return inMemoryStorage.has(name) + }, + async delete(name: string) { + inMemoryStorage.delete(name) + inMemoryStorage.delete(`${name}:ts`) + } +}) -const useLocalStorage = ({ cacheLife }: { cacheLife: number }) => { +const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { // there isn't state here now, but will be eventually + const remove = async (name: string) => { + const cache = getCache() + delete cache[name] + localStorage.setItem(cacheName, JSON.stringify(cache)) + } + const has = async (responseID: string): Promise => { const cache = getCache() return !!(cache[responseID] && cache[responseID].response) } - const get = async (responseID: string): Promise => { + const get = async (responseID: string): Promise => { const cache = getCache() - const { body, headers, status, statusText } = (cache[responseID] ? cache[responseID].response : {}) as any + if (!cache[responseID]) { + return + } + + const { expiration, response: { body, headers, status, statusText } } = cache[responseID] as any + if (expiration < Date.now()) { + delete cache[name] + localStorage.setItem(cacheName, JSON.stringify(cache)) + return + } + return new Response(body, { status, statusText, @@ -50,21 +78,31 @@ const useLocalStorage = ({ cacheLife }: { cacheLife: number }) => { const set = async (responseID: string, response: Response): Promise => { const cache = getCache() + const responseObject = toResponseObject(response, await tryGetData(response, defaults.data)) cache[responseID] = { - response: toResponseObject(response), - timestamp: Date.now(), - ttl: cacheLife || 24 * 3600000 + response: responseObject, + expiration: Date.now() + cacheLife } localStorage.setItem(cacheName, JSON.stringify(cache)) } - const remove = async (...responseIDs: string[]) => { - const cache = getCache() - responseIDs.forEach(id => delete cache[id]) - localStorage.setItem(cacheName, cache) - } - return { get, set, has, delete: remove } } -export default useCache \ No newline at end of file +/** + * Eventually, this will be replaced by use-react-storage, so + * having this as a hook allows us to have minimal changes in + * the future when switching over. + */ +type UseCacheArgs = { persist: boolean, cacheLife: number } +const useCache = ({ persist, cacheLife }: UseCacheArgs): Cache => { + const { isNative, isServer, isBrowser } = useSSR() + invariant(!(isServer && persist), 'There is no persistant storage on the Server currently! 🙅‍♂️') + invariant(!(isNative && !isServer && !isBrowser), 'React Native support is not yet implemented!') + + // right now we're not worrying about react-native + if (persist) return getLocalStorage({ cacheLife: cacheLife || (24 * 3600000) }) + return getMemoryStorage({ cacheLife }) +} + +export default useCache diff --git a/src/useFetch.ts b/src/useFetch.ts index 87eaef53..e6b8f5bc 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -20,7 +20,6 @@ import useCache from './useCache' const { CACHE_FIRST } = CachePolicies - function useFetch(...args: UseFetchArgs): UseFetch { const { customOptions, requestInit, defaults, dependencies } = useFetchArgs(...args) const { @@ -70,30 +69,23 @@ function useFetch(...args: UseFetchArgs): UseFetch { path, method, theController, - cachePolicy, cacheLife, cache, - persist, routeOrBody, body, interceptors.request ) - if (response.isCached && cachePolicy === CACHE_FIRST) { + if (response.isCached && (persist || cachePolicy === CACHE_FIRST)) { setLoading(true) - if (response.isExpired) { - cache.delete(response.id) - cache.delete(response.ageID) - } else { - try { - res.current.data = await tryGetData(response.cached, defaults.data) - data.current = res.current.data as TData - setLoading(false) - return data.current - } catch (err) { - error.current = err - setLoading(false) - } + try { + res.current.data = await tryGetData(response.cached, defaults.data) + data.current = res.current.data as TData + setLoading(false) + return data.current + } catch (err) { + error.current = err + setLoading(false) } } @@ -116,9 +108,8 @@ function useFetch(...args: UseFetchArgs): UseFetch { newRes = await fetch(url, options) res.current = newRes.clone() - if (cachePolicy === CACHE_FIRST) { - cache.set(response.id, newRes.clone()) - if (cacheLife > 0) cache.set(response.ageID, Date.now()) + if (persist || cachePolicy === CACHE_FIRST) { + await cache.set(response.id, newRes.clone()) } newData = await tryGetData(newRes, defaults.data) @@ -128,11 +119,6 @@ function useFetch(...args: UseFetchArgs): UseFetch { invariant('data' in res.current, 'You must have `data` field on the Response returned from your `interceptors.response`') data.current = res.current.data as TData - if (persist) { - const defaultPersistenceLength = 24 * 3600000 - await persistentStorage.setItem(response.id, data.current, cacheLife || defaultPersistenceLength) - } - if (Array.isArray(data.current) && !!(data.current.length % perPage)) hasMore.current = false } catch (err) { if (attempts.current > 0) return doFetch(routeOrBody, body) diff --git a/src/utils.ts b/src/utils.ts index 4b93792b..0958c796 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -176,28 +176,30 @@ export const responseKeys: ResponseKeys[] = [...responseFields, ...responseMetho export const toResponseObject = (res?: Response | MutableRefObject, data?: any) => Object.defineProperties( {}, responseKeys.reduce((acc: any, field: ResponseKeys) => { - if (responseFields.includes(field as any)) { - acc[field] = { - get: () => { - const response = res instanceof Response ? res : res && res.current - if (!response) return - if (field === 'data') return data.current - const clonedResponse = ('clone' in response ? response.clone() : {}) as Res - return clonedResponse[field as (NonFunctionKeys> | 'data')] - }, - enumerable: true + if (responseFields.includes(field as any)) { + acc[field] = { + get: () => { + const response = res instanceof Response ? res : res && res.current + if (!response) return + if (field === 'data') return data.current + const clonedResponse = ('clone' in response ? response.clone() : {}) as Res + if (field === 'body' && clonedResponse.body) return clonedResponse.body.toString() + return clonedResponse[field as (NonFunctionKeys> | 'data')] + }, + enumerable: true + } + } else if (responseMethods.includes(field as any)) { + acc[field] = { + value: () => { + const response = res instanceof Response ? res : res && res.current + if (!response) return + const clonedResponse = ('clone' in response ? response.clone() : {}) as Res + return clonedResponse[field as Exclude>, 'data'>]() + }, + enumerable: true + } } - } else if (responseMethods.includes(field as any)) { - acc[field] = { - value: () => { - const response = res instanceof Response ? res : res && res.current - if (!response) return - const clonedResponse = ('clone' in response ? response.clone() : {}) as Res - return clonedResponse[field as Exclude>, 'data'>]() - }, - enumerable: true - } - } - return acc -}, {})) + return acc + }, {})) + export const emptyCustomResponse = toResponseObject() From b45dd936b3b514f350e6d6bb05f2dd7cfe47dab1 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sat, 14 Mar 2020 07:48:17 +0100 Subject: [PATCH 16/24] Remove redundant invariant --- src/useFetch.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/useFetch.ts b/src/useFetch.ts index e6b8f5bc..734b46ab 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -39,7 +39,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { const cache = useCache({ persist, cacheLife }) - const { isBrowser, isServer } = useSSR() + const { isServer } = useSSR() const controller = useRef() const res = useRef>({} as Res) @@ -51,8 +51,6 @@ function useFetch(...args: UseFetchArgs): UseFetch { const [loading, setLoading] = useState(defaults.loading) - invariant(!persist || isBrowser, 'Persistence is only supported on browsers') - const makeFetch = useCallback((method: HTTPMethod): FetchData => { const doFetch = async ( routeOrBody?: string | BodyInit | object, From 4680997df90e8ed660fe22629a0f0f3cb581c228 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sat, 14 Mar 2020 20:12:26 +0100 Subject: [PATCH 17/24] Edit RN support message for persistant cache --- src/useCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useCache.ts b/src/useCache.ts index 4cfd71ba..0d60dfd8 100644 --- a/src/useCache.ts +++ b/src/useCache.ts @@ -98,7 +98,7 @@ type UseCacheArgs = { persist: boolean, cacheLife: number } const useCache = ({ persist, cacheLife }: UseCacheArgs): Cache => { const { isNative, isServer, isBrowser } = useSSR() invariant(!(isServer && persist), 'There is no persistant storage on the Server currently! 🙅‍♂️') - invariant(!(isNative && !isServer && !isBrowser), 'React Native support is not yet implemented!') + invariant(!(isNative && !isServer && !isBrowser), 'React Native support for persistant cache is not yet implemented. 🙅‍♂️') // right now we're not worrying about react-native if (persist) return getLocalStorage({ cacheLife: cacheLife || (24 * 3600000) }) From 6a156d1aa04f9774150bb7dfd9b51e1f6cd17f7b Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sat, 14 Mar 2020 21:03:47 +0100 Subject: [PATCH 18/24] Serialise Response object for local storage --- src/useCache.ts | 5 ++--- src/utils.ts | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/useCache.ts b/src/useCache.ts index 0d60dfd8..335c03d8 100644 --- a/src/useCache.ts +++ b/src/useCache.ts @@ -1,7 +1,6 @@ import useSSR from 'use-ssr' -import { invariant, toResponseObject, tryGetData } from './utils' +import { invariant, serialiseResponse } from './utils' import { Cache } from './types' -import { defaults } from './useFetchArgs' const cacheName = 'useHTTPcache' @@ -78,7 +77,7 @@ const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { const set = async (responseID: string, response: Response): Promise => { const cache = getCache() - const responseObject = toResponseObject(response, await tryGetData(response, defaults.data)) + const responseObject = await serialiseResponse(response) cache[responseID] = { response: responseObject, expiration: Date.now() + cacheLife diff --git a/src/utils.ts b/src/utils.ts index 0958c796..63093214 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -183,7 +183,6 @@ export const toResponseObject = (res?: Response | MutableRefObject< if (!response) return if (field === 'data') return data.current const clonedResponse = ('clone' in response ? response.clone() : {}) as Res - if (field === 'body' && clonedResponse.body) return clonedResponse.body.toString() return clonedResponse[field as (NonFunctionKeys> | 'data')] }, enumerable: true @@ -203,3 +202,24 @@ export const toResponseObject = (res?: Response | MutableRefObject< }, {})) export const emptyCustomResponse = toResponseObject() + +const headersAsObject = (headers: Headers): object => { + const obj: any = {} + headers.forEach((value, key) => { + obj[key] = value + }) + return obj +} + +export const serialiseResponse = async (response: Response) => { + const body = await response.text() + const status = response.status + const statusText = response.statusText + const headers = headersAsObject(response.headers) + return { + body, + status, + statusText, + headers + } +} From 6e72cea87dd6af74211379877fdf1b2d0d48e36d Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sat, 14 Mar 2020 21:58:47 +0100 Subject: [PATCH 19/24] Refactor useCache separate storage modules --- src/storage/localStorage.ts | 61 ++++++++++++++++++++++++ src/storage/memoryStorage.ts | 31 +++++++++++++ src/useCache.ts | 89 ++---------------------------------- 3 files changed, 95 insertions(+), 86 deletions(-) create mode 100644 src/storage/localStorage.ts create mode 100644 src/storage/memoryStorage.ts diff --git a/src/storage/localStorage.ts b/src/storage/localStorage.ts new file mode 100644 index 00000000..06b6a42f --- /dev/null +++ b/src/storage/localStorage.ts @@ -0,0 +1,61 @@ +import { serialiseResponse } from '../utils' +import { Cache } from '../types' + +const cacheName = 'useHTTPcache' + +const getCache = () => { + try { + return JSON.parse(localStorage.getItem(cacheName) || '{}') + } catch (err) { + localStorage.removeItem(cacheName) + return {} + } +} +const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { + // there isn't state here now, but will be eventually + + const remove = async (name: string) => { + const cache = getCache() + delete cache[name] + localStorage.setItem(cacheName, JSON.stringify(cache)) + } + + const has = async (responseID: string): Promise => { + const cache = getCache() + return !!(cache[responseID] && cache[responseID].response) + } + + const get = async (responseID: string): Promise => { + const cache = getCache() + if (!cache[responseID]) { + return + } + + const { expiration, response: { body, headers, status, statusText } } = cache[responseID] as any + if (expiration < Date.now()) { + delete cache[responseID] + localStorage.setItem(cacheName, JSON.stringify(cache)) + return + } + + return new Response(body, { + status, + statusText, + headers: new Headers(headers || {}) + }) + } + + const set = async (responseID: string, response: Response): Promise => { + const cache = getCache() + const responseObject = await serialiseResponse(response) + cache[responseID] = { + response: responseObject, + expiration: Date.now() + cacheLife + } + localStorage.setItem(cacheName, JSON.stringify(cache)) + } + + return { get, set, has, delete: remove } +} + +export default getLocalStorage diff --git a/src/storage/memoryStorage.ts b/src/storage/memoryStorage.ts new file mode 100644 index 00000000..d9c73bd0 --- /dev/null +++ b/src/storage/memoryStorage.ts @@ -0,0 +1,31 @@ +import { Cache } from '../types' + +const inMemoryStorage = new Map() +const getMemoryStorage = ({ cacheLife }: { cacheLife: number }): Cache => ({ + async get(name: string) { + const item = inMemoryStorage.get(name) as Response | undefined + if (!item) return + + const expiration = inMemoryStorage.get(`${name}:ts`) + if (expiration && expiration > 0 && expiration < Date.now()) { + inMemoryStorage.delete(name) + inMemoryStorage.delete(`${name}:ts`) + return + } + + return item + }, + async set(name: string, data: Response) { + inMemoryStorage.set(name, data) + inMemoryStorage.set(`${name}:ts`, cacheLife > 0 ? Date.now() + cacheLife : 0) + }, + async has(name: string) { + return inMemoryStorage.has(name) + }, + async delete(name: string) { + inMemoryStorage.delete(name) + inMemoryStorage.delete(`${name}:ts`) + } +}) + +export default getMemoryStorage diff --git a/src/useCache.ts b/src/useCache.ts index 335c03d8..182d204f 100644 --- a/src/useCache.ts +++ b/src/useCache.ts @@ -1,92 +1,9 @@ import useSSR from 'use-ssr' -import { invariant, serialiseResponse } from './utils' +import { invariant } from './utils' import { Cache } from './types' -const cacheName = 'useHTTPcache' - -const getCache = () => { - try { - return JSON.parse(localStorage[cacheName] || '{}') - } catch (err) { - localStorage.removeItem(cacheName) - return {} - } -} - -const inMemoryStorage = new Map() -const getMemoryStorage = ({ cacheLife }: { cacheLife: number }): Cache => ({ - async get(name: string) { - const item = inMemoryStorage.get(name) as Response | undefined - if (!item) return - - const expiration = inMemoryStorage.get(`${name}:ts`) - if (expiration && expiration > 0 && expiration < Date.now()) { - inMemoryStorage.delete(name) - inMemoryStorage.delete(`${name}:ts`) - return - } - - return item - }, - async set(name: string, data: Response) { - inMemoryStorage.set(name, data) - inMemoryStorage.set(`${name}:ts`, cacheLife > 0 ? Date.now() + cacheLife : 0) - }, - async has(name: string) { - return inMemoryStorage.has(name) - }, - async delete(name: string) { - inMemoryStorage.delete(name) - inMemoryStorage.delete(`${name}:ts`) - } -}) - -const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { - // there isn't state here now, but will be eventually - - const remove = async (name: string) => { - const cache = getCache() - delete cache[name] - localStorage.setItem(cacheName, JSON.stringify(cache)) - } - - const has = async (responseID: string): Promise => { - const cache = getCache() - return !!(cache[responseID] && cache[responseID].response) - } - - const get = async (responseID: string): Promise => { - const cache = getCache() - if (!cache[responseID]) { - return - } - - const { expiration, response: { body, headers, status, statusText } } = cache[responseID] as any - if (expiration < Date.now()) { - delete cache[name] - localStorage.setItem(cacheName, JSON.stringify(cache)) - return - } - - return new Response(body, { - status, - statusText, - headers: new Headers(headers || {}) - }) - } - - const set = async (responseID: string, response: Response): Promise => { - const cache = getCache() - const responseObject = await serialiseResponse(response) - cache[responseID] = { - response: responseObject, - expiration: Date.now() + cacheLife - } - localStorage.setItem(cacheName, JSON.stringify(cache)) - } - - return { get, set, has, delete: remove } -} +import getLocalStorage from './storage/localStorage' +import getMemoryStorage from './storage/memoryStorage' /** * Eventually, this will be replaced by use-react-storage, so From 2bbd1b1d2ef4e670de565e59f04f9b7520745534 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sat, 14 Mar 2020 21:58:58 +0100 Subject: [PATCH 20/24] Add tests for localStorageCache --- src/storage/__tests__/localStorage.test.ts | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/storage/__tests__/localStorage.test.ts diff --git a/src/storage/__tests__/localStorage.test.ts b/src/storage/__tests__/localStorage.test.ts new file mode 100644 index 00000000..093911d0 --- /dev/null +++ b/src/storage/__tests__/localStorage.test.ts @@ -0,0 +1,86 @@ +import { Cache } from '../../types' +import getLocalStorage from '../localStorage' + +import mockdate from 'mockdate' + +const localStorageMock = (function() { + const store: any = {} + + return { + store, + getItem: function(key: string) { + return store[key] || null + }, + setItem: function(key: string, value: string) { + store[key] = value.toString() + } + } +})() + +Object.defineProperty(global, 'localStorage', { + value: localStorageMock +}) + +describe('localStorage cache', () => { + let cache: Cache + const cacheLife = 3600000 // an hour + + beforeEach(() => { + cache = getLocalStorage({ cacheLife }) + }) + + afterAll((): void => { + mockdate.reset() + }) + + beforeAll((): void => { + mockdate.set('2020-01-01T00:00:00.000Z') + }) + + it('stores and recreates response', async () => { + const body = 'response body' + const status = 200 + const statusText = 'OK' + const headers = new Headers({ 'content-type': 'application/json' }) + const response = new Response( + body, + { + status, + statusText, + headers + } + ) + const responseID = 'aID' + + await cache.set(responseID, response) + const received = await cache.get(responseID) as Response + + expect(await received.text()).toEqual(body) + expect(received.ok).toBeTruthy() + expect(received.status).toEqual(status) + expect(received.statusText).toEqual(statusText) + expect(received.headers.get('content-type')).toEqual('application/json') + }) + + it('clears cache on expiration', async () => { + const body = 'response body' + const status = 200 + const statusText = 'OK' + const headers = new Headers({ 'content-type': 'application/json' }) + const response = new Response( + body, + { + status, + statusText, + headers + } + ) + const responseID = 'aID' + + await cache.set(responseID, response) + mockdate.set('2020-01-01T02:00:00.000Z') + await cache.get(responseID) + + expect(localStorageMock.store.useHTTPcache).toEqual('{}') + }) +}) From 37a8a65d59cceff56d6a16976580d58171151b09 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sun, 15 Mar 2020 19:15:53 +0100 Subject: [PATCH 21/24] Minor edit in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3b4c3d5..5a7d6367 100644 --- a/README.md +++ b/README.md @@ -701,7 +701,7 @@ const options = { // The time in milliseconds that cache data remains fresh. cacheLife: 0, - // Sets wether to persist data ot not after page refresh. Only available on Browser. + // Sets whether to persist the cache after page refresh. Only available in Browser. persist: false, // used to be `baseUrl`. You can set your URL this way instead of as the 1st argument From c7c8a1403ce7c3eb2dd2fd04000a804d62593b40 Mon Sep 17 00:00:00 2001 From: Mattias Rost Date: Sun, 15 Mar 2020 21:09:51 +0100 Subject: [PATCH 22/24] Remove NO_CACHE from persistence tests --- src/__tests__/useFetch.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx index 92c13f93..58471fd1 100644 --- a/src/__tests__/useFetch.test.tsx +++ b/src/__tests__/useFetch.test.tsx @@ -837,7 +837,7 @@ describe('useFetch - BROWSER - persistence', (): void => { it('should fetch once', async (): Promise => { const { waitForNextUpdate } = renderHook( - () => useFetch({ url: 'https://persist.com', persist: true, cachePolicy: NO_CACHE }, []) + () => useFetch({ url: 'https://persist.com', persist: true }, []) ) await waitForNextUpdate() @@ -849,7 +849,7 @@ describe('useFetch - BROWSER - persistence', (): void => { fetch.mockResponse(JSON.stringify(unexpected)) const { result, waitForNextUpdate } = renderHook( - () => useFetch({ url: 'https://persist.com', persist: true, cachePolicy: NO_CACHE }, []) + () => useFetch({ url: 'https://persist.com', persist: true }, []) ) await waitForNextUpdate() @@ -862,7 +862,7 @@ describe('useFetch - BROWSER - persistence', (): void => { mockdate.set('2020-01-02 02:00:00') const { waitForNextUpdate } = renderHook( - () => useFetch({ url: 'https://persist.com', persist: true, cachePolicy: NO_CACHE }, []) + () => useFetch({ url: 'https://persist.com', persist: true }, []) ) await waitForNextUpdate() From b12ba33a6104d9bdbe6feabb4101208f2f374736 Mon Sep 17 00:00:00 2001 From: Alex Cory Date: Sun, 15 Mar 2020 17:06:51 -0700 Subject: [PATCH 23/24] missed setting response into memory from cache, readme cleanup, tests failing but unrelated to this PR --- README.md | 2 +- docs/README.md | 2 +- src/__tests__/useFetch.test.tsx | 2 +- src/useFetch.ts | 11 +++++------ 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2dfbdacc..cafca8a9 100644 --- a/README.md +++ b/README.md @@ -805,7 +805,7 @@ const options = { // The time in milliseconds that cache data remains fresh. cacheLife: 0, - // Sets whether to persist the cache after page refresh. Only available in Browser. + // Allows caching to persist after page refresh. Only supported in the Browser currently. persist: false, // used to be `baseUrl`. You can set your URL this way instead of as the 1st argument diff --git a/docs/README.md b/docs/README.md index 6cb3598b..3f016c9b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -768,7 +768,7 @@ const options = { // The time in milliseconds that cache data remains fresh. cacheLife: 0, - // Sets wether to persist data ot not after page refresh. Only available on Browser. + // Allows caching to persist after page refresh. Only supported in the Browser currently. persist: false, // used to be `baseUrl`. You can set your URL this way instead of as the 1st argument diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx index ecfbfd11..bfb1a000 100644 --- a/src/__tests__/useFetch.test.tsx +++ b/src/__tests__/useFetch.test.tsx @@ -722,7 +722,7 @@ describe('useFetch - BROWSER - suspense', (): void => { ) expect(container.textContent).toMatchInlineSnapshot('"fallback"') - await test.act((): any => new Promise(resolve => setTimeout(resolve, 110))) + await test.act((): any => new Promise(resolve => setTimeout(resolve, 210))) expect(container.textContent).toMatchInlineSnapshot('"yay suspense"') }) diff --git a/src/useFetch.ts b/src/useFetch.ts index b6477821..5bf4040c 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -83,6 +83,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { if (response.isCached && (persist || cachePolicy === CACHE_FIRST)) { try { + res.current = response.cached as Res res.current.data = await tryGetData(response.cached, defaults.data) data.current = res.current.data as TData if (!suspense) setLoading(false) @@ -196,11 +197,6 @@ function useFetch(...args: UseFetchArgs): UseFetch { // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => request.abort, []) - const final = Object.assign, UseFetchObjectReturn>( - [request, response, loading, error.current], - { request, response, ...request } - ) - if (suspense && suspender.current) { if (isServer) throw new Error('Suspense on server side is not yet supported! 🙅‍♂️') switch (suspenseStatus.current) { @@ -210,7 +206,10 @@ function useFetch(...args: UseFetchArgs): UseFetch { throw error.current } } - return final + return Object.assign, UseFetchObjectReturn>( + [request, response, loading, error.current], + { request, response, ...request } + ) } export { useFetch } From 01a1c0f05f41e0f0faa40fc3a81f6f325d9ee5d2 Mon Sep 17 00:00:00 2001 From: Alex Cory Date: Tue, 17 Mar 2020 16:28:57 -0700 Subject: [PATCH 24/24] errors on server or native when using `persist`, adding cache to useFetch return, added `clear` method to `cache` --- src/__tests__/doFetchArgs.test.tsx | 18 ++++++++++++------ src/__tests__/useFetch.test.tsx | 1 - src/storage/localStorage.ts | 10 +++++++--- src/storage/memoryStorage.ts | 5 ++++- src/types.ts | 2 ++ src/useCache.ts | 14 ++++++++------ src/useFetch.ts | 7 ++++--- src/useMutation.ts | 4 ++-- src/useQuery.ts | 4 ++-- src/utils.ts | 2 +- yarn.lock | 18 +++++++++--------- 11 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/__tests__/doFetchArgs.test.tsx b/src/__tests__/doFetchArgs.test.tsx index 073168ca..e819b01c 100644 --- a/src/__tests__/doFetchArgs.test.tsx +++ b/src/__tests__/doFetchArgs.test.tsx @@ -13,7 +13,8 @@ describe('doFetchArgs: general usages', (): void => { const expectedRoute = '/test' const cache = useCache({ persist: false, - cacheLife: defaults.cacheLife + cacheLife: defaults.cacheLife, + cachePolicy: defaults.cachePolicy }) const { url, options } = await doFetchArgs( {}, @@ -41,7 +42,8 @@ describe('doFetchArgs: general usages', (): void => { const controller = new AbortController() const cache = useCache({ persist: false, - cacheLife: defaults.cacheLife + cacheLife: defaults.cacheLife, + cachePolicy: defaults.cachePolicy }) const { options, url } = await doFetchArgs( {}, @@ -69,7 +71,8 @@ describe('doFetchArgs: general usages', (): void => { const controller = new AbortController() const cache = useCache({ persist: false, - cacheLife: defaults.cacheLife + cacheLife: defaults.cacheLife, + cachePolicy: defaults.cachePolicy }) const { url } = await doFetchArgs( {}, @@ -89,7 +92,8 @@ describe('doFetchArgs: general usages', (): void => { const controller = new AbortController() const cache = useCache({ persist: false, - cacheLife: defaults.cacheLife + cacheLife: defaults.cacheLife, + cachePolicy: defaults.cachePolicy }) const interceptors = { request(options: any) { @@ -127,7 +131,8 @@ describe('doFetchArgs: Errors', (): void => { const controller = new AbortController() const cache = useCache({ persist: false, - cacheLife: defaults.cacheLife + cacheLife: defaults.cacheLife, + cachePolicy: defaults.cachePolicy }) // AKA, the last 2 arguments of doFetchArgs are both objects // try { @@ -168,7 +173,8 @@ describe('doFetchArgs: Errors', (): void => { const controller = new AbortController() const cache = useCache({ persist: false, - cacheLife: defaults.cacheLife + cacheLife: defaults.cacheLife, + cachePolicy: defaults.cachePolicy }) // AKA, the last 2 arguments of doFetchArgs are both arrays // try { diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx index bfb1a000..0b03a774 100644 --- a/src/__tests__/useFetch.test.tsx +++ b/src/__tests__/useFetch.test.tsx @@ -702,7 +702,6 @@ describe('useFetch - BROWSER - suspense', (): void => { afterEach((): void => { fetch.resetMocks() cleanup() - test.cleanup() }) diff --git a/src/storage/localStorage.ts b/src/storage/localStorage.ts index 06b6a42f..e3294159 100644 --- a/src/storage/localStorage.ts +++ b/src/storage/localStorage.ts @@ -1,4 +1,4 @@ -import { serialiseResponse } from '../utils' +import { serializeResponse } from '../utils' import { Cache } from '../types' const cacheName = 'useHTTPcache' @@ -47,7 +47,7 @@ const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { const set = async (responseID: string, response: Response): Promise => { const cache = getCache() - const responseObject = await serialiseResponse(response) + const responseObject = await serializeResponse(response) cache[responseID] = { response: responseObject, expiration: Date.now() + cacheLife @@ -55,7 +55,11 @@ const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { localStorage.setItem(cacheName, JSON.stringify(cache)) } - return { get, set, has, delete: remove } + const clear = async () => { + localStorage.setItem(cacheName, JSON.stringify({})) + } + + return { get, set, has, delete: remove, clear } } export default getLocalStorage diff --git a/src/storage/memoryStorage.ts b/src/storage/memoryStorage.ts index d9c73bd0..91f99053 100644 --- a/src/storage/memoryStorage.ts +++ b/src/storage/memoryStorage.ts @@ -25,7 +25,10 @@ const getMemoryStorage = ({ cacheLife }: { cacheLife: number }): Cache => ({ async delete(name: string) { inMemoryStorage.delete(name) inMemoryStorage.delete(`${name}:ts`) - } + }, + async clear() { + return inMemoryStorage.clear() + } }) export default getMemoryStorage diff --git a/src/types.ts b/src/types.ts index 6096d663..edc19f72 100644 --- a/src/types.ts +++ b/src/types.ts @@ -127,6 +127,7 @@ export interface ReqBase { data: TData | undefined loading: boolean error: Error + cache: Cache } export interface Res extends Response { @@ -163,6 +164,7 @@ export type Cache = { set: (name: string, data: Response) => Promise has: (name: string) => Promise delete: (...names: string[]) => Promise + clear: () => void } export interface CustomOptions { diff --git a/src/useCache.ts b/src/useCache.ts index 182d204f..a86e6014 100644 --- a/src/useCache.ts +++ b/src/useCache.ts @@ -1,20 +1,22 @@ import useSSR from 'use-ssr' import { invariant } from './utils' -import { Cache } from './types' +import { Cache, CachePolicies } from './types' import getLocalStorage from './storage/localStorage' import getMemoryStorage from './storage/memoryStorage' +const { NETWORK_ONLY, NO_CACHE } = CachePolicies /** * Eventually, this will be replaced by use-react-storage, so * having this as a hook allows us to have minimal changes in * the future when switching over. */ -type UseCacheArgs = { persist: boolean, cacheLife: number } -const useCache = ({ persist, cacheLife }: UseCacheArgs): Cache => { - const { isNative, isServer, isBrowser } = useSSR() - invariant(!(isServer && persist), 'There is no persistant storage on the Server currently! 🙅‍♂️') - invariant(!(isNative && !isServer && !isBrowser), 'React Native support for persistant cache is not yet implemented. 🙅‍♂️') +type UseCacheArgs = { persist: boolean, cacheLife: number, cachePolicy: CachePolicies } +const useCache = ({ persist, cacheLife, cachePolicy }: UseCacheArgs): Cache => { + const { isNative, isServer } = useSSR() + invariant(!(isServer && persist), 'There is no persistent storage on the Server currently! 🙅‍♂️') + invariant(!(isNative && persist), 'React Native support for persistent cache is not yet implemented. 🙅‍♂️') + invariant(!(persist && [NO_CACHE, NETWORK_ONLY].includes(cachePolicy)), `You cannot use option 'persist' with cachePolicy: ${cachePolicy} 🙅‍♂️`) // right now we're not worrying about react-native if (persist) return getLocalStorage({ cacheLife: cacheLife || (24 * 3600000) }) diff --git a/src/useFetch.ts b/src/useFetch.ts index 5bf4040c..53edbf96 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -38,7 +38,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { suspense } = customOptions - const cache = useCache({ persist, cacheLife }) + const cache = useCache({ persist, cacheLife, cachePolicy }) const { isServer } = useSSR() @@ -81,7 +81,7 @@ function useFetch(...args: UseFetchArgs): UseFetch { if (!suspense) setLoading(true) error.current = undefined - if (response.isCached && (persist || cachePolicy === CACHE_FIRST)) { + if (response.isCached && cachePolicy === CACHE_FIRST) { try { res.current = response.cached as Res res.current.data = await tryGetData(response.cached, defaults.data) @@ -174,7 +174,8 @@ function useFetch(...args: UseFetchArgs): UseFetch { mutate: (mutation, variables) => post({ mutation, variables }), loading: loading, error: error.current, - data: data.current + data: data.current, + cache } const response = toResponseObject(res, data) diff --git a/src/useMutation.ts b/src/useMutation.ts index e60c4cca..36f4fa8d 100644 --- a/src/useMutation.ts +++ b/src/useMutation.ts @@ -49,7 +49,7 @@ export const useMutation = ( MUTATION = urlOrMutation as string } - const { loading, error, ...request } = useFetch(url as string) + const { loading, error, cache, ...request } = useFetch(url as string) const mutate = useCallback( (inputs?: object): Promise => request.mutate(MUTATION, inputs), @@ -60,6 +60,6 @@ export const useMutation = ( return Object.assign, ObjectDestructure>( [data, loading, error, mutate], - { data, loading, error, mutate } + { data, loading, error, mutate, cache } ) } diff --git a/src/useQuery.ts b/src/useQuery.ts index 1e309c09..a9b85b6d 100644 --- a/src/useQuery.ts +++ b/src/useQuery.ts @@ -49,7 +49,7 @@ export const useQuery = ( QUERY = urlOrQuery as string } - const { loading, error, ...request } = useFetch(url as string) + const { loading, error, cache, ...request } = useFetch(url as string) const query = useCallback( (variables?: object): Promise => request.query(QUERY, variables), @@ -60,6 +60,6 @@ export const useQuery = ( return Object.assign, ObjectDestructure>( [data, loading, error, query], - { data, loading, error, query } + { data, loading, error, query, cache } ) } diff --git a/src/utils.ts b/src/utils.ts index 63093214..3dffd6a8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -211,7 +211,7 @@ const headersAsObject = (headers: Headers): object => { return obj } -export const serialiseResponse = async (response: Response) => { +export const serializeResponse = async (response: Response) => { const body = await response.text() const status = response.status const statusText = response.statusText diff --git a/yarn.lock b/yarn.lock index 4b2013f0..2cb6c654 100644 --- a/yarn.lock +++ b/yarn.lock @@ -357,9 +357,9 @@ type-detect "4.0.8" "@testing-library/dom@^7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.0.2.tgz#9edc922dfe2644a3f0ff70f256d8c78a525b88c3" - integrity sha512-5pqMlPaBC/5bvvFtuNTTcB4/Vg8NjZfs11LQQc08JhpMfx82W85nVw3re4D92A/C5O8zDkxPxyYPWNp8PtnQ/w== + version "7.0.4" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.0.4.tgz#89909046b4a2818d423dd2c786faee4ddbe32838" + integrity sha512-+vrLcGDvopLPsBB7JgJhf8ZoOhBSeCsI44PKJL9YoKrP2AvCkqrTg+z77wEEZJ4tSNdxV0kymil7hSvsQQ7jMQ== dependencies: "@babel/runtime" "^7.8.4" "@types/testing-library__dom" "^6.12.1" @@ -385,9 +385,9 @@ "@types/react-test-renderer" "^16.8.2" "@testing-library/react@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.0.0.tgz#a07338270ffc9e20f73a1614114288bcd3cd15a8" - integrity sha512-5MhwMCRqYq6uK5A6ZMZRUeZT79shhK6j9jMrN/NZMWkNNolK+LNgEYk8y+r7zBlU9rg/f531G2lyQrw/Vo74rg== + version "10.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.0.1.tgz#4f5e2a8836257c5bd3df640b21d7bea5a0d83ead" + integrity sha512-sMHWud2dcymOzq2AhEniICSijEwKeTiBX+K0y36FYNY7wH2t0SIP1o732Bf5dDY0jYoMC2hj2UJSVpZC/rDsWg== dependencies: "@babel/runtime" "^7.8.7" "@testing-library/dom" "^7.0.2" @@ -3704,9 +3704,9 @@ realpath-native@^1.1.0: util.promisify "^1.0.0" regenerator-runtime@^0.13.4: - version "0.13.4" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz#e96bf612a3362d12bb69f7e8f74ffeab25c7ac91" - integrity sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g== + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2"