paint-brush
HTTP Polling - the Good, the Bad and the Uglyby@pyotruk
266 reads

HTTP Polling - the Good, the Bad and the Ugly

by pyotrukSeptember 17th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Explore three approaches to implementing short HTTP polling using client-side JavaScript. This article covers a basic method with callbacks, a more modern approach using async/await, and a streamlined solution with RxJS for handling complex asynchronous logic. Discover the strengths and considerations of each technique to choose the best fit for your project.
featured image - HTTP Polling - the Good, the Bad and the Ugly
pyotruk HackerNoon profile picture


In this article, I'll cover three approaches to implementing short HTTP polling, using client-side JavaScript (TypeScript). Short polling involves making repeated, separate requests to check the status of a resource.


Imagine we have an API with two endpoints: postTask and getTask. After posting a task (using the postTask endpoint), we need to continuously poll the getTask endpoint until it returns a status of either DONE or FAILED.

We'll start by exploring the Ugly approach.

The Ugly

The first thing that comes into mind is a recursive setTimeout:

function handlePolling(
  taskId: string,
  done: (result: string) => void,
  failed: (err: string) => void,
): void {
  API.getTask(taskId).then(response => {
    switch (response.status) {
      case 'IN_PROGRESS':
        setTimeout(() => handlePolling(taskId, done, failed), 1000);
        break;
      case 'DONE':
        return done(response.result);
      case 'FAILED':
        return failed('polling failed');
      default:
        return failed(`unexpected status = ${response.status}`);
    }
  }).catch(err => {
    failed(`getTask failed - ${err}`);
  });
}


This approach is quite straightforward but comes with a couple of notable flaws:


  • Callbacks: done & failed.

    While this is largely a matter of preference, using callbacks can start to resemble the infamous "callback hell" from early Node.js days. If we tried to return a Promise, we'd encounter a branching structure since each Promise could either resolve or reject. As a result, we're forced to stick with callbacks for simplicity.

  • Recursion

    The bigger issue is that recursion can make debugging more difficult. Each recursive call adds complexity, making it harder to track the flow and pinpoint where things go wrong.

The (Not-So) Bad

Let’s rewrite it in a more linear fashion with async & await:

const sleep = (timeout: number) => new Promise((resolve) => setTimeout(resolve, timeout));

async function handlePolling(taskId: string): Promise<string> {
  while (true) {
    try {
      await sleep(1000);
      response = await API.getTask(taskId);

      switch (response.status) {
        case 'IN_PROGRESS':
          continue;
        case 'DONE':
          return response.result;
        case 'FAILED':
          return Promise.reject('polling failed');
        default:
          return Promise.reject(`unexpected status = ${response.status}`);
      }
    } catch (err) {
      return Promise.reject(`getTask failed - ${err}`);
    }
  }
}


This approach is much cleaner. We've encapsulated setTimeout within a sleep function, and we now return a Promise that can be awaited. The code is more readable and easier to maintain.


The only detail that stands out is the while(true) loop. This can be improved by using while(status === 'IN_PROGRESS'), which makes the intent clearer. Additionally, it's a good idea to implement a safety mechanism to limit the number of getTask requests. This helps protect against the rare case where the API might get stuck in an infinite loop or experience an unexpected delay.

The Good

The approach I prefer the most is using RxJS — a library specifically designed for handling event streams. You can think of it as “lodash for Promises.” RxJS provides a powerful and flexible way to manage asynchronous events like HTTP polling, making it easier to handle complex workflows in a more declarative manner, e.g.:


function handlePolling(taskId: string) {
  return new Promise<string>((resolve, reject) => {
    interval(1000).pipe(
      switchMap(() => API.getTask(taskId)), // runs the request each time the interval emits
      takeWhile(response => response.status === 'IN_PROGRESS', true),
      filter(response => response.status !== 'IN_PROGRESS'),
    ).subscribe({
      next: response => {
        switch (response.status) {
          case 'DONE':
            return response.result;
          case 'FAILED':
            return Promise.reject('polling failed');
          default:
            return Promise.reject(`unexpected status = ${response.status}`);
        }
      },
      error: err => reject(`getTask failed - ${err}`),
    });
}


What I love about RxJS is how straightforward it makes the code. With the takeWhile operator, we clearly define when to stop the loop, and by using the filter operator, we can skip handling any responses where the status is still IN_PROGRESS. This creates a clean, declarative flow that's easy to read and maintain.

Conclusion

Both the "Good" and the "Not-So-Bad" approaches are viable ways to handle HTTP polling. However, if your project involves a significant amount of asynchronous logic — such as polling, interdependent async tasks, background loading or complex calculations — then it’s worth considering adopting RxJS. Its powerful tools for managing streams of asynchronous events can greatly simplify your code and make it more maintainable in the long run.