diff --git a/README.md b/README.md index d1efcb7..cbef566 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ To solve the problems listed above, `redux-rest-easy` **generates actions, reduc It also provides **sensible defaults**, allowing you to use it with **almost no configuration**, but also to **customize** anything you would like. +And the cherry on the top: it works seamlessly with [redux-offline](https://github.com/redux-offline/redux-offline) and [redux-persist](https://github.com/rt2zz/redux-persist)! + [Scroll down](#minimal-example) for a small example, or [browse the documentation](#api) to get started! To learn more about the problem and solution, you can also read the [release article][release-article]. ## API @@ -61,6 +63,7 @@ import { * [connect](./docs/api/connect.md) - connect your components to the state so the magic can happen * [reset](./docs/api/reset.md) - reset `redux-rest-easy`'s whole state (you can reset parts of the state with actions generated by `createResource`) * [initializeNetworkHelpers](./docs/api/initializeNetworkHelpers.md) - provide your own network handlers (optional, fallback to included defaults) +* [getPersistableState](./docs/api/getPersistableState.md) - transform the state before storing it, in order to later persist it (using [redux-offline](https://github.com/redux-offline/redux-offline), [redux-persist](https://github.com/rt2zz/redux-persist), or friends) ## Internals @@ -70,10 +73,10 @@ import { ## Core principles -1. [Preflight checks](./docs/principles/preflight.md) -2. [Actions](./docs/principles/actions.md) -3. [Reducers](./docs/principles/reducers.md) -4. [Selectors](./docs/principles/selectors.md) +1. [Preflight checks](./docs/principles/preflight.md) +2. [Actions](./docs/principles/actions.md) +3. [Reducers](./docs/principles/reducers.md) +4. [Selectors](./docs/principles/selectors.md) ## Minimal Example @@ -86,7 +89,7 @@ const users = createResource('users')({ retrieve: { method: 'GET', url: 'https://my-api.com/users', - afterHook: () => console.log('Users retrieved successfuly'), + afterHook: () => console.log('Users retrieved successfully'), }, }); @@ -178,9 +181,11 @@ Redux-rest-easy also uses [redux-thunk][redux-thunk] to handle async actions, an Thanks goes to these people ([emoji key][emojis]): + | [
Adrien HARNAY](https://adrien.harnay.me)
[📝](#blog-Zephir77167 "Blogposts") [💻](https://github.com/Brigad/redux-rest-easy/commits?author=Zephir77167 "Code") [📖](https://github.com/Brigad/redux-rest-easy/commits?author=Zephir77167 "Documentation") [🤔](#ideas-Zephir77167 "Ideas, Planning, & Feedback") [🚇](#infra-Zephir77167 "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-Zephir77167 "Reviewed Pull Requests") [⚠️](https://github.com/Brigad/redux-rest-easy/commits?author=Zephir77167 "Tests") | [
Thibault Malbranche](https://github.com/Titozzz)
[🐛](https://github.com/Brigad/redux-rest-easy/issues?q=author%3ATitozzz "Bug reports") [💻](https://github.com/Brigad/redux-rest-easy/commits?author=Titozzz "Code") [🤔](#ideas-Titozzz "Ideas, Planning, & Feedback") [👀](#review-Titozzz "Reviewed Pull Requests") | [
Grisha Ghukasyan](https://github.com/eole1712)
[🤔](#ideas-eole1712 "Ideas, Planning, & Feedback") | [
Aymeric Beaumet](https://aymericbeaumet.com)
[🤔](#ideas-aymericbeaumet "Ideas, Planning, & Feedback") | [
Jess](https://github.com/Pinesy)
[🐛](https://github.com/Brigad/redux-rest-easy/issues?q=author%3APinesy "Bug reports") [📖](https://github.com/Brigad/redux-rest-easy/commits?author=Pinesy "Documentation") | | :---: | :---: | :---: | :---: | :---: | + This project follows the [all-contributors][all-contributors] specification. diff --git a/__tests__/internals/action-creator/preflight/isCacheExpired.test.js b/__tests__/internals/action-creator/preflight/isCacheExpired.test.js index f0bcda5..e8a27f7 100644 --- a/__tests__/internals/action-creator/preflight/isCacheExpired.test.js +++ b/__tests__/internals/action-creator/preflight/isCacheExpired.test.js @@ -5,14 +5,14 @@ import isCacheExpired from '../../../../src/internals/action-creator/preflight/i const URL = 'https://api.co/fruits'; const OTHER_URL = 'https://api.co/fruits?page1'; - const RESOURCE_NAME = 'fruits'; -const END_DATE = moment - .utc() - .year(2017) - .month(0) - .date(1) - .startOf('day'); + +const MOMENT_NOW = moment(Date.UTC(2017, 0, 1)); +const EXPIRE_AT_NOW = new Date(Date.UTC(2017, 0, 1)).toISOString(); +const EXPIRE_AT_ONE_SEC = new Date( + Date.UTC(2017, 0, 1) + 1 * 1000, +).toISOString(); +const EXPIRE_AT_NEVER = 'never'; const EMPTY_STATE = {}; @@ -25,8 +25,8 @@ const STATE_FILLED_WITH_OTHER_REQUEST = { [OTHER_URL]: { resourceName: RESOURCE_NAME, resourceId: null, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: true, hasFailed: false, didInvalidate: false, @@ -40,8 +40,8 @@ const SUCCEEDED_STATE = { [URL]: { resourceName: RESOURCE_NAME, resourceId: null, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: true, hasFailed: false, didInvalidate: false, @@ -55,8 +55,8 @@ const FAILED_STATE = { [URL]: { resourceName: RESOURCE_NAME, resourceId: null, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: false, hasFailed: true, didInvalidate: false, @@ -70,8 +70,8 @@ const INVALIDATED_STATE = { [URL]: { resourceName: RESOURCE_NAME, resourceId: null, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: true, hasFailed: false, didInvalidate: true, @@ -80,6 +80,15 @@ const INVALIDATED_STATE = { }, }; +const injectExpireAtInState = (state, expireAt) => ({ + requests: { + [URL]: { + ...state.requests[URL], + expireAt, + }, + }, +}); + describe('isCacheExpired', () => { afterEach(() => { mockdate.reset(); @@ -92,118 +101,314 @@ describe('isCacheExpired', () => { }); test('non cacheable requests', () => { - expect(isCacheExpired(SUCCEEDED_STATE, 'POST', URL, 0)).toBe(true); - expect(isCacheExpired(SUCCEEDED_STATE, 'PATCH', URL, 0)).toBe(true); - expect(isCacheExpired(SUCCEEDED_STATE, 'PUT', URL, 0)).toBe(true); - expect(isCacheExpired(SUCCEEDED_STATE, 'DELETE', URL, 0)).toBe(true); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NOW), + 'POST', + URL, + ), + ).toBe(true); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NOW), + 'PATCH', + URL, + ), + ).toBe(true); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NOW), + 'PUT', + URL, + ), + ).toBe(true); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NOW), + 'DELETE', + URL, + ), + ).toBe(true); }); test('state empty of concerned url', () => { - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); - - expect(isCacheExpired(EMPTY_STATE, 'GET', URL, 0)).toBe(true); - expect(isCacheExpired(HALF_FILLED_STATE, 'GET', URL, 0)).toBe(true); - expect(isCacheExpired(STATE_FILLED_WITH_OTHER_REQUEST, 'GET', URL, 0)).toBe( - true, - ); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + + expect(isCacheExpired(EMPTY_STATE, 'GET', URL)).toBe(true); + expect( + isCacheExpired( + injectExpireAtInState(HALF_FILLED_STATE, EXPIRE_AT_NOW), + 'GET', + URL, + ), + ).toBe(true); + expect( + isCacheExpired( + injectExpireAtInState(STATE_FILLED_WITH_OTHER_REQUEST, EXPIRE_AT_NOW), + 'GET', + URL, + ), + ).toBe(true); }); test('succeeded state, cacheLifetime = 0', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, 0)).toBe(false); - - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, 0)).toBe(true); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NOW), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NOW), + 'GET', + URL, + ), + ).toBe(true); }); test('failed state, cacheLifetime = 0', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, 0)).toBe(true); - - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, 0)).toBe(true); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NOW), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NOW), + 'GET', + URL, + ), + ).toBe(true); }); test('invalidated state, cacheLifetime = 0', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(INVALIDATED_STATE, 'GET', URL, 0)).toBe(true); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(INVALIDATED_STATE, EXPIRE_AT_NOW), + 'GET', + URL, + ), + ).toBe(true); }); test('succeeded state, cacheLifetime = 1', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, 1)).toBe(false); - - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, 1)).toBe(false); - - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, 1)).toBe(false); - - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, 1)).toBe(false); - - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, 1)).toBe(true); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(true); }); test('failed state, cacheLifetime = 1', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, 1)).toBe(true); - - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, 1)).toBe(true); - - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, 1)).toBe(true); - - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, 1)).toBe(true); - - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, 1)).toBe(true); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(true); }); test('invalidated state, cacheLifetime = 1', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(INVALIDATED_STATE, 'GET', URL, 1)).toBe(true); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(INVALIDATED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + URL, + ), + ).toBe(true); }); test('succeeded state, cacheLifetime = Infinity', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, Infinity)).toBe(false); - - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, Infinity)).toBe(false); - - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, Infinity)).toBe(false); - - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, Infinity)).toBe(false); - - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); - expect(isCacheExpired(SUCCEEDED_STATE, 'GET', URL, Infinity)).toBe(false); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(false); + + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(SUCCEEDED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(false); }); test('failed state, cacheLifetime = Infinity', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, Infinity)).toBe(true); - - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, Infinity)).toBe(true); - - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, Infinity)).toBe(true); - - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, Infinity)).toBe(true); - - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); - expect(isCacheExpired(FAILED_STATE, 'GET', URL, Infinity)).toBe(true); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(true); + + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); + expect( + isCacheExpired( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(true); }); test('invalidated state, cacheLifetime = Infinity', () => { - mockdate.set(END_DATE); - expect(isCacheExpired(INVALIDATED_STATE, 'GET', URL, Infinity)).toBe(true); + mockdate.set(MOMENT_NOW); + expect( + isCacheExpired( + injectExpireAtInState(INVALIDATED_STATE, EXPIRE_AT_NEVER), + 'GET', + URL, + ), + ).toBe(true); }); }); diff --git a/__tests__/internals/action-creator/preflight/isSmartCacheAvailable.test.js b/__tests__/internals/action-creator/preflight/isSmartCacheAvailable.test.js index d0ea26a..466ec9b 100644 --- a/__tests__/internals/action-creator/preflight/isSmartCacheAvailable.test.js +++ b/__tests__/internals/action-creator/preflight/isSmartCacheAvailable.test.js @@ -5,10 +5,15 @@ import isSmartCacheAvailable from '../../../../src/internals/action-creator/pref const URL = 'https://api.co/fruits'; const OTHER_URL = 'https://api.co/fruits?page1'; - const RESOURCE_NAME = 'fruits'; const RESOURCE_ID = 2; -const END_DATE = moment(Date.UTC(2017, 0, 1)); + +const MOMENT_NOW = moment(Date.UTC(2017, 0, 1)); +const EXPIRE_AT_NOW = new Date(Date.UTC(2017, 0, 1)).toISOString(); +const EXPIRE_AT_ONE_SEC = new Date( + Date.UTC(2017, 0, 1) + 1 * 1000, +).toISOString(); +const EXPIRE_AT_NEVER = 'never'; const EMPTY_STATE = {}; @@ -21,8 +26,8 @@ const STATE_FILLED_WITH_OTHER_ID = { [OTHER_URL]: { resourceName: RESOURCE_NAME, resourceId: RESOURCE_ID + 1, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: true, hasFailed: false, didInvalidate: false, @@ -39,8 +44,8 @@ const SUCCEEDED_STATE_1 = { [URL]: { resourceName: RESOURCE_NAME, resourceId: RESOURCE_ID, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: true, hasFailed: false, didInvalidate: false, @@ -57,8 +62,8 @@ const SUCCEEDED_STATE_2 = { [URL]: { resourceName: RESOURCE_NAME, resourceId: null, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: true, hasFailed: false, didInvalidate: false, @@ -75,8 +80,8 @@ const FAILED_STATE = { [URL]: { resourceName: RESOURCE_NAME, resourceId: RESOURCE_ID, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: false, hasFailed: true, didInvalidate: false, @@ -90,8 +95,8 @@ const INVALIDATED_STATE = { [URL]: { resourceName: RESOURCE_NAME, resourceId: RESOURCE_ID, - startedAt: END_DATE, - endedAt: END_DATE, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, hasSucceeded: true, hasFailed: false, didInvalidate: true, @@ -103,7 +108,16 @@ const INVALIDATED_STATE = { }, }; -describe('isCacheExpired', () => { +const injectExpireAtInState = (state, expireAt) => ({ + requests: { + [URL]: { + ...state.requests[URL], + expireAt, + }, + }, +}); + +describe('isSmartCacheAvailable', () => { afterEach(() => { mockdate.reset(); }); @@ -117,475 +131,472 @@ describe('isCacheExpired', () => { test('non cacheable requests', () => { expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NOW), 'POST', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NOW), 'PATCH', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NOW), 'PUT', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NOW), 'DELETE', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); }); test('state empty of resource/id', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable(EMPTY_STATE, 'GET', RESOURCE_NAME, RESOURCE_ID, 0), ).toBe(false); expect( isSmartCacheAvailable( - HALF_FILLED_STATE, + injectExpireAtInState(HALF_FILLED_STATE, EXPIRE_AT_NOW), 'GET', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); expect( isSmartCacheAvailable( - STATE_FILLED_WITH_OTHER_ID, + injectExpireAtInState(STATE_FILLED_WITH_OTHER_ID, EXPIRE_AT_NOW), 'GET', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); }); test('succeeded state 1, cacheLifetime = 0', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NOW), 'GET', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NOW), 'GET', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); }); test('succeeded state 2, cacheLifetime = 0', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_NOW), 'GET', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_NOW), 'GET', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); }); test('failed state, cacheLifetime = 0', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( - isSmartCacheAvailable(FAILED_STATE, 'GET', RESOURCE_NAME, RESOURCE_ID, 0), + isSmartCacheAvailable( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NOW), + 'GET', + RESOURCE_NAME, + RESOURCE_ID, + ), ).toBe(false); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( - isSmartCacheAvailable(FAILED_STATE, 'GET', RESOURCE_NAME, RESOURCE_ID, 0), + isSmartCacheAvailable( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NOW), + 'GET', + RESOURCE_NAME, + RESOURCE_ID, + ), ).toBe(false); }); test('invalidated state, cacheLifetime = 0', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - INVALIDATED_STATE, + injectExpireAtInState(INVALIDATED_STATE, EXPIRE_AT_NOW), 'GET', RESOURCE_NAME, RESOURCE_ID, - 0, ), ).toBe(false); }); test('succeeded state 1, cacheLifetime = 1', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(false); }); test('succeeded state 2, cacheLifetime = 1', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(false); }); test('failed state, cacheLifetime = 1', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( - isSmartCacheAvailable(FAILED_STATE, 'GET', RESOURCE_NAME, RESOURCE_ID, 1), + isSmartCacheAvailable( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + RESOURCE_NAME, + RESOURCE_ID, + ), ).toBe(false); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( - isSmartCacheAvailable(FAILED_STATE, 'GET', RESOURCE_NAME, RESOURCE_ID, 1), + isSmartCacheAvailable( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + RESOURCE_NAME, + RESOURCE_ID, + ), ).toBe(false); - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); expect( - isSmartCacheAvailable(FAILED_STATE, 'GET', RESOURCE_NAME, RESOURCE_ID, 1), + isSmartCacheAvailable( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + RESOURCE_NAME, + RESOURCE_ID, + ), ).toBe(false); - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); expect( - isSmartCacheAvailable(FAILED_STATE, 'GET', RESOURCE_NAME, RESOURCE_ID, 1), + isSmartCacheAvailable( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + RESOURCE_NAME, + RESOURCE_ID, + ), ).toBe(false); - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); expect( - isSmartCacheAvailable(FAILED_STATE, 'GET', RESOURCE_NAME, RESOURCE_ID, 1), + isSmartCacheAvailable( + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_ONE_SEC), + 'GET', + RESOURCE_NAME, + RESOURCE_ID, + ), ).toBe(false); }); test('invalidated state, cacheLifetime = 1', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - INVALIDATED_STATE, + injectExpireAtInState(INVALIDATED_STATE, EXPIRE_AT_ONE_SEC), 'GET', RESOURCE_NAME, RESOURCE_ID, - 1, ), ).toBe(false); }); test('succeeded state 1, cacheLifetime = Infinity', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_1, + injectExpireAtInState(SUCCEEDED_STATE_1, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); }); test('succeeded state 2, cacheLifetime = Infinity', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); expect( isSmartCacheAvailable( - SUCCEEDED_STATE_2, + injectExpireAtInState(SUCCEEDED_STATE_2, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(true); }); test('failed state, cacheLifetime = Infinity', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - FAILED_STATE, + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(false); - mockdate.set(moment(END_DATE).add(1, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); expect( isSmartCacheAvailable( - FAILED_STATE, + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(false); - mockdate.set(moment(END_DATE).add(999, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); expect( isSmartCacheAvailable( - FAILED_STATE, + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(false); - mockdate.set(moment(END_DATE).add(1000, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); expect( isSmartCacheAvailable( - FAILED_STATE, + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(false); - mockdate.set(moment(END_DATE).add(1001, 'milliseconds')); + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); expect( isSmartCacheAvailable( - FAILED_STATE, + injectExpireAtInState(FAILED_STATE, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(false); }); test('invalidated state, cacheLifetime = Infinity', () => { - mockdate.set(END_DATE); + mockdate.set(MOMENT_NOW); expect( isSmartCacheAvailable( - INVALIDATED_STATE, + injectExpireAtInState(INVALIDATED_STATE, EXPIRE_AT_NEVER), 'GET', RESOURCE_NAME, RESOURCE_ID, - Infinity, ), ).toBe(false); }); diff --git a/__tests__/internals/actions/generateActionCreatorActions/__snapshots__/generateActionCreatorAction.test.js.snap b/__tests__/internals/actions/generateActionCreatorActions/__snapshots__/generateActionCreatorAction.test.js.snap index 815c7c7..2cef50d 100644 --- a/__tests__/internals/actions/generateActionCreatorActions/__snapshots__/generateActionCreatorAction.test.js.snap +++ b/__tests__/internals/actions/generateActionCreatorActions/__snapshots__/generateActionCreatorAction.test.js.snap @@ -2,6 +2,7 @@ exports[`generateActionCreatorAction empty payload 1`] = ` Object { + "cacheLifetime": 0, "payload": undefined, "principalResourceIds": undefined, "resourceId": "2", @@ -12,6 +13,7 @@ Object { exports[`generateActionCreatorAction filled payload without principalResourceIds 1`] = ` Object { + "cacheLifetime": 0, "payload": Object { "fruits": Object { "1": "cherry", @@ -26,6 +28,7 @@ Object { exports[`generateActionCreatorAction filled payload, array principalResourceIds 1`] = ` Object { + "cacheLifetime": 0, "payload": Object { "fruits": Object { "1": "cherry", @@ -42,6 +45,7 @@ Object { exports[`generateActionCreatorAction filled payload, string principalResourceIds 1`] = ` Object { + "cacheLifetime": 0, "payload": Object { "fruits": Object { "1": "cherry", @@ -56,8 +60,20 @@ Object { } `; +exports[`generateActionCreatorAction no action type 1`] = ` +Object { + "cacheLifetime": 0, + "payload": undefined, + "principalResourceIds": undefined, + "resourceId": undefined, + "type": undefined, + "url": undefined, +} +`; + exports[`generateActionCreatorAction no args 1`] = ` Object { + "cacheLifetime": undefined, "payload": undefined, "principalResourceIds": undefined, "resourceId": undefined, @@ -68,6 +84,7 @@ Object { exports[`generateActionCreatorAction no id, payload 1`] = ` Object { + "cacheLifetime": 0, "payload": undefined, "principalResourceIds": undefined, "resourceId": undefined, @@ -78,6 +95,7 @@ Object { exports[`generateActionCreatorAction no payload 1`] = ` Object { + "cacheLifetime": 0, "payload": undefined, "principalResourceIds": undefined, "resourceId": "2", @@ -88,6 +106,7 @@ Object { exports[`generateActionCreatorAction no url, id, payload 1`] = ` Object { + "cacheLifetime": 0, "payload": undefined, "principalResourceIds": undefined, "resourceId": undefined, diff --git a/__tests__/internals/actions/generateActionCreatorActions/generateActionCreatorAction.test.js b/__tests__/internals/actions/generateActionCreatorActions/generateActionCreatorAction.test.js index 6213b54..8af149e 100644 --- a/__tests__/internals/actions/generateActionCreatorActions/generateActionCreatorAction.test.js +++ b/__tests__/internals/actions/generateActionCreatorActions/generateActionCreatorAction.test.js @@ -1,5 +1,6 @@ import generateActionCreatorAction from '../../../../src/internals/actions/generateActionCreatorActions/generateActionCreatorAction'; +const CACHE_LIFETIME = 0; const ACTION_TYPE = '@@rest-easy/fruits/eat/REQUEST'; const NORMALIZED_URL = 'eat:https://api.co/fruits'; const ID = '2'; @@ -10,25 +11,38 @@ describe('generateActionCreatorAction', () => { expect(generateActionCreatorAction()()).toMatchSnapshot(); }); + test('no action type', () => { + expect(generateActionCreatorAction(CACHE_LIFETIME)()).toMatchSnapshot(); + }); + test('no url, id, payload', () => { - expect(generateActionCreatorAction(ACTION_TYPE)()).toMatchSnapshot(); + expect( + generateActionCreatorAction(CACHE_LIFETIME, ACTION_TYPE)(), + ).toMatchSnapshot(); }); test('no id, payload', () => { expect( - generateActionCreatorAction(ACTION_TYPE)(NORMALIZED_URL), + generateActionCreatorAction(CACHE_LIFETIME, ACTION_TYPE)(NORMALIZED_URL), ).toMatchSnapshot(); }); test('no payload', () => { expect( - generateActionCreatorAction(ACTION_TYPE)(NORMALIZED_URL, ID), + generateActionCreatorAction(CACHE_LIFETIME, ACTION_TYPE)( + NORMALIZED_URL, + ID, + ), ).toMatchSnapshot(); }); test('empty payload', () => { expect( - generateActionCreatorAction(ACTION_TYPE)(NORMALIZED_URL, ID, {}), + generateActionCreatorAction(CACHE_LIFETIME, ACTION_TYPE)( + NORMALIZED_URL, + ID, + {}, + ), ).toMatchSnapshot(); }); @@ -40,7 +54,11 @@ describe('generateActionCreatorAction', () => { }; expect( - generateActionCreatorAction(ACTION_TYPE)(NORMALIZED_URL, ID, payload), + generateActionCreatorAction(CACHE_LIFETIME, ACTION_TYPE)( + NORMALIZED_URL, + ID, + payload, + ), ).toMatchSnapshot(); }); @@ -52,7 +70,12 @@ describe('generateActionCreatorAction', () => { }; expect( - generateActionCreatorAction(ACTION_TYPE)(NORMALIZED_URL, ID, payload, ID), + generateActionCreatorAction(CACHE_LIFETIME, ACTION_TYPE)( + NORMALIZED_URL, + ID, + payload, + ID, + ), ).toMatchSnapshot(); }); @@ -64,7 +87,7 @@ describe('generateActionCreatorAction', () => { }; expect( - generateActionCreatorAction(ACTION_TYPE)( + generateActionCreatorAction(CACHE_LIFETIME, ACTION_TYPE)( NORMALIZED_URL, ID, payload, diff --git a/__tests__/internals/persistence/getPrunedForPersistenceState.test.js b/__tests__/internals/persistence/getPrunedForPersistenceState.test.js new file mode 100644 index 0000000..c2d235c --- /dev/null +++ b/__tests__/internals/persistence/getPrunedForPersistenceState.test.js @@ -0,0 +1,338 @@ +import mockdate from 'mockdate'; +import moment from 'moment'; + +import getPrunedForPersistenceState from '../../../src/internals/persistence/getPrunedForPersistenceState'; + +const URL_1 = 'https://api.co/fruits?page1'; +const URL_2 = 'https://api.co/fruits?page2'; +const URL_3 = 'https://api.co/fruits?page3'; +const URL_4 = 'https://api.co/fruits?page4'; +const RESOURCE_NAME = 'fruits'; +const RESOURCE_NAME_2 = 'vegetables'; +const RESOURCE_NAME_3 = 'sauces'; + +const MOMENT_NOW = moment(Date.UTC(2017, 0, 1)); +const EXPIRE_AT_ONE_SEC = new Date( + Date.UTC(2017, 0, 1) + 1 * 1000, +).toISOString(); +const EXPIRE_AT_NEVER = 'never'; + +const STATE_REQUESTS = { + requests: { + [URL_1]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: null, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + }, + [URL_2]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: true, + fromCache: false, + }, + [URL_3]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, + expireAt: EXPIRE_AT_ONE_SEC, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + }, + [URL_4]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + }, + }, +}; + +const STATE_RESOURCES = { + requests: { + [URL_1]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: null, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + payloadIds: { + [RESOURCE_NAME]: [1, 2, 3], + }, + }, + [URL_4]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + payloadIds: { + [RESOURCE_NAME]: [3, 4, 5], + }, + }, + }, + resources: { + [RESOURCE_NAME]: { + 1: 'banana', + 2: 'apple', + 3: 'cherry', + 4: 'pineapple', + 5: 'raspberry', + }, + [RESOURCE_NAME_2]: { + 1: 'carrot', + 2: 'potato', + }, + }, +}; + +const STATE_RESOURCES_OTHER_ORDER = { + requests: { + [URL_4]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + payloadIds: { + [RESOURCE_NAME]: [3, 4, 5], + }, + }, + [URL_1]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: null, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + payloadIds: { + [RESOURCE_NAME]: [1, 2, 3], + }, + }, + }, + resources: { + [RESOURCE_NAME_2]: { + 1: 'carrot', + 2: 'potato', + }, + [RESOURCE_NAME]: { + 1: 'banana', + 2: 'apple', + 3: 'cherry', + 4: 'pineapple', + 5: 'raspberry', + }, + }, +}; + +const STATE_RESOLVERS_HASHES = { + requests: { + [URL_1]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: null, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + payloadIds: { + [RESOURCE_NAME]: [1, 2, 3], + }, + }, + [URL_4]: { + resourceName: RESOURCE_NAME, + resourceId: null, + startedAt: MOMENT_NOW, + endedAt: MOMENT_NOW, + expireAt: EXPIRE_AT_NEVER, + hasSucceeded: true, + hasFailed: false, + didInvalidate: false, + fromCache: false, + payloadIds: { + [RESOURCE_NAME]: [3, 4, 5], + [RESOURCE_NAME_3]: [1, 2], + }, + }, + }, + resources: { + [RESOURCE_NAME]: { + 1: 'banana', + 2: 'apple', + 3: 'cherry', + 4: 'pineapple', + 5: 'raspberry', + }, + [RESOURCE_NAME_2]: { + 1: 'carrot', + 2: 'potato', + }, + [RESOURCE_NAME_3]: { + 1: 'chocolate', + 2: 'fudge', + }, + }, + resolversHashes: { + requests: { + [URL_1]: { + [RESOURCE_NAME]: 'URL_1_HASH', + }, + [URL_4]: { + [RESOURCE_NAME]: 'URL_4_HASH', + }, + }, + resources: { + [RESOURCE_NAME]: 'RESOURCE_HASH', + [RESOURCE_NAME_2]: 'RESOURCE_2_HASH', + [RESOURCE_NAME_3]: 'RESOURCE_3_HASH', + }, + }, +}; + +describe('getPrunedForPersistenceState', () => { + afterEach(() => { + mockdate.reset(); + }); + + test('invalid state', () => { + expect(Object.keys(getPrunedForPersistenceState()).length).toBe(0); + expect(Object.keys(getPrunedForPersistenceState(null)).length).toBe(0); + }); + + test('empty state', () => { + expect(Object.keys(getPrunedForPersistenceState({})).length).toBe(0); + }); + + test('requests: no endedAt', () => { + expect( + getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_1], + ).toBeUndefined(); + }); + + test('requests: didInvalidate', () => { + expect( + getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_2], + ).toBeUndefined(); + }); + + test('requests: cache expired', () => { + mockdate.set(MOMENT_NOW); + expect( + getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_3], + ).not.toBeUndefined(); + + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); + expect( + getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_3], + ).not.toBeUndefined(); + + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); + expect( + getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_3], + ).not.toBeUndefined(); + + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); + expect( + getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_3], + ).toBeUndefined(); + }); + + test('requests: expiredAt never', () => { + mockdate.set(MOMENT_NOW); + expect( + getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_4] + .didInvalidate, + ).toBe(true); + }); + + test('resources first order', () => { + const prunedState = getPrunedForPersistenceState(STATE_RESOURCES); + const prunedStateResourcesIds = Object.keys( + prunedState.resources[RESOURCE_NAME], + ); + + expect(prunedStateResourcesIds.length).toBe(3); + + expect(prunedStateResourcesIds.includes('1')).toBe(false); + expect(prunedStateResourcesIds.includes('2')).toBe(false); + + expect(prunedStateResourcesIds.includes('3')).toBe(true); + expect(prunedStateResourcesIds.includes('4')).toBe(true); + expect(prunedStateResourcesIds.includes('5')).toBe(true); + + expect(prunedState.resources[RESOURCE_NAME_2]).toBeUndefined(); + }); + + test('resources second order', () => { + const prunedState = getPrunedForPersistenceState( + STATE_RESOURCES_OTHER_ORDER, + ); + const prunedStateResourcesIds = Object.keys( + prunedState.resources[RESOURCE_NAME], + ); + + expect(prunedStateResourcesIds.length).toBe(3); + + expect(prunedStateResourcesIds.includes('1')).toBe(false); + expect(prunedStateResourcesIds.includes('2')).toBe(false); + + expect(prunedStateResourcesIds.includes('3')).toBe(true); + expect(prunedStateResourcesIds.includes('4')).toBe(true); + expect(prunedStateResourcesIds.includes('5')).toBe(true); + + expect(prunedState.resources[RESOURCE_NAME_2]).toBeUndefined(); + }); + + test('resolversHashes', () => { + const prunedStateResolversHashes = getPrunedForPersistenceState( + STATE_RESOLVERS_HASHES, + ).resolversHashes; + + expect(prunedStateResolversHashes.requests[URL_1]).toBeUndefined(); + expect( + prunedStateResolversHashes.requests[URL_4][RESOURCE_NAME], + ).not.toBeUndefined(); + expect(prunedStateResolversHashes.resources[RESOURCE_NAME]).toBeUndefined(); + expect( + prunedStateResolversHashes.resources[RESOURCE_NAME_2], + ).toBeUndefined(); + expect( + prunedStateResolversHashes.resources[RESOURCE_NAME_3], + ).not.toBeUndefined(); + }); +}); diff --git a/__tests__/internals/reducer/generateReducer/__snapshots__/getReducerCases.test.js.snap b/__tests__/internals/reducer/generateReducer/__snapshots__/getReducerCases.test.js.snap index 051c42d..bb2dedf 100644 --- a/__tests__/internals/reducer/generateReducer/__snapshots__/getReducerCases.test.js.snap +++ b/__tests__/internals/reducer/generateReducer/__snapshots__/getReducerCases.test.js.snap @@ -204,6 +204,7 @@ Object { "eat:https://api.co/fruits": Object { "didInvalidate": false, "endedAt": "2017-01-01T00:00:00.000Z", + "expireAt": "2017-01-01T00:00:00.000Z", "fromCache": false, "hasFailed": false, "hasSucceeded": true, @@ -250,6 +251,7 @@ Object { "eat:https://api.co/fruits": Object { "didInvalidate": false, "endedAt": "2017-01-01T00:00:00.000Z", + "expireAt": "2017-01-01T00:00:00.000Z", "fromCache": true, "hasFailed": false, "hasSucceeded": true, @@ -288,6 +290,7 @@ Object { "eat:https://api.co/fruits": Object { "didInvalidate": false, "endedAt": null, + "expireAt": null, "fromCache": false, "hasFailed": false, "hasSucceeded": false, diff --git a/__tests__/internals/reducer/generateReducer/getReducerCases.test.js b/__tests__/internals/reducer/generateReducer/getReducerCases.test.js index 2b1c08e..7bede21 100644 --- a/__tests__/internals/reducer/generateReducer/getReducerCases.test.js +++ b/__tests__/internals/reducer/generateReducer/getReducerCases.test.js @@ -3,6 +3,7 @@ import moment from 'moment'; import getReducerCases from '../../../../src/internals/reducer/generateReducer/getReducerCases'; +const CACHE_LIFETIME = 0; const RESOURCE_NAME = 'fruits'; const RESOURCE_ID = 2; const ACTION_NAME = 'eat'; @@ -105,6 +106,7 @@ describe('getReducerCases', () => { }, }, principalResourceIds: ['2', '1', '3'], + cacheLifetime: CACHE_LIFETIME, }; expect( @@ -135,6 +137,7 @@ describe('getReducerCases', () => { }, }, principalResourceIds: ['1'], + cacheLifetime: CACHE_LIFETIME, }; expect( diff --git a/__tests__/internals/utils/hasCacheExpired.test.js b/__tests__/internals/utils/hasCacheExpired.test.js index a8daa5b..23734e2 100644 --- a/__tests__/internals/utils/hasCacheExpired.test.js +++ b/__tests__/internals/utils/hasCacheExpired.test.js @@ -3,49 +3,53 @@ import moment from 'moment'; import hasCacheExpired from '../../../src/internals/utils/hasCacheExpired'; -const MOMENT_END_DATE = moment(Date.UTC(2017, 0, 1)); -const END_DATE = new Date(Date.UTC(2017, 0, 1)).toISOString(); +const MOMENT_NOW = moment(Date.UTC(2017, 0, 1)); +const EXPIRE_AT_NOW = new Date(Date.UTC(2017, 0, 1)).toISOString(); +const EXPIRE_AT_ONE_SEC = new Date( + Date.UTC(2017, 0, 1) + 1 * 1000, +).toISOString(); +const EXPIRE_AT_NEVER = 'never'; describe('hasCacheExpired', () => { test('cacheLifetime = 0', () => { - mockdate.set(MOMENT_END_DATE); - expect(hasCacheExpired(END_DATE, 0)).toBe(false); + mockdate.set(MOMENT_NOW); + expect(hasCacheExpired(EXPIRE_AT_NOW, 0)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(1, 'milliseconds')); - expect(hasCacheExpired(END_DATE, 0)).toBe(true); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_NOW, 0)).toBe(true); }); test('cacheLifetime = 1', () => { - mockdate.set(MOMENT_END_DATE); - expect(hasCacheExpired(END_DATE, 1)).toBe(false); + mockdate.set(MOMENT_NOW); + expect(hasCacheExpired(EXPIRE_AT_ONE_SEC)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(1, 'milliseconds')); - expect(hasCacheExpired(END_DATE, 1)).toBe(false); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_ONE_SEC)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(999, 'milliseconds')); - expect(hasCacheExpired(END_DATE, 1)).toBe(false); + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_ONE_SEC)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(1000, 'milliseconds')); - expect(hasCacheExpired(END_DATE, 1)).toBe(false); + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_ONE_SEC)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(1001, 'milliseconds')); - expect(hasCacheExpired(END_DATE, 1)).toBe(true); + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_ONE_SEC)).toBe(true); }); test('cacheLifetime = Infinity', () => { - mockdate.set(MOMENT_END_DATE); - expect(hasCacheExpired(END_DATE, Infinity)).toBe(false); + mockdate.set(MOMENT_NOW); + expect(hasCacheExpired(EXPIRE_AT_NEVER)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(1, 'milliseconds')); - expect(hasCacheExpired(END_DATE, Infinity)).toBe(false); + mockdate.set(moment(MOMENT_NOW).add(1, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_NEVER)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(999, 'milliseconds')); - expect(hasCacheExpired(END_DATE, Infinity)).toBe(false); + mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_NEVER)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(1000, 'milliseconds')); - expect(hasCacheExpired(END_DATE, Infinity)).toBe(false); + mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_NEVER)).toBe(false); - mockdate.set(moment(MOMENT_END_DATE).add(1001, 'milliseconds')); - expect(hasCacheExpired(END_DATE, Infinity)).toBe(false); + mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds')); + expect(hasCacheExpired(EXPIRE_AT_NEVER)).toBe(false); }); }); diff --git a/docs/api/createResource.md b/docs/api/createResource.md index 78b2bb6..2c5e6e5 100644 --- a/docs/api/createResource.md +++ b/docs/api/createResource.md @@ -153,5 +153,5 @@ export { #### Tips * The above example is very exhaustive, but you can export only what you really need -* Naming is a mater of personal preference, use what works for you +* Naming is a matter of personal preference, use what works for you * You can alternately just export `users`, and spare yourself the trouble of mapping the names. Then just use the selectors like so: `users.selectors.resource.getResource()`. Again, use what works for you diff --git a/docs/api/createResource/actions.md b/docs/api/createResource/actions.md index 5e8db56..924320c 100644 --- a/docs/api/createResource/actions.md +++ b/docs/api/createResource/actions.md @@ -35,12 +35,12 @@ These functions can be imported in components and directly dispatched. #### Properties -1. (_urlParams_): (`map`) An object to replace dynamic parameters in the URL (see [actions config documentation](./actionsConfig.md#properties)) -2. (_query_): (`map`) An object to prepend a query to the URL -3. (_body_): (`map`) An object to specify the body of the request (e.g. POST) -4. (_onSuccess_): (`(normalizedPayload, otherArgs) : undefined`) A hook which will be invoked after the request has performed successfuly. Useful to update the UI accordingly, at the component level -5. (_onFailure_): (`(error) : undefined`) A hook which will be invoked when the request fails. Useful to update the UI accordingly, at the component level -6. (_...otherArgs_): (`any`) You can also pass any other args you may need in `beforeHook`, `normalizer`, `afterHook`, and `onSuccess`. They will be forwarded to these functions, as an object +1. (_urlParams_): (`map`) An object to replace dynamic parameters in the URL (see [actions config documentation](./actionsConfig.md#properties)) +2. (_query_): (`map`) An object to prepend a query to the URL +3. (_body_): (`map`) An object to specify the body of the request (e.g. POST) +4. (_onSuccess_): (`(normalizedPayload, otherArgs) : undefined`) A hook which will be invoked after the request has performed successfully. Useful to update the UI accordingly, at the component level +5. (_onFailure_): (`(error) : undefined`) A hook which will be invoked when the request fails. Useful to update the UI accordingly, at the component level +6. (_...otherArgs_): (`any`) You can also pass any other args you may need in `beforeHook`, `normalizer`, `afterHook`, and `onSuccess`. They will be forwarded to these functions, as an object #### Example diff --git a/docs/api/createResource/actionsConfig.md b/docs/api/createResource/actionsConfig.md index b8d9e2c..de0f304 100644 --- a/docs/api/createResource/actionsConfig.md +++ b/docs/api/createResource/actionsConfig.md @@ -6,7 +6,7 @@ Map of `` which defines the actions you will be able to perfo 1. (_method_) **mandatory**: (`string`) The method of the action. Can be one of: `GET`, `PATCH`, `PUT`, `POST`, `DELETE` 2. (_url_) **mandatory**: (`string || () : string`) The URL on which the action has to fetch. For dynamic parameters, prefix them with `::` for the resource ID (e.g. `::userId`), and `:` for other parameters (e.g. `:userType`), and they will get replaced with the `urlParams` you will provide (see [actions documentation](./actions.md#properties)) -3. (_beforeHook_): (`(urlParams, query, body, otherArgs, dispatch) : undefined || any`) A hook which can be invoked just before performing the request. Will be awaited if async. If it returns a non falsy value, the return will be used as the body for the principal request +3. (_beforeHook_): (`(urlParams, query, body, otherArgs, dispatch) : undefined || any`) A hook which can be invoked just before performing the request. Will be awaited if async. If it returns a non-falsy value, the return will be used as the body for the principal request 4. (_normalizer_): (`(payload, resources, urlParams, query, body, otherArgs) : { entities: normalizedPayload, result: principalResourceId }`) A function which will be invoked to normalize the payload of the request. It is expected to return an object with `entities` and `result`, respectively containing the normalized payload and the sorted ids, just as [normalizr](https://github.com/paularmstrong/normalizr) does. 5. (_afterHook_): (`(normalizedPayload, urlParams, query, body, otherArgs, dispatch) : undefined`) A hook which can be invoked after performing the request and normalizing the payload. Will be awaited if async 6. (_networkHelpers_): (`map`) A map of handlers used when performing network requests. Override the default ones, and the ones specified using `initializeNetworkHelpers`. Documentation on the content of the map can be found [here](../initializeNetworkHelpers.md#arguments) diff --git a/docs/api/createResource/selectors.md b/docs/api/createResource/selectors.md index ec17af9..50b99f2 100644 --- a/docs/api/createResource/selectors.md +++ b/docs/api/createResource/selectors.md @@ -39,11 +39,11 @@ _Available to any connected component._ ### `getResource(state, applyDenormalizer = true)`: `array` -Will return the whole resource (or an empty array). Will be denormalized by default, but can be overriden. +Will return the whole resource (or an empty array). Will be denormalized by default, but can be overridden. ### `getResourceById(state, id, applyDenormalizer = true)`: `object` -Will return the object corresponding to the id passed as a parameter (or null). Will be denormalized by default, but can be overriden. +Will return the object corresponding to the id passed as a parameter (or null). Will be denormalized by default, but can be overridden. ## Action.resource @@ -75,7 +75,7 @@ _Only available to connected components which specifically requested a resource ### `getResource(state, ownProps, applyDenormalizer = true)`: `array` -Will return the resource corresponding to the payload of the request (or an empty array). Will be denormalized by default, but can be overriden. +Will return the resource corresponding to the payload of the request (or an empty array). Will be denormalized by default, but can be overridden. ### `couldPerform(state, ownProps)`: `bool` diff --git a/docs/api/getPersistableState.md b/docs/api/getPersistableState.md new file mode 100644 index 0000000..c385137 --- /dev/null +++ b/docs/api/getPersistableState.md @@ -0,0 +1,48 @@ +# `getPersistableState(state)` + +Returns a state pruned from outdated data. [Read more](../principles/persistence.md) about what is removed from the state exactly. + +#### Arguments + +1. (_state_): The current Redux state + +#### Returns + +(_state_): A new state, pruned from outdated data + +#### Example + +This example demonstrates how to integrate [redux-offline](https://github.com/redux-offline/redux-offline) with redux-rest-easy, by creating a [transform with redux-persist](https://github.com/rt2zz/redux-persist#transforms), a dependency of redux-offline. + +```js +import { getPersistableState } from '@brigad/redux-rest-easy'; +import { offline } from '@redux-offline/redux-offline'; +import offlineConfig from '@redux-offline/redux-offline/lib/defaults'; +import { applyMiddleware, compose, createStore } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +// No need to manually install redux-persist if you already have installed redux-offline +import { createTransform } from 'redux-persist'; + +const restEasyTransform = createTransform(getPersistableState, null, { + whitelist: ['restEasy'], +}); + +const createPersistedStore = (persistCallback, initialStore = {}) => { + const offlineCustomConfig = { + ...offlineConfig, + persistCallback, + persistOptions: { + whitelist: ['restEasy'], + transforms: [restEasyTransform], + }, + }; + + return createStore( + reducers, + initialStore, + compose(applyMiddleware(thunkMiddleware), offline(offlineCustomConfig)), + ); +}; + +export default createPersistedStore; +``` diff --git a/docs/api/initializeNetworkHelpers.md b/docs/api/initializeNetworkHelpers.md index af3e61d..c9fbef7 100644 --- a/docs/api/initializeNetworkHelpers.md +++ b/docs/api/initializeNetworkHelpers.md @@ -118,5 +118,5 @@ initializeNetworkHelpers(networkHelpers); #### Tips * You should do this before any network request performs (usually at the entry point of your app) -* Only specifying `getToken` is often enough to make your app work. Use the other options if you needs custom headers or handlers! +* Only specifying `getToken` is often enough to make your app work. Use the other options if you need custom headers or handlers! * To provide helpers on a per-action basis, use [the actions configuration](./createResource/actionsConfig.md#properties) diff --git a/docs/api/reducer/reducers.md b/docs/api/reducer/reducers.md index 239d674..afb7215 100644 --- a/docs/api/reducer/reducers.md +++ b/docs/api/reducer/reducers.md @@ -77,7 +77,7 @@ const state = { ### RECEIVE_FROM_CACHE -When requesting a couple of resource/id which is already in state and cache is still fresh, the request will be store din a success state with the property `fromCache` to **true**. +When requesting a couple of resource/id which is already in state and cache is still fresh, the request will be stored in a success state with the property `fromCache` to **true**. ```js const state = { diff --git a/docs/principles/actions.md b/docs/principles/actions.md index 4a05351..33d7bdc 100644 --- a/docs/principles/actions.md +++ b/docs/principles/actions.md @@ -1,6 +1,6 @@ # Action -This processus defines the set of steps performed by `redux-reast-easy` actions, from the moment the request is performed to the moment we receive the payload. +This process defines the set of steps performed by `redux-rest-easy` actions, from the moment the request is performed to the moment we receive the payload. See [createResource documentation](../api/createResource.md#arguments) for the list of action parameters. @@ -10,7 +10,7 @@ Dispatching the **REQUEST** action will trigger the corresponding reducer, and c ### Execute the beforeHook -Specifying an _optional_ function `action.beforeHook` when defining an action will allow to execute this function everytime the preflight checks are passed for this action. If it returns a non falsy value, the return value will be used as the body for the principal request. Useful to perform series of calls. +Specifying an _optional_ function `action.beforeHook` when defining an action will allow executing this function everytime the preflight checks are passed for this action. If it returns a non-falsy value, the return value will be used as the body for the principal request. Useful to perform series of calls. ### Perform the request @@ -32,7 +32,7 @@ Dispatching the **FAIL** action will trigger the corresponding reducer and updat ### Execute the afterHook -Specifying an _optional_ function `action.afterHook` when defining an action will allow to execute this function every time a request has successfuly performed. +Specifying an _optional_ function `action.afterHook` when defining an action will allow executing this function every time a request has successfully performed. ### Execute the onSuccess/onFailure diff --git a/docs/principles/persistence.md b/docs/principles/persistence.md new file mode 100644 index 0000000..da59cc0 --- /dev/null +++ b/docs/principles/persistence.md @@ -0,0 +1,10 @@ +# Persistence + +Redux-rest-easy works out-of-the-box with libraries such as [redux-offline](https://github.com/redux-offline/redux-offline) and [redux-persist](https://github.com/rt2zz/redux-persist). However, the library will need to clean the state before persisting it, so that it never ends up in an inconsistent shape. + +Here is a list of actions performed when you call [getPersistableState](../api/getPersistableState.md): + +* in-progress, expired and invalidated requests are deleted +* requests with `expireAt` set to `never` (`cacheLifetime` set to `Infinity`) are invalidated +* resources which are no longer referenced by any request are deleted +* resolversHashes which refer to no longer existing requests or modified resources are deleted diff --git a/docs/principles/preflight.md b/docs/principles/preflight.md index ee52f06..8390cbc 100644 --- a/docs/principles/preflight.md +++ b/docs/principles/preflight.md @@ -4,7 +4,7 @@ Before performing a request, the library will check whether it should, based on ### Should perform -Here are the conditions that would immeditately cancel a request: +Here are the conditions that would immediately cancel a request: | Request method | Condition | Explanation | | -------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | diff --git a/docs/principles/reducers.md b/docs/principles/reducers.md index c718242..18e64eb 100644 --- a/docs/principles/reducers.md +++ b/docs/principles/reducers.md @@ -4,4 +4,4 @@ Unlike traditional Redux apps, which have multiple reducers per resource, `redux The state is split into two keys: `requests` and `resources`. `requests` holds the metadata related to requests, and `resources` holds the data. -For extensive documentation on the effect or `redux-rest-easy` reducers on your state, see the [reducers documentation](../api/reducer/reducers.md). +For extensive documentation on the effect of `redux-rest-easy` reducers on your state, see the [reducers documentation](../api/reducer/reducers.md). diff --git a/docs/principles/selectors.md b/docs/principles/selectors.md index f7574a7..44415cd 100644 --- a/docs/principles/selectors.md +++ b/docs/principles/selectors.md @@ -1,6 +1,6 @@ # Selectors -Selectors are functions which allow to access any relevant information stored in the state. They are helpful, especially because `redux-rest-easy` state is generated and managed by the library itself. +Selectors are functions which allow accessing any relevant information stored in the state. They are helpful, especially because `redux-rest-easy` state is generated and managed by the library itself. There are 3 types of selectors : diff --git a/src/getPersistableState.js b/src/getPersistableState.js new file mode 100644 index 0000000..130cecd --- /dev/null +++ b/src/getPersistableState.js @@ -0,0 +1,5 @@ +import getPrunedForPersistenceState from './internals/persistence/getPrunedForPersistenceState'; + +const getPersistableState = state => getPrunedForPersistenceState(state); + +export default getPersistableState; diff --git a/src/index.js b/src/index.js index 65d8631..6c62a9f 100644 --- a/src/index.js +++ b/src/index.js @@ -7,3 +7,4 @@ export { default as reset } from './reset'; export { default as initializeNetworkHelpers, } from './initializeNetworkHelpers'; +export { default as getPersistableState } from './getPersistableState'; diff --git a/src/internals/action-creator/generateThunk.js b/src/internals/action-creator/generateThunk.js index 2e963e1..a6019c0 100644 --- a/src/internals/action-creator/generateThunk.js +++ b/src/internals/action-creator/generateThunk.js @@ -28,6 +28,7 @@ const generateThunk = ( const actionCreatorActions = generateActionCreatorActions( resourceName, actionName, + cacheLifetime, ); const actionCreator = generateActionCreator( actionName, @@ -54,16 +55,8 @@ const generateThunk = ( let action; if (shouldPerform(state, normalizedURL)) { - if (isCacheExpired(state, method, normalizedURL, cacheLifetime)) { - if ( - isSmartCacheAvailable( - state, - method, - resourceName, - resourceId, - cacheLifetime, - ) - ) { + if (isCacheExpired(state, method, normalizedURL)) { + if (isSmartCacheAvailable(state, method, resourceName, resourceId)) { action = () => dispatch( actionCreatorActions.RECEIVE_FROM_CACHE( diff --git a/src/internals/action-creator/preflight/isCacheExpired.js b/src/internals/action-creator/preflight/isCacheExpired.js index 9772519..9f6a6e9 100644 --- a/src/internals/action-creator/preflight/isCacheExpired.js +++ b/src/internals/action-creator/preflight/isCacheExpired.js @@ -1,6 +1,6 @@ import hasCacheExpired from '../../utils/hasCacheExpired'; -const isCacheExpired = (state, method, normalizedURL, cacheLifetime) => { +const isCacheExpired = (state, method, normalizedURL) => { if ( method !== 'GET' || !state @@ -11,13 +11,11 @@ const isCacheExpired = (state, method, normalizedURL, cacheLifetime) => { return true; } - const { hasSucceeded, didInvalidate, endedAt } = state.requests[ + const { hasSucceeded, didInvalidate, expireAt } = state.requests[ normalizedURL ]; - return ( - !hasSucceeded || didInvalidate || hasCacheExpired(endedAt, cacheLifetime) - ); + return !hasSucceeded || didInvalidate || hasCacheExpired(expireAt); }; export default isCacheExpired; diff --git a/src/internals/action-creator/preflight/isSmartCacheAvailable.js b/src/internals/action-creator/preflight/isSmartCacheAvailable.js index 446f69b..b24f2d6 100644 --- a/src/internals/action-creator/preflight/isSmartCacheAvailable.js +++ b/src/internals/action-creator/preflight/isSmartCacheAvailable.js @@ -1,12 +1,6 @@ import hasCacheExpired from '../../utils/hasCacheExpired'; -const isSmartCacheAvailable = ( - state, - method, - resourceName, - resourceId, - cacheLifetime, -) => { +const isSmartCacheAvailable = (state, method, resourceName, resourceId) => { if ( method !== 'GET' || !state @@ -21,7 +15,7 @@ const isSmartCacheAvailable = ( ({ hasSucceeded, didInvalidate, - endedAt, + expireAt, resourceName: name, resourceId: id, payloadIds, @@ -32,7 +26,7 @@ const isSmartCacheAvailable = ( || (payloadIds && payloadIds[resourceName] && payloadIds[resourceName].includes(resourceId))) - && !hasCacheExpired(endedAt, cacheLifetime), + && !hasCacheExpired(expireAt), ); }; diff --git a/src/internals/actions/generateActionCreatorActions.js b/src/internals/actions/generateActionCreatorActions.js index 97df365..cbc0066 100644 --- a/src/internals/actions/generateActionCreatorActions.js +++ b/src/internals/actions/generateActionCreatorActions.js @@ -1,7 +1,11 @@ import generateActionCreatorAction from './generateActionCreatorActions/generateActionCreatorAction'; import generateActionCreatorActionTypes from './generateActionCreatorActions/generateActionCreatorActionTypes'; -const generateActionCreatorActions = (resourceName, actionName) => { +const generateActionCreatorActions = ( + resourceName, + actionName, + cacheLifetime, +) => { const { REQUEST, RECEIVE, @@ -10,10 +14,13 @@ const generateActionCreatorActions = (resourceName, actionName) => { } = generateActionCreatorActionTypes(resourceName, actionName); return { - REQUEST: generateActionCreatorAction(REQUEST), - RECEIVE: generateActionCreatorAction(RECEIVE), - FAIL: generateActionCreatorAction(FAIL), - RECEIVE_FROM_CACHE: generateActionCreatorAction(RECEIVE_FROM_CACHE), + REQUEST: generateActionCreatorAction(cacheLifetime, REQUEST), + RECEIVE: generateActionCreatorAction(cacheLifetime, RECEIVE), + FAIL: generateActionCreatorAction(cacheLifetime, FAIL), + RECEIVE_FROM_CACHE: generateActionCreatorAction( + cacheLifetime, + RECEIVE_FROM_CACHE, + ), }; }; diff --git a/src/internals/actions/generateActionCreatorActions/generateActionCreatorAction.js b/src/internals/actions/generateActionCreatorActions/generateActionCreatorAction.js index 9c9307e..663b209 100644 --- a/src/internals/actions/generateActionCreatorActions/generateActionCreatorAction.js +++ b/src/internals/actions/generateActionCreatorActions/generateActionCreatorAction.js @@ -1,4 +1,4 @@ -const generateActionCreatorAction = actionType => ( +const generateActionCreatorAction = (cacheLifetime, actionType) => ( normalizedURL, resourceId, normalizedPayload, @@ -15,6 +15,7 @@ const generateActionCreatorAction = actionType => ( typeof principalResourceIds === 'string' ? [principalResourceIds] : principalResourceIds, + cacheLifetime, }); export default generateActionCreatorAction; diff --git a/src/internals/persistence/getPrunedForPersistenceState.js b/src/internals/persistence/getPrunedForPersistenceState.js new file mode 100644 index 0000000..6c1cf0c --- /dev/null +++ b/src/internals/persistence/getPrunedForPersistenceState.js @@ -0,0 +1,110 @@ +import hasCacheExpired from '../utils/hasCacheExpired'; + +const getPrunedForPersistenceState = (state) => { + if (!state || !Object.keys(state).length) { + return {}; + } + + const newRequests = Object.entries(state.requests || {}).reduce( + (allRequests, [key, request]) => ({ + ...allRequests, + ...(request.endedAt + && !request.didInvalidate + && !hasCacheExpired(request.expireAt) + ? { + [key]: + request.expireAt === 'never' + ? { ...request, didInvalidate: true } + : request, + } + : {}), + }), + {}, + ); + + const referencedResources = Object.values(newRequests || {}).reduce( + (allResources, request) => ({ + ...allResources, + ...Object.entries(request.payloadIds || {}).reduce( + (requestResources, [resourceName, resourceIds]) => ({ + ...requestResources, + [resourceName]: [ + ...(requestResources[resourceName] || []), + ...resourceIds.map(id => id.toString()), + ], + }), + {}, + ), + }), + {}, + ); + + const newResources = Object.entries(state.resources || {}).reduce( + (allResources, [resourceName, resourceMap]) => ({ + ...allResources, + [resourceName]: Object.entries(resourceMap || {}).reduce( + (allResource, [resourceId, resourceItem]) => ({ + ...allResource, + ...(referencedResources[resourceName] + && referencedResources[resourceName].includes(resourceId) + ? { [resourceId]: resourceItem } + : {}), + }), + {}, + ), + }), + {}, + ); + + const newResourcesCleaned = Object.entries(newResources || {}).reduce( + (allResources, [resourceName, resourceMap]) => ({ + ...allResources, + ...(Object.keys(resourceMap).length + ? { [resourceName]: resourceMap } + : {}), + }), + {}, + ); + + const newResolversHashes = { + ...(state.resolversHashes || {}), + requests: Object.entries( + state.resolversHashes && state.resolversHashes.requests + ? state.resolversHashes.requests + : {}, + ).reduce( + (requestsHashes, [key, hashesByResourceName]) => ({ + ...requestsHashes, + ...(newRequests[key] ? { [key]: hashesByResourceName } : {}), + }), + {}, + ), + resources: Object.entries( + state.resolversHashes && state.resolversHashes.resources + ? state.resolversHashes.resources + : {}, + ).reduce( + (resourcesHashes, [key, hash]) => ({ + ...resourcesHashes, + ...(newResourcesCleaned[key] + && Object.keys( + state.resources && state.resources[key] ? state.resources[key] : {}, + ).length === Object.keys(newResourcesCleaned[key] || {}).length + ? { [key]: hash } + : {}), + }), + {}, + ), + }; + + const newState = { + ...state, + requests: newRequests, + resources: newResourcesCleaned, + resolversHashes: newResolversHashes, + }; + + return newState; +}; + +export default getPrunedForPersistenceState; diff --git a/src/internals/reducer/generateReducer/getReducerCases.js b/src/internals/reducer/generateReducer/getReducerCases.js index af33520..52cd4df 100644 --- a/src/internals/reducer/generateReducer/getReducerCases.js +++ b/src/internals/reducer/generateReducer/getReducerCases.js @@ -19,6 +19,7 @@ const REDUCER_CASES = { resourceId, startedAt: new Date().toISOString(), endedAt: null, + expireAt: null, hasSucceeded: state.requests && state.requests[normalizedURL] ? state.requests[normalizedURL].hasSucceeded @@ -45,6 +46,7 @@ const REDUCER_CASES = { url: normalizedURL, payload: normalizedPayload, principalResourceIds, + cacheLifetime = 0, }, ) => { const { resourceName } = getInfosFromActionType(type); @@ -57,6 +59,12 @@ const REDUCER_CASES = { ? state.requests[normalizedURL] : {}), endedAt: new Date().toISOString(), + expireAt: + cacheLifetime !== Infinity + ? new Date( + new Date().getTime() + cacheLifetime * 1000, + ).toISOString() + : 'never', hasSucceeded: true, hasFailed: false, didInvalidate: false, @@ -106,6 +114,7 @@ const REDUCER_CASES = { resourceId, payload: normalizedPayload, principalResourceIds, + cacheLifetime = 0, }, ) => { const { resourceName } = getInfosFromActionType(type); @@ -121,6 +130,12 @@ const REDUCER_CASES = { resourceId, startedAt: new Date().toISOString(), endedAt: new Date().toISOString(), + expireAt: + cacheLifetime !== Infinity + ? new Date( + new Date().getTime() + cacheLifetime * 1000, + ).toISOString() + : 'never', hasSucceeded: true, hasFailed: false, didInvalidate: false, diff --git a/src/internals/utils/hasCacheExpired.js b/src/internals/utils/hasCacheExpired.js index fa7cbf8..6d89cb6 100644 --- a/src/internals/utils/hasCacheExpired.js +++ b/src/internals/utils/hasCacheExpired.js @@ -1,4 +1,4 @@ -const hasCacheExpired = (endedAt, cacheLifetime) => - new Date() - new Date(endedAt) > cacheLifetime * 1000; +const hasCacheExpired = expireAt => + expireAt !== 'never' && (!expireAt || new Date() > new Date(expireAt)); export default hasCacheExpired;