import { createMiddleware, MiddlewareStore } from '@chipp972/redux-helpers'; import * as R from 'ramda'; import { setRequestParams, submitComplete, submitFailure, submitPending, submitSuccess } from './redux-ajax.actions'; import { actions, noUrlProvidedErrorMessage, reducerKey } from './redux-ajax.constants'; import { isRequestAbortable } from './redux-ajax.helpers'; import { getResponse, getStatus } from './redux-ajax.selector'; import { AjaxMiddlewareConfig, Fetch, GetRequestBody, GetRequestData, MiddlewareHandlersParams, PrepareRequest, ReduxAjaxAction, ReduxAjaxRequest, RequestMethod, RequestStatus } from './redux-ajax.type'; const prepareRequest: PrepareRequest = ({ store, action }) => { const { requestId, requestContent = {}, onRequestAborted } = action.data; const abortController = new AbortController(); const { signal } = abortController; if (onRequestAborted) { signal.onabort = onRequestAborted; } store.dispatch(setRequestParams({ requestId, signal, abortController, ...requestContent })); }; const getRequestBody: GetRequestBody = ({ method = RequestMethod.get, transformData, body }) => { const isGetOrHead = /get|head/i.test(method); if (isGetOrHead) { return undefined; } return !!transformData ? transformData(body as object) : (body as BodyInit); }; const getRequestData: GetRequestData = ({ action, store }) => { const { requestId, transformData } = action.data; prepareRequest({ store, action }); const request: ReduxAjaxRequest | undefined = R.path([reducerKey, requestId], store.getState()); if (!request || !request.url) { throw new Error(noUrlProvidedErrorMessage); } return { ...request, body: getRequestBody({ method: request.method, transformData, body: request.body }) } as ReduxAjaxRequest & { url: string; body: BodyInit }; }; export const handleSuccess = ({ store, requestId, response = {}, callback }: MiddlewareHandlersParams & { response?: object }) => { store.dispatch(submitSuccess({ requestId, response })); callback && callback(response); }; export const handleError = ({ store, requestId, error, callback }: MiddlewareHandlersParams & { error: object }) => { store.dispatch(submitFailure({ requestId, error })); callback && callback(error); }; export const handleComplete = ({ store, requestId, callback }: MiddlewareHandlersParams & { callback?: () => void }) => { store.dispatch(submitComplete({ requestId })); callback && callback(); }; const runRequest = (store: MiddlewareStore, typedAction: ReduxAjaxAction, fetchFn: Fetch) => { const { requestId } = typedAction.data; const { url, ...requestOptions } = getRequestData({ action: typedAction, store }); store.dispatch(submitPending({ requestId })); return fetchFn(url, { // Precise default credentials for old safari version credentials: 'same-origin', ...requestOptions }); }; export const reduxAjaxMiddleware = ({ fetchFn }: AjaxMiddlewareConfig) => createMiddleware({ // eslint-disable-next-line max-statements, complexity [actions.AJAX_REQUEST_INTENTED]: async ({ store, action, next }) => { const typedAction = action as ReduxAjaxAction; const state = store.getState(); const { requestId, onRequestSuccess, onRequestFailure, onRequestComplete, isCached } = typedAction.data; const requestStatus = getStatus(requestId)(state) || RequestStatus.PREPARING; if ([RequestStatus.INTENTED, RequestStatus.PENDING].includes(requestStatus)) { return; } next(action); try { const response = isCached && requestStatus === RequestStatus.SUCCESS ? getResponse(requestId)(state) : await runRequest(store, typedAction, fetchFn); handleSuccess({ store, requestId, response, callback: onRequestSuccess }); } catch (error) { handleError({ store, requestId, error, callback: onRequestFailure }); } finally { // For the middlewares (it won't change the state) handleComplete({ store, requestId, callback: onRequestComplete }); } }, [actions.AJAX_REQUEST_ABORT]: ({ store, action, next }) => { const { requestId } = action.data as { requestId: string }; const request: ReduxAjaxRequest | undefined = R.path([reducerKey, requestId], store.getState()); if (isRequestAbortable(request)) { ((request as ReduxAjaxRequest).abortController as AbortController).abort(); } return next(action); } });