From 8f7c60e999f954d92c1c7d4ee5675fb4e5b820fd Mon Sep 17 00:00:00 2001 From: teroneko Date: Fri, 3 Dec 2021 00:07:51 +0000 Subject: [PATCH] feat: promise action is now typed and usable as promise BREAKING CHANGE: Each promise action can now be awaited at root after it has been surpassed the promise middleware without accessing it through promiseAction.meta.promise first. --- README.md | 36 +++++++++++++++++----- package-lock.json | 4 +-- src/index.ts | 76 ++++++++++++++++++++++++++-------------------- test/index.test.ts | 8 ++--- 4 files changed, 77 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index ad3a148..555e204 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Initially forked from [@adobe/redux-saga-promise](https://github.com/adobe/redux - [Promise action with type](#promise-action-with-type) - [Promise action with type and payload](#promise-action-with-type-and-payload) - [Promise action with type and payload creator](#promise-action-with-type-and-payload-creator) +- [Promise action await](#promise-action-await) - [Promise action fulfillment or rejection](#promise-action-fulfillment-or-rejection) - [implementPromiseAction](#implementpromiseaction) - [resolvePromiseAction](#resolvepromiseaction) @@ -71,10 +72,12 @@ sagaMiddleware.run(rootSaga) ## Promise action creation -Use the following to create promise actions. +Use one of following method to create a promise action. ### Promise action with type +Create a promise action (creator) with action-type. + ```typescript import { promiseActionFactory } from "@teroneko/redux-saga-promise" @@ -88,6 +91,8 @@ const action = createAction(type_as_string)() ### Promise action with type and payload +Create a promise action (creator) with action-type and payload. + ```typescript import { promiseActionFactory } from "@teroneko/redux-saga-promise" @@ -101,6 +106,8 @@ const action = createAction(type_as_string)({} as payload_type) // ### Promise action with type and payload creator +Create a promise action (creator) with a action-type and an individial payload (creator). + ```typescript import { promiseActionFactory } from "@teroneko/redux-saga-promise" @@ -112,6 +119,21 @@ const actionCreator = createAction(type_as_string, (payload: payload_type) => { const action = createAction(type_as_string, (payload: payload_type) => { payload })({} as payload_type) // "as payload_type" just to show intention ``` +## Promise action await + +Await a promise action after it has been dispatched towards the redux store. + +> :warning: Keep in mind that the action is not awaitable after its creation but as soon it surpassed the middleware! + +```typescript +// Internally all members of the promiseAction (without +// promise capabilities) gets assigned to the promise. +const promise = store.dispatch(promiseAction()); +const resolvedValue = await promise; +``` + +Feel free to use `then`, `catch` or `finally` on `promise`. + ## Promise action fulfillment or rejection Either you use @@ -290,14 +312,12 @@ Additionally, all the helper functions will throw a custom `Error` subclass `Con ```typescript const promiseAction = promiseActionFactory().create("MY_ACTION"); -declare const type_of_promise_that_resides_in_promise_action: typeof promiseAction.types.promise; -const promise = store.dispatch(promiseAction()).meta.promise; // or -const promise = store.dispatch(promiseAction()) as any as typeof promiseAction.types.promise; - -declare const type_of_trigger_action_that_got_created_from_the_simple_or_advanced_action_creator: typeof promiseAction.types.triggerAction; -declare const type_of_resolved_action_that_got_created_from_the_simple_or_advanced_action_creator: typeof promiseAction.types.resolvedAction; -declare const type_of_rejected_action_that_got_created_from_the_simple_or_advanced_action_creator: typeof promiseAction.types.rejectedAction; +declare const type_of_promise_returned_when_surpassing_promise_middleware: typeof promiseAction.types.promise; declare const type_of_resolved_value_from_promise_of_promise_action: typeof promiseAction.types.resolveValue; + +declare const type_of_trigger_action_that_got_created_from_the_action_creator: typeof promiseAction.types.triggerAction; +declare const type_of_resolved_action_that_got_created_from_the_action_creator: typeof promiseAction.types.resolvedAction; +declare const type_of_rejected_action_that_got_created_from_the_action_creator: typeof promiseAction.types.rejectedAction; ``` ### Sagas diff --git a/package-lock.json b/package-lock.json index 42f29d7..8f48db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "@teroneko/redux-saga-promise", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "2.0.1", + "version": "2.0.2", "license": "MIT", "dependencies": { "@reduxjs/toolkit": "^1.6.2", diff --git a/src/index.ts b/src/index.ts index 37709fb..c48138e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,37 @@ 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 { _ActionCreatorWithPreparedPayload } from "@reduxjs/toolkit/dist/createAction"; +import { ActionCreatorWithPayload, _ActionCreatorWithPreparedPayload } from "@reduxjs/toolkit/dist/createAction"; import { ArgumentError } from "./ArgumentError"; import { ConfigurationError } from "./ConfigurationError"; -type PromiseFromMeta = { - promise?: Promise; -}; - -type PromiseActionsFromMeta = { +type MetaOnlyPromiseActions = { promiseActions: { resolved: ActionCreatorWithPayload; rejected: ActionCreatorWithPayload; }, -} & PromiseFromMeta; +}; -type PromiseResolutionFromMeta = { +type MetaOnlyPromiseResolution = { promiseResolution: { resolve: (value: V) => void; reject: (error: any) => void; } }; -type SagaPromiseMeta = PromiseActionsFromMeta; -type SagaPromiseMetaMutated = SagaPromiseMeta & PromiseResolutionFromMeta; +type SagaPromiseMeta = MetaOnlyPromiseActions; +type SagaPromiseMetaWithPromiseResolution = SagaPromiseMeta & MetaOnlyPromiseResolution; -type SagaPromiseActionBase> = PayloadAction; +type SagaPromiseActionBase> = PayloadAction; type SagaPromiseAction = SagaPromiseActionBase>; -type SagaPromiseActionMutated = SagaPromiseActionBase>; +type SagaPromiseActionWithPromiseResolution = SagaPromiseActionBase>; -type ActionCreatorWithPreparedPayloadAndMeta, PA extends PrepareAction> = - ActionCreatorWithPreparedPayload, P, T, never, ReturnType extends { - meta: infer InferM & M; - } ? InferM : M>; +type MetaFromActionCreator, PA extends PrepareAction> = ReturnType extends { + meta: infer _M; +} ? _M : M; type Sagas = { implement: (action: SagaPromiseAction, executor: TriggerExecutor) => Generator>>, void, any>, @@ -45,11 +39,11 @@ type Sagas = { reject: (action: SagaPromiseAction, error: any) => ReturnType }; -type SagasFromAction = { +type ActionOnlySagas = { sagas: Sagas }; -type TypesFromAction> = { +type TypesFromAction> = { types: { triggerAction: SagaPromiseActionBase, resolvedAction: PayloadAction, @@ -59,13 +53,29 @@ type TypesFromAction, TA extends PayloadActionCreator> = ActionCreatorWithPreparedPayloadAndMeta; +interface TriggerActionCreator, PA extends PrepareAction> extends ActionCreatorWithPreparedPayload, P, T, MetaFromActionCreator> { + /** + * Calling this {@link redux#ActionCreator} with `Args` will return + * an Action with a payload of type `P` and (depending on the `PrepareAction` + * method used) a `meta`- and `error` property of types `M` and `E` respectively. + */ + (...args: Parameters): PayloadAction; +} -export type SagaPromiseActionCreator> = TriggerActionCreator, TA> & { +interface PromiseTriggerActionCreator, PA extends PrepareAction> extends ActionCreatorWithPreparedPayload, P, T, MetaFromActionCreator> { + /** + * Calling this {@link redux#ActionCreator} with `Args` will return + * an Action with a payload of type `P` and (depending on the `PrepareAction` + * method used) a `meta`- and `error` property of types `M` and `E` respectively. + */ + (...args: Parameters): PayloadAction & Promise; +} + +export type SagaPromiseActionCreator> = PromiseTriggerActionCreator, TA> & { trigger: SagaPromiseActionCreator resolved: ActionCreatorWithPayload; rejected: ActionCreatorWithPayload; -} & SagasFromAction & TypesFromAction>; +} & ActionOnlySagas & TypesFromAction>; export type SagaPromisePreparedActionCreator> = SagaPromiseActionCreator["payload"], T, _ActionCreatorWithPreparedPayload>; @@ -73,21 +83,21 @@ function isTriggerAction(action: SagaPromiseAction) { return action?.meta?.promiseActions.resolved != null; } -function isActionSagaPromise(action: SagaPromiseAction, method): action is SagaPromiseActionMutated { +function isActionSagaPromise(action: SagaPromiseAction, method): action is SagaPromiseActionWithPromiseResolution { if (!isTriggerAction(action)) throw new ArgumentError(`redux-saga-promise: ${method}: first argument must be promise trigger action, got ${action}`); - if (!isFunction((action as SagaPromiseActionMutated)?.meta?.promiseResolution?.resolve)) throw new ConfigurationError(`redux-saga-promise: ${method}: Unable to execute--it seems that promiseMiddleware has not been not included before SagaMiddleware`); + if (!isFunction((action as SagaPromiseActionWithPromiseResolution)?.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 extends { - meta: PromiseFromMeta; + meta: MetaOnlyPromiseActions; } ? V : never; -function resolvePromise(action: SagaPromiseActionMutated, value: any) { +function resolvePromise(action: SagaPromiseActionWithPromiseResolution, value: any) { return action.meta.promiseResolution.resolve(value); } -function rejectPromise(action: SagaPromiseActionMutated, error: any) { +function rejectPromise(action: SagaPromiseActionWithPromiseResolution, error: any) { return action.meta.promiseResolution.reject(error); } @@ -216,8 +226,8 @@ interface PromiseActionFactory { /** * @template V Resolve type contraint for promise. */ -export function promiseActionFactory() { - return { +export function promiseActionFactory() { + return >{ create(type: any, prepareAction?: any) { if (arguments.length === 0) { throw new ArgumentError("Type was expected"); @@ -233,7 +243,7 @@ export function promiseActionFactory() { return createPromiseAction(type); }, - } as any as PromiseActionFactory; + }; } /** @@ -247,7 +257,7 @@ export function promiseActionFactory() { export const promiseMiddleware: Middleware = (store: MiddlewareAPI) => (next: Dispatch) => (action) => { if (isTriggerAction(action)) { const promise = new Promise((resolve, reject) => next(merge(action, { - meta: { + meta: >{ promiseResolution: { resolve: (value) => { resolve(value); @@ -258,10 +268,10 @@ export const promiseMiddleware: Middleware = (store: MiddlewareAPI) => (next: Di store.dispatch(action.meta.promiseActions.rejected(error)); }, }, - } as PromiseResolutionFromMeta, + }, }))); - return merge(promise, { meta: { promise } as PromiseFromMeta }); + return merge(promise, action); } return next(action); diff --git a/test/index.test.ts b/test/index.test.ts index 5495633..4e8bed2 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -133,7 +133,7 @@ describe("implementPromiseAction", function () { const resolveValue = "resolveValue"; // Dispatch the promise action - const { promise } = store.dispatch(promiseAction(triggerPayload)).meta; + const promise = store.dispatch(promiseAction(triggerPayload)); expect(promise instanceof Promise).toBeTruthy(); // Verify trigger payload has been reduced @@ -159,7 +159,7 @@ describe("implementPromiseAction", function () { const rejectMessage = "rejectMessage"; // Dispatch the promise action - const { promise } = store.dispatch(promiseAction(triggerPayload)).meta; + const promise = store.dispatch(promiseAction(triggerPayload)); // Verify trigger payload has been reduced expect(store.getState().trigger === triggerPayload).toBeTruthy(); @@ -207,7 +207,7 @@ describe("resolvePromiseAction", function () { const resolveValue = "resolveValue"; // Dispatch the promise action, monitor resolution - const { promise } = store.dispatch(promiseAction(triggerPayload)).meta; + const promise = store.dispatch(promiseAction(triggerPayload)); // Verify trigger payload has been reduced expect(store.getState().trigger === triggerPayload).toBeTruthy(); @@ -255,7 +255,7 @@ describe("rejectPromiseAction", function () { const rejectMessage = "rejectMessage"; // Dispatch the promise action, monitor rejection - const { promise } = store.dispatch(promiseAction(triggerPayload)).meta; + const promise = store.dispatch(promiseAction(triggerPayload)); // Verify trigger payload has been reduced expect(store.getState().trigger === triggerPayload).toBeTruthy();