Skip to content

Commit

Permalink
feat(persistence): additional options
Browse files Browse the repository at this point in the history
  • Loading branch information
adrienharnay authored and Titozzz committed Mar 21, 2018
1 parent 57c243f commit d7243c9
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 47 deletions.
132 changes: 105 additions & 27 deletions __tests__/internals/persistence/getPrunedForPersistenceState.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ 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 URL_PENDING = 'https://api.co/fruits?page1';
const URL_INVALIDATED = 'https://api.co/fruits?page2';
const URL_SOON_TO_EXPIRE = 'https://api.co/fruits?page3';
const URL_NEVER_EXPIRE = 'https://api.co/fruits?page4';
const RESOURCE_NAME = 'fruits';
const RESOURCE_NAME_2 = 'vegetables';
const RESOURCE_NAME_3 = 'sauces';
Expand All @@ -19,7 +19,7 @@ const EXPIRE_AT_NEVER = 'never';

const STATE_REQUESTS = {
requests: {
[URL_1]: {
[URL_PENDING]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand All @@ -30,7 +30,7 @@ const STATE_REQUESTS = {
didInvalidate: false,
fromCache: false,
},
[URL_2]: {
[URL_INVALIDATED]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand All @@ -41,7 +41,7 @@ const STATE_REQUESTS = {
didInvalidate: true,
fromCache: false,
},
[URL_3]: {
[URL_SOON_TO_EXPIRE]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand All @@ -52,7 +52,7 @@ const STATE_REQUESTS = {
didInvalidate: false,
fromCache: false,
},
[URL_4]: {
[URL_NEVER_EXPIRE]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand All @@ -68,7 +68,7 @@ const STATE_REQUESTS = {

const STATE_RESOURCES = {
requests: {
[URL_1]: {
[URL_PENDING]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand All @@ -82,7 +82,21 @@ const STATE_RESOURCES = {
[RESOURCE_NAME]: [1, 2, 3],
},
},
[URL_4]: {
[URL_INVALIDATED]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
endedAt: MOMENT_NOW,
expireAt: EXPIRE_AT_NEVER,
hasSucceeded: true,
hasFailed: false,
didInvalidate: true,
fromCache: false,
payloadIds: {
[RESOURCE_NAME]: [1, 2, 3],
},
},
[URL_NEVER_EXPIRE]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand Down Expand Up @@ -114,7 +128,7 @@ const STATE_RESOURCES = {

const STATE_RESOURCES_OTHER_ORDER = {
requests: {
[URL_4]: {
[URL_NEVER_EXPIRE]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand All @@ -128,7 +142,7 @@ const STATE_RESOURCES_OTHER_ORDER = {
[RESOURCE_NAME]: [3, 4, 5],
},
},
[URL_1]: {
[URL_PENDING]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand All @@ -142,6 +156,20 @@ const STATE_RESOURCES_OTHER_ORDER = {
[RESOURCE_NAME]: [1, 2, 3],
},
},
[URL_INVALIDATED]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
endedAt: MOMENT_NOW,
expireAt: EXPIRE_AT_NEVER,
hasSucceeded: true,
hasFailed: false,
didInvalidate: true,
fromCache: false,
payloadIds: {
[RESOURCE_NAME]: [1, 2, 3],
},
},
},
resources: {
[RESOURCE_NAME_2]: {
Expand All @@ -160,7 +188,7 @@ const STATE_RESOURCES_OTHER_ORDER = {

const STATE_RESOLVERS_HASHES = {
requests: {
[URL_1]: {
[URL_PENDING]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand All @@ -174,7 +202,7 @@ const STATE_RESOLVERS_HASHES = {
[RESOURCE_NAME]: [1, 2, 3],
},
},
[URL_4]: {
[URL_NEVER_EXPIRE]: {
resourceName: RESOURCE_NAME,
resourceId: null,
startedAt: MOMENT_NOW,
Expand Down Expand Up @@ -209,11 +237,11 @@ const STATE_RESOLVERS_HASHES = {
},
resolversHashes: {
requests: {
[URL_1]: {
[RESOURCE_NAME]: 'URL_1_HASH',
[URL_PENDING]: {
[RESOURCE_NAME]: 'URL_PENDING_HASH',
},
[URL_4]: {
[RESOURCE_NAME]: 'URL_4_HASH',
[URL_NEVER_EXPIRE]: {
[RESOURCE_NAME]: 'URL_NEVER_EXPIRE_HASH',
},
},
resources: {
Expand All @@ -240,46 +268,96 @@ describe('getPrunedForPersistenceState', () => {

test('requests: no endedAt', () => {
expect(
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_1],
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_PENDING],
).toBeUndefined();
});

test('requests: didInvalidate', () => {
expect(
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_2],
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_INVALIDATED],
).toBeUndefined();
});

test('requests: cache expired', () => {
mockdate.set(MOMENT_NOW);
expect(
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_3],
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_SOON_TO_EXPIRE],
).not.toBeUndefined();

mockdate.set(moment(MOMENT_NOW).add(999, 'milliseconds'));
expect(
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_3],
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_SOON_TO_EXPIRE],
).not.toBeUndefined();

mockdate.set(moment(MOMENT_NOW).add(1000, 'milliseconds'));
expect(
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_3],
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_SOON_TO_EXPIRE],
).not.toBeUndefined();

mockdate.set(moment(MOMENT_NOW).add(1001, 'milliseconds'));
expect(
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_3],
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_SOON_TO_EXPIRE],
).toBeUndefined();
});

test('requests: expiredAt never', () => {
mockdate.set(MOMENT_NOW);
expect(
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_4]
getPrunedForPersistenceState(STATE_REQUESTS).requests[URL_NEVER_EXPIRE]
.didInvalidate,
).toBe(true);
});

test('requests: always persist string', () => {
expect(
getPrunedForPersistenceState(STATE_RESOURCES, {
alwaysPersist: RESOURCE_NAME,
}).requests[URL_INVALIDATED],
).not.toBeUndefined();
});

test('requests: always persist array', () => {
expect(
getPrunedForPersistenceState(STATE_RESOURCES, {
alwaysPersist: [RESOURCE_NAME],
}).requests[URL_INVALIDATED],
).not.toBeUndefined();
});

test('requests: always persist no endedAt string', () => {
expect(
getPrunedForPersistenceState(STATE_RESOURCES, {
alwaysPersist: RESOURCE_NAME,
}).requests[URL_PENDING],
).toBeUndefined();
});

test('requests: always persist no endedAt array', () => {
expect(
getPrunedForPersistenceState(STATE_RESOURCES, {
alwaysPersist: [RESOURCE_NAME],
}).requests[URL_PENDING],
).toBeUndefined();
});

test('requests: never persist string', () => {
mockdate.set(MOMENT_NOW);
expect(
getPrunedForPersistenceState(STATE_RESOURCES, {
neverPersist: RESOURCE_NAME,
}).requests[URL_NEVER_EXPIRE],
).toBeUndefined();
});

test('requests: never persist array', () => {
mockdate.set(MOMENT_NOW);
expect(
getPrunedForPersistenceState(STATE_RESOURCES, {
neverPersist: [RESOURCE_NAME],
}).requests[URL_NEVER_EXPIRE],
).toBeUndefined();
});

test('resources first order', () => {
const prunedState = getPrunedForPersistenceState(STATE_RESOURCES);
const prunedStateResourcesIds = Object.keys(
Expand Down Expand Up @@ -323,9 +401,9 @@ describe('getPrunedForPersistenceState', () => {
STATE_RESOLVERS_HASHES,
).resolversHashes;

expect(prunedStateResolversHashes.requests[URL_1]).toBeUndefined();
expect(prunedStateResolversHashes.requests[URL_PENDING]).toBeUndefined();
expect(
prunedStateResolversHashes.requests[URL_4][RESOURCE_NAME],
prunedStateResolversHashes.requests[URL_NEVER_EXPIRE][RESOURCE_NAME],
).not.toBeUndefined();
expect(prunedStateResolversHashes.resources[RESOURCE_NAME]).toBeUndefined();
expect(
Expand Down
26 changes: 21 additions & 5 deletions docs/api/getPersistableState.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# `getPersistableState(state)`
# `getPersistableState(state, options)`

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
1. (_state_): (`object`) The current Redux state
2. (_persistOptions_): (`object`) An object containing additional, optional options for the pruning:

A. (_alwaysPersist_): (`string || array<string>`) String or array of strings containing resource name(s). Request performing on the resource(s) will **always** be persisted, expired or not
B. (_neverPersist_): (`string || array<string>`) String or array of strings containing resource name(s). Request performing on the resource(s) will **never** be persisted, expired or not

#### Returns

Expand All @@ -23,9 +27,16 @@ 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 restEasyTransform = createTransform(
state =>
getPersistableState(state, {
persist: { alwaysPersist: ['tokens'], neverPersist: ['configuration'] },
}),
null,
{
whitelist: ['restEasy'],
},
);

const createPersistedStore = (persistCallback, initialStore = {}) => {
const offlineCustomConfig = {
Expand All @@ -46,3 +57,8 @@ const createPersistedStore = (persistCallback, initialStore = {}) => {

export default createPersistedStore;
```

#### Tips

* `persistOptions.alwaysPersist` is a great way to persist resources you would never want to expire
* `persistOptions.neverPersist` is a great way to clear some resources each time your app starts
11 changes: 7 additions & 4 deletions docs/principles/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Redux-rest-easy works out-of-the-box with libraries such as [redux-offline](http

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
* In-progress requests are deleted. _This is to avoid pending requests never resolving._
* Expired and invalidated requests are deleted. _This is to avoid keeping outdated requests possibly forever._
* Requests with `expireAt` set to `never` (`cacheLifetime` set to `Infinity`) are invalidated. _This is to avoid trusting remote data forever. Can still be done via persistOptions.alwaysPersist_
* Resources which are no longer referenced by any request are deleted. _This is to avoidkeeping outdated resources in the state._
* resolversHashes which refer to no longer existing requests or modified resources are deleted. _This is to avoidkeeping outdated hashes in the state._

The first action is mandatory to avoid inconsistent state, but the others won't be applied to requests performing on resources defined in `persistOptions.alwaysPersist` and `persistOptions.neverPersist`. Instead, such requests will always/never be persisted.
3 changes: 2 additions & 1 deletion src/getPersistableState.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import getPrunedForPersistenceState from './internals/persistence/getPrunedForPersistenceState';

const getPersistableState = state => getPrunedForPersistenceState(state);
const getPersistableState = (state, persistOptions) =>
getPrunedForPersistenceState(state, persistOptions);

export default getPersistableState;
44 changes: 34 additions & 10 deletions src/internals/persistence/getPrunedForPersistenceState.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
/* eslint-disable no-nested-ternary */

import hasCacheExpired from '../utils/hasCacheExpired';

const getPrunedForPersistenceState = (state) => {
const getPrunedForPersistenceState = (
state,
{ alwaysPersist, neverPersist } = {},
) => {
if (!state || !Object.keys(state).length) {
return {};
}

const alwaysPersistResources
= typeof alwaysPersist === 'string' ? [alwaysPersist] : alwaysPersist;
const neverPersistResources
= typeof neverPersist === 'string' ? [neverPersist] : neverPersist;

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,
}
: {}),
&& alwaysPersistResources
&& request.payloadIds
&& Object.keys(request.payloadIds).some(resourceName =>
alwaysPersistResources.includes(resourceName),
)
? { [key]: request }
: !(
neverPersistResources
&& request.payloadIds
&& Object.keys(request.payloadIds).some(resourceName =>
neverPersistResources.includes(resourceName),
)
)
&& request.endedAt
&& !request.didInvalidate
&& !hasCacheExpired(request.expireAt)
? {
[key]:
request.expireAt === 'never'
? { ...request, didInvalidate: true }
: request,
}
: {}),
}),
{},
);
Expand Down

0 comments on commit d7243c9

Please sign in to comment.