Skip to content

yuhr/callable-deferred-promise

Repository files navigation

Callable Deferred Promise

NPM npm semantic-release microbundle

A thin wrapper class to create a callable deferred Promise.

Features

  • Zero-dependency
  • TypeScript support
  • ESM/CJS/UMD support

Example

import CallableDeferredPromise from "callable-deferred-promise"

type Options = { foo: "bar" }
const performSomeAsyncTask = async (
  options?: Options | undefined,
): Promise<void> => {
  // ...
}
const it = {
  resolves: new CallableDeferredPromise<[Options], void>(
    (args: [Options] | undefined) => resolve => {
      // `args` is `undefined` if it wasn't called
      if (args) {
        const [options] = args
        resolve(performSomeAsyncTask(options))
      } else {
        resolve(performSomeAsyncTask())
      }
    },
  ),
}

// use without a call
await it.resolves

// use with a call
const options: Options = { foo: "bar" }
await it.resolves(options)

Why?

When your library is for networking, and it has some “core” API that has to be called every time your library is used, such as for initialization, then it may contain some async operations such as to establish connection to the backend, and such API may accept some options.

Say Database is a class, and Database.connect is a static async function which resolves to an instance of Database. Possible usage may look like:

// Initialize without options
const database = await Database.connect()

// Initialize with options
const database = await Database.connect({ ...options })

But calling a function without arguments looks a bit nonsense. Fortunately we have a way to remove the empty parentheses: a callable deferred promise.

// Initialize without options
const database = await Database.connect

// Initialize with options
const database = await Database.connect({ ...options })

The function passed to the constructor of CallableDeferredPromise is executed at the first time it was awaited, that's why this is called “deferred”. Even if the instance of CallableDeferredPromise was called, it's just to set the arguments to the internal variable, and the original function is not invoked immediately. You always have to await to run the original function.

CAVEAT: Use it carefully. It's unusual to perform some code without a call, so it may confuse users. You should use it only for the exact “core” API, which is the most appearance-sensitive part of your library in terms of branding (e.g. if your brand name is Foobar Online and the domain name is “foobar.online”, you want the initialization idiom for your library to be await Foobar.online).