Skip to content

Commit 1f29629

Browse files
authored
feat(compute-key): add support for mapKeyToCacheKey (#71)
this option allows consumers to directly vary their cache key
1 parent 8fa57c1 commit 1f29629

File tree

3 files changed

+115
-10
lines changed

3 files changed

+115
-10
lines changed

README.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,37 @@ const BookList = ({ genre }) => {
518518
};
519519
```
520520
521+
### Controlling the Cache Key
522+
523+
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.
524+
525+
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.
526+
527+
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.
528+
529+
Therefore, mapKeyToCacheKey is defined to transform the url to always have the same protocol in the cacheKey.
530+
531+
```jsx
532+
import React from 'react';
533+
import { useFetchye } from 'fetchye';
534+
535+
const BookList = ({ ssl }) => {
536+
const { isLoading, data } = useFetchye(`${ssl ? 'https' : 'http'}://example.com/api/books/`,
537+
{
538+
mapKeyToCacheKey: (key) => key.replace('https://', 'http://'),
539+
}
540+
);
541+
542+
if (isLoading) {
543+
return (<p>Loading...</p>);
544+
}
545+
546+
return (
547+
{/* Render data */}
548+
);
549+
};
550+
```
551+
521552
### SSR
522553
523554
#### One App SSR
@@ -717,12 +748,13 @@ const { isLoading, data, error, run } = useFetchye(key, { defer: Boolean, mapOpt
717748
718749
**Options**
719750
720-
| name | type | required | description |
721-
|---|---|---|---|
722-
| `mapOptionsToKey` | `(options: Options) => transformedOptions` | `false` | A function that maps options to the key that will become part of the cache key |
723-
| `defer` | `Boolean` | `false` | Prevents execution of `useFetchye` on each render in favor of using the returned `run` function. *Defaults to `false`* |
724-
| `initialData` | `Object` | `false` | Seeds the initial data on first render of `useFetchye` to accomodate server side rendering *Defaults to `undefined`* |
725-
| `...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)) |
751+
| name | type | required | description |
752+
|--------------------|-------------------------------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
753+
| `mapOptionsToKey` | `(options: Options) => transformedOptions` | `false` | A function that maps options to the key that will become part of the cache key |
754+
| `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 |
755+
| `defer` | `Boolean` | `false` | Prevents execution of `useFetchye` on each render in favor of using the returned `run` function. *Defaults to `false`* |
756+
| `initialData` | `Object` | `false` | Seeds the initial data on first render of `useFetchye` to accomodate server side rendering *Defaults to `undefined`* |
757+
| `...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)) |
726758
727759
**Returns**
728760

packages/fetchye/__tests__/computeKey.spec.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,18 @@
1414
* permissions and limitations under the License.
1515
*/
1616

17+
import computeHash from 'object-hash';
1718
import { computeKey } from '../src/computeKey';
1819

20+
jest.mock('object-hash', () => {
21+
const originalComputeHash = jest.requireActual('object-hash');
22+
return jest.fn(originalComputeHash);
23+
});
24+
1925
describe('computeKey', () => {
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
});
2029
it('should return an object', () => {
2130
expect(computeKey('abcd', {})).toMatchInlineSnapshot(`
2231
Object {
@@ -58,4 +67,54 @@ describe('computeKey', () => {
5867
};
5968
expect(computeKey('uri', firstOptions).hash).toBe(computeKey('uri', secondOptions).hash);
6069
});
70+
71+
it('should return a different, stable hash, if the option mapKeyToCacheKey is passed', () => {
72+
const key = 'abcd';
73+
const mappedKey = 'efgh';
74+
const { hash: mappedHash1 } = computeKey(key, { mapKeyToCacheKey: () => mappedKey });
75+
const { hash: mappedHash2 } = computeKey(key, { mapKeyToCacheKey: () => mappedKey });
76+
77+
const { hash: unmappedHash } = computeKey(key, {});
78+
79+
expect(mappedHash1).toBe(mappedHash2);
80+
expect(mappedHash1).not.toBe(unmappedHash);
81+
});
82+
83+
it('should return the same key if the option mapKeyToCacheKey returns the same string as the key', () => {
84+
const key = 'abcd';
85+
const { hash: mappedHash } = computeKey(key, { mapKeyToCacheKey: (_key) => _key });
86+
87+
const { hash: unmappedHash } = computeKey(key, {});
88+
89+
expect(mappedHash).toBe(unmappedHash);
90+
});
91+
92+
it('should pass generated cacheKey to the underlying hash function along with the options, and return the un-mapped key to the caller', () => {
93+
const computedKey = computeKey(() => 'abcd', {
94+
mapKeyToCacheKey: (key, options) => `${key.toUpperCase()}-${options.optionKeyMock}`,
95+
optionKeyMock: 'optionKeyValue',
96+
});
97+
expect(computedKey.key).toBe('abcd');
98+
expect(computeHash).toHaveBeenCalledWith(['ABCD-optionKeyValue', { optionKeyMock: 'optionKeyValue' }], { respectType: false });
99+
});
100+
101+
it('should return false if mapKeyToCacheKey throws error', () => {
102+
expect(
103+
computeKey(() => 'abcd', {
104+
mapKeyToCacheKey: () => {
105+
throw new Error('error');
106+
},
107+
})
108+
).toEqual(false);
109+
});
110+
111+
it('should return false if mapKeyToCacheKey returns false', () => {
112+
expect(computeKey(() => 'abcd', { mapKeyToCacheKey: () => false })).toEqual(false);
113+
});
114+
115+
it('should throw an error if mapKeyToCacheKey is defined and not a function', () => {
116+
expect(() => computeKey(() => 'abcd',
117+
{ mapKeyToCacheKey: 'string' }
118+
)).toThrow('mapKeyToCacheKey must be a function');
119+
});
61120
});

packages/fetchye/src/computeKey.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import computeHash from 'object-hash';
1818
import mapHeaderNamesToLowerCase from './mapHeaderNamesToLowerCase';
1919

2020
export const computeKey = (key, options) => {
21-
const { headers, ...restOfOptions } = options;
21+
const { headers, mapKeyToCacheKey, ...restOfOptions } = options;
2222
const nextOptions = { ...restOfOptions };
2323
if (headers) {
2424
nextOptions.headers = mapHeaderNamesToLowerCase(headers);
2525
}
2626

27+
let nextKey = key;
2728
if (typeof key === 'function') {
28-
let nextKey;
2929
try {
3030
nextKey = key(nextOptions);
3131
} catch (error) {
@@ -34,9 +34,23 @@ export const computeKey = (key, options) => {
3434
if (!nextKey) {
3535
return false;
3636
}
37-
return { key: nextKey, hash: computeHash([nextKey, nextOptions], { respectType: false }) };
3837
}
39-
return { key, hash: computeHash([key, nextOptions], { respectType: false }) };
38+
39+
let cacheKey = nextKey;
40+
if (mapKeyToCacheKey !== undefined && typeof mapKeyToCacheKey === 'function') {
41+
try {
42+
cacheKey = mapKeyToCacheKey(nextKey, nextOptions);
43+
} catch (error) {
44+
return false;
45+
}
46+
if (!cacheKey) {
47+
return false;
48+
}
49+
} else if (mapKeyToCacheKey !== undefined) {
50+
throw new TypeError('mapKeyToCacheKey must be a function');
51+
}
52+
53+
return { key: nextKey, hash: computeHash([cacheKey, nextOptions], { respectType: false }) };
4054
};
4155

4256
export default computeKey;

0 commit comments

Comments
 (0)