diff --git a/lib/composables/global-swr-config/global-swr-config.spec.ts b/lib/composables/global-swr-config/global-swr-config.spec.ts index 85b8e28..21d0e29 100644 --- a/lib/composables/global-swr-config/global-swr-config.spec.ts +++ b/lib/composables/global-swr-config/global-swr-config.spec.ts @@ -13,6 +13,19 @@ import { import { useSWRConfig, configureGlobalSWR } from '.'; +const throwError = (error: Error) => { + throw error; +}; + +vi.mock('vue', async () => { + const original = (await vi.importActual('vue')) as Record; + + return { + ...original, + provide: vi.fn(original.provide as AnyFunction), + }; +}); + describe('useSWRConfig', () => { it.each([ [{}], @@ -37,15 +50,6 @@ describe('useSWRConfig', () => { }); describe('configureGlobalSWR', () => { - vi.mock('vue', async () => { - const original = (await vi.importActual('vue')) as Record; // Step 2. - - return { - ...original, - provide: vi.fn(original.provide as AnyFunction), - }; - }); - const provideMock = provide as Mock; it('should provide the default config if none is provided', () => { @@ -78,37 +82,57 @@ describe('configureGlobalSWR', () => { }); }); -describe('mutate', () => { - const cacheProvider = mockedCache; +// eslint-disable-next-line prettier/prettier +describe.each([ + 'default', + 'injected' +])('mutate - %s', (approach) => { + const isDefaultApproach = approach === 'default'; + const cacheProvider = isDefaultApproach ? defaultConfig.cacheProvider : mockedCache; const defaultKey = 'Default key'; const useSWRConfigWrapped = () => - useInjectedSetup( - () => configureGlobalSWR({ cacheProvider }), - () => useSWRConfig(), - ); + isDefaultApproach + ? useSetup(() => useSWRConfig()) + : useInjectedSetup( + () => configureGlobalSWR({ cacheProvider }), + () => useSWRConfig(), + ); beforeEach(() => { cacheProvider.clear(); - setDataToMockedCache(defaultKey, { data: 'cached data' }); + setDataToMockedCache(defaultKey, { data: 'cached data' }, cacheProvider); }); - it('should write in the cache the value resolved from promise passed to mutate', async () => { - const { mutate } = useSWRConfigWrapped(); + it.each([ + ['sync value', 'sync value'], + [Promise.resolve('resolved value'), 'resolved value'], + [{ data: 'sync obj' }, { data: 'sync obj' }], + [() => 'returned value', 'returned value'], + [() => Promise.resolve('returned promise'), 'returned promise'], + [() => ({ data: 'returned obj' }), { data: 'returned obj' }], + ])( + 'should write in the cache the value returned from function passed to mutate when that key does not exists in the cache yet: #%#', + async (mutateVal, expected) => { + cacheProvider.clear(); - await mutate(defaultKey, Promise.resolve('resolved value')); + const { mutate } = useSWRConfigWrapped(); + const key = 'key-1'; - expect(getDataFromMockedCache(defaultKey)?.data).toEqual('resolved value'); - }); + await mutate(key, mutateVal); + expect(getDataFromMockedCache(key, cacheProvider)).toBeDefined(); + expect(getDataFromMockedCache(key, cacheProvider)?.data).toEqual(expected); + }, + ); it('should write in the cache the value returned from function passed to mutate', async () => { const { mutate } = useSWRConfigWrapped(); await mutate(defaultKey, () => 'sync resolved value'); - expect(getDataFromMockedCache(defaultKey)?.data).toEqual('sync resolved value'); + expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('sync resolved value'); await mutate(defaultKey, () => Promise.resolve('async resolved value')); - expect(getDataFromMockedCache(defaultKey)?.data).toEqual('async resolved value'); + expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('async resolved value'); }); it.each([ @@ -121,12 +145,9 @@ describe('mutate', () => { (cachedData) => { const updateFn = vi.fn(); - setDataToMockedCache(defaultKey, { data: cachedData }); + setDataToMockedCache(defaultKey, { data: cachedData }, cacheProvider); - const { mutate } = useInjectedSetup( - () => configureGlobalSWR({ cacheProvider }), - () => useSWRConfig(), - ); + const { mutate } = useSWRConfigWrapped(); mutate(defaultKey, updateFn); @@ -137,61 +158,68 @@ describe('mutate', () => { it('should use the value resolved from updateFn for mutate`s return value', async () => { const { mutate } = useSWRConfigWrapped(); + const expected = 'resolved value'; - expect(await mutate(defaultKey, () => 'resolved data')).toEqual('resolved data'); - expect(await mutate(defaultKey, () => Promise.resolve('resolved data'))).toEqual( - 'resolved data', - ); - expect.assertions(2); + expect(await mutate(defaultKey, expected)).toEqual(expected); + expect(await mutate(defaultKey, () => expected)).toEqual(expected); + expect(await mutate(defaultKey, () => Promise.resolve(expected))).toEqual(expected); + expect.assertions(3); }); - it('should re-throw if an error ocours inside updateFn or promise passed rejects', async () => { + it.each([ + [new Error('sync error'), () => throwError(new Error('sync error'))], + [new Error('async error'), () => Promise.reject(new Error('async error'))], + ])('should re-throw if an error ocours inside updateFn: #%#', async (error, updateFn) => { const { mutate } = useSWRConfigWrapped(); - const syncError = new Error('sync error'); - const asyncError = new Error('async error'); - const promiseError = new Error('promise error'); + await expect(mutate(defaultKey, updateFn)).rejects.toThrowError(error); - await expect( - mutate(defaultKey, () => { - throw syncError; - }), - ).rejects.toThrowError(syncError); + expect.assertions(1); + }); - await expect(mutate(defaultKey, () => Promise.reject(asyncError))).rejects.toThrowError( - asyncError, - ); - await expect(mutate(defaultKey, Promise.reject(promiseError))).rejects.toThrowError( - promiseError, - ); - expect.assertions(3); + it('should re-throw if promise passed to mutate rejects', async () => { + const { mutate } = useSWRConfigWrapped(); + + const promiseError = new Error('promise error'); + const promise = Promise.reject(promiseError); + + await expect(mutate(defaultKey, promise)).rejects.toThrowError(promiseError); + expect.assertions(1); }); - it('should write `optimisticData` to cache right away and ser to resolved value from updateFn after', async () => { + it('should write `optimisticData` to cache right away and set to resolved value from updateFn after', async () => { const { mutate } = useSWRConfigWrapped(); const promise = mutate(defaultKey, Promise.resolve('resolved data'), { optimisticData: 'optimistic data', }); - expect(getDataFromMockedCache(defaultKey)?.data).toEqual('optimistic data'); + expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('optimistic data'); await promise; - expect(getDataFromMockedCache(defaultKey)?.data).toEqual('resolved data'); + expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('resolved data'); }); - it('should write rollback data writed in cache whe using `opoptimisticData` and `rollbackOnError`', async () => { - const { mutate } = useSWRConfigWrapped(); - - try { - await mutate(defaultKey, Promise.reject(), { - optimisticData: 'optimistic data', - rollbackOnError: true, - }); - } catch (error) { - expect(getDataFromMockedCache(defaultKey)?.data).toEqual('cached data'); - } - - expect.assertions(1); - }); + it.each([ + { optimisticData: 'optimistic data', updateFn: () => Promise.reject() }, + { optimisticData: 'optimistic data', updateFn: () => throwError(new Error()) }, + { optimisticData: undefined, updateFn: () => Promise.reject() }, + { optimisticData: undefined, updateFn: () => throwError(new Error()) }, + ])( + 'should rollback data writed in cache when `optimisticData = $optimisticData` and `rollbackOnError = true`: #%#', + async ({ optimisticData, updateFn }) => { + const { mutate } = useSWRConfigWrapped(); + + try { + await mutate(defaultKey, updateFn, { + optimisticData, + rollbackOnError: true, + }); + } catch (error) { + expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('cached data'); + } + + expect.assertions(1); + }, + ); }); diff --git a/lib/composables/global-swr-config/index.ts b/lib/composables/global-swr-config/index.ts index a91e3d1..bf3dfcb 100644 --- a/lib/composables/global-swr-config/index.ts +++ b/lib/composables/global-swr-config/index.ts @@ -1,9 +1,9 @@ -import { computed, inject, provide, unref, shallowReadonly, toRefs } from 'vue'; +import { computed, inject, provide, unref, shallowReadonly, toRefs, ref } from 'vue'; import { MaybeRef } from '@vueuse/core'; import { defaultConfig, globalConfigKey } from '@/config'; -import { AnyFunction, Key, SWRConfig } from '@/types'; -import { isUndefined, mergeConfig, serializeKey } from '@/utils'; +import { AnyFunction, CacheState, Key, SWRConfig } from '@/types'; +import { isUndefined, mergeConfig, serializeKey, isFunction } from '@/utils'; import { useScopeState } from '@/composables/scope-state'; export type MutateOptions = { @@ -12,6 +12,13 @@ export type MutateOptions = { revalidate?: boolean; }; +const createCacheState = (data: unknown): CacheState => ({ + data, + error: undefined, + isValidating: false, + fetchedIn: new Date(), +}); + export const useSWRConfig = () => { const contextConfig = inject( globalConfigKey, @@ -28,18 +35,17 @@ export const useSWRConfig = () => { options: MutateOptions = {}, ) => { const { key } = serializeKey(_key); - const cacheState = contextConfig.value.cacheProvider.get(key); + const cache = cacheProvider.value; + const cacheState = cache.get(key); + const hasCache = !isUndefined(cacheState); const { optimisticData, rollbackOnError, revalidate = true } = options; - if (!cacheState) return; - - const { data } = toRefs(cacheState); + const { data } = hasCache ? toRefs(cacheState) : { data: ref() }; const dataInCache = data.value; - const resultPromise: unknown | Promise = - typeof updateFnOrPromise === 'function' - ? updateFnOrPromise(cacheState.data) - : updateFnOrPromise; + const resultPromise: unknown | Promise = isFunction(updateFnOrPromise) + ? updateFnOrPromise(dataInCache) + : updateFnOrPromise; if (optimisticData) { data.value = optimisticData; @@ -55,6 +61,8 @@ export const useSWRConfig = () => { throw error; } + cache.set(key, hasCache ? cacheState : createCacheState(data)); + const revalidationCallbackcs = revalidateCache.value.get(key) || []; if (revalidate && revalidationCallbackcs.length) { diff --git a/lib/utils/test/index.ts b/lib/utils/test/index.ts index 6355b76..b4b4352 100644 --- a/lib/utils/test/index.ts +++ b/lib/utils/test/index.ts @@ -95,10 +95,14 @@ export const useSetupInServer = (setup: () => V) => { export const mockedCache = reactive(new Map()); -export const setDataToMockedCache = (key: Key, data: UnwrapRef>) => { +export const setDataToMockedCache = ( + key: Key, + data: UnwrapRef>, + cache = mockedCache, +) => { const { key: serializedKey } = serializeKey(key); - mockedCache.set(serializedKey, { + cache.set(serializedKey, { error: data.error, data: data.data, isValidating: data.isValidating || false, @@ -106,10 +110,10 @@ export const setDataToMockedCache = (key: Key, data: UnwrapRef { +export const getDataFromMockedCache = (key: Key, cache = mockedCache) => { const { key: serializedKey } = serializeKey(key); - return mockedCache.get(serializedKey); + return cache.get(serializedKey); }; export const dispatchEvent = (eventName: string, target: Element | Window | Document) => {