Adding to the NodeJS Package Heap: async-wait-for-promise

Categories
NodeJS logo

tl;dr - I published async-wait-for-promise to NPM which helps you wait for the result of a promise with a timeout. It’s very similar but slightly different from some other small NPM packages. You can find the code on GitLab.

Well I guess we all joke about things like this until you’re part of it. I recently found that I wanted to wait for a specific condition in my testing code but couldn’t find code that would work exactly like I wanted to use it, so I wrote my own package – async-wait-for-promise.

Prior Art

In my search for a library that already performed this functionality I did the bare minimum of at least trying to use some pre-existing libraries to see if they could fit my usecase, but none of them fit quite right:

wait-until

On NPM

This was good, but I wanted the condition to return a Promise, for two reasons:

  • Being able to return a function that did some other work
  • Use that nice async/await syntax

async-wait-until

On NPM

This package was really weird, it actually didn’t really work for me at all, and the condition wasn’t a promise which was the same issue with wait-until.

promise-timeout

On NPM

I’ve used this in the past and it’s fantastic, but it’s not quite right for my usecase this time, as I want to wait on a condition, not just a timeout. Maybe I want a condition and a timeout but not only a timeout.

The code that powers async-wait-for-promise

Surprised that I couldn’t find something that matched well enough so I wrote my own, The logic basically amounts to this:

/**
 * Wait until a given function produces a value
 *
 * @param {object} opts
 * @param {Function} opts.fn - the function used ot control checking, waiting until true is returned
 * @param {number} opts.intervalMS - the amount of time to wait in milliseconds
 * @param {number} opts.timeoutMS - the total amount of time to wait before issuing a TimeoutError
 * @returns {Promise<void>}
 */
export async function waitUntil<T>(
  fn: () => Promise<T | null>,
  opts?: {
    intervalMS?: number,
    timeoutMS?: number,
  },
): Promise<T> {
  const intervalMS = opts?.intervalMS ?? 500;
  const timeoutMS = opts?.timeoutMS ?? 10000;

  return new Promise<T>((resolve, reject) => {
    // Check every second for function to evaluate to true
    const startTime = new Date().getTime();
    const interval = setInterval(() => {
      // Ensure opts are still properly formed
      if (!fn || typeof fn !== "function") {
        clearInterval(interval);
        reject(new Error("function became invalid/missing"));
        return;
      }

      // If we've waited too long then clear interval and exit
      const elapsedMs = new Date().getTime() - startTime;
      if (elapsedMs >= timeoutMS) {
        clearInterval(interval);
        reject(new Error("function never resolved to true before timeout"));
        return;
      }

      return fn()
        .then(res => {
          if (res === null) { return; }

          clearInterval(interval);
          resolve(res);
        })
        .catch((err) => {
          clearInterval(interval);
          reject(err);
        });

    }, intervalMS);
  });
}

I did waver over whether to use setInterval or do a while loop or something, but the choice is pretty obvious there, use the built-ins that are nice and efficient instead of tying up a thread.

An example of when I needed this code (and essentially wrote it)

Here’s an example of some code where I could used this package had it existed:

        // Wait for a given function to evaluate to true
        if (opts.waitFor && opts.waitFor.fn) {
            // Check every second for function to evaluate to true
            const startTime = new Date().getTime();
            const interval = setInterval(() => {
                // Ensure opts are still properly formed
                if (!opts || !opts.waitFor || !opts.waitFor.fn || !opts.waitFor.fn.timeoutMs) {
                    clearInterval(interval);
                    reject(new Error("waitFor object became improperly formed"));
                    return;
                }

                // If we've waited too long then clear interval and exit
                const elapsedMs = new Date().getTime() - startTime;
                if (elapsedMs >= opts.waitFor.fn.timeoutMs) {
                    clearInterval(interval);
                    reject(new Error("function never resolved to true before timeout"));
                    return;
                }

                // If we haven't waited too long, check the function
                opts.waitFor.fn.check({containerProcess, opts})
                    .then(res => {
                        if (!res) { return; }
                        clearInterval(interval);
                        resolve({containerProcess, opts});
                    })
                    .catch(() => undefined);
            }, 1000);

            return;
        }

BONUS: Makefile NPM release tooling

Releasing to NPM was pretty easy, used some Makefile lines from another project to make things easy:

#############
# Packaging #
#############

PACKAGE_FILENAME ?= $(PACKAGE_NAME)-v$(VERSION).tgz
TARGET_DIR ?= target
PACKAGE_PATH ?= $(TARGET_DIR)/$(PACKAGE_FILENAME)

target-dir:
    mkdir -p $(TARGET_DIR)

print-package-filename:
    @echo "$(PACKAGE_FILENAME)"

# NOTE: if you try to test this package locally (ex. using `yarn add path/to/async-wait-for-promise-<version>.tgz`),
# you will have to `yarn cache clean` between every update.
# as one command: `yarn cache clean && yarn remove async-wait-for-promise && yarn add path/to/async-wait-for-promise-v0.1.0.tgz`
package: clean build target-dir
    $(YARN) pack
    mv $(PACKAGE_FILENAME) $(TARGET_DIR)/

publish: package
    $(YARN) publish \
        --tag latest \
        --new-version $(VERSION) \
        $(PACKAGE_PATH)

publish-prerelease: package
    $(YARN) publish \
        --tag pre \
        --new-version $(VERSION) \
        $(PACKAGE_PATH)

Wrapup

Well the code is out there, feel free to take a look at it and tell me how I can make it better. Wasn’t hard to write, and I look forward to using it myself in the future