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
.
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
This was good, but I wanted the condition to return a Promise
, for two reasons:
async
/await
syntaxasync-wait-until
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
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.
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.
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;
}
Makefile
NPM release toolingReleasing 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)
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