Skip to content

Commit

Permalink
Added meta cacheKey and cacheSize to cache multiple responses per act…
Browse files Browse the repository at this point in the history
…ion type
  • Loading branch information
klis87 committed Apr 6, 2019
1 parent 29d13f8 commit c5cf7fe
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 22 deletions.
68 changes: 63 additions & 5 deletions packages/redux-saga-requests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ With this request action, assuming `id = 1`, following actions will be dispatche
```js
{
type: 'DELETE_BOOK_SUCCESS',
data: 'a server response',
response: { data: 'some data' },
data: 'some data',
meta: {
id: 1, // got from request action meta
requestAction: {
Expand Down Expand Up @@ -940,8 +941,57 @@ const fetchBooks = () => ({
```
What will happen now, is that after a succesfull book fetch (to be specific after `FETCH_BOOKS_SUCCESS` is dispatched),
any `FETCH_BOOKS` actions for `10` seconds will be totally ignored, they won't touch your reducers and no request will be sent.
You could also use `cache: true` to cache forever.
any `FETCH_BOOKS` actions for `10` seconds won't trigger any AJAX calls and the following `FETCH_BOOKS_SUCCESS` will contain
cached previous server response. You could also use `cache: true` to cache forever.
Another use case is that you might want to keep a separate cache for the same request action based on a cache key. For example:
```js
const fetchBook = id => ({
type: FETCH_BOOK,
request: { url: `/books/${id}`},
meta: {
cache: true,
cacheKey: id // must be string
},
});

/* then, you will achieve the following behaviour:
- GET /books/1 - make request
- GET /books/1 - cache hit
- GET /books/1 - cache hit
- GET /books/2 - make request
- GET /books/2 - cache hit
- GET /books/1 - cache hit
*/
```
The only problem with above is that if you happen to request with too many different cache keys,
then cache would take too much memory. The remedy to this problem is `cacheSize`:
```js
const fetchBook = id => ({
type: FETCH_BOOK,
request: { url: `/books/${id}`},
meta: {
cache: true,
cacheKey: id // must be string
cacheSize: 2 // any integer bigger or equal 1
},
});

/* then, you will achieve the following behaviour:
- GET /books/1 - make request
- GET /books/1 - cache hit
- GET /books/1 - cache hit
- GET /books/2 - make request
- GET /books/2 - cache hit
- GET /books/1 - cache hit
- GET /books/3 - make request
- GET /books/3 - cache hit
- GET /books/2 - cache hit
- GET /books/1 - make request! because /books/3 invalidated /books/1 as
it was the oldest cache and our cache size was set to 2
*/
```
If you need to clear the cache for some reason, you can use `clearRequestsCache` action:
```js
Expand All @@ -955,8 +1005,16 @@ function* cacheSaga() {
}
```
Also, if you happen to use `sendRequest` with cacheable action with `dispatchRequestAction: true`,
be aware that it could return `{ cacheHit: true }` instead of object with `error` or `response` key.
Additionally, you can use `getRequestCache` action for debugging purposes:
```js
import { put } from 'redux-saga/effects';
import { getRequestCache } from 'redux-saga-requests';

function* cacheSaga() {
const currentCache = yield put(getRequestCache());
}
```
## Usage with Fetch API [:arrow_up:](#table-of-content)
Expand Down
8 changes: 7 additions & 1 deletion packages/redux-saga-requests/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { success, error, abort, clearRequestsCache } from './actions';
export {
success,
error,
abort,
getRequestCache,
clearRequestsCache,
} from './actions';
export { createRequestsReducer, requestsReducer } from './reducers';
export {
requestsPromiseMiddleware,
Expand Down
47 changes: 31 additions & 16 deletions packages/redux-saga-requests/src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const isCacheValid = cache =>
const getNewCacheTimeout = cache =>
cache === true ? null : cache * 1000 + Date.now();

const getCacheKey = action => action.type + (action.meta.cacheKey || '');

export const requestsCacheMiddleware = () => {
const cacheMap = new Map();

This comment has been minimized.

Copy link
@megawac

megawac May 6, 2019

Contributor

In my implementation I implemented this as a WeakMap and I was keying off the list of objects that were available. E.g. if I was making a library I would fetch books { title, author, numPages } and if I wanted to fetch a book ({ title, pages: [{...}], ... } I'd key off the libraries book reference.

This comment has been minimized.

Copy link
@megawac

megawac May 6, 2019

Contributor

This removes the ability to do the queue size stuff but it does open the door to automatic cache clearing as items are removed.

This comment has been minimized.

Copy link
@klis87

klis87 May 7, 2019

Author Owner

Thanks for shearing. I did it this way because my primary use case is to have the same item types fetched with different context. For instance imagine /:organisationId/books . I guess your use case is different, but to fully understand it, I would need to see your request actions and how they are called.

Do you think that those 2 use cases could be handled/combined within one middleware?

This comment has been minimized.

Copy link
@megawac

megawac May 7, 2019

Contributor

Do you think that those 2 use cases could be handled/combined within one middleware?
I'm not sure they are pretty different use cases.

I think your implementation is a better general implementation. The strategy I used to implement caching was in a factory pattern. The factory has a saga that intercepts requests and decides whether to use the cache or submit the request. The reason I did it this way was so that I would have the ability to customize the caching methods later on as the intention is to eventually place items from my "library" in an indexed db and circumvent redux-saga-requests entirely. After looking at your latest implementation of caching I think it may of been possible to do this in middleware in retrospect, however I'd need a way of structuring schema in the meta.

I'd be happy to share some snippets of what I'm doing with your library privately. These packages have bene a joy to work with :D

This comment has been minimized.

Copy link
@megawac

megawac May 7, 2019

Contributor

This is what I'm doing currently

const requestCacheMap = new WeakMap();

const getFeaturesFromCache = createAction(`get ${type} from cache`);
function cacheRequestSaga(action) {
  // Cache the result of the request to avoid having to refetch the asset.
  requestCacheMap.set(action.meta.map, action.payload.data);
}

const getFeaturesAction = createAction(`get ${type}`, ({ map }) => {
  return {
    request: {
      url: `/v2/maps/${url}/?map=${map.id}`
    },
  };
}, ({ map }) => ({ map }));
function* getFeaturesSaga({ mapId }) {
  const { data: maps } = yield select((state) => state.maps);
  const map = maps.find((map) => map.id === mapId);

  if (requestCacheMap.has(map)) {
    console.info(`Loading ${type} from cache.`);
    return yield put(getFeaturesFromCache({
      data: requestCacheMap.get(map)
    }));
  }

  return yield call(dispatchRequest, getFeaturesAction, { map });
}

function* rootSaga() {
  takeEvery(success(getFeaturesAction), cacheRequestSaga)
}

This comment has been minimized.

Copy link
@klis87

klis87 May 12, 2019

Author Owner

Interesting WeakMap usage. I am not sure but I guess that indeed it should be possible to use similar logic in middleware. https://github.com/klis87/redux-saga-requests/blob/master/packages/redux-saga-requests/src/middleware.js#L54 could be a good start. And I don't think actually that passing schema in meta would be even necessary. You could just pass map object as action.meta.cacheKey and that's it. In theory we could even support both approaches by adding extra config to requestsCacheMiddleware, which would use WeakMap instead of Map and would have different const getCacheKey = action => action.type + (action.meta.cacheKey || '') implementation, probably like action => action.meta.cacheKey


Expand All @@ -67,27 +69,40 @@ export const requestsCacheMiddleware = () => {
return null;
}

if (
isRequestAction(action) &&
cacheMap.get(action.type) &&
isCacheValid(cacheMap.get(action.type))
) {
return next({
...action,
meta: {
...action.meta,
cacheResponse: cacheMap.get(action.type).response,
},
});
}

if (
if (isRequestAction(action) && action.meta && action.meta.cache) {
const cacheKey = getCacheKey(action);
const cacheValue = cacheMap.get(cacheKey);

if (cacheValue && isCacheValid(cacheValue)) {
return next({
...action,
meta: {
...action.meta,
cacheResponse: cacheValue.response,
},
});
} else if (cacheValue && !isCacheValid(cacheValue)) {
cacheMap.delete(cacheKey);
}
} else if (
isSuccessAction(action) &&
action.meta &&
action.meta.cache &&
!action.meta.cacheResponse
) {
cacheMap.set(getRequestActionFromResponse(action).type, {
const requestAction = getRequestActionFromResponse(action);

if (action.meta.cacheKey && action.meta.cacheSize) {
const currentCacheKeys = Array.from(cacheMap.keys()).filter(k =>
k.startsWith(requestAction.type),
);

if (action.meta.cacheSize === currentCacheKeys.length) {
cacheMap.delete(currentCacheKeys[0]);
}
}

cacheMap.set(getCacheKey(requestAction), {
response: getActionPayload(action).response,
expiring: getNewCacheTimeout(action.meta.cache),
});
Expand Down
73 changes: 73 additions & 0 deletions packages/redux-saga-requests/src/middleware.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,77 @@ describe('requestsCacheMiddleware', () => {
store.dispatch(action);
expect(store.getActions()).toEqual([action]);
});

const wrapWithCacheResponse = action => ({
...action,
meta: { ...action.meta, cacheResponse: { data: action.meta.cacheKey } },
});

it('supports meta cacheKey', () => {
const mockStore = configureStore([requestsCacheMiddleware()]);
const store = mockStore({});
const createRequestAction = id => ({
type: 'REQUEST',
request: { url: `/${id}` },
meta: { cache: true, cacheKey: id },
});
const createResponseAction = id =>
createSuccessAction(createRequestAction(id), id, {
data: id,
});

store.dispatch(createRequestAction('1'));
store.dispatch(createResponseAction('1'));
store.dispatch(createRequestAction('1'));
store.dispatch(createResponseAction('1'));
store.dispatch(createRequestAction('2'));
store.dispatch(createResponseAction('2'));
store.dispatch(createRequestAction('1'));
store.dispatch(createResponseAction('1'));

expect(store.getActions()).toEqual([
createRequestAction('1'),
createResponseAction('1'),
wrapWithCacheResponse(createRequestAction('1')),
createResponseAction('1'),
createRequestAction('2'),
createResponseAction('2'),
wrapWithCacheResponse(createRequestAction('1')),
createResponseAction('1'),
]);
});

it('supports meta cacheSize', () => {
const mockStore = configureStore([requestsCacheMiddleware()]);
const store = mockStore({});
const createRequestAction = id => ({
type: 'REQUEST',
request: { url: `/${id}` },
meta: { cache: true, cacheKey: id, cacheSize: 1 },
});
const createResponseAction = id =>
createSuccessAction(createRequestAction(id), id, {
data: id,
});

store.dispatch(createRequestAction('1'));
store.dispatch(createResponseAction('1'));
store.dispatch(createRequestAction('1'));
store.dispatch(createResponseAction('1'));
store.dispatch(createRequestAction('2'));
store.dispatch(createResponseAction('2'));
store.dispatch(createRequestAction('1'));
store.dispatch(createResponseAction('1'));

expect(store.getActions()).toEqual([
createRequestAction('1'),
createResponseAction('1'),
wrapWithCacheResponse(createRequestAction('1')),
createResponseAction('1'),
createRequestAction('2'),
createResponseAction('2'),
createRequestAction('1'),
createResponseAction('1'),
]);
});
});
2 changes: 2 additions & 0 deletions packages/redux-saga-requests/types/index.d.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const requestAction: RequestAction = {
abortOn: ['ABORT'],
takeLatest: false,
cache: 1,
cacheKey: 'key',
cacheSize: 2,
customKey: 'customValue',
},
};
Expand Down
2 changes: 2 additions & 0 deletions packages/redux-saga-requests/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type RequestActionMeta = {
takeLatest?: boolean;
abortOn?: FilterOnActionCallback | string | string[];
cache?: boolean | number;
cacheKey?: string;
cacheSize?: number;
};

export type RequestAction =
Expand Down

0 comments on commit c5cf7fe

Please sign in to comment.