Skip to content

Commit

Permalink
feat: promise action is now typed and usable as promise
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
teneko committed Dec 3, 2021
1 parent 01ac73e commit 8f7c60e
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 47 deletions.
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"

Expand All @@ -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"

Expand All @@ -101,6 +106,8 @@ const action = createAction<payload_type>(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"

Expand All @@ -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
Expand Down Expand Up @@ -290,14 +312,12 @@ Additionally, all the helper functions will throw a custom `Error` subclass `Con
```typescript
const promiseAction = promiseActionFactory<number>().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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

76 changes: 43 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,49 @@
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<V> = {
promise?: Promise<V>;
};

type PromiseActionsFromMeta<V, T extends string> = {
type MetaOnlyPromiseActions<V, T extends string> = {
promiseActions: {
resolved: ActionCreatorWithPayload<V, T>;
rejected: ActionCreatorWithPayload<any, `${T}/rejected`>;
},
} & PromiseFromMeta<V>;
};

type PromiseResolutionFromMeta<V> = {
type MetaOnlyPromiseResolution<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 SagaPromiseMeta<V, T extends string> = MetaOnlyPromiseActions<V, T>;
type SagaPromiseMetaWithPromiseResolution<V, T extends string> = SagaPromiseMeta<V, T> & MetaOnlyPromiseResolution<V>;

type SagaPromiseActionBase<V, P, T extends string, M extends PromiseActionsFromMeta<V, T>> = PayloadAction<P, T, M, never>;
type SagaPromiseActionBase<V, P, T extends string, M extends SagaPromiseMeta<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 SagaPromiseActionWithPromiseResolution<V, P, T extends string> = SagaPromiseActionBase<V, P, T, SagaPromiseMetaWithPromiseResolution<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 MetaFromActionCreator<V, T extends string, M extends SagaPromiseMeta<V, T>, PA extends PrepareAction<any>> = ReturnType<PA> extends {
meta: infer _M;
} ? _M : 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> = {
type ActionOnlySagas<V, P, T extends string> = {
sagas: Sagas<V, P, T>
};

type TypesFromAction<V, P, T extends string, M extends PromiseActionsFromMeta<V, T>> = {
type TypesFromAction<V, P, T extends string, M extends SagaPromiseMeta<V, T>> = {
types: {
triggerAction: SagaPromiseActionBase<V, P, T, M>,
resolvedAction: PayloadAction<V, T>,
Expand All @@ -59,35 +53,51 @@ type TypesFromAction<V, P, T extends string, M extends PromiseActionsFromMeta<V,
}
};

type TriggerActionCreator<V, P, T extends string, M extends PromiseActionsFromMeta<V, T>, TA extends PayloadActionCreator<any, T>> = ActionCreatorWithPreparedPayloadAndMeta<V, P, T, M, TA>;
interface TriggerActionCreator<V, P, T extends string, M extends SagaPromiseMeta<V, T>, PA extends PrepareAction<any>> extends ActionCreatorWithPreparedPayload<Parameters<PA>, P, T, MetaFromActionCreator<V, T, M, PA>> {
/**
* 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<PA>): PayloadAction<P, T, M, never>;
}

export type SagaPromiseActionCreator<V, P, T extends string, TA extends PayloadActionCreator<any, T>> = TriggerActionCreator<V, P, T, PromiseActionsFromMeta<V, T>, TA> & {
interface PromiseTriggerActionCreator<V, P, T extends string, M extends SagaPromiseMeta<V, T>, PA extends PrepareAction<any>> extends ActionCreatorWithPreparedPayload<Parameters<PA>, P, T, MetaFromActionCreator<V, T, M, PA>> {
/**
* 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<PA>): PayloadAction<P, T, M, never> & Promise<V>;
}

export type SagaPromiseActionCreator<V, P, T extends string, TA extends PayloadActionCreator<any, T>> = PromiseTriggerActionCreator<V, P, T, SagaPromiseMeta<V, T>, TA> & {
trigger: SagaPromiseActionCreator<V, P, T, TA>
resolved: ActionCreatorWithPayload<V, T>;
rejected: ActionCreatorWithPayload<any, `${T}/rejected`>;
} & SagasFromAction<V, P, T> & TypesFromAction<V, P, T, PromiseActionsFromMeta<V, T>>;
} & ActionOnlySagas<V, P, T> & TypesFromAction<V, P, T, SagaPromiseMeta<V, T>>;

export type SagaPromisePreparedActionCreator<V, T extends string, TA extends PrepareAction<any>> = SagaPromiseActionCreator<V, ReturnType<TA>["payload"], T, _ActionCreatorWithPreparedPayload<TA, T>>;

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> {
function isActionSagaPromise(action: SagaPromiseAction<any, any, any>, method): action is SagaPromiseActionWithPromiseResolution<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`);
if (!isFunction((action as SagaPromiseActionWithPromiseResolution<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>;
meta: MetaOnlyPromiseActions<infer V, any>;
} ? V : never;

function resolvePromise(action: SagaPromiseActionMutated<any, any, any>, value: any) {
function resolvePromise(action: SagaPromiseActionWithPromiseResolution<any, any, any>, value: any) {
return action.meta.promiseResolution.resolve(value);
}

function rejectPromise(action: SagaPromiseActionMutated<any, any, any>, error: any) {
function rejectPromise(action: SagaPromiseActionWithPromiseResolution<any, any, any>, error: any) {
return action.meta.promiseResolution.reject(error);
}

Expand Down Expand Up @@ -216,8 +226,8 @@ interface PromiseActionFactory<V> {
/**
* @template V Resolve type contraint for promise.
*/
export function promiseActionFactory<V = any>() {
return {
export function promiseActionFactory<V = unknown>() {
return <PromiseActionFactory<V>>{
create(type: any, prepareAction?: any) {
if (arguments.length === 0) {
throw new ArgumentError("Type was expected");
Expand All @@ -233,7 +243,7 @@ export function promiseActionFactory<V = any>() {

return createPromiseAction(type);
},
} as any as PromiseActionFactory<V>;
};
}

/**
Expand All @@ -247,7 +257,7 @@ export function promiseActionFactory<V = any>() {
export const promiseMiddleware: Middleware = (store: MiddlewareAPI) => (next: Dispatch) => (action) => {
if (isTriggerAction(action)) {
const promise = new Promise((resolve, reject) => next(merge(action, {
meta: {
meta: <MetaOnlyPromiseResolution<any>>{
promiseResolution: {
resolve: (value) => {
resolve(value);
Expand All @@ -258,10 +268,10 @@ export const promiseMiddleware: Middleware = (store: MiddlewareAPI) => (next: Di
store.dispatch(action.meta.promiseActions.rejected(error));
},
},
} as PromiseResolutionFromMeta<any>,
},
})));

return merge(promise, { meta: { promise } as PromiseFromMeta<any> });
return merge(promise, action);
}

return next(action);
Expand Down
8 changes: 4 additions & 4 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 8f7c60e

Please sign in to comment.