Skip to content

Commit

Permalink
feat: add expected behavior from mutate function
Browse files Browse the repository at this point in the history
  • Loading branch information
edumudu committed Jul 17, 2022
1 parent 65409d2 commit e481a29
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 11 deletions.
133 changes: 131 additions & 2 deletions lib/composables/global-swr-config/global-swr-config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { provide, ref } from 'vue';
import { provide, reactive, ref, UnwrapRef } from 'vue';
import { Mock } from 'vitest';
import flushPromises from 'flush-promises';

import { defaultConfig, globalConfigKey } from '@/config';
import { AnyFunction, SWRConfig } from '@/types';
import { AnyFunction, CacheState, Key, SWRConfig } from '@/types';
import { useInjectedSetup, useSetup } from '@/utils/test';
import { MapAdapter } from '@/cache';

import { useSWRConfig, configureGlobalSWR } from '.';

Expand Down Expand Up @@ -71,3 +73,130 @@ describe('configureGlobalSWR', () => {
});
});
});

describe('mutate', () => {
const cacheProvider = reactive(new MapAdapter());
const defaultKey = 'Default key';

const useSWRConfigWrapped = () =>
useInjectedSetup(
() => configureGlobalSWR({ cacheProvider }),
() => useSWRConfig(),
);

const setDataToCache = (key: Key, data: UnwrapRef<Partial<CacheState>>) => {
cacheProvider.set(key, {
error: ref(data.error),
data: ref(data.data),
isValidation: ref(data.isValidating || false),
fetchedIn: ref(data.fetchedIn || new Date()),
});
};

beforeEach(() => {
cacheProvider.clear();
setDataToCache(defaultKey, { data: 'cached data' });
});

it('should write in the cache the value resolved from promise passed to mutate', async () => {
const { mutate } = useSWRConfigWrapped();

await mutate(defaultKey, Promise.resolve('resolved value'));

expect(cacheProvider.get(defaultKey)?.data).toEqual('resolved value');
});

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(cacheProvider.get(defaultKey)?.data).toEqual('sync resolved value');

await mutate(defaultKey, () => Promise.resolve('async resolved value'));
expect(cacheProvider.get(defaultKey)?.data).toEqual('async resolved value');
});

it.each([
'cached value',
1000,
{ id: 1, name: 'John', email: 'john@example.com' },
['orange', 'apple', 'banana'],
])(
'should call update function passing the current cached data to first argument',
(cachedData) => {
const updateFn = vi.fn();

setDataToCache(defaultKey, { data: cachedData });

const { mutate } = useInjectedSetup(
() => configureGlobalSWR({ cacheProvider }),
() => useSWRConfig(),
);

mutate(defaultKey, updateFn);

expect(updateFn).toBeCalled();
expect(updateFn).toBeCalledWith(cachedData);
},
);

it('should use the value resolved from updateFn for mutate`s return value', async () => {
const { mutate } = useSWRConfigWrapped();

expect(await mutate(defaultKey, () => 'resolved data')).toEqual('resolved data');
expect(await mutate(defaultKey, () => Promise.resolve('resolved data'))).toEqual(
'resolved data',
);
expect.assertions(2);
});

it('should re-throw if an error ocours inside updateFn or promise passed rejects', async () => {
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, () => {
throw syncError;
}),
).rejects.toThrowError(syncError);

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 write `optimisticData` to cache right away and ser to resolved value from updateFn after', async () => {
const { mutate } = useSWRConfigWrapped();

const promise = mutate(defaultKey, Promise.resolve('resolved data'), {
optimisticData: 'optimistic data',
});

expect(cacheProvider.get(defaultKey)?.data).toEqual('optimistic data');

await promise;
expect(cacheProvider.get(defaultKey)?.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(cacheProvider.get(defaultKey)?.data).toEqual('cached data');
}

expect.assertions(1);
});
});
44 changes: 37 additions & 7 deletions lib/composables/global-swr-config/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
import { computed, inject, provide, unref, shallowReadonly } from 'vue';
import { computed, inject, provide, unref, shallowReadonly, toRefs } from 'vue';
import { MaybeRef } from '@vueuse/core';

import { defaultConfig, globalConfigKey } from '@/config';
import { SWRConfig } from '@/types';
import { AnyFunction, SWRConfig } from '@/types';
import { mergeConfig } from '@/utils';

export type MutateOptions = {
optimisticData?: unknown;
rollbackOnError?: boolean;
};

export const useSWRConfig = () => {
const contextConfig = inject(
globalConfigKey,
computed(() => defaultConfig),
);

const cacheProvider = computed(() => contextConfig.value.cacheProvider);

const mutate = <Data = any>(key: string, value: Data) => {
const cachedValue = cacheProvider.value.get(key);
const mutate = async <UpdateFn extends Promise<unknown> | AnyFunction>(
key: string,
updateFnOrPromise: UpdateFn,
options: MutateOptions = {},
) => {
const cachedValue = contextConfig.value.cacheProvider.get(key);
const { optimisticData, rollbackOnError } = options;

if (!cachedValue) return;

cachedValue.data.value = value;
const { data } = toRefs(cachedValue);
const currentData = data.value;

const resultPromise =
typeof updateFnOrPromise === 'function'
? updateFnOrPromise(cachedValue.data)
: updateFnOrPromise;

if (optimisticData) {
data.value = optimisticData;
}

try {
data.value = await resultPromise;
} catch (error) {
if (rollbackOnError) {
data.value = currentData;
}

throw error;
}

return data.value;
};

return {
Expand Down
5 changes: 3 additions & 2 deletions lib/composables/swr/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { computed, ref, readonly, watch, toRefs, toRef } from 'vue';
import { toReactive, useEventListener } from '@vueuse/core';

import type { SWRComposableConfig, SWRFetcher, SWRKey } from '@/types';
import type { OmitFirstArrayIndex, SWRComposableConfig, SWRFetcher, SWRKey } from '@/types';
import { serializeKey } from '@/utils';
import { mergeConfig } from '@/utils/merge-config';
import { useSWRConfig } from '@/composables/global-swr-config';
Expand Down Expand Up @@ -84,6 +84,7 @@ export const useSWR = <Data = any, Error = any>(
data: readonly(data),
error: readonly(error),
isValidating: readonly(isValidating),
mutate: (newValue: Data) => mutate(key.value, newValue),
mutate: (...params: OmitFirstArrayIndex<Parameters<typeof mutate>>) =>
mutate(key.value, ...params),
};
};
1 change: 1 addition & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './generics';
export * from './lib';
export * from './utils';
1 change: 1 addition & 0 deletions lib/types/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type OmitFirstArrayIndex<T> = T extends [any, ...infer Rest] ? Rest : never;

0 comments on commit e481a29

Please sign in to comment.