diff --git a/packages/redux-saga-requests/src/actions.js b/packages/redux-saga-requests/src/actions.js index 4203df50a..e610ae572 100644 --- a/packages/redux-saga-requests/src/actions.js +++ b/packages/redux-saga-requests/src/actions.js @@ -2,6 +2,7 @@ import { SUCCESS_SUFFIX, ERROR_SUFFIX, ABORT_SUFFIX, + GET_REQUEST_CACHE, CLEAR_REQUESTS_CACHE, } from './constants'; @@ -15,16 +16,18 @@ export const abort = getActionWithSuffix(ABORT_SUFFIX); const isFSA = action => !!action.payload; -export const createSuccessAction = (action, data) => ({ +export const createSuccessAction = (action, data, response) => ({ type: success(action.type), ...(isFSA(action) ? { payload: { data, + response, }, } : { data, + response, }), meta: { ...action.meta, @@ -89,6 +92,8 @@ export const isErrorAction = action => export const isAbortAction = action => isResponseAction(action) && action.type.endsWith(ABORT_SUFFIX); +export const getRequestCache = () => ({ type: GET_REQUEST_CACHE }); + export const clearRequestsCache = (...actionTypes) => ({ type: CLEAR_REQUESTS_CACHE, actionTypes, diff --git a/packages/redux-saga-requests/src/actions.spec.js b/packages/redux-saga-requests/src/actions.spec.js index d9eb87179..188958afc 100644 --- a/packages/redux-saga-requests/src/actions.spec.js +++ b/packages/redux-saga-requests/src/actions.spec.js @@ -41,9 +41,12 @@ describe('actions', () => { request: { url: '/' }, }; - expect(createSuccessAction(requestAction, 'data')).toEqual({ + expect( + createSuccessAction(requestAction, 'data', { data: 'data' }), + ).toEqual({ type: 'REQUEST_SUCCESS', data: 'data', + response: { data: 'data' }, meta: { requestAction, }, @@ -58,10 +61,13 @@ describe('actions', () => { }, }; - expect(createSuccessAction(requestAction, 'data')).toEqual({ + expect( + createSuccessAction(requestAction, 'data', { data: 'data' }), + ).toEqual({ type: 'REQUEST_SUCCESS', payload: { data: 'data', + response: { data: 'data' }, }, meta: { requestAction, @@ -78,9 +84,12 @@ describe('actions', () => { }, }; - expect(createSuccessAction(requestAction, 'data')).toEqual({ + expect( + createSuccessAction(requestAction, 'data', { data: 'data' }), + ).toEqual({ type: 'REQUEST_SUCCESS', data: 'data', + response: { data: 'data' }, meta: { requestAction, asPromise: true, diff --git a/packages/redux-saga-requests/src/constants.js b/packages/redux-saga-requests/src/constants.js index 8c05a152c..14584e553 100644 --- a/packages/redux-saga-requests/src/constants.js +++ b/packages/redux-saga-requests/src/constants.js @@ -9,6 +9,7 @@ export const INTERCEPTORS = { ON_SUCCESS: 'onSuccess', ON_ABORT: 'onAbort', }; +export const GET_REQUEST_CACHE = 'GET_REQUEST_CACHE'; export const CLEAR_REQUESTS_CACHE = 'CLEAR_REQUESTS_CACHE'; export const INCORRECT_PAYLOAD_ERROR = "Incorrect payload for request action. Action must have form of { type: 'TYPE', request: {} }, { type: 'TYPE', request: [{}, {}] }, { type: 'TYPE', payload: { request: {} } } or { type: 'TYPE', payload: { request: [{}, {}] } }"; diff --git a/packages/redux-saga-requests/src/constants.spec.js b/packages/redux-saga-requests/src/constants.spec.js index f07a1b0c0..b59af6455 100644 --- a/packages/redux-saga-requests/src/constants.spec.js +++ b/packages/redux-saga-requests/src/constants.spec.js @@ -3,6 +3,7 @@ import { ERROR_SUFFIX, ABORT_SUFFIX, REQUESTS_CONFIG, + GET_REQUEST_CACHE, CLEAR_REQUESTS_CACHE, INCORRECT_PAYLOAD_ERROR, } from './constants'; @@ -32,6 +33,12 @@ describe('constants', () => { }); }); + describe('GET_REQUEST_CACHE', () => { + it('has correct value', () => { + expect(GET_REQUEST_CACHE).toBe('GET_REQUEST_CACHE'); + }); + }); + describe('CLEAR_REQUESTS_CACHE', () => { it('has correct value', () => { expect(CLEAR_REQUESTS_CACHE).toBe('CLEAR_REQUESTS_CACHE'); diff --git a/packages/redux-saga-requests/src/middleware.js b/packages/redux-saga-requests/src/middleware.js index 890e99b15..d8ffc1af2 100644 --- a/packages/redux-saga-requests/src/middleware.js +++ b/packages/redux-saga-requests/src/middleware.js @@ -1,10 +1,11 @@ -import { CLEAR_REQUESTS_CACHE } from './constants'; +import { GET_REQUEST_CACHE, CLEAR_REQUESTS_CACHE } from './constants'; import { success, isRequestAction, isSuccessAction, isResponseAction, getRequestActionFromResponse, + getActionPayload, } from './actions'; const shouldActionBePromisified = (action, auto) => @@ -42,15 +43,20 @@ export const requestsPromiseMiddleware = ({ auto = false } = {}) => { }; }; -const isCacheValid = cache => cache === true || Date.now() <= cache; +const isCacheValid = cache => + cache.expiring === null || Date.now() <= cache.expiring; -const getNewCacheValue = cache => - cache === true ? cache : cache * 1000 + Date.now(); +const getNewCacheTimeout = cache => + cache === true ? null : cache * 1000 + Date.now(); export const requestsCacheMiddleware = () => { const cacheMap = new Map(); return () => next => action => { + if (action.type === GET_REQUEST_CACHE) { + return cacheMap; + } + if (action.type === CLEAR_REQUESTS_CACHE) { if (action.actionTypes.length === 0) { cacheMap.clear(); @@ -66,14 +72,25 @@ export const requestsCacheMiddleware = () => { cacheMap.get(action.type) && isCacheValid(cacheMap.get(action.type)) ) { - return null; + return next({ + ...action, + meta: { + ...action.meta, + cacheResponse: cacheMap.get(action.type).response, + }, + }); } - if (isSuccessAction(action) && action.meta && action.meta.cache) { - cacheMap.set( - getRequestActionFromResponse(action).type, - getNewCacheValue(action.meta.cache), - ); + if ( + isSuccessAction(action) && + action.meta && + action.meta.cache && + !action.meta.cacheResponse + ) { + cacheMap.set(getRequestActionFromResponse(action).type, { + response: getActionPayload(action).response, + expiring: getNewCacheTimeout(action.meta.cache), + }); } return next(action); diff --git a/packages/redux-saga-requests/src/middleware.spec.js b/packages/redux-saga-requests/src/middleware.spec.js index 8ede74239..25ec6b270 100644 --- a/packages/redux-saga-requests/src/middleware.spec.js +++ b/packages/redux-saga-requests/src/middleware.spec.js @@ -6,6 +6,7 @@ import { error, abort, createSuccessAction, + getRequestCache, clearRequestsCache, } from './actions'; import { @@ -240,6 +241,20 @@ describe('requestsPromiseMiddleware', () => { }); describe('requestsCacheMiddleware', () => { + it('doesnt dispatch anything on getRequestCache action', () => { + const mockStore = configureStore([requestsCacheMiddleware()]); + const store = mockStore({}); + store.dispatch(getRequestCache()); + expect(store.getActions()).toEqual([]); + }); + + it('returns map on getRequestCache action', () => { + const mockStore = configureStore([requestsCacheMiddleware()]); + const store = mockStore({}); + const result = store.dispatch(getRequestCache()); + expect(result).toEqual(new Map()); + }); + it('doesnt affect non request actions', () => { const mockStore = configureStore([requestsCacheMiddleware()]); const store = mockStore({}); @@ -247,22 +262,21 @@ describe('requestsCacheMiddleware', () => { const result = store.dispatch(action); expect(result).toEqual(action); expect(store.getActions()).toEqual([action]); + expect(store.dispatch(getRequestCache())).toEqual(new Map()); }); it('doesnt affect request actions with no meta cache', () => { const mockStore = configureStore([requestsCacheMiddleware()]); const store = mockStore({}); const action = { type: 'REQUEST', request: { url: '/' } }; - const responseAction = createSuccessAction(action, null); + const responseAction = createSuccessAction(action, null, null); store.dispatch(action); store.dispatch(responseAction); - store.clearActions(); - const result = store.dispatch(action); - expect(result).toEqual(action); - expect(store.getActions()).toEqual([action]); + expect(store.getActions()).toEqual([action, responseAction]); + expect(store.dispatch(getRequestCache())).toEqual(new Map()); }); - it('doesnt affect request actions after clearing the whole cache', () => { + it('adds cacheResponse to request action when meta cache is true', () => { const mockStore = configureStore([requestsCacheMiddleware()]); const store = mockStore({}); const action = { @@ -270,52 +284,91 @@ describe('requestsCacheMiddleware', () => { request: { url: '/' }, meta: { cache: true }, }; - const responseAction = createSuccessAction(action, null); + const responseAction = createSuccessAction(action, null, { data: 'data' }); store.dispatch(action); store.dispatch(responseAction); - store.dispatch(clearRequestsCache()); + expect(store.getActions()).toEqual([action, responseAction]); + expect(store.dispatch(getRequestCache())).toEqual( + new Map([ + [ + 'REQUEST', + { + expiring: null, + response: { data: 'data' }, + }, + ], + ]), + ); store.clearActions(); - const result = store.dispatch(action); - expect(result).toEqual(action); - expect(store.getActions()).toEqual([action]); + store.dispatch(action); + expect(store.getActions()).toEqual([ + { + type: 'REQUEST', + request: { url: '/' }, + meta: { cache: true, cacheResponse: { data: 'data' } }, + }, + ]); }); - it('doesnt affect request actions after clearing action cache', () => { + it('adds cacheResponse to request action when meta cache is integer and valid', () => { + advanceTo(new Date(2018, 1, 1, 0, 0, 0)); const mockStore = configureStore([requestsCacheMiddleware()]); const store = mockStore({}); const action = { type: 'REQUEST', request: { url: '/' }, - meta: { cache: true }, + meta: { cache: 1 }, }; - const responseAction = createSuccessAction(action, null); + const responseAction = createSuccessAction(action, null, { data: 'data' }); store.dispatch(action); store.dispatch(responseAction); - store.dispatch(clearRequestsCache('REQUEST', 'ANOTHER')); + expect(store.getActions()).toEqual([action, responseAction]); store.clearActions(); - const result = store.dispatch(action); - expect(result).toEqual(action); - expect(store.getActions()).toEqual([action]); + advanceBy(1000); + store.dispatch(action); + clear(); + expect(store.getActions()).toEqual([ + { + type: 'REQUEST', + request: { url: '/' }, + meta: { cache: 1, cacheResponse: { data: 'data' } }, + }, + ]); }); - it('doesnt dispatch request with meta cache true', () => { + it('doesnt add cacheResponse to request action when meta cache is integer and invalid', () => { + advanceTo(new Date(2018, 1, 1, 0, 0, 0)); const mockStore = configureStore([requestsCacheMiddleware()]); const store = mockStore({}); const action = { type: 'REQUEST', request: { url: '/' }, - meta: { cache: true }, + meta: { cache: 1 }, }; - const responseAction = createSuccessAction(action, null); + const responseAction = createSuccessAction(action, null, { data: 'data' }); store.dispatch(action); store.dispatch(responseAction); + advanceBy(1000); + store.dispatch(action); + store.dispatch( + createSuccessAction( + { + type: 'REQUEST', + request: { url: '/' }, + meta: { cache: 1, cacheResponse: { data: 'data' } }, + }, + null, + { data: 'data' }, + ), + ); + advanceBy(1); store.clearActions(); - const result = store.dispatch(action); - expect(result).toEqual(null); - expect(store.getActions()).toEqual([]); + store.dispatch(action); + clear(); + expect(store.getActions()).toEqual([action]); }); - it('clearing unrelated cache doesnt affect valid cache', () => { + it('doesnt add cacheResponse to request action after the whole cache is cleared', () => { const mockStore = configureStore([requestsCacheMiddleware()]); const store = mockStore({}); const action = { @@ -323,53 +376,31 @@ describe('requestsCacheMiddleware', () => { request: { url: '/' }, meta: { cache: true }, }; - const responseAction = createSuccessAction(action, null); + const responseAction = createSuccessAction(action, null, { data: 'data' }); store.dispatch(action); store.dispatch(responseAction); - store.dispatch(clearRequestsCache('NOT_RELATED')); + store.dispatch(clearRequestsCache()); + expect(store.dispatch(getRequestCache())).toEqual(new Map()); store.clearActions(); - const result = store.dispatch(action); - expect(result).toEqual(null); - expect(store.getActions()).toEqual([]); - }); - - it('doesnt dispatch request with meta cache as integer when cache valid', () => { - advanceTo(new Date(2018, 1, 1, 0, 0, 0)); - const mockStore = configureStore([requestsCacheMiddleware()]); - const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { cache: 1 }, - }; - const responseAction = createSuccessAction(action, null); store.dispatch(action); - store.dispatch(responseAction); - store.clearActions(); - advanceBy(1000); - const result = store.dispatch(action); - clear(); - expect(result).toEqual(null); - expect(store.getActions()).toEqual([]); + expect(store.getActions()).toEqual([action]); }); - it('dispatches request with meta cache as integer when cache expired', () => { - advanceTo(new Date(2018, 1, 1, 0, 0, 0)); + it('doesnt add cacheResponse to request action after specific action cache is cleared', () => { const mockStore = configureStore([requestsCacheMiddleware()]); const store = mockStore({}); const action = { type: 'REQUEST', request: { url: '/' }, - meta: { cache: 1 }, + meta: { cache: true }, }; - const responseAction = createSuccessAction(action, null); + const responseAction = createSuccessAction(action, null, { data: 'data' }); store.dispatch(action); store.dispatch(responseAction); + store.dispatch(clearRequestsCache('REQUEST')); + expect(store.dispatch(getRequestCache())).toEqual(new Map()); store.clearActions(); - advanceBy(1001); - const result = store.dispatch(action); - clear(); - expect(result).toEqual(action); + store.dispatch(action); expect(store.getActions()).toEqual([action]); }); }); diff --git a/packages/redux-saga-requests/src/sagas.js b/packages/redux-saga-requests/src/sagas.js index 5e93f5034..37671cee9 100644 --- a/packages/redux-saga-requests/src/sagas.js +++ b/packages/redux-saga-requests/src/sagas.js @@ -83,12 +83,7 @@ export function* sendRequest( if (dispatchRequestAction && !silent) { action = { ...action, meta: { ...action.meta, runByWatcher: false } }; - const requestResponse = yield put(action); - - // only possible when using requestsCacheMiddleware - if (requestResponse === null) { - return { cacheHit: true }; - } + action = yield put(action); // to be affected by requestsCacheMiddleware } const driver = yield call(getDriver, requestsConfig, action); @@ -116,7 +111,9 @@ export function* sendRequest( let responseError; try { - if (!Array.isArray(actionPayload.request)) { + if (action.meta && action.meta.cacheResponse) { + response = action.meta.cacheResponse; + } else if (!Array.isArray(actionPayload.request)) { response = yield call( [driver, 'sendRequest'], actionPayload.request, @@ -185,7 +182,7 @@ export function* sendRequest( ); if (!silent) { - yield put(createSuccessAction(action, successPayload)); + yield put(createSuccessAction(action, successPayload, response)); } return { response }; diff --git a/packages/redux-saga-requests/src/sagas.spec.js b/packages/redux-saga-requests/src/sagas.spec.js index 1436e050b..eef44219e 100644 --- a/packages/redux-saga-requests/src/sagas.spec.js +++ b/packages/redux-saga-requests/src/sagas.spec.js @@ -189,15 +189,28 @@ describe('sagas', () => { expect(sagaError).toEqual(INCORRECT_PAYLOAD_ERROR); }); - it('returns cache hit when request action dispatch returns null', () => { - const action = { type: 'FETCH', request: { url: '/url' } }; + it('gets response from cache when available in meta cacheResponse', () => { + const action = { + type: 'FETCH', + request: { url: '/url' }, + meta: { cache: 1 }, + }; + + const actionWithCacheResponse = { + ...action, + meta: { ...action.meta, cacheResponse: { data: 'data' } }, + }; return expectSaga(sendRequest, action, { dispatchRequestAction: true }) .provide([ [getContext(REQUESTS_CONFIG), config], - [matchers.put.actionType(action.type), null], + [matchers.put.actionType(action.type), actionWithCacheResponse], ]) - .returns({ cacheHit: true }) + .put( + createSuccessAction(actionWithCacheResponse, 'data', { + data: 'data', + }), + ) .run(); }); @@ -205,7 +218,10 @@ describe('sagas', () => { const action = { type: 'FETCH', request: { url: '/url' } }; return expectSaga(sendRequest, action, { dispatchRequestAction: true }) - .provide([[getContext(REQUESTS_CONFIG), config]]) + .provide([ + [getContext(REQUESTS_CONFIG), config], + [matchers.put.actionType(action.type), action], + ]) .put({ type: 'FETCH', request: { url: '/url' }, @@ -240,7 +256,7 @@ describe('sagas', () => { return expectSaga(sendRequest, action) .provide([[getContext(REQUESTS_CONFIG), config]]) - .put(createSuccessAction(action, 'response')) + .put(createSuccessAction(action, 'response', { data: 'response' })) .returns({ response: { data: 'response' } }) .run(); }); @@ -255,7 +271,7 @@ describe('sagas', () => { { ...config, driver: { default: dummyDriver() } }, ], ]) - .put(createSuccessAction(action, 'response')) + .put(createSuccessAction(action, 'response', { data: 'response' })) .returns({ response: { data: 'response' } }) .run(); }); @@ -277,7 +293,7 @@ describe('sagas', () => { }, ], ]) - .put(createSuccessAction(action, 'response')) + .put(createSuccessAction(action, 'response', { data: 'response' })) .returns({ response: { data: 'response' } }) .run(); }); @@ -289,8 +305,17 @@ describe('sagas', () => { }; return expectSaga(sendRequest, action) - .provide([[getContext(REQUESTS_CONFIG), config]]) - .put(createSuccessAction(action, ['response', 'response'])) + .provide([ + [getContext(REQUESTS_CONFIG), config], + [matchers.put.actionType(action.type), action], + ]) + .put( + createSuccessAction( + action, + ['response', 'response'], + [{ data: 'response' }, { data: 'response' }], + ), + ) .returns({ response: [{ data: 'response' }, { data: 'response' }] }) .run(); });