diff --git a/.editorconfig b/.editorconfig index fcd0337..92809f0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,11 +1,12 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = space -indent_size = 2 -charset = utf-8 -trim_trailing_whitespace = false -insert_final_newline = true +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true diff --git a/package.json b/package.json index 4faea26..2ee87f2 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,54 @@ -{ - "name": "@teroneko/redux-saga-promise", - "version": "1.2.2", - "description": "Create actions that return promises, which are resolved or rejected by a redux saga", - "main": "dist/src/index.js", - "typings": "dist/src/index.d.ts", - "scripts": { - "build": "npx tsc", - "pretest": "npm run build", - "test": "npx jest", - "prepublishOnly": "npm run test" - }, - "keywords": [ - "redux", - "saga", - "action", - "promise", - "promises", - "resolve", - "reject" - ], - "author": "teroneko", - "license": "MIT", - "devDependencies": { - "@types/jest": "^27.0.2", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", - "eslint": "^7.32.0", - "eslint-config-airbnb": "^18.2.1", - "eslint-config-airbnb-typescript": "^14.0.1", - "eslint-plugin-import": "^2.24.2", - "jest": "^27.3.1", - "ts-jest": "^27.0.7", - "typescript": "^4.4.4" - }, - "dependencies": { - "@reduxjs/toolkit": "^1.6.2", - "@types/lodash": "^4.14.176", - "lodash": "^4.17.21", - "redux": "^4.1.2", - "redux-saga": "^1.1.3" - }, - "directories": { - "test": "test" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/teroneko/redux-saga-promise.git" - }, - "bugs": { - "url": "https://github.com/teroneko/redux-saga-promise/issues" - }, - "homepage": "https://github.com/teroneko/redux-saga-promise#readme" -} +{ + "name": "@teroneko/redux-saga-promise", + "version": "1.2.2", + "description": "Create actions that return promises, which are resolved or rejected by a redux saga", + "main": "dist/src/index.js", + "typings": "dist/src/index.d.ts", + "scripts": { + "build": "npx tsc", + "pretest": "npm run build", + "test": "npx jest", + "prepublishOnly": "rm -r dist && npm run test" + }, + "keywords": [ + "redux", + "saga", + "action", + "promise", + "promises", + "resolve", + "reject" + ], + "author": "teroneko", + "license": "MIT", + "devDependencies": { + "@types/jest": "^27.0.2", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", + "eslint": "^7.32.0", + "eslint-config-airbnb": "^18.2.1", + "eslint-config-airbnb-typescript": "^14.0.1", + "eslint-plugin-import": "^2.24.2", + "jest": "^27.3.1", + "ts-jest": "^27.0.7", + "typescript": "^4.4.4" + }, + "dependencies": { + "@reduxjs/toolkit": "^1.6.2", + "@types/lodash": "^4.14.176", + "lodash": "^4.17.21", + "redux": "^4.1.2", + "redux-saga": "^1.1.3" + }, + "directories": { + "test": "test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/teroneko/redux-saga-promise.git" + }, + "bugs": { + "url": "https://github.com/teroneko/redux-saga-promise/issues" + }, + "homepage": "https://github.com/teroneko/redux-saga-promise#readme" +} diff --git a/src/index.ts b/src/index.ts index 2a30257..c5a97e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,23 @@ import { Dispatch, Middleware, MiddlewareAPI } from "redux"; import { + ActionCreatorWithPayload, ActionCreatorWithPreparedPayload, createAction, PayloadAction, PayloadActionCreator, PrepareAction, } from "@reduxjs/toolkit"; import { isFunction, merge } from "lodash"; -import { call } from "redux-saga/effects"; +import { call, CallEffect, SagaReturnType } from "redux-saga/effects"; import { ArgumentError } from "./ArgumentError"; import { ConfigurationError } from "./ConfigurationError"; -const promiseSymbol = Symbol.for("@teroneko/redux-saga-promise"); - -type SymbolTagged = { [promiseSymbol]: T }; - -type PromiseInstanceFromMeta = { +type PromiseFromMeta = { promise?: Promise; }; -type PromiseActionsFromMeta, RJA extends PayloadActionCreator> = { +type PromiseActionsFromMeta = { promiseActions: { - resolved: RSA; - rejected: RJA; + resolved: ActionCreatorWithPayload; + rejected: ActionCreatorWithPayload; }, -} & PromiseInstanceFromMeta & SymbolTagged<{ resolveValueType: V }>; +} & PromiseFromMeta; type PromiseResolutionFromMeta = { promiseResolution: { @@ -29,43 +26,67 @@ type PromiseResolutionFromMeta = { } }; -type ResolvablePromiseActionsFromMeta, RJA extends PayloadActionCreator> = PromiseActionsFromMeta & PromiseResolutionFromMeta; +type SagaPromiseMeta = PromiseActionsFromMeta; +type SagaPromiseMetaMutated = SagaPromiseMeta & PromiseResolutionFromMeta; -type PromiseActionsFromTriggerAction = { - trigger: TA; - resolved: RSA; - rejected: RJA; -}; +type SagaPromiseActionBase> = PayloadAction; +type SagaPromiseAction = SagaPromiseActionBase>; +type SagaPromiseActionMutated = SagaPromiseActionBase>; -type ActionCreatorWithPreparedPayloadAndMeta, PA extends PrepareAction> = +type ActionCreatorWithPreparedPayloadAndMeta, PA extends PrepareAction> = ActionCreatorWithPreparedPayload, P, T, never, ReturnType extends { meta: infer InferM & M; } ? InferM : M>; -type PayloadActionAndMeta> = PayloadAction; +type Sagas = { + implement: (action: SagaPromiseAction, executor: TriggerExecutor) => Generator>>, void, any>, + resolve: (action: SagaPromiseAction, value: V) => ReturnType, + reject: (action: SagaPromiseAction, error: any) => ReturnType +}; + +type SagasFromAction = { + sagas: Sagas +}; + +type TypesFromAction> = { + types: { + triggerAction: SagaPromiseActionBase, + resolvedAction: PayloadAction, + rejectedAction: PayloadAction + promise: Promise, + resolveValue: V + } +}; + +type TriggerActionCreator, TPAC extends PayloadActionCreator> = ActionCreatorWithPreparedPayloadAndMeta; -function isTriggerAction(action: PayloadAction>): action is PayloadAction> { +type SagaPromiseActionCreatorBase, TPAC extends PayloadActionCreator> = TriggerActionCreator & { + trigger: SagaPromiseActionCreatorBase + resolved: ActionCreatorWithPayload; + rejected: ActionCreatorWithPayload; +} & SagasFromAction & TypesFromAction; + +export type SagaPromiseActionCreator> = SagaPromiseActionCreatorBase, TPAC>; + +function isTriggerAction(action: SagaPromiseAction) { return action?.meta?.promiseActions.resolved != null; } -function verify(action, method) { - if (!isTriggerAction(action)) throw new ArgumentError(`redux-saga-promise: ${method}: First argument must be a promise trigger action, but got ${action}`); - if (!isFunction(action?.meta?.promiseResolution?.resolve)) throw new ConfigurationError(`redux-saga-promise: ${method}: Unable to execute - it seems that the passed action was not processed by the promiseMiddleware. (1. Did you included the promiseMiddlware before SagaMiddleware? 2. Have you dispatched the action to the store before using it?)`); +function isActionSagaPromise(action: SagaPromiseAction, method): action is SagaPromiseActionMutated { + 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`); + return true; } -type ResolveValueFromTriggerAction> = A extends { - meta: { - [promiseSymbol]: { - resolveValueType: infer V - }; - } +type ResolveValueFromTriggerAction = TAction extends { + meta: PromiseFromMeta; } ? V : never; -function resolvePromise>>(action: TA, value: ResolveValueFromTriggerAction) { +function resolvePromise(action: SagaPromiseActionMutated, value: any) { return action.meta.promiseResolution.resolve(value); } -function rejectPromise>>(action: TA, error: any) { +function rejectPromise(action: SagaPromiseActionMutated, error: any) { return action.meta.promiseResolution.reject(error); } @@ -74,8 +95,10 @@ function rejectPromise>(action: TA, executor: TriggerExecutor>) { - verify(action, "implementPromiseAction"); +export function* implementPromiseAction>(action: TAction, executor: TriggerExecutor>) { + if (!isActionSagaPromise(action, "implementPromiseAction")) { + return; // Never hit, exception is thrown before + } try { resolvePromise(action, yield call(executor)); @@ -87,16 +110,22 @@ export function* implementPromiseAction>(action: TA, value: ResolveValueFromTriggerAction) { - verify(action, "resolvePromiseAction"); +export function* resolvePromiseAction>(action: TAction, value: ResolveValueFromTriggerAction) { + if (!isActionSagaPromise(action, "resolvePromiseAction")) { + return; // Never hit, exception is thrown before + } + yield call(resolvePromise, action, value); } /** * Saga to reject a promise. */ -export function* rejectPromiseAction>(action: TA, error: any) { - verify(action, "rejectPromiseAction"); +export function* rejectPromiseAction>(action: TAction, error: any) { + if (!isActionSagaPromise(action, "rejectPromiseAction")) { + return; // Never hit, exception is thrown before + } + yield call(rejectPromise, action, error); } @@ -109,52 +138,39 @@ function createPromiseActions(type: T) { type TriggerExecutor = (() => PromiseLike | RT | Iterator); -function createUpdatedTrigger>( +function wrapTriggerAction>( type: T, - triggerAction: TA, -) { + triggerAction: TPAC, +): SagaPromiseActionCreator { const { resolvedAction, rejectedAction } = createPromiseActions(type); - const updatedTrigger = createAction(type, (...args: any[]) => merge(triggerAction.apply(null, args), { - meta: { + const updatedTrigger = , TPAC>>createAction(type, (...args: any[]) => merge(triggerAction.apply(null, args), { + meta: >{ promiseActions: { resolved: resolvedAction, rejected: rejectedAction, }, - [promiseSymbol]: {}, - } as PromiseActionsFromMeta, - })) as ActionCreatorWithPreparedPayloadAndMeta, typeof triggerAction>; - - const types: { - triggerAction: PayloadActionAndMeta>, - resolvedAction: PayloadAction, - rejectedAction: PayloadAction - promise: Promise>>>, - resolvedValue: V - } = {} as any; - - const sagas = { - implement: implementPromiseAction as >(action: TA2, executor: TriggerExecutor) => ReturnType, - resolve: resolvePromiseAction as (action: TA2, value: ResolveValueFromTriggerAction) => ReturnType, - reject: rejectPromiseAction as (action: TA2, error: any) => ReturnType, + }, + })); + + const sagas = >{ + implement: implementPromiseAction, + resolve: resolvePromiseAction, + reject: rejectPromiseAction, }; - return Object.assign(updatedTrigger, { + return Object.assign(updatedTrigger, { trigger: updatedTrigger, resolved: resolvedAction, rejected: rejectedAction, sagas, - }) as (typeof updatedTrigger - & SymbolTagged - & PromiseActionsFromTriggerAction - & { sagas: typeof sagas } - & { /** Only used for type resolution. It does not contain any values. */ types: typeof types }); + }); } function createPromiseAction(type: T) { const triggerAction = createAction(type); - return createUpdatedTrigger( + return wrapTriggerAction( type, triggerAction, ); @@ -163,7 +179,7 @@ function createPromiseAction(type: function createPreparedPromiseAction = PrepareAction, T extends string = string>(type: T, prepareAction: PA) { const triggerAction = createAction(type, prepareAction); - return createUpdatedTrigger["payload"], T, typeof triggerAction>( + return wrapTriggerAction["payload"], T, typeof triggerAction>( type, triggerAction, ); @@ -226,7 +242,7 @@ export const promiseMiddleware: Middleware = (store: MiddlewareAPI) => (next: Di } as PromiseResolutionFromMeta, }))); - return merge(promise, { meta: { promise } as PromiseInstanceFromMeta }); + return merge(promise, { meta: { promise } as PromiseFromMeta }); } return next(action);