Skip to content

Commit

Permalink
feat: cache updater function for multiple keys. (#304)
Browse files Browse the repository at this point in the history
* feat: implemented general cache updater function

* test: added tests

* docs: included cache update warning
  • Loading branch information
arthurfiorette authored Jul 29, 2022
1 parent 24ff867 commit f0cd5d1
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 44 deletions.
5 changes: 5 additions & 0 deletions docs/pages/per-request-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ with the request id.

Here's an example with some basic login:

Using a function instead of an object is supported but not recommended, as it's better to
just consume the response normally and write your own code after it. But it`s here in case
you need it.

```ts
// Some requests id's
let profileInfoId;
Expand All @@ -112,6 +116,7 @@ axios.post<{ auth: { user: User } }>(

cachedValue.data = data;

// This returned value will be returned in next calls to the cache.
return cachedValue;
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,13 @@ export type CacheProperties<R = unknown, D = unknown> = {
*
* The id used is the same as the id on `CacheRequestConfig['id']`, auto-generated or not.
*
* **Using a function instead of an object is supported but not recommended, as it's
* better to just consume the response normally and write your own code after it. But
* it`s here in case you need it.**
*
* @default {{}}
*/
update: Record<string, CacheUpdater<R, D>>;
update: CacheUpdater<R, D>;

/**
* If the request should handle `ETag` and `If-None-Match` support. Use a string to
Expand Down
33 changes: 26 additions & 7 deletions src/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ export type KeyGenerator<R = unknown, D = unknown> = (

export type MaybePromise<T> = T | Promise<T> | PromiseLike<T>;

export type CacheUpdater<R, D> =
| 'delete'
| ((
cached: Exclude<StorageValue, LoadingStorageValue>,
response: CacheAxiosResponse<R, D>
) => MaybePromise<CachedStorageValue | 'delete' | 'ignore'>);

/**
* You can use a `number` to ensure an max time (in seconds) that the cache can be reused.
*
Expand All @@ -58,3 +51,29 @@ export type StaleIfErrorPredicate<R, D> =
cache: LoadingStorageValue & { previous: 'stale' },
error: Record<string, unknown>
) => MaybePromise<number | boolean>);

export type CacheUpdaterFn<R, D> = (
response: CacheAxiosResponse<R, D>
) => MaybePromise<void>;

/**
* A record for a custom cache updater for each specified request id.
*
* `delete` -> Deletes the request cache `predicate()` -> Determines if the cache can be
* reused, deleted or modified.
*/
export type CacheUpdaterRecord<R, D> = {
[requestId: string]:
| 'delete'
| ((
cached: Exclude<StorageValue, LoadingStorageValue>,
response: CacheAxiosResponse<R, D>
) => MaybePromise<CachedStorageValue | 'delete' | 'ignore'>);
};

/**
* Updates any specified request cache by applying the response for this network call.
*
* You can use a function to implement your own cache updater function.
*/
export type CacheUpdater<R, D> = CacheUpdaterFn<R, D> | CacheUpdaterRecord<R, D>;
13 changes: 9 additions & 4 deletions src/util/update-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import type { AxiosStorage } from '../storage/types';
import type { CacheUpdater } from './types';

/** Function to update all caches, from CacheProperties.update, with the new data. */
export async function updateCache<T, D>(
export async function updateCache<R, D>(
storage: AxiosStorage,
data: CacheAxiosResponse<T, D>,
entries: Record<string, CacheUpdater<T, D>>
data: CacheAxiosResponse<R, D>,
cacheUpdater: CacheUpdater<R, D>
): Promise<void> {
for (const [cacheKey, updater] of Object.entries(entries)) {
// Global cache update function.
if (typeof cacheUpdater === `function`) {
return cacheUpdater(data);
}

for (const [cacheKey, updater] of Object.entries(cacheUpdater)) {
if (updater === 'delete') {
await storage.remove(cacheKey, data.config);
continue;
Expand Down
82 changes: 50 additions & 32 deletions test/util/update-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,87 +2,105 @@ import type { CachedStorageValue } from '../../src/storage/types';
import { defaultKeyGenerator } from '../../src/util/key-generator';
import { mockAxios } from '../mocks/axios';

const cacheKey = defaultKeyGenerator({ url: 'https://example.com/' });
const cachedValue: CachedStorageValue = {
const CACHE_KEY = defaultKeyGenerator({ url: 'https://example.com/' });
const CACHED_VALUE: CachedStorageValue = Object.freeze({
createdAt: Date.now(),
state: 'cached',
ttl: Number.MAX_SAFE_INTEGER, // never expires
data: {
data: 'value',
headers: {},
status: 200,
statusText: '200 OK'
}
};
data: { data: 'value', headers: {}, status: 200, statusText: '200 OK' }
});

describe('Tests update-cache', () => {
it('tests for delete key with CacheUpdaterFn', async () => {
const axios = mockAxios({});
await axios.storage.set(CACHE_KEY, CACHED_VALUE);

await axios.get('other-key', {
cache: { update: () => axios.storage.remove(CACHE_KEY) }
});

const cacheValue1 = await axios.storage.get(CACHE_KEY);
expect(cacheValue1).toStrictEqual({ state: 'empty' });

//

await axios.storage.set(CACHE_KEY, CACHED_VALUE);

await axios.get('other-key2', {
cache: { update: () => axios.storage.remove(CACHE_KEY) }
});

const cacheValue3 = await axios.storage.get(CACHE_KEY);
expect(cacheValue3).toStrictEqual({ state: 'empty' });
});

it('tests for delete key', async () => {
const axios = mockAxios({});
await axios.storage.set(cacheKey, cachedValue);
await axios.storage.set(CACHE_KEY, CACHED_VALUE);

await axios.get('other-key', {
cache: { update: { [cacheKey]: 'delete' } }
cache: { update: { [CACHE_KEY]: 'delete' } }
});

const cacheValue1 = await axios.storage.get(cacheKey);
const cacheValue1 = await axios.storage.get(CACHE_KEY);
expect(cacheValue1).toStrictEqual({ state: 'empty' });

//

await axios.storage.set(cacheKey, cachedValue);
await axios.storage.set(CACHE_KEY, CACHED_VALUE);

await axios.get('other-key2', {
cache: {
update: {
[cacheKey]: () => 'delete'
[CACHE_KEY]: () => 'delete'
}
}
});

const cacheValue2 = await axios.storage.get(cacheKey);
const cacheValue2 = await axios.storage.get(CACHE_KEY);
expect(cacheValue2).toStrictEqual({ state: 'empty' });

//

await axios.storage.set(cacheKey, cachedValue);
await axios.storage.set(CACHE_KEY, CACHED_VALUE);

await axios.get('other-key3', {
cache: { update: { [cacheKey]: () => Promise.resolve('delete') } }
cache: { update: { [CACHE_KEY]: () => Promise.resolve('delete') } }
});

const cacheValue3 = await axios.storage.get(cacheKey);
const cacheValue3 = await axios.storage.get(CACHE_KEY);
expect(cacheValue3).toStrictEqual({ state: 'empty' });
});

it('tests for ignore key', async () => {
const axios = mockAxios({});
await axios.storage.set(cacheKey, cachedValue);
await axios.storage.set(CACHE_KEY, CACHED_VALUE);

await axios.get('other-key', {
cache: { update: { [cacheKey]: () => 'ignore' } }
cache: { update: { [CACHE_KEY]: () => 'ignore' } }
});

const cacheValue = await axios.storage.get(cacheKey);
expect(cacheValue).toStrictEqual(cachedValue);
const cacheValue = await axios.storage.get(CACHE_KEY);
expect(cacheValue).toStrictEqual(CACHED_VALUE);

//

await axios.get('other-key2', {
cache: { update: { [cacheKey]: async () => Promise.resolve('ignore') } }
cache: { update: { [CACHE_KEY]: async () => Promise.resolve('ignore') } }
});

const cacheValue2 = await axios.storage.get(cacheKey);
expect(cacheValue2).toStrictEqual(cachedValue);
const cacheValue2 = await axios.storage.get(CACHE_KEY);
expect(cacheValue2).toStrictEqual(CACHED_VALUE);
});

it('tests for new cached storage value', async () => {
const axios = mockAxios({});
await axios.storage.set(cacheKey, cachedValue);
await axios.storage.set(CACHE_KEY, CACHED_VALUE);

await axios.get('other-key', {
cache: {
update: {
[cacheKey]: (cached) => {
[CACHE_KEY]: (cached) => {
if (cached.state !== 'cached') {
return 'ignore';
}
Expand All @@ -96,28 +114,28 @@ describe('Tests update-cache', () => {
}
});

const cacheValue = await axios.storage.get(cacheKey);
expect(cacheValue).not.toStrictEqual(cachedValue);
const cacheValue = await axios.storage.get(CACHE_KEY);
expect(cacheValue).not.toStrictEqual(CACHED_VALUE);
expect(cacheValue.data?.data).toBe(1);
});

it('tests updateCache with key is loading', async () => {
const axios = mockAxios({});
await axios.storage.set(cacheKey, { state: 'loading', previous: 'empty' });
await axios.storage.set(CACHE_KEY, { state: 'loading', previous: 'empty' });

const handler = jest.fn();

await axios.get('other-key', {
cache: {
update: {
[cacheKey]: handler
[CACHE_KEY]: handler
}
}
});

expect(handler).not.toHaveBeenCalled();

const cacheValue = await axios.storage.get(cacheKey);
const cacheValue = await axios.storage.get(CACHE_KEY);
expect(cacheValue.state).toBe('loading');
});

Expand Down

0 comments on commit f0cd5d1

Please sign in to comment.