Skip to content
This repository has been archived by the owner on Jun 19, 2023. It is now read-only.

Commit

Permalink
Merge pull request #343 from meetup/WP-456_api-state-module
Browse files Browse the repository at this point in the history
WP 456 'API state' module
  • Loading branch information
mmcgahan committed Aug 14, 2017
2 parents 5aadcbb + 7d60ead commit 2871a7f
Show file tree
Hide file tree
Showing 32 changed files with 499 additions and 425 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@

- **Moved** `src/components/Redirect` --> `require('src/router').Redirect`
- **Moved** `src/components/Forbidden` --> `require('src/router').Forbidden`
- **Moved** `src/actions/apiActionCreators`: The action `type` constants and
action creators are exported from `require('src/api-state')`:

- `API_REQ`
- `API_RESP_SUCCESS`
- `API_RESP_COMPLETE`
- `API_RESP_ERROR`
- `API_RESP_FAIL`
- `requestAll`
- `get`
- `post`
- `patch`
- `del`

- **Moved** `src/actions/syncActionCreators`: The action `type` constants and
action creators are exported from `require('src/router')`

- `SERVER_RENDER`
- `LOCATION_CHANGE`
- `locationChange`

- **Moved + renamed** `src/middleware/platform:getEpicMiddleware` has moved to
`require('src/api-state').getApiMiddleware`

## [5.1]

Expand Down
55 changes: 3 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ In general, application-specific code will live outside of this package.
- [Auth flow from `requestAuthPlugin`](docs/auth.md)
- [Analytics/tracking](docs/Tracking.md)
- [Application state management](docs/State.md)
- ['Query': structuring data requests](docs/Queries.md) - GET/POST/PATCH/DELETE requests to REST API
- [Rendering in consumer applications](docs/Rendering.md)
- [Caching - API and static assets](docs/Caching.md)

Expand All @@ -28,6 +27,9 @@ In general, application-specific code will live outside of this package.
- [Language plugin for Hapi](src/plugins/language/README.md)
- [API proxy plugin for Hapi](src/plugins/api-proxy/README.md)
- [Click and Activity tracking](src/plugins/tracking/README.md)
- [API State module](src/api-state/README.md)
- ['Query': structuring data requests](src/api-state/Queries.md) -
GET/POST/PATCH/DELETE requests to REST API

# Releases

Expand Down Expand Up @@ -104,57 +106,6 @@ The [server module](./src/server.js) exports a `startServer` function that consu
a mapping of locale codes to app-rendering Observables, plus any app-specific
server routes and plugins. See the code comments for usage details.

## Middleware/Epics

The built-in middleware provides core functionality for interacting with
API data - managing authenticated user sessions, syncing with the current
URL location, caching data, and POSTing data to the API.

Additional middleware can be passed to the `makeRenderer` function for
each specific application's client and server entry points.

### Epic middleware

Based on `redux-observable`, this middleware provides the following
functionality through "Epics":

#### Sync `epics/sync.js`

This epic is currently only responsible for fetching the data from the API
server on initial render or client-side
user navigation.

**on `server/RENDER` or `LOCATION_CHANGE`**, which provide a `location` (URL):

1. Match `location` to defined `routes` and extract the `renderProps` like URL
path and querystring params
2. Check `routes` for `query` functions that return data needs, and process
them into an array
3. Trigger `API_REQ` containing the `queries`

**on `API_REQ`**, which provides `queries`:
1. Send the queries to the application server, which will make the
corresponding external API calls.
2. When the application server returns data, trigger `API_SUCCESS` action
containing API response array and query array
3. If the application server responds with an error, trigger `API_ERROR`

#### Cache `epics/cache.js`

See [the Caching docs](./docs/Caching.md#cache-middleware)

##### Disable cache

By design, the cache masks slow responses from the API and can create a 'flash'
of stale content before the API responds with the latest data. In development,
this behavior is not always desirable so you can disable the cache by adding
a `__nocache` param to the query string. The cache will remain disabled until the
the page is refreshed/reloaded without the param in the querystring.

```
http://localhost:8000/ny-tech/?__nocache
```

## Client

### Rendering 'empty' state with `<NotFound>`
Expand Down
9 changes: 9 additions & 0 deletions flow-typed/api-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare type QueryState = {
query: Query,
response: ?QueryResponse,
};
declare type ApiState = {
[string]: QueryResponse,
inFlight: Array<string>,
fail?: boolean,
};
31 changes: 0 additions & 31 deletions src/actions/cacheActionCreators.js

This file was deleted.

File renamed without changes.
66 changes: 66 additions & 0 deletions src/api-state/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# API state

Redux state management for Meetup REST API data

## Middleware/Epics

The API middleware provides core functionality for interacting with
API data - managing authenticated user sessions, syncing with the current
URL location, caching data, and POSTing data to the API.

### Sync `/sync.js`

This epic is currently only responsible for fetching the data from the API
server on initial render or client-side
user navigation.

**on `SERVER_RENDER` or `LOCATION_CHANGE`**, which provide a `location` (URL):

1. Match `location` to defined `routes` and extract the `renderProps` like URL
path and querystring params
2. Check `routes` for `query` functions that return data needs, and process
them into an array
3. Trigger `API_REQ` containing the `queries`

**on `API_REQ`**, which provides `queries`:
1. Send the queries to the application server, which will make the
corresponding external API calls.
2. When the application server returns data, trigger `API_SUCCESS` action
containing API response array and query array
3. If the application server responds with an error, trigger `API_ERROR`

### Cache `/cache.js`

See [the Caching docs](./docs/Caching.md#cache-middleware)

#### Disable cache

By design, the cache masks slow responses from the API and can create a 'flash'
of stale content before the API responds with the latest data. In development,
this behavior is not always desirable so you can disable the cache by adding
a `__nocache` param to the query string. The cache will remain disabled until the
the page is refreshed/reloaded without the param in the querystring.

```
http://localhost:8000/ny-tech/?__nocache
```

## Reducer

The `api` export of the `api-state` module is a reducer that will return data
from the API using [Queries](Queries.md).

## Action creators

To send/receive data to/from the REST API, use `requestAll`, `get`, `post`,
`patch`, and `del` action creators from `api-state`.

See the [Queries documentation](Queries.md) for more details on usage.

## Dependencies

- redux-observable
- rxjs
- mwp-tracking-plugin/util/clickState
- mwp-router
- mwp-router/util
26 changes: 26 additions & 0 deletions src/api-state/cache/cacheActionCreators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// @flow
export const CACHE_SET = 'CACHE_SET';
export const CACHE_REQUEST = 'CACHE_REQUEST';
export const CACHE_SUCCESS = 'CACHE_SUCCESS';
export const CACHE_CLEAR = 'CACHE_CLEAR';

type QueryStateAC = QueryState => FluxStandardAction;

export const cacheSet: QueryStateAC = ({ query, response }) => ({
type: CACHE_SET,
payload: { query, response },
});

export const cacheRequest = (queries: Array<Query>) => ({
type: CACHE_REQUEST,
payload: queries,
});

export const cacheSuccess: QueryStateAC = ({ query, response }) => ({
type: CACHE_SUCCESS,
payload: { query, response },
});

export const cacheClear = () => ({
type: CACHE_CLEAR,
});
10 changes: 3 additions & 7 deletions src/epics/cache.js → src/api-state/cache/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@ import 'rxjs/add/operator/reduce';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/mergeMap';
import { combineEpics } from 'redux-observable';
import { API_REQ, API_RESP_SUCCESS } from '../actions/apiActionCreators';
import {
CACHE_CLEAR,
CACHE_SET,
cacheSuccess,
} from '../actions/cacheActionCreators';
import { API_REQ, API_RESP_SUCCESS } from '../sync/apiActionCreators';
import { CACHE_CLEAR, CACHE_SET, cacheSuccess } from './cacheActionCreators';

import { makeCache, cacheReader, cacheWriter } from '../util/cacheUtils';
import { makeCache, cacheReader, cacheWriter } from './util';

export function checkEnable() {
if (typeof window !== 'undefined' && window.location) {
Expand Down
9 changes: 4 additions & 5 deletions src/epics/cache.test.js → src/api-state/cache/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import {
MOCK_API_RESULT,
} from 'meetup-web-mocks/lib/app';

import { epicIgnoreAction } from '../util/testUtils';
import { epicIgnoreAction } from '../../util/testUtils';
import * as api from '../sync/apiActionCreators';

import { makeCache } from '../util/cacheUtils';

import getCacheEpic from './cache';
import * as api from '../actions/apiActionCreators';
import { makeCache } from './util';
import getCacheEpic from './';

const MOCK_QUERY = mockQuery(MOCK_RENDERPROPS);
const MOCK_SUCCESS_ACTION = api.success({
Expand Down
20 changes: 16 additions & 4 deletions src/util/cacheUtils.js → src/api-state/cache/util.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @flow
/**
* This function performs feature sniffing to determine whether the preferred
* IndexedDB cache is available, otherwise it falls back to a simple
Expand All @@ -11,7 +12,13 @@
* @returns {Object} an object with Promise-based `get`, `set`, `delete`, and
* `clear` methods
*/
export function makeCache() {
type Cache = {
get: string => Promise<QueryResponse>,
set: (string, QueryResponse) => Promise<true>,
delete: string => Promise<true>,
clear: () => Promise<true>,
};
export function makeCache(): Cache {
if (typeof window === 'undefined' || !window.indexedDB) {
const _data = {};
return {
Expand Down Expand Up @@ -43,10 +50,12 @@ export function makeCache() {
* @param {Object} query query for app data
* @return {Promise} resolves with cache hit, otherwise rejects
*/
export const cacheReader = cache => query =>
export const cacheReader = (cache: Cache) => (
query: Query
): Promise<QueryState> =>
cache
.get(JSON.stringify(query))
.then(response => ({ query, response }))
.then((response: QueryResponse) => ({ query, response }))
.catch(err => ({ query, response: null })); // errors don't matter - just return null

/**
Expand All @@ -57,7 +66,10 @@ export const cacheReader = cache => query =>
* @param {Object} response plain object API response for the query
* @return {Promise}
*/
export const cacheWriter = cache => (query, response) => {
export const cacheWriter = (cache: Cache) => (
query: Query,
response: QueryResponse
) => {
const method = (query.meta || {}).method || 'get';
if (method.toLowerCase() !== 'get') {
return Promise.resolve(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { makeCache, cacheWriter, cacheReader } from '../util/cacheUtils';
import { makeCache, cacheWriter, cacheReader } from './util';

describe('cache utils', () => {
it('creates a Promise-based cache', function() {
Expand Down
25 changes: 19 additions & 6 deletions src/middleware/epic.js → src/api-state/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { combineEpics, createEpicMiddleware } from 'redux-observable';

import getSyncEpic from '../epics/sync';
import getCacheEpic from '../epics/cache';
import { postEpic, deleteEpic } from '../epics/mutate'; // DEPRECATED
import getSyncEpic from './sync';
import getCacheEpic from './cache';
import { postEpic, deleteEpic } from './mutate'; // DEPRECATED

// export specific values of internal modules
export {
API_REQ,
API_RESP_SUCCESS,
API_RESP_COMPLETE,
API_RESP_ERROR,
API_RESP_FAIL,
requestAll,
get,
post,
patch,
del,
} from './sync/apiActionCreators';
export { api, app } from './reducer';

/**
* The middleware is exported as a getter because it needs the application's
Expand All @@ -13,7 +28,7 @@ import { postEpic, deleteEpic } from '../epics/mutate'; // DEPRECATED
* order to render the application. We may want to write a server-specific
* middleware that doesn't include the other epics if performance is an issue
*/
const getPlatformMiddleware = (routes, fetchQueries, baseUrl) =>
export const getApiMiddleware = (routes, fetchQueries, baseUrl) =>
createEpicMiddleware(
combineEpics(
getSyncEpic(routes, fetchQueries, baseUrl),
Expand All @@ -22,5 +37,3 @@ const getPlatformMiddleware = (routes, fetchQueries, baseUrl) =>
deleteEpic // DEPRECATED
)
);

export default getPlatformMiddleware;
2 changes: 1 addition & 1 deletion src/epics/mutate.js → src/api-state/mutate/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as api from '../actions/apiActionCreators';
import * as api from '../sync/apiActionCreators';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/filter';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import 'rxjs/add/operator/toPromise';

import { MOCK_POST_ACTION, MOCK_DELETE_ACTION } from 'meetup-web-mocks/lib/app';

import { epicIgnoreAction } from '../util/testUtils';
import { epicIgnoreAction } from '../../util/testUtils';

import { postEpic, deleteEpic } from './mutate';
import * as api from '../actions/apiActionCreators';
import { postEpic, deleteEpic } from './';
import * as api from '../sync/apiActionCreators';

describe('postEpic', () => {
it('does not pass through arbitrary actions', epicIgnoreAction(postEpic));
Expand Down
Loading

0 comments on commit 2871a7f

Please sign in to comment.