This article is based on my real experience with refactoring a small piece of code with polling function so I won’t be starting from the scratch. I know its not rocket science, but I was looking for the solution for polling in the past and I found similar articles very helpful. It’s also a nice demonstration of how async/await
and higher-order functions could help to code maintainability and readability.
I have been using the following piece of code for some polling functionality for a while.
function originalPoll(fn, end) {
async function checkCondition() {
const result = await fn();
console.log("result", result);
if (result < 3) {
setTimeout(checkCondition, 3000);
} else {
end();
}
}
checkCondition();
}
It takes function fn
and calls it every 3 seconds until it gets required result (I simplified it here to a condition result < 3
), then it calls callback end
passed as the second parameter. The function somehow works and does what I need. But, it’s not possible to re-use it with a different condition. So I decided to refactor it a little bit. After a few minutes of thinking and tweaking I finally ended up with this simplification:
async function poll(fn, fnCondition, ms) {
let result = await fn();
while (fnCondition(result)) {
await wait(ms);
result = await fn();
}
return result;
}
function wait(ms = 1000) {
return new Promise(resolve => {
console.log(`waiting ${ms} ms...`);
setTimeout(resolve, ms);
});
}
This function still calls the fn
function repeatedly, but now it takes also another parameter fnCondition
which is called with the result of calling the fn
function. Function poll
will be calling the function fn
until the function fnCondition
returns false
. I also extracted setTimeout
function, it improves the readability of the polling function and keeps its responsibility straightforward (I don’t care how the waiting is implemented at this abstraction level). We also got rid of function inside a function which just added unnecessary complexity.
I didn’t start with a test first to be honest. Anyway, I still wanted to check my design, provide some safety for the future refactoring and also to document how to call the poll
function. We can nicely achieve all of that by adding some tests:
describe("poll", () => {
it("returns result of the last call", async () => {
const fn = createApiStub();
const fnCondition = result => result < 3;
const finalResult = await poll(fn, fnCondition, 1000);
expect(finalResult).toEqual(3);
});
it("calls api many times while condition is satisfied", async () => {
const fn = createApiStub();
const fnCondition = result => result < 3;
await poll(fn, fnCondition, 1000);
expect(fn).toHaveBeenCalledTimes(3);
});
function createApiStub() {
let counter = 0;
const testApi = () => {
console.log("calling api", counter);
counter++;
return counter;
};
return jest.fn(() => testApi());
}
});
I’m using Jest as a testing library. It has great support for testing async functions and provides stubs with assertions by default. Function createApiStub
is here just for the purpose of the test and assertions and it'll represent our real API call or whatever function we would want to poll.
You can find and run the code in this CodeSandbox: Polling with async/await - CodeSandbox.
Top comments (2)
I have similar code where I would like to use polling.
What doesnt make sense how you are in first example
branching on result if await makes code flow synchronous,
so if-else in async block should be called after returning result from
asynchronous fn().
Thanks for the great article. Has been really helpful in getting my async-driven polling mechanism in place. Still a bit to go (polling 2 URLs in parallel but resolving just one Promise) but this was a great push in the right direction.