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

Categories

This post still working for you?

It's been a while since this was posted. Hopefully the information in here is still useful to you (if it isn't please let me know!). If you want to get the new stuff as soon as it's out though, sign up to the mailing list below.

Join the Mailing list
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

Like what you're reading? Get it in your inbox