loading...

Polling with async/await

jakubkoci profile image jakubkoci ・2 min read

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.

Posted on by:

Discussion

pic
Editor guide
 

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.