Skip to content

Commit

Permalink
Merge pull request #10 from Bloomca/improvement/change-caching
Browse files Browse the repository at this point in the history
Improvement/change caching
  • Loading branch information
Bloomca authored Jun 9, 2017
2 parents aaba5c9 + 43c49a8 commit 8392667
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 10 deletions.
17 changes: 15 additions & 2 deletions docs/api/createTile.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,28 @@ const userTile = createTile({
// downloaded, then it won't be downloaded again at all, unless
// we will invoke with the second parameter `forceAsync: true`:
// dispatch(actions.hn_api.user({ id: 'someID' }, { forceAsync: true }));
//
// also, it means that there will be only one simulatenous request
// other dispatches will return exactly the same promise
caching: true,
});
```

## Caching

As it was already mentioned, you can set property `caching` to `true`, and it will make same requests to query only once, and after you will have to send an object with a key `forceAsync: true` as a second parameter to invoked function, to execute your function again.
Asynchronous tiles support caching out of the box, you just have to set property `caching` to `true`. It will make two things happen – it won't invoke the same function if the data is already presented there, and also it will prevent the same function be invoked again in case it is already being processing (but dispatched action will return exactly the same promise, so you can safely await for it, and then query the state - it will be an updated value). The latter case is interesting – it basically means that we get rid of race conditions, and we are safe to query same endpoints in a declarative way, without worrying of several requests to same endpoints.

If you have already dispatched an action with enabled caching, and you want to invoke this action again, then you would have to send an object with a key `forceAsync: true` as a second parameter to invoked function:
```js
dispatch(actions.api.users({ id: 'someID' }, { forceAsync: true }));
```

Though the same promise thing might seem as a magical part, it is not! In order to make it work for each requests in Node.js, we keep this object inside middleware (so it belongs to the instance of a store), and it means that in order to make it work we have to use [redux-tiles' middleware](./createMiddleware.md), or pass `promisesStorage` object to [redux-thunk](https://github.com/gaearon/redux-thunk):
```js
applyMiddleware(thunk.withExtraArgument({ promisesStorage: {} }))
```

But also there is another caching, which is enabled if you use [our middleware](./createMiddleware.md). It tracks all requests based on type of the module and nesting, and stores promises, and in case it was invoked again, it will simple wait existing promise, so you can await result without any additional requests. Because of that, you should be very careful on Node.js – please, instantiate store for each request, in case you want to dispatch some async requests.
Redux-tiles' middleware will inject this object automatically. This object is also crucial for server-side rendering, in case we want to prefetch data – this object collects all requests (regardless of caching; it will just filter same actions if caching is enabled) and `waitTiles` will await all of them.

## Function

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-tiles",
"version": "0.4.10",
"version": "0.5.0",
"description": "Library to create and easily compose redux pieces together in less verbose manner",
"jsnext:main": "lib/es2015/index.js",
"module": "lib/es2015/index.js",
Expand Down
18 changes: 11 additions & 7 deletions src/tiles/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ export function asyncAction({
const path: string[]|null = nesting ? nesting(params) : null;

const getIdentificator: string = createType({ type, path });
const activePromise: Promise<any>|undefined = promisesStorage[getIdentificator];

if (activePromise) {
return activePromise;
if (caching) {
const activePromise: Promise<any>|undefined = promisesStorage[getIdentificator];

if (activePromise) {
return activePromise;
}
}

if (caching && !forceAsync) {
Expand All @@ -70,10 +73,7 @@ export function asyncAction({
payload: { path }
});

const promise: Promise<any> = fn({ params, dispatch, getState, ...middlewares });
promisesStorage[getIdentificator] = promise;

return promise
const promise: Promise<any> = fn({ params, dispatch, getState, ...middlewares })
.then((data: any) => {
dispatch({
type: SUCCESS,
Expand All @@ -89,6 +89,10 @@ export function asyncAction({
});
promisesStorage[getIdentificator] = undefined;
});

promisesStorage[getIdentificator] = promise;

return promise;
});
}

Expand Down
15 changes: 15 additions & 0 deletions test/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ test('action should detect thunk middleware', () => {
expect(fn.calledWith({ dispatch, getState, params })).toBe(true);
});

test('action should detect thunk middleware with additional params', () => {
const fn = stub().returns(Promise.resolve());
const tile = createTile({
type: 'Some',
fn
});

const dispatch = () => {};
const getState = () => {};
const params = {};
const additionalParams = { some: true };
tile.action(params)(dispatch, getState, additionalParams);
expect(fn.calledWith({ dispatch, getState, params, some: true })).toBe(true);
});

test('action should detect our middleware', () => {
const fn = stub().returns(Promise.resolve());
const tile = createTile({
Expand Down
39 changes: 39 additions & 0 deletions test/tiles.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createTile, createSyncTile, createEntities, createMiddleware } from '../src';
import { createStore, applyMiddleware } from 'redux';
import { sleep } from 'delounce';
import { spy } from 'sinon';

test('createTile should be able to reflect all passed info', () => {
Expand Down Expand Up @@ -146,4 +147,42 @@ test('createTile should update values after dispatching action with rejection co
await store.dispatch(actions.some('some'));
const result = selectors.some(store.getState());
expect(result).toEqual({ isPending: false, data: null, error: { some: true } });
});

test('createTile should keep only one active request if caching', async () => {
const someTile = createTile({
type: 'some',
caching: true,
fn: async () => {
await sleep(10);

return { some: true };
}
});
const tiles = [someTile];
const { reducer, actions, selectors } = createEntities(tiles);
const { middleware } = createMiddleware();
const store = createStore(reducer, applyMiddleware(middleware));
const promise1 = store.dispatch(actions.some('some'));
const promise2 = store.dispatch(actions.some('some'));
expect(promise1).toBe(promise2);
});

test('createTile should keep different requests if caching', async () => {
const someTile = createTile({
type: 'some',
caching: true,
fn: async () => {
await sleep(10);

return { some: true };
}
});
const tiles = [someTile];
const { reducer, actions, selectors } = createEntities(tiles);
const { middleware } = createMiddleware();
const store = createStore(reducer, applyMiddleware(middleware));
const promise1 = store.dispatch(actions.some('some'));
const promise2 = store.dispatch(actions.some('some'));
expect(promise1).toBe(promise2);
});

0 comments on commit 8392667

Please sign in to comment.