Skip to content

Commit

Permalink
feat(compute-key): add support for mapKeyToCacheKey (#71)
Browse files Browse the repository at this point in the history
this option allows consumers to directly vary their cache key
  • Loading branch information
code-forger committed Mar 6, 2023
1 parent 8fa57c1 commit 1f29629
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 10 deletions.
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,37 @@ const BookList = ({ genre }) => {
};
```
### Controlling the Cache Key
By passing mapKeyToCacheKey as an option you can customize the cacheKey without affecting the key. This allows you to control the cacheKey directly to enable advanced behaviour in your cache.
Note: This option can lead to unexpected behaviour in many cases. Customizing the cacheKey in this way could lead to accidental collisions that lead to fetchye providing the 'wrong' cache for some of your calls, or unnecessary cache-misses causing significant performance degradation.
In this example the client can dynamically switch between http and https depending on the needs of the user, but should keep the same cache key.
Therefore, mapKeyToCacheKey is defined to transform the url to always have the same protocol in the cacheKey.
```jsx
import React from 'react';
import { useFetchye } from 'fetchye';

const BookList = ({ ssl }) => {
const { isLoading, data } = useFetchye(`${ssl ? 'https' : 'http'}://example.com/api/books/`,
{
mapKeyToCacheKey: (key) => key.replace('https://', 'http://'),
}
);

if (isLoading) {
return (<p>Loading...</p>);
}

return (
{/* Render data */}
);
};
```
### SSR
#### One App SSR
Expand Down Expand Up @@ -717,12 +748,13 @@ const { isLoading, data, error, run } = useFetchye(key, { defer: Boolean, mapOpt
**Options**
| name | type | required | description |
|---|---|---|---|
| `mapOptionsToKey` | `(options: Options) => transformedOptions` | `false` | A function that maps options to the key that will become part of the cache key |
| `defer` | `Boolean` | `false` | Prevents execution of `useFetchye` on each render in favor of using the returned `run` function. *Defaults to `false`* |
| `initialData` | `Object` | `false` | Seeds the initial data on first render of `useFetchye` to accomodate server side rendering *Defaults to `undefined`* |
| `...restOptions` | `ES6FetchOptions` | `true` | Contains any ES6 Compatible `fetch` option. (See [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Supplying_request_options)) |
| name | type | required | description |
|--------------------|-------------------------------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `mapOptionsToKey` | `(options: Options) => transformedOptions` | `false` | A function that maps options to the key that will become part of the cache key |
| `mapKeyToCacheKey` | `(key: String, options: Options) => cacheKey: String` | `false` | A function that maps the key for use as the cacheKey allowing direct control of the cacheKey |
| `defer` | `Boolean` | `false` | Prevents execution of `useFetchye` on each render in favor of using the returned `run` function. *Defaults to `false`* |
| `initialData` | `Object` | `false` | Seeds the initial data on first render of `useFetchye` to accomodate server side rendering *Defaults to `undefined`* |
| `...restOptions` | `ES6FetchOptions` | `true` | Contains any ES6 Compatible `fetch` option. (See [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Supplying_request_options)) |
**Returns**
Expand Down
59 changes: 59 additions & 0 deletions packages/fetchye/__tests__/computeKey.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@
* permissions and limitations under the License.
*/

import computeHash from 'object-hash';
import { computeKey } from '../src/computeKey';

jest.mock('object-hash', () => {
const originalComputeHash = jest.requireActual('object-hash');
return jest.fn(originalComputeHash);
});

describe('computeKey', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return an object', () => {
expect(computeKey('abcd', {})).toMatchInlineSnapshot(`
Object {
Expand Down Expand Up @@ -58,4 +67,54 @@ describe('computeKey', () => {
};
expect(computeKey('uri', firstOptions).hash).toBe(computeKey('uri', secondOptions).hash);
});

it('should return a different, stable hash, if the option mapKeyToCacheKey is passed', () => {
const key = 'abcd';
const mappedKey = 'efgh';
const { hash: mappedHash1 } = computeKey(key, { mapKeyToCacheKey: () => mappedKey });
const { hash: mappedHash2 } = computeKey(key, { mapKeyToCacheKey: () => mappedKey });

const { hash: unmappedHash } = computeKey(key, {});

expect(mappedHash1).toBe(mappedHash2);
expect(mappedHash1).not.toBe(unmappedHash);
});

it('should return the same key if the option mapKeyToCacheKey returns the same string as the key', () => {
const key = 'abcd';
const { hash: mappedHash } = computeKey(key, { mapKeyToCacheKey: (_key) => _key });

const { hash: unmappedHash } = computeKey(key, {});

expect(mappedHash).toBe(unmappedHash);
});

it('should pass generated cacheKey to the underlying hash function along with the options, and return the un-mapped key to the caller', () => {
const computedKey = computeKey(() => 'abcd', {
mapKeyToCacheKey: (key, options) => `${key.toUpperCase()}-${options.optionKeyMock}`,
optionKeyMock: 'optionKeyValue',
});
expect(computedKey.key).toBe('abcd');
expect(computeHash).toHaveBeenCalledWith(['ABCD-optionKeyValue', { optionKeyMock: 'optionKeyValue' }], { respectType: false });
});

it('should return false if mapKeyToCacheKey throws error', () => {
expect(
computeKey(() => 'abcd', {
mapKeyToCacheKey: () => {
throw new Error('error');
},
})
).toEqual(false);
});

it('should return false if mapKeyToCacheKey returns false', () => {
expect(computeKey(() => 'abcd', { mapKeyToCacheKey: () => false })).toEqual(false);
});

it('should throw an error if mapKeyToCacheKey is defined and not a function', () => {
expect(() => computeKey(() => 'abcd',
{ mapKeyToCacheKey: 'string' }
)).toThrow('mapKeyToCacheKey must be a function');
});
});
22 changes: 18 additions & 4 deletions packages/fetchye/src/computeKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import computeHash from 'object-hash';
import mapHeaderNamesToLowerCase from './mapHeaderNamesToLowerCase';

export const computeKey = (key, options) => {
const { headers, ...restOfOptions } = options;
const { headers, mapKeyToCacheKey, ...restOfOptions } = options;
const nextOptions = { ...restOfOptions };
if (headers) {
nextOptions.headers = mapHeaderNamesToLowerCase(headers);
}

let nextKey = key;
if (typeof key === 'function') {
let nextKey;
try {
nextKey = key(nextOptions);
} catch (error) {
Expand All @@ -34,9 +34,23 @@ export const computeKey = (key, options) => {
if (!nextKey) {
return false;
}
return { key: nextKey, hash: computeHash([nextKey, nextOptions], { respectType: false }) };
}
return { key, hash: computeHash([key, nextOptions], { respectType: false }) };

let cacheKey = nextKey;
if (mapKeyToCacheKey !== undefined && typeof mapKeyToCacheKey === 'function') {
try {
cacheKey = mapKeyToCacheKey(nextKey, nextOptions);
} catch (error) {
return false;
}
if (!cacheKey) {
return false;
}
} else if (mapKeyToCacheKey !== undefined) {
throw new TypeError('mapKeyToCacheKey must be a function');
}

return { key: nextKey, hash: computeHash([cacheKey, nextOptions], { respectType: false }) };
};

export default computeKey;

0 comments on commit 1f29629

Please sign in to comment.