promise-toolbox

    0.19.2 • Public • Published

    promise-toolbox

    Package Version Build Status PackagePhobia Latest Commit

    Essential utils for promises.

    Features:

    • compatible with all promise implementations
    • small (< 150 KB with all dependencies, < 5 KB with gzip)
    • nice with ES2015 / ES2016 syntax

    Table of contents:

    Node & Browserify/Webpack

    Installation of the npm package:

    > npm install --save promise-toolbox
    

    Browser

    You can directly use the build provided at unpkg.com:

    <script src="https://unpkg.com/promise-toolbox@0.8/dist/umd.js"></script>

    Usage

    Promise support

    If your environment may not natively support promises, you should use a polyfill such as native-promise-only.

    On Node, if you want to use a specific promise implementation, Bluebird for instance to have better performance, you can override the global Promise variable:

    global.Promise = require("bluebird");

    Note that it should only be done at the application level, never in a library!

    Imports

    You can either import all the tools directly:

    import * as PT from "promise-toolbox";
    
    console.log(PT.isPromise(value));

    Or import individual tools from the main module:

    import { isPromise } from "promise-toolbox";
    
    console.log(isPromise(value));

    Each tool is also exported with a p prefix to work around reserved keywords and to help differentiate with other tools (like lodash.map):

    import { pCatch, pMap } from "promise-toolbox";

    If you are bundling your application (Browserify, Rollup, Webpack, etc.), you can cherry-pick the tools directly:

    import isPromise from "promise-toolbox/isPromise";
    import pCatch from "promise-toolbox/catch";

    API

    Cancelation

    This library provides an implementation of CancelToken from the cancelable promises specification.

    A cancel token is an object which can be passed to asynchronous functions to represent cancelation state.

    import { CancelToken } from "promise-toolbox";

    Creation

    A cancel token is created by the initiator of the async work and its cancelation state may be requested at any time.

    // Create a token which requests cancelation when a button is clicked.
    const token = new CancelToken(cancel => {
      $("#some-button").on("click", () => cancel("button clicked"));
    });
    const { cancel, token } = CancelToken.source();

    A list of existing tokens can be passed to source() to make the created token follow their cancelation:

    // `source.token` will be canceled (synchronously) as soon as `token1` or
    // `token2` or token3` is, with the same reason.
    const { cancel, token } = CancelToken.source([token1, token2, token3]);

    Consumption

    The receiver of the token (the function doing the async work) can:

    1. synchronously check whether cancelation has been requested
    2. synchronously throw if cancelation has been requested
    3. register a callback that will be executed if cancelation is requested
    4. pass the token to subtasks
    // 1.
    if (token.reason) {
      console.log("cancelation has been requested", token.reason.message);
    }
    
    // 2.
    try {
      token.throwIfRequested();
    } catch (reason) {
      console.log("cancelation has been requested", reason.message);
    }
    
    // 3.
    token.promise.then(reason => {
      console.log("cancelation has been requested", reason.message);
    });
    
    // 4.
    subtask(token);

    See asyncFn.cancelable for an easy way to create async functions with built-in cancelation support.

    Registering async handlers

    Asynchronous handlers are executed on token cancelation and the promise returned by the cancel function will wait for all handlers to settle.

    function httpRequest(cancelToken, opts) {
      const req = http.request(opts);
      req.end();
      cancelToken.addHandler(() => {
        req.abort();
    
        // waits for the socket to really close for the cancelation to be
        // complete
        return fromEvent(req, "close");
      });
      return fromEvent(req, "response");
    }
    
    const { cancel, token } = CancelToken.source();
    
    httpRequest(token, {
      hostname: "example.org",
    }).then(response => {
      // do something with the response of the request
    });
    
    // wraps with Promise.resolve() because cancel only returns a promise
    // if a handler has returned a promise
    Promise.resolve(cancel()).then(() => {
      // the request has been properly canceled
    });

    Is cancel token?

    if (CancelToken.isCancelToken(value)) {
      console.log("value is a cancel token");
    }

    @cancelable decorator

    This is deprecated, instead explicitely pass a cancel token or an abort signal:

    const asyncFunction = async (a, b, { cancelToken = CancelToken.none } = {}) => {
      cancelToken.promise.then(() => {
        // do stuff regarding the cancelation request.
      });
    
      // do other stuff.
    };

    Make your async functions cancelable.

    If the first argument passed to the cancelable function is not a cancel token, a new one is created and injected and the returned promise will have a cancel() method.

    import { cancelable, CancelToken } from "promise-toolbox";
    
    const asyncFunction = cancelable(async ($cancelToken, a, b) => {
      $cancelToken.promise.then(() => {
        // do stuff regarding the cancelation request.
      });
    
      // do other stuff.
    });
    
    // Either a cancel token is passed:
    const source = CancelToken.source();
    const promise1 = asyncFunction(source.token, "foo", "bar");
    source.cancel("reason");
    
    // Or the returned promise will have a cancel() method:
    const promise2 = asyncFunction("foo", "bar");
    promise2.cancel("reason");

    If the function is a method of a class or an object, you can use cancelable as a decorator:

    class MyClass {
      @cancelable
      async asyncMethod($cancelToken, a, b) {
        // ...
      }
    }

    Compatibility with AbortSignal

    A cancel token can be created from an abort signal:

    const token = CancelToken.from(abortSignal);

    If abortSignal is already a CancelToken, it will be returned directly, making it a breeze to create code accepting both :-)

    A cancel token is API compatible with an abort signal and can be used as such:

    const { cancel, token } = CancelToken.source();
    
    await fetch(url, { signal: token });

    Resource management

    See Bluebird documentation for a good explanation.

    Creation

    A disposable is a simple object, which contains a dispose method and possibily a value:

    const disposable = { dispose: () => db.close(), value: db };

    The dispose method may be asynchronous and return a promise.

    As a convenience, you can use the Disposable class:

    import { Disposable } from "promise-toolbox";
    
    const disposable = new Disposable(() => db.close(), db);

    If the process is more complicated, maybe because this disposable depends on other disposables, you can use a generator function alongside the Disposable.factory decorator:

    const getTable = Disposable.factory(async function*() {
      // simply yield a disposable to use it
      const db = yield getDb();
    
      const table = await db.getTable();
      try {
        // yield the value to expose it
        yield table;
      } finally {
        // this is where you can dispose of the resource
        await table.close();
      }
    });

    Combination

    Independent disposables can be acquired and disposed in parallel, to achieve this, you can use Disposable.all:

    const combined = await Disposable.all([disposable1, disposable2]);

    Similarly to Promise.all, the value of such a disposable, is an array whose values are the values of the disposables combined.

    Consumption

    To ensure all resources are properly disposed of, disposables must never be used manually, but via the Disposable.use function:

    import { Disposable } from "promise-toolbox";
    
    await Disposable.use(
      // Don't await the promise here, resource acquisition should be handled by
      // `Disposable.use` otherwise, in case of failure, other resources may failed
      // to be disposed of.
      getTable(),
    
      // If the function can throw synchronously, a wrapper function can be passed
      // directly to `Disposable.use`.
      () => getTable(),
    
      async (table1, table2) => {
        // do something with table1 and table 2
        //
        // both `table1` and `table2` are guaranteed to be deallocated by the time
        // the promise returned by `Disposable.use` is settled
      }
    );

    For more complex use cases, just like Disposable.factory, the handler can be a generator function when no disposables are passed:

    await Disposable.use(async function*() {
      const table1 = yield getTable();
      const table2 = yield getTable();
    
      // do something with table1 and table 2
      //
      // both `table1` and `table2` are guaranteed to be deallocated by the time
      // the promise returned by `Disposable.use` is settled
    });

    Functions

    asyncFn(generator)

    Create an async function from a generator function

    Similar to Bluebird.coroutine.

    import { asyncFn } from 'promise-toolbox'
    
    const getUserName = asyncFn(function * (db, userId)) {
      const user = yield db.getRecord(userId)
      return user.name
    })

    asyncFn.cancelable(generator, [getCancelToken])

    Like asyncFn(generator) but the created async function supports cancelation.

    Similar to CAF.

    import { asyncFn, CancelToken } from 'promise-toolbox'
    
    const getUserName = asyncFn.cancelable(function * (cancelToken, db, userId)) {
      // this yield will throw if the cancelToken is activated
      const user = yield db.getRecord(userId)
      return user.name
    })
    
    const source = CancelToken.source()
    
    getUserName(source.token, db, userId).then(
      name => {
        console.log('user name is', name)
      },
      error => {
        console.error(error)
      }
    )
    
    // only wait 5 seconds to fetch the user from the database
    setTimeout(source.cancel, 5e3)
    const cancelableAsyncFunction = asyncFn.cancelable(function*(
      cancelToken,
      ...args
    ) {
      // await otherAsyncFunction() but will throw if cancelToken is activated
      yield otherAsyncFunction();
    
      // if aborting on cancelation is unwanted (eg because the called function
      // already handles cancelation), wrap the promise in an array
      yield [otherAsyncFunction(cancelToken)];
    
      // cancelation, just like any rejection, can be catch
      try {
        yield otherAsyncFunction();
      } catch (error) {
        if (CancelToken.isCancelToken(error)) {
          // do some clean-up here
          // the rest of the function has been added as an async handler of the
          // CancelToken which will make `cancel` waits for it
        }
    
        throw error;
      }
    
      return result;
    });

    If the cancel token is not the first param of the decorated function, a getter should be passed to asyncFn.cancelable, it's called with the same context and arguments as the decorated function and returns the cancel token:

    const cancelableAsyncFunction = asyncFn.cancelable(
      function*(arg1, arg2, options) {
        // function logic
      },
      (_arg1, _arg2, { cancelToken = CancelToken.none } = {}) => cancelToken;
    );

    defer()

    Discouraged but sometimes necessary way to create a promise.

    import { defer } from "promise-toolbox";
    
    const { promise, resolve } = defer();
    
    promise.then(value => {
      console.log(value);
    });
    
    resolve(3);

    fromCallback(fn, arg1, ..., argn)

    Easiest and most efficient way to promisify a function call.

    import { fromCallback } from "promise-toolbox";
    
    // callback is appended to the list of arguments passed to the function
    fromCallback(fs.readFile, "foo.txt").then(content => {
      console.log(content);
    });
    
    // if the callback does not go at the end, you can wrap the call
    fromCallback(cb => foo("bar", cb, "baz")).then(() => {
      // ...
    });
    
    // you can use `.call` to specify the context of execution
    fromCallback.call(thisArg, fn, ...args).then(() => {
      // ...
    });
    
    // finally, if you want to call a method, you can pass its name instead of a
    // function
    fromCallback.call(object, "method", ...args).then(() => {
      // ...
    });

    fromEvent(emitter, event, [options]) => Promise

    Wait for one event. The first parameter of the emitted event is used to resolve/reject the promise.

    const promise = fromEvent(emitter, "foo", {
      // whether the promise resolves to an array of all the event args
      // instead of simply the first arg
      array: false,
    
      // whether the error event can reject the promise
      ignoreErrors: false,
    
      // name of the error event
      error: "error",
    });
    
    promise.then(
      value => {
        console.log("foo event was emitted with value", value);
      },
      reason => {
        console.error("an error has been emitted", reason);
      }
    );

    fromEvents(emitter, successEvents, errorEvents) => Promise

    Wait for one of multiple events. The array of all the parameters of the emitted event is used to resolve/reject the promise.

    The array also has an event property indicating which event has been emitted.

    fromEvents(emitter, ["foo", "bar"], ["error1", "error2"]).then(
      event => {
        console.log(
          "event %s have been emitted with values",
          event.name,
          event.args
        );
      },
      reasons => {
        console.error(
          "error event %s has been emitted with errors",
          event.names,
          event.args
        );
      }
    );

    isPromise(value)

    import { isPromise } from "promise-toolbox";
    
    if (isPromise(foo())) {
      console.log("foo() returns a promise");
    }

    nodeify(fn)

    From async functions return promises, create new ones taking node-style callbacks.

    import { nodeify } = require('promise-toolbox')
    
    const writable = new Writable({
      write: nodeify(async function (chunk, encoding) {
        // ...
      })
    })

    pipe(fns)

    Create a new function from the composition of async functions.

    import { pipe } from "promise-toolbox";
    
    const getUserPreferences = pipe(getUser, getPreferences);

    pipe(value, ...fns)

    Makes value flow through a list of async functions.

    import { pipe } from "promise-toolbox";
    
    const output = await pipe(
      input, // plain value or promise
      transform1, // sync or async function
      transform2,
      transform3
    );

    promisify(fn, [ context ]) / promisifyAll(obj)

    From async functions taking node-style callbacks, create new ones returning promises.

    import fs from "fs";
    import { promisify, promisifyAll } from "promise-toolbox";
    
    // Promisify a single function.
    //
    // If possible, the function name is kept and the new length is set.
    const readFile = promisify(fs.readFile);
    
    // Or all functions (own or inherited) exposed on a object.
    const fsPromise = promisifyAll(fs);
    
    readFile(__filename).then(content => console.log(content));
    
    fsPromise.readFile(__filename).then(content => console.log(content));

    retry(fn, options, [args])

    Retries an async function when it fails.

    import { retry } from "promise-toolbox";
    
    (async () => {
      await retry(
        async () => {
          const response = await fetch("https://pokeapi.co/api/v2/pokemon/3/");
    
          if (response.status === 500) {
            // no need to retry in this case
            throw retry.bail(new Error(response.statusText));
          }
    
          if (response.status !== 200) {
            throw new Error(response.statusText);
          }
    
          return response.json();
        },
        {
          // predicate when to retry, default on always but programmer errors
          // (ReferenceError, SyntaxError and TypeError)
          //
          // similar to `promise-toolbox/catch`, it can be a constructor, an object,
          // a function, or an array of the previous
          when: { message: "my error message" },
    
          // this function is called before a retry is scheduled (before the delay)
          async onRetry(error) {
            console.warn("attempt", this.attemptNumber, "failed with error", error);
            console.warn("next try in", this.delay, "milliseconds");
    
            // Other information available:
            // - this.fn: function that failed
            // - this.arguments: arguments passed to fn
            // - this.this: context passed to fn
    
            // This function can throw to prevent any retry.
    
            // The retry delay will start only after this function has finished.
          },
    
          // delay before a retry, default to 1000 ms
          delay: 2000,
    
          // number of tries including the first one, default to 10
          //
          // cannot be used with `retries`
          tries: 3,
    
          // number of retries (excluding the initial run), default to undefined
          //
          // cannot be used with `tries`
          retries: 4,
    
          // instead of passing `delay`, `tries` and `retries`, you can pass an
          // iterable of delays to use to retry
          //
          // in this example, it will retry 3 times, first after 1 second, then
          // after 2 seconds and one last time after 4 seconds
          //
          // for more advanced uses, see https://github.com/JsCommunity/iterable-backoff
          delays: [1e3, 2e3, 4e3],
        }
      );
    })().catch(console.error.bind(console));

    The most efficient way to make a function automatically retry is to wrap it:

    MyClass.prototype.myMethod = retry.wrap(MyClass.prototype.myMethod, {
      delay: 1e3,
      retries: 10,
      when: MyError,
    });

    In that case options can also be a function which will be used to compute the options from the context and the arguments:

    MyClass.prototype.myMethod = retry.wrap(
      MyClass.prototype.myMethod,
      function getOptions(arg1, arg2) {
        return this._computeRetryOptions(arg1, arg2);
      }
    );

    try(fn)

    Starts a chain of promises.

    import PromiseToolbox from "promise-toolbox";
    
    const getUserById = id =>
      PromiseToolbox.try(() => {
        if (typeof id !== "number") {
          throw new Error("id must be a number");
        }
        return db.getUserById(id);
      });

    Note: similar to Promise.resolve().then(fn) but calls fn() synchronously.

    wrapApply(fn, args, [thisArg]) / wrapCall(fn, arg, [thisArg])

    Wrap a call to a function to always return a promise.

    function getUserById(id) {
      if (typeof id !== "number") {
        throw new TypeError("id must be a number");
      }
      return db.getUser(id);
    }
    
    wrapCall(getUserById, "foo").catch(error => {
      // id must be a number
    });

    Pseudo-methods

    This function can be used as if they were methods, i.e. by passing the promise (or promises) as the context.

    This is extremely easy using ES2016's bind syntax.

    const promises = [Promise.resolve("foo"), Promise.resolve("bar")];
    
    promises::all().then(values => {
      console.log(values);
    });
    // → [ 'foo', 'bar' ]

    If you are still an older version of ECMAScript, fear not: simply pass the promise (or promises) as the first argument of the .call() method:

    const promises = [Promise.resolve("foo"), Promise.resolve("bar")];
    
    all.call(promises).then(function(values) {
      console.log(values);
    });
    // → [ 'foo', 'bar' ]

    promise::asCallback(cb)

    Register a node-style callback on this promise.

    import { asCallback } from "promise-toolbox";
    
    // This function can be used either with node-style callbacks or with
    // promises.
    function getDataFor(input, callback) {
      return dataFromDataBase(input)::asCallback(callback);
    }

    promise::catch(predicate, cb)

    Similar to Promise#catch() but:

    • support predicates
    • do not catch ReferenceError, SyntaxError or TypeError unless they match a predicate because they are usually programmer errors and should be handled separately.
    somePromise
      .then(() => {
        return a.b.c.d();
      })
      ::pCatch(TypeError, ReferenceError, reason => {
        // Will end up here on programmer error
      })
      ::pCatch(NetworkError, TimeoutError, reason => {
        // Will end up here on expected everyday network errors
      })
      ::pCatch(reason => {
        // Catch any unexpected errors
      });

    promise::delay(ms, [value])

    Delays the resolution of a promise by ms milliseconds.

    Note: the rejection is not delayed.

    console.log(await Promise.resolve("500ms passed")::delay(500));
    // → 500 ms passed

    Also works with a value:

    console.log(await delay(500, "500ms passed"));
    // → 500 ms passed

    Like setTimeout in Node, it is possible to unref the timer:

    await delay(500).unref();

    collection::forEach(cb)

    Iterates in order over a collection, or promise of collection, which contains a mix of promises and values, waiting for each call of cb to be resolved before the next one.

    The returned promise will resolve to undefined when the iteration is complete.

    ["foo", Promise.resolve("bar")]::forEach(value => {
      console.log(value);
    
      // Wait for the promise to be resolve before the next item.
      return new Promise(resolve => setTimeout(resolve, 10));
    });
    // →
    // foo
    // bar

    promise::ignoreErrors()

    Ignore (operational) errors for this promise.

    import { ignoreErrors } from "promise-toolbox";
    
    // will not emit an unhandled rejection error if the file does not
    // exist
    readFileAsync("foo.txt")
      .then(content => {
        console.log(content);
      })
      ::ignoreErrors();
    
    // will emit an unhandled rejection error due to the typo
    readFileAsync("foo.txt")
      .then(content => {
        console.lgo(content); // typo
      })
      ::ignoreErrors();

    promise::finally(cb)

    Execute a handler regardless of the promise fate. Similar to the finally block in synchronous codes.

    The resolution value or rejection reason of the initial promise is forwarded unless the callback rejects.

    import { pFinally } from "promise-toolbox";
    
    function ajaxGetAsync(url) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.addEventListener("error", reject);
        xhr.addEventListener("load", resolve);
        xhr.open("GET", url);
        xhr.send(null);
      })::pFinally(() => {
        $("#ajax-loader-animation").hide();
      });
    }

    promise::reflect()

    Returns a promise which resolves to an objects which reflects the resolution of this promise.

    import { reflect } from "promise-toolbox";
    
    const inspection = await promise::reflect();
    
    if (inspection.isFulfilled()) {
      console.log(inspection.value());
    } else {
      console.error(inspection.reason());
    }

    promises::some(count)

    Waits for count promises in a collection to be resolved.

    import { some } from "promise-toolbox";
    
    const [first, seconds] = await [
      ping("ns1.example.org"),
      ping("ns2.example.org"),
      ping("ns3.example.org"),
      ping("ns4.example.org"),
    ]::some(2);

    promise::suppressUnhandledRejections()

    Suppress unhandled rejections, needed when error handlers are attached asynchronously after the promise has rejected.

    Similar to Bluebird#suppressUnhandledRejections().

    const promise = getUser()::suppressUnhandledRejections();
    $(document).on("ready", () => {
      promise.catch(error => {
        console.error("error while getting user", error);
      });
    });

    promise::tap(onResolved, onRejected)

    Like .then() but the original resolution/rejection is forwarded.

    Like ::finally(), if the callback rejects, it takes over the original resolution/rejection.

    import { tap } from "promise-toolbox";
    
    // Contrary to .then(), using ::tap() does not change the resolution
    // value.
    const promise1 = Promise.resolve(42)::tap(value => {
      console.log(value);
    });
    
    // Like .then, the second param is used in case of rejection.
    const promise2 = Promise.reject(42)::tap(null, reason => {
      console.error(reason);
    });

    promise::tapCatch(onRejected)

    Alias to promise:tap(null, onRejected).

    promise::timeout(ms, [cb or rejectionValue])

    Call a callback if the promise is still pending after ms milliseconds. Its resolution/rejection is forwarded.

    If the callback is omitted, the returned promise is rejected with a TimeoutError.

    import { timeout, TimeoutError } from "promise-toolbox";
    
    await doLongOperation()::timeout(100, () => {
      return doFallbackOperation();
    });
    
    await doLongOperation()::timeout(100);
    
    await doLongOperation()::timeout(
      100,
      new Error("the long operation has failed")
    );

    Note: 0 is a special value which disable the timeout, useful if the delay is configurable in your app.

    Development

    # Install dependencies
    > npm install
    
    # Run the tests
    > npm test
    
    # Continuously compile
    > npm run dev
    
    # Continuously run the tests
    > npm run dev-test
    
    # Build for production
    > npm run build
    

    Contributions

    Contributions are very welcomed, either on the documentation or on the code.

    You may:

    • report any issue you've encountered;
    • fork and create a pull request.

    License

    ISC © Julien Fontanet

    Install

    npm i promise-toolbox

    DownloadsWeekly Downloads

    3,919

    Version

    0.19.2

    License

    ISC

    Unpacked Size

    99 kB

    Total Files

    59

    Last publish

    Collaborators

    • avatar
    • avatar
    • avatar