Skip to content

Commit

Permalink
Framework: Get HTTP data (#22549)
Browse files Browse the repository at this point in the history
Prior work in #19060

TL;DR

p4TIVU-86D-p2

This is a read-only abstraction built upon the data layer to make it easy to add "shove it" kinds of data into Calypso from an API call without needing to write reducers, query components, data layer handlers, and other common snippets traditionally required for new data sources.

Do you have a reducer that does nothing more than store data opaquely on a single action type? Do you have a query component you don't think you need? This is for you
  • Loading branch information
dmsnell authored May 2, 2018
1 parent 10d1704 commit 6374d40
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 0 deletions.
2 changes: 2 additions & 0 deletions client/state/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ export const HELP_TICKET_CONFIGURATION_REQUEST_FAILURE =
'HELP_TICKET_CONFIGURATION_REQUEST_FAILURE';
export const HELP_TICKET_CONFIGURATION_REQUEST_SUCCESS =
'HELP_TICKET_CONFIGURATION_REQUEST_SUCCESS';
export const HTTP_DATA_REQUEST = 'HTTP_DATA_FETCH';
export const HTTP_DATA_TICK = 'HTTP_DATA_TICK';
export const HTTP_REQUEST = 'HTTP_REQUEST';
export const I18N_LANGUAGE_NAMES_REQUEST = 'I18N_LANGUAGE_NAMES_REQUEST';
export const I18N_LANGUAGE_NAMES_ADD = 'I18N_LANGUAGE_NAMES_ADD';
Expand Down
86 changes: 86 additions & 0 deletions client/state/data-layer/http-data/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/** @format */
/**
* Internal dependencies
*/
import { HTTP_DATA_REQUEST, HTTP_DATA_TICK } from 'state/action-types';

export const reducer = ( state = 0, { type } ) => ( HTTP_DATA_TICK === type ? state + 1 : state );

export const httpData = new Map();

export const update = ( id, state, data ) => {
const lastUpdated = Date.now();
const item = httpData.get( id );
const hasItem = item !== undefined;

// We could have left out the keys for
// the previous properties if they didn't
// exist but I wanted to make sure we can
// get our hidden classes to optimize here.
switch ( state ) {
case 'failure':
return httpData.set( id, {
state,
data: hasItem ? item.data : undefined,
error: data,
lastUpdated: hasItem ? item.lastUpdated : -Infinity,
pendingSince: undefined,
} );

case 'pending':
return httpData.set( id, {
state,
data: hasItem ? item.data : undefined,
error: undefined,
lastUpdated: hasItem ? item.lastUpdated : -Infinity,
pendingSince: lastUpdated,
} );

case 'success':
return httpData.set( id, {
state,
data,
error: undefined,
lastUpdated,
pendingSince: undefined,
} );
}
};

const empty = Object.freeze( {
state: 'uninitialized',
data: undefined,
error: undefined,
lastUpdated: -Infinity,
pendingSince: undefined,
} );

let dispatch;

export const enhancer = next => ( ...args ) => {
const store = next( ...args );

dispatch = store.dispatch;

return store;
};

export const getHttpData = id => httpData.get( id ) || empty;

export const requestHttpData = ( id, fetchAction, { fromApi, freshness } ) => {
const data = getHttpData( id );
const { state, lastUpdated } = data;

if (
'uninitialized' === state ||
( 'number' === typeof freshness && 'pending' !== state && Date.now() - lastUpdated > freshness )
) {
if ( 'development' === process.env.NODE_ENV && 'function' !== typeof dispatch ) {
throw new Error( 'Cannot use HTTP data without injecting Redux store enhancer!' );
}

dispatch( { type: HTTP_DATA_REQUEST, id, fetch: fetchAction, fromApi } );
}

return data;
};
79 changes: 79 additions & 0 deletions client/state/data-layer/http-data/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/** @format */
/**
* Internal dependencies
*/
import { HTTP_DATA_REQUEST, HTTP_DATA_TICK } from 'state/action-types';
import { dispatchRequestEx } from 'state/data-layer/wpcom-http/utils';
import { update } from './common';

const fetch = action => {
update( action.id, 'pending' );

return [
{
...action.fetch,
onSuccess: action,
onFailure: action,
onProgress: action,
},
{ type: HTTP_DATA_TICK },
];
};

const onError = ( action, error ) => {
update( action.id, 'failure', error );

return { type: HTTP_DATA_TICK };
};

/**
* Transforms API response data into storable data
* Returns pairs of data ids and data plus an error indicator
*
* [ error?, [ [ id, data ], [ id, data ], … ] ]
*
* @example:
* --input--
* { data: { sites: {
* 14: { is_active: true, name: 'foo' },
* 19: { is_active: false, name: 'bar' }
* } } }
*
* --output--
* [ [ 'site-names-14', 'foo' ] ]
*
* @param {*} data input data from API response
* @param {function} fromApi transforms API response data
* @return {Array<boolean, Array<Array<string, *>>>} output data to store
*/
const parseResponse = ( data, fromApi ) => {
try {
return [ undefined, fromApi( data ) ];
} catch ( error ) {
return [ error, undefined ];
}
};

const onSuccess = ( action, apiData ) => {
const [ error, data ] =
'function' === typeof apiData ? parseResponse( apiData, action.fromApi ) : [ undefined, [] ];

if ( undefined !== error ) {
return onError( action, error );
}

update( action.id, 'success', apiData );
data.forEach( ( [ id, resource ] ) => update( id, 'success', resource ) );

return { type: HTTP_DATA_TICK };
};

export default {
[ HTTP_DATA_REQUEST ]: [
dispatchRequestEx( {
fetch,
onSuccess,
onError,
} ),
],
};
2 changes: 2 additions & 0 deletions client/state/data-layer/wpcom-api-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
import { bypassDataLayer } from './utils';
import { mergeHandlers } from 'state/action-watchers/utils';
import wpcomHttpHandlers from './wpcom-http';
import httpData from './http-data';
import httpHandlers from 'state/http';
import thirdPartyHandlers from './third-party';
import wpcomHandlers from './wpcom';

const mergedHandlers = mergeHandlers(
httpData,
httpHandlers,
wpcomHttpHandlers,
thirdPartyHandlers,
Expand Down
6 changes: 6 additions & 0 deletions client/state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ import geo from './geo/reducer';
import googleAppsUsers from './google-apps-users/reducer';
import googleMyBusiness from './google-my-business/reducer';
import help from './help/reducer';
import {
enhancer as httpDataEnhancer,
reducer as httpData,
} from 'state/data-layer/http-data/common';
import i18n from './i18n/reducer';
import invites from './invites/reducer';
import inlineHelpSearchResults from './inline-help/reducer';
Expand Down Expand Up @@ -136,6 +140,7 @@ const reducers = {
happinessEngineers,
happychat,
help,
httpData,
i18n,
inlineHelpSearchResults,
invites,
Expand Down Expand Up @@ -233,6 +238,7 @@ export function createReduxStore( initialState = {} ) {

const enhancers = [
isBrowser && window.app && window.app.isDebug && consoleDispatcher,
httpDataEnhancer,
applyMiddleware( ...middlewares ),
isBrowser && window.app && window.app.isDebug && actionLogger,
isBrowser && window.devToolsExtension && window.devToolsExtension(),
Expand Down
6 changes: 6 additions & 0 deletions client/state/resource-ids.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @format */

export const readerTags = () => 'reader-tags';

// for example…
export const post = ( siteId, postId ) => `post-${ siteId }-${ postId }`;
3 changes: 3 additions & 0 deletions client/state/selectors/get-http-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** @format */

export { getHttpData as default } from 'state/data-layer/http-data/common';

0 comments on commit 6374d40

Please sign in to comment.