UNPKG

reprehenderitiure

Version:

Create actions that return promises, which are resolved or rejected by a redux saga

253 lines (214 loc) 10.3 kB
import { Dispatch, Middleware, MiddlewareAPI } from "redux"; import { ActionCreatorWithPayload, ActionCreatorWithPreparedPayload, createAction, PayloadAction, PayloadActionCreator, PrepareAction, } from "@reduxjs/toolkit"; import { isFunction, merge } from "lodash"; import { call, CallEffect, SagaReturnType } from "redux-saga/effects"; import { ArgumentError } from "./ArgumentError"; import { ConfigurationError } from "./ConfigurationError"; type PromiseFromMeta<V> = { promise?: Promise<V>; }; type PromiseActionsFromMeta<V, T extends string> = { promiseActions: { resolved: ActionCreatorWithPayload<V, T>; rejected: ActionCreatorWithPayload<any, `${T}/rejected`>; }, } & PromiseFromMeta<V>; type PromiseResolutionFromMeta<V> = { promiseResolution: { resolve: (value: V) => void; reject: (error: any) => void; } }; type SagaPromiseMeta<V, T extends string> = PromiseActionsFromMeta<V, T>; type SagaPromiseMetaMutated<V, T extends string> = SagaPromiseMeta<V, T> & PromiseResolutionFromMeta<V>; type SagaPromiseActionBase<V, P, T extends string, M extends PromiseActionsFromMeta<V, T>> = PayloadAction<P, T, M, never>; type SagaPromiseAction<V, P, T extends string> = SagaPromiseActionBase<V, P, T, SagaPromiseMeta<V, T>>; type SagaPromiseActionMutated<V, P, T extends string> = SagaPromiseActionBase<V, P, T, SagaPromiseMetaMutated<V, T>>; type ActionCreatorWithPreparedPayloadAndMeta<V, P, T extends string, M extends PromiseActionsFromMeta<V, T>, PA extends PrepareAction<any>> = ActionCreatorWithPreparedPayload<Parameters<PA>, P, T, never, ReturnType<PA> extends { meta: infer InferM & M; } ? InferM : M>; type Sagas<V, P, T extends string> = { implement: (action: SagaPromiseAction<V, P, T>, executor: TriggerExecutor<V>) => Generator<CallEffect<SagaReturnType<TriggerExecutor<V>>>, void, any>, resolve: (action: SagaPromiseAction<V, P, T>, value: V) => ReturnType<typeof resolvePromiseAction>, reject: (action: SagaPromiseAction<V, P, T>, error: any) => ReturnType<typeof rejectPromiseAction> }; type SagasFromAction<V, P, T extends string> = { sagas: Sagas<V, P, T> }; type TypesFromAction<V, P, T extends string, M extends PromiseActionsFromMeta<V, T>> = { types: { triggerAction: SagaPromiseActionBase<V, P, T, M>, resolvedAction: PayloadAction<V, T>, rejectedAction: PayloadAction<any, `${T}/rejected`> promise: Promise<V>, resolveValue: V } }; type TriggerActionCreator<V, P, T extends string, M extends PromiseActionsFromMeta<V, T>, TPAC extends PayloadActionCreator<any, any>> = ActionCreatorWithPreparedPayloadAndMeta<V, P, T, M, TPAC>; type SagaPromiseActionCreatorBase<V, P, T extends string, M extends PromiseActionsFromMeta<V, T>, TPAC extends PayloadActionCreator<any, any>> = TriggerActionCreator<V, P, T, M, TPAC> & { trigger: SagaPromiseActionCreatorBase<V, P, T, M, TPAC> resolved: ActionCreatorWithPayload<V, T>; rejected: ActionCreatorWithPayload<any, `${T}/rejected`>; } & SagasFromAction<V, P, T> & TypesFromAction<V, P, T, M>; export type SagaPromiseActionCreator<V, P, T extends string, TPAC extends PayloadActionCreator<any, any>> = SagaPromiseActionCreatorBase<V, P, T, SagaPromiseMeta<V, T>, TPAC>; function isTriggerAction(action: SagaPromiseAction<any, any, any>) { return action?.meta?.promiseActions.resolved != null; } function isActionSagaPromise(action: SagaPromiseAction<any, any, any>, method): action is SagaPromiseActionMutated<any, any, any> { if (!isTriggerAction(action)) throw new ArgumentError(`redux-saga-promise: ${method}: first argument must be promise trigger action, got ${action}`); if (!isFunction((action as SagaPromiseActionMutated<any, any, any>)?.meta?.promiseResolution?.resolve)) throw new ConfigurationError(`redux-saga-promise: ${method}: Unable to execute--it seems that promiseMiddleware has not been not included before SagaMiddleware`); return true; } type ResolveValueFromTriggerAction<TAction> = TAction extends { meta: PromiseFromMeta<infer V>; } ? V : never; function resolvePromise(action: SagaPromiseActionMutated<any, any, any>, value: any) { return action.meta.promiseResolution.resolve(value); } function rejectPromise(action: SagaPromiseActionMutated<any, any, any>, error: any) { return action.meta.promiseResolution.reject(error); } /** * Saga to resolve or reject promise depending on the executor function returns or throws. * * @param executor A function that returns a value or throws a error that get applied to promise. */ export function* implementPromiseAction<TAction extends SagaPromiseAction<any, any, any>>(action: TAction, executor: TriggerExecutor<ResolveValueFromTriggerAction<TAction>>) { if (!isActionSagaPromise(action, "implementPromiseAction")) { return; // Never hit, exception is thrown before } try { resolvePromise(action, yield call(executor)); } catch (error) { rejectPromise(action, error); } } /** * Saga to resolve a promise. */ export function* resolvePromiseAction<TAction extends SagaPromiseAction<any, any, any>>(action: TAction, value: ResolveValueFromTriggerAction<TAction>) { if (!isActionSagaPromise(action, "resolvePromiseAction")) { return; // Never hit, exception is thrown before } yield call(resolvePromise, action, value); } /** * Saga to reject a promise. */ export function* rejectPromiseAction<TAction extends SagaPromiseAction<any, any, any>>(action: TAction, error: any) { if (!isActionSagaPromise(action, "rejectPromiseAction")) { return; // Never hit, exception is thrown before } yield call(rejectPromise, action, error); } function createPromiseActions<V, T extends string>(type: T) { return { resolvedAction: createAction<V>(`${type}/resolved`), rejectedAction: createAction<any>(`${type}/rejected`), }; } type TriggerExecutor<RT> = (() => PromiseLike<RT> | RT | Iterator<any, RT, any>); function wrapTriggerAction<V, P, T extends string, TPAC extends PayloadActionCreator<any, any>>( type: T, triggerAction: TPAC, ): SagaPromiseActionCreator<V, P, T, TPAC> { const { resolvedAction, rejectedAction } = createPromiseActions<V, T>(type); const updatedTrigger = <TriggerActionCreator<V, P, T, SagaPromiseMeta<V, T>, TPAC>>createAction(type, (...args: any[]) => merge(triggerAction.apply(null, args), { meta: <SagaPromiseMeta<V, T>>{ promiseActions: { resolved: resolvedAction, rejected: rejectedAction, }, }, })); const sagas = <Sagas<V, P, T>>{ implement: implementPromiseAction, resolve: resolvePromiseAction, reject: rejectPromiseAction, }; return <any>Object.assign(updatedTrigger, { trigger: updatedTrigger, resolved: resolvedAction, rejected: rejectedAction, sagas, }); } function createPromiseAction<V = any, P = void, T extends string = string>(type: T) { const triggerAction = createAction<P, T>(type); return wrapTriggerAction<V, P, T, typeof triggerAction>( type, triggerAction, ); } function createPreparedPromiseAction<V, PA extends PrepareAction<any> = PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA) { const triggerAction = createAction<PA, T>(type, prepareAction); return wrapTriggerAction<V, ReturnType<PA>["payload"], T, typeof triggerAction>( type, triggerAction, ); } /** * @template V Resolve type contraint for promise. */ export function promiseActionFactory<V = any>() { return { /** * A utility function to create an action creator for the given action type * string. The action creator accepts a single argument, which will be included * in the action object as a field called payload. The action creator function * will also have its toString() overriden so that it returns the action type, * allowing it to be used in reducer logic that is looking for that action type. * The created action contains promise actions to make redux-saga-promise work. * * @param type The action type to use for created actions. */ simple: <P = void, T extends string = string>(type: T) => createPromiseAction<V, P>(type), /** * A utility function to create an action creator for the given action type * string. The action creator accepts a single argument, which will be included * in the action object as a field called payload. The action creator function * will also have its toString() overriden so that it returns the action type, * allowing it to be used in reducer logic that is looking for that action type. * The created action contains promise actions to make redux-saga-promise work. * * @param type The action type to use for created actions. * @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }. * If this is given, the resulting action creator will pass its arguments to this method to calculate payload & meta. */ advanced: <PA extends PrepareAction<any> = PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA) => createPreparedPromiseAction<V, PA>(type, prepareAction), }; } /** * For a trigger action a promise is created and returned, and the action's * meta.promise is augmented with resolve and reject functions for use * by the sagas. (This middleware must come before sagaMiddleware so that * the sagas will have those functions available.) * * Non-actionPromiseFactory actions won't get processed in any kind. */ export const promiseMiddleware: Middleware = (store: MiddlewareAPI) => (next: Dispatch) => (action) => { if (isTriggerAction(action)) { const promise = new Promise((resolve, reject) => next(merge(action, { meta: { promiseResolution: { resolve: (value) => { resolve(value); store.dispatch(action.meta.promiseActions.resolved(value)); }, reject: (error) => { reject(error); store.dispatch(action.meta.promiseActions.rejected(error)); }, }, } as PromiseResolutionFromMeta<any>, }))); return merge(promise, { meta: { promise } as PromiseFromMeta<any> }); } return next(action); }; export * from "./ArgumentError"; export * from "./ConfigurationError";