Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compute-key): add support for mapKeyToCacheKey #71

Merged
merged 10 commits into from
Mar 6, 2023
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
60 changes: 60 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,55 @@ 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', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test cannot assert that the key is different as the spec states since it only generates one key. There is nothing to compare it to

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in a693a07

expect(computeKey(() => 'abcd', {
mapKeyToCacheKey: () => 'efgh',
})).toMatchInlineSnapshot(`
Object {
"hash": "a0e09d568bb5b47c046b0fac7a61ca10196151cc",
"key": "abcd",
code-forger marked this conversation as resolved.
Show resolved Hide resolved
}
`);
});

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 the same key if the option mapKeyToCacheKey returns the same string as the key', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has the same issue as the one on line 71

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in a693a07

expect(computeKey(() => 'abcd', {
mapKeyToCacheKey: key => key,
})).toMatchInlineSnapshot(`
Object {
"hash": "037ace2918f4083eda9c4be34cccb93de5051b5a",
"key": "abcd",
}
`);
});

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('should1 throw an error if mapKeyToCacheKey is defined and not a function', () => {
code-forger marked this conversation as resolved.
Show resolved Hide resolved
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;
code-forger marked this conversation as resolved.
Show resolved Hide resolved
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;