Nth Permutation Mathematics

    @teroneko/redux-saga-promise
    TypeScript icon, indicating that this package has built-in type declarations

    3.0.0 • Public • Published

    @teroneko/redux-saga-promise

    Use promises in redux sagas with TypeScript-first approach

    Prologue

    This projects aims to create promise actions (where each action has its deferred promise after surpassing the middleware) that are able to be fullfilled or rejected in redux-saga-manner.

    Initially forked from @adobe/redux-saga-promise but completelly revamped to use createAction from @reduxjs/toolkit to support TypeScript.

    Table of Contents

    Overview

    • Redux middlware, namely promiseMiddleware, used to transform a promise-action-marked action (not promise action yet) to a fully qualified promise action that owns a deferred promise to allow a deferred fulfillment or rejection.
    • Saga helpers namely
      • implementPromiseAction() to resolve and reject by passing either a generator function, an asnyc function or simply a function where the return value is used to fulfill the deferred promise inside the promise action,
      • resolvePromiseAction() to resolve-only the deferred promise inside the promise action and
      • rejectPromiseAction() to reject-only the deferred promise inside the promise action.
    • Lifecyle actions namely
      • promiseAction.trigger (same instance like promiseAction) created by you (via promiseAction()) and dispatched against the redux store,
      • promiseAction.resolved created after the deferred promise has been resolved and
      • promiseAction.rejected created after the deferred promise has been rejected
      • can be used in reducers or wherever you would need these actions.
    • TypeScript helper types

    Installation

    npm install @teroneko/redux-saga-promise
    

    Promise middleware integration

    You must include include promiseMiddleware in the middleware chain, and it must come before sagaMiddleware:

    import { applyMiddleware, createStore } from "redux"
    import { promiseMiddleware }            from "@teroneko/redux-saga-promise"
    import createSagaMiddleware             from "redux-saga"
    
    // ...assuming rootReducer and rootSaga are defined
    const sagaMiddleware = createSagaMiddleware()
    const store          = createStore(rootReducer, {}, applyMiddleware(promiseMiddleware, sagaMiddleware))
    sagaMiddleware.run(rootSaga)

    Promise action creation

    Use one of following method to create a promise action.

    Promise action with type

    Create a promise action (creator) with action-type.

    import { promiseActionFactory } from "@teroneko/redux-saga-promise"
    
    // creates a promise action with only a type (string)
    const actionCreator = promiseActionFactory<resolve_value_type_for_promise>().create(type_as_string)
    const action = promiseActionFactory<resolve_value_type_for_promise>().create(type_as_string)()
    // equivalent to
    const actionCreator = createAction(type_as_string)
    const action = createAction(type_as_string)()

    Promise action with type and payload

    Create a promise action (creator) with action-type and payload.

    import { promiseActionFactory } from "@teroneko/redux-saga-promise"
    
    // creates a promise action with type (string) and payload (any)
    const actionCreator = promiseActionFactory<resolve_value_type_for_promise>().create<payload_type>(type_as_string)
    const action = promiseActionFactory<resolve_value_type_for_promise>().create<payload_type>(type_as_string)({} as payload_type) // "as payload_type" just to show intention
    // equivalent to
    const actionCreator = createAction<payload_type>(type_as_string)
    const action = createAction<payload_type>(type_as_string)({} as payload_type) // "as payload_type" just to show intention

    Promise action with type and payload creator

    Create a promise action (creator) with a action-type and an individial payload (creator).

    import { promiseActionFactory } from "@teroneko/redux-saga-promise"
    
    // creates a promise action with type (string) and payload creator (function)
    const actionCreator = promiseActionFactory<resolve_value_type_for_promise>().create(type_as_string, (payload: payload_type) => { payload })
    const action = promiseActionFactory<resolve_value_type_for_promise>().create(type_as_string, (payload: payload_type) => { payload })({} as payload_type) // "as payload_type" just to show intention
    // equivalent to
    const actionCreator = createAction(type_as_string, (payload: payload_type) => { payload })
    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.

    ⚠️ Keep in mind that the action is not awaitable after its creation but as soon it surpassed the middleware!

    // 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

    because it is up to you as the implementer to resolve or reject the promise"s action in a saga.

    implementPromiseAction

    The most convenient way! You give this helper a saga function which it will execute. If the saga function succesfully returns a value, the promise will resolve with that value. If the saga function throws an error, the promise will be rejected with that error. For example:

    import { call, takeEvery }        from "redux-saga/effects"
    import { promises as fsPromises } from "fs"
    import { implementPromiseAction } from "@teroneko/redux-saga-promise"
    
    import promiseAction from "./promiseAction"
    
    //
    // Asynchronously read a file, resolving the promise with the file"s
    // contents, or rejecting the promise if the file can"t be read.
    //
    function * handlePromiseAction (action) {
      yield call(implementPromiseAction, action, function * () {
        // 
        // Implemented as a simple wrapper around fsPromises.readFile.
        // Rejection happens implicilty if fsPromises.readFile fails.
        //
        const { path } = action.payload
        return yield call(fsPromises.readFile, path, { encoding: "utf8" })
      })
    }
    
    export function * rootSaga () {
      yield takeEvery(promiseAction, handlePromiseAction)
    })

    If you call implementPromiseAction() with a first argument that is not a promise action, it will throw an error (see Argument Validation below).

    resolvePromiseAction

    Sometimes you may want finer control, or want to be more explicit when you know an operation won"t fail. This helper causes the promise to resolve with the passed value. For example:

    import { call, delay, takeEvery } from "redux-saga/effects"
    import { resolvePromiseAction }   from "@teroneko/redux-saga-promise"
    
    import promiseAction from "./promiseAction"
    
    //
    // Delay a given number of seconds then resolve with the given value.
    //
    function * handlePromiseAction (action) {
      const { seconds, value } = action.payload
      yield delay(seconds*1000)
      yield call(resolvePromiseAction, action, value)
    }
    
    function * rootSaga () {
      yield takeEvery(promiseAction, handlePromiseAction)
    })

    If you call resolvePromiseAction() with a first argument that is not a promise action, it will throw an error (see Argument Validation below).

    rejectPromiseAction

    Sometimes you may want finer control, or want to explicitly fail without needing to throw. This helper causes the promise to reject with the passed value, which typically should be an Error. For example:

    import { call, takeEvery }     from "redux-saga/effects"
    import { rejectPromiseAction } from "@teroneko/redux-saga-promise"
    
    import promiseAction from "./promiseAction"
    
    //
    // TODO: Implement this!   Failing for now
    //
    function * handlePromiseAction (action) {
      yield call(rejectPromiseAction, action, new Error("Sorry, promiseAction is not implemented yet")
    }
    
    function * rootSaga () {
      yield takeEvery(promiseAction, handlePromiseAction)
    })

    If you call rejectPromiseAction() with a first argument that is not a promise action, it will throw an error (see Argument Validation below).

    Promise action's reducable lifecycle actions

    Commonly you want the redux store to reflect the status of a promise action: whether it"s pending, what the resolved value is, or what the rejected error is.

    Behind the scenes, promiseAction = promiseActionFactory().create("MY_ACTION") actually creates a suite of three actions:

    • promiseAction.trigger: An alias for promiseAction, which is what you dispatch that then creates the promise.

    • promiseAction.resolved: Dispatched automatically by promiseMiddleware when the promise is resolved; its payload is the resolved value of the promise

    • promiseAction.rejected: Dispatched automatically by promiseMiddleware when the promise is rejected; its payload is the rejection error of the promise

    You can easily use them in handleActions of redux-actions or createReducer of @reduxjs/toolkit:

    import { handleActions } from "redux-actions"
    import promiseAction from "./promiseAction"
    
    //
    // For the readFile wrapper described above, we can keep track of the file in the store
    //
    export const reducer = handleActions({
        [promiseAction.trigger]:  (state, { payload: { path } }) => ({ ...state, file: { path, status: "reading"} }),
        [promiseAction.resolved]: (state, { payload: contents }) => ({ ...state, file: { path: state.file.path, status: "read", contents } }),
        [promiseAction.rejected]: (state, { payload: error })    => ({ ...state, file: { path: state.file.path, status: "failed", error } }),
      }, {})

    Promise action caveats in sagas

    In the sagas that perform your business logic, you may at times want to dispatch a promise action and wait for it to resolve. You can do that using redux-saga"s putResolve Effect:

    const result = yield putResolve(myPromiseAction)

    This dispatches the action and waits for the promise to resolve, returning the resolved value. Or if the promise rejects it will bubble up an error.

    Caution! If you use put() instead of putResolve(), the saga will continue execution immediately without waiting for the promise to resolve.

    Argument validation

    To avoid accidental confusion, all the helper functions validate their arguments and will throw a custom Error subclass ArgumentError in case of error. This error will be bubbled up by redux-saga as usual, and as usual you can catch it in a saga otherwise it will will bubble up to the onError hook. If you want to, you can test the error type, e.g.:

    import { applyMiddleware, compose, createStore } from "redux"
    import { ArgumentError, promiseMiddleware }      from "@teroneko/redux-saga-promise"
    import createSagaMiddleware                      from "redux-saga"
    
    // ...assuming rootReducer and rootSaga are defined
    const sagaMiddleware = createSagaMiddleware({ onError: (error) => {
      if (error instanceof ArgumentError) {
        console.log("Oops, programmer error! I called redux-saga-promise incorrectly:", error)
      } else {
        // ...
      }
    })
    const store = createStore(rootReducer, {}, compose(applyMiddleware(promiseMiddleware, sagaMiddleware)))
    sagaMiddleware.run(rootSaga)

    Additionally, all the helper functions will throw a custom Error subclass ConfigurationError if promiseMiddleware was not properly included in the store.

    TypeScript helpers

    Types

    promiseAction.types does not really exist. It only exists as TypeScript-type to make use of typeof:

    const promiseAction = promiseActionFactory<number>().create("MY_ACTION");
    
    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

    redux-saga cannot infer the parameters and return type of promiseAction correctly when using the call effect or equivalent, so you can use the pre-typed sagas:

    const { implement, resolve, reject } = promiseAction.sagas;
    
    // Instead of this...
    call(implementPromiseAction, promiseAction(), () => 2);
    // ... use this for better TypeScript support:
    call(promiseAction.sagas.implement, promiseAction(), () => 2);

    Contributing

    Build and test

    package.json defines the following scripts:

    • npm build: transpiles the source, placing the result in dist/src/index.js
    • npm test: builds, and then runs the test suite.

    The tests are written using ts-jest;

    Licensing

    This project is licensed under the MIT License. See LICENSE for more information.

    Install

    npm i @teroneko/redux-saga-promise

    DownloadsWeekly Downloads

    111

    Version

    3.0.0

    License

    MIT

    Unpacked Size

    96.1 kB

    Total Files

    34

    Last publish

    Collaborators

    • teroneko