Skip to content

Commit

Permalink
refactor: sw-2479 promiseMiddleware, catch rejections (#1397)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdcabrera authored Aug 28, 2024
1 parent 0402ec4 commit 34ebed5
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 18 deletions.
9 changes: 0 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@
"react-use": "^17.5.1",
"redux": "^5.0.1",
"redux-logger": "^3.0.6",
"redux-promise-middleware": "^6.2.0",
"redux-thunk": "^3.1.0",
"victory": "37.0.2",
"victory-create-container": "37.0.2"
Expand Down
78 changes: 78 additions & 0 deletions src/redux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
<dd></dd>
<dt><a href="#Middleware.module_MultiActionMiddleware">MultiActionMiddleware</a></dt>
<dd></dd>
<dt><a href="#Middleware.module_PromiseMiddleware">PromiseMiddleware</a></dt>
<dd></dd>
<dt><a href="#Middleware.module_StatusMiddleware">StatusMiddleware</a></dt>
<dd></dd>
<dt><a href="#Reducers.module_AppReducer">AppReducer</a></dt>
Expand Down Expand Up @@ -1062,6 +1064,82 @@ Allow passing an array of actions for batch dispatch.
</tr> </tbody>
</table>

<a name="Middleware.module_PromiseMiddleware"></a>

## PromiseMiddleware

* [PromiseMiddleware](#Middleware.module_PromiseMiddleware)
* [~ActionType](#Middleware.module_PromiseMiddleware..ActionType) : <code>Object</code>
* [~createPromise(config)](#Middleware.module_PromiseMiddleware..createPromise) ⇒ <code>function</code>
* [~promiseMiddleware(config)](#Middleware.module_PromiseMiddleware..promiseMiddleware) ⇒ <code>function</code>

<a name="Middleware.module_PromiseMiddleware..ActionType"></a>

### PromiseMiddleware~ActionType : <code>Object</code>
Redux default action types for promiseMiddleware

**Kind**: inner constant of [<code>PromiseMiddleware</code>](#Middleware.module_PromiseMiddleware)
<a name="Middleware.module_PromiseMiddleware..createPromise"></a>

### PromiseMiddleware~createPromise(config) ⇒ <code>function</code>
Function: createPromise
Description: The main createPromise accepts a configuration
object and returns the middleware.

**Kind**: inner method of [<code>PromiseMiddleware</code>](#Middleware.module_PromiseMiddleware)
<table>
<thead>
<tr>
<th>Param</th><th>Type</th>
</tr>
</thead>
<tbody>
<tr>
<td>config</td><td><code>object</code></td>
</tr><tr>
<td>config.isCatchRejection</td><td><code>boolean</code></td>
</tr><tr>
<td>config.promiseTypeDelimiter</td><td><code>string</code></td>
</tr><tr>
<td>config.promiseTypeSuffixPending</td><td><code>string</code></td>
</tr><tr>
<td>config.promiseTypeSuffixFulfilled</td><td><code>string</code></td>
</tr><tr>
<td>config.promiseTypeSuffixRejected</td><td><code>string</code></td>
</tr> </tbody>
</table>

<a name="Middleware.module_PromiseMiddleware..promiseMiddleware"></a>

### PromiseMiddleware~promiseMiddleware(config) ⇒ <code>function</code>
Promise middleware
Base code, https://github.com/pburtchaell/redux-promise-middleware
Modified to allow configuration and "isCatchRejection".

**Kind**: inner method of [<code>PromiseMiddleware</code>](#Middleware.module_PromiseMiddleware)
<table>
<thead>
<tr>
<th>Param</th><th>Type</th><th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>config</td><td><code>object</code></td><td></td>
</tr><tr>
<td>config.isCatchRejection</td><td><code>boolean</code></td><td><p>Catch the returned promise. Helps avoid the &quot;[uncaught in promise]&quot; error</p>
</td>
</tr><tr>
<td>config.promiseTypeDelimiter</td><td><code>string</code></td><td></td>
</tr><tr>
<td>config.promiseTypeSuffixPending</td><td><code>string</code></td><td></td>
</tr><tr>
<td>config.promiseTypeSuffixFulfilled</td><td><code>string</code></td><td></td>
</tr><tr>
<td>config.promiseTypeSuffixRejected</td><td><code>string</code></td><td></td>
</tr> </tbody>
</table>

<a name="Middleware.module_StatusMiddleware"></a>

## StatusMiddleware
Expand Down
2 changes: 1 addition & 1 deletion src/redux/actions/__tests__/platformActions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { platformActions } from '../platformActions';
import { appReducer } from '../../reducers';

describe('PlatformActions', () => {
const middleware = [promiseMiddleware];
const middleware = [promiseMiddleware()];
const generateStore = () =>
createStore(
combineReducers({
Expand Down
2 changes: 1 addition & 1 deletion src/redux/actions/__tests__/rhsmActions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { rhsmConstants } from '../../../services/rhsm/rhsmConstants';
import { rhsmActions } from '../rhsmActions';

describe('RhsmActions', () => {
const middleware = [multiActionMiddleware, promiseMiddleware];
const middleware = [multiActionMiddleware, promiseMiddleware()];
const generateStore = () =>
createStore(
combineReducers({
Expand Down
2 changes: 1 addition & 1 deletion src/redux/actions/__tests__/userActions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { appReducer } from '../../reducers';
import { userActions } from '../userActions';

describe('UserActions', () => {
const middleware = [promiseMiddleware];
const middleware = [promiseMiddleware()];
const generateStore = () =>
createStore(
combineReducers({
Expand Down
4 changes: 2 additions & 2 deletions src/redux/middleware/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createLogger } from 'redux-logger';
import promiseMiddleware from 'redux-promise-middleware';
import { thunk as thunkMiddleware } from 'redux-thunk';
import { notificationsMiddleware } from '@redhat-cloud-services/frontend-components-notifications';
import { promiseMiddleware } from './promiseMiddleware';
import { multiActionMiddleware } from './multiActionMiddleware';
import { statusMiddleware } from './statusMiddleware';
import { actionRecordMiddleware } from './actionRecordMiddleware';
Expand Down Expand Up @@ -34,7 +34,7 @@ const reduxMiddleware = [
thunkMiddleware,
statusMiddleware(),
multiActionMiddleware,
promiseMiddleware,
promiseMiddleware({ isCatchRejection: true }),
actionRecordMiddleware({
id: process.env.REACT_APP_UI_LOGGER_ID,
app: { version: process.env.REACT_APP_UI_VERSION }
Expand Down
230 changes: 230 additions & 0 deletions src/redux/middleware/promiseMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { helpers } from '../../common/helpers';

/**
* @memberof Middleware
* @module PromiseMiddleware
*/

/**
* Redux default action types for promiseMiddleware
*
* @type {{Fulfilled: string, Rejected: string, Pending: string}}
*/
const ActionType = {
Pending: 'PENDING',
Fulfilled: 'FULFILLED',
Rejected: 'REJECTED'
};

/**
* Function: createPromise
* Description: The main createPromise accepts a configuration
* object and returns the middleware.
*
* @param {object} config
* @param {boolean} config.isCatchRejection
* @param {string} config.promiseTypeDelimiter
* @param {string} config.promiseTypeSuffixPending
* @param {string} config.promiseTypeSuffixFulfilled
* @param {string} config.promiseTypeSuffixRejected
* @returns {Function}
*/
const createPromise = ({
promiseTypeDelimiter: PROMISE_TYPE_DELIMITER = '_',
promiseTypeSuffixPending = ActionType.Pending,
promiseTypeSuffixFulfilled = ActionType.Fulfilled,
promiseTypeSuffixRejected = ActionType.Rejected,
isCatchRejection = false
} = {}) => {
const PROMISE_TYPE_SUFFIXES = [promiseTypeSuffixPending, promiseTypeSuffixFulfilled, promiseTypeSuffixRejected];
return ref => {
const { dispatch } = ref;

return next => action => {
/**
* Instantiate variables to hold:
* (1) the promise
* (2) the data for optimistic updates
*/
let promise;
let data;

/**
* There are multiple ways to dispatch a promise. The first step is to
* determine if the promise is defined:
* (a) explicitly (action.payload.promise is the promise)
* (b) implicitly (action.payload is the promise)
* (c) as an async function (returns a promise when called)
*
* If the promise is not defined in one of these three ways, we don't do
* anything and move on to the next middleware in the middleware chain.
*/

// Step 1a: Is there a payload?
if (action.payload) {
const PAYLOAD = action.payload;

// Step 1.1: Is the promise implicitly defined?
if (helpers.isPromise(PAYLOAD)) {
promise = PAYLOAD;
}

// Step 1.2: Is the promise explicitly defined?
else if (helpers.isPromise(PAYLOAD.promise)) {
promise = PAYLOAD.promise;
data = PAYLOAD.data;
}

// Step 1.3: Is the promise returned by an async function?
else if (typeof PAYLOAD === 'function' || typeof PAYLOAD.promise === 'function') {
promise = PAYLOAD.promise ? PAYLOAD.promise() : PAYLOAD();
data = PAYLOAD.promise ? PAYLOAD.data : undefined;

// Step 1.3.1: Is the return of action.payload a promise?
if (!helpers.isPromise(promise)) {
// If not, move on to the next middleware.
return next({
...action,
payload: promise
});
}
}

// Step 1.4: If there's no promise, move on to the next middleware.
else {
return next(action);
}

// Step 1b: If there's no payload, move on to the next middleware.
} else {
return next(action);
}

/**
* Instantiate and define constants for:
* (1) the action type
* (2) the action meta
*/
const TYPE = action.type;
const META = action.meta;

/**
* Instantiate and define constants for the action type suffixes.
* These are appended to the end of the action type.
*/
const [PENDING, FULFILLED, REJECTED] = PROMISE_TYPE_SUFFIXES;

/**
* Function: getAction
* Description: This function constructs and returns a rejected
* or fulfilled action object. The action object is based off the Flux
* Standard Action (FSA).
*
* Given an original action with the type FOO:
*
* The rejected object model will be:
* {
* error: true,
* type: 'FOO_REJECTED',
* payload: ...,
* meta: ... (optional)
* }
*
* The fulfilled object model will be:
* {
* type: 'FOO_FULFILLED',
* payload: ...,
* meta: ... (optional)
* }
*
* @param {unknown} newPayload
* @param {boolean} isRejected
* @returns {{payload: any, meta: any, type: string, error: any}}
*/
const getAction = (newPayload, isRejected) => ({
// Concatenate the type string property.
type: [TYPE, isRejected ? REJECTED : FULFILLED].join(PROMISE_TYPE_DELIMITER),

// Include the payload property.
...(newPayload === null || typeof newPayload === 'undefined'
? {}
: {
payload: newPayload
}),

// If the original action includes a meta property, include it.
...(META !== undefined ? { meta: META } : {}),

// If the action is rejected, include an error property.
...(isRejected
? {
error: true
}
: {})
});

const handleReject = reason => {
const rejectedAction = getAction(reason, true);
dispatch(rejectedAction);

if (isCatchRejection === false) {
throw reason;
}
};

const handleFulfill = (value = null) => {
const resolvedAction = getAction(value, false);
dispatch(resolvedAction);

return { value, action: resolvedAction };
};

/**
* First, dispatch the pending action:
* This object describes the pending state of a promise and will include
* any data (for optimistic updates) and/or meta from the original action.
*/
next({
// Concatenate the type string.
type: [TYPE, PENDING].join(PROMISE_TYPE_DELIMITER),

// Include payload (for optimistic updates) if it is defined.
...(data !== undefined ? { payload: data } : {}),

// Include meta data if it is defined.
...(META !== undefined ? { meta: META } : {})
});

/**
* Second, dispatch a rejected or fulfilled action and move on to the
* next middleware.
*/
return promise.then(handleFulfill, handleReject);
};
};
};

/**
* Promise middleware
* Base code, https://github.com/pburtchaell/redux-promise-middleware
* Modified to allow configuration and "isCatchRejection".
*
* @param {object} config
* @param {boolean} config.isCatchRejection Catch the returned promise. Helps avoid the "[uncaught in promise]" error
* @param {string} config.promiseTypeDelimiter
* @param {string} config.promiseTypeSuffixPending
* @param {string} config.promiseTypeSuffixFulfilled
* @param {string} config.promiseTypeSuffixRejected
* @returns {Function}
*/
const promiseMiddleware =
config =>
({ dispatch } = {}) => {
if (typeof dispatch === 'function') {
return createPromise(config)({ dispatch });
}

return null;
};

export { promiseMiddleware as default, promiseMiddleware, createPromise, ActionType };
Loading

0 comments on commit 34ebed5

Please sign in to comment.