npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tstsx/poll

v0.0.2

Published

English | [한국어](./README.ko.md)

Readme

English | 한국어

@tstsx/poll

Robust polling utility with retry logic, custom intervals, and abort support.

Why?

Polling asynchronous operations is a common pattern in web development, but implementing it correctly with retries, intervals, and error handling can be tricky. This library provides:

  • Automatic retries: Configure maximum retry attempts for failed promises
  • Flexible intervals: Use fixed delays or dynamic intervals based on attempt number
  • Conditional continuation: Continue polling even on successful results based on custom logic
  • Abort support: Cancel polling operations with AbortSignal
  • Result history: Access complete history of all polling attempts
  • Type-safe: Full TypeScript support with detailed type information

Perfect for: API polling, task status checks, health monitoring, progressive data loading, and asynchronous workflows.

Installation

npm install @tstsx/poll

Usage

Basic Example

import { poll } from '@tstsx/poll';

// Poll an API endpoint until it returns success
const result = await poll(
  async () => {
    const response = await fetch('/api/task/status');
    return response.json();
  },
  {
    interval: 1000,    // Wait 1 second between attempts
    retryCount: 5      // Retry up to 5 times on failure
  }
);

console.log(result.result); // Final successful result
console.log(result.history); // History of all attempts

Conditional Continuation

// Continue polling until task is complete
const { result } = await poll(
  async () => {
    const response = await fetch('/api/task/123');
    return response.json();
  },
  {
    interval: 2000,
    retryCount: 10,
    shouldContinue: (data) => data.status !== 'completed'
  }
);

console.log('Task completed:', result);

Dynamic Intervals

// Exponential backoff: 1s, 2s, 4s, 8s, ...
const { result } = await poll(
  fetchData,
  {
    interval: (attemptNumber) => Math.pow(2, attemptNumber) * 1000,
    retryCount: 5
  }
);

Abort Support

const controller = new AbortController();

// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30000);

try {
  const { result } = await poll(
    fetchData,
    {
      signal: controller.signal,
      interval: 1000,
      retryCount: 100
    }
  );
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Polling was cancelled');
  }
}

Error Handling

import { PollError } from '@tstsx/poll';

try {
  const { result } = await poll(
    fetchData,
    {
      interval: 1000,
      retryCount: 3
    }
  );
} catch (error) {
  if (error instanceof PollError) {
    console.log('Polling failed after all retries');
    console.log('Attempt history:', error.results);
    // error.results contains all attempts:
    // [[error1, null], [error2, null], [null, success], ...]
  }
}

API

poll(promise, options?)

Executes a promise repeatedly with retry logic and interval delays.

Parameters:

  • promise: Function returning a Promise to execute
  • options: (Optional) Configuration object

Returns:

  • Promise<{ result: T, history: ResultHistory<T> }>

Throws:

  • PollError: When maximum retry count is reached
  • AbortError: When polling is cancelled via AbortSignal

Options

type Options<T> = {
  // Interval to wait before next attempt (ms)
  interval?: number | ((attemptNumber: number) => number);
  
  // Maximum number of retries on failure (default: 5)
  retryCount?: number;
  
  // Function to determine if polling should continue (default: always stop on success)
  shouldContinue?: (result: T) => boolean;
  
  // Signal to abort polling
  signal?: AbortSignal;
};

Result Types

type SuccessResult<T> = [null, T];
type FailResult = [unknown, null];
type ResultHistory<T> = ReadonlyArray<SuccessResult<T> | FailResult>;

type Result<T> = {
  result: T;              // Final successful result
  history: ResultHistory<T>; // All attempt results
};

PollError

class PollError<T> extends Error {
  readonly results: ResultHistory<T>; // All attempt results
}

Real-World Examples

API Task Status Polling

async function waitForTaskCompletion(taskId: string) {
  try {
    const { result } = await poll(
      async () => {
        const response = await fetch(`/api/tasks/${taskId}`);
        return response.json();
      },
      {
        interval: 2000,
        retryCount: 30,
        shouldContinue: (task) => task.status === 'pending' || task.status === 'running'
      }
    );
    
    return result;
  } catch (error) {
    if (error instanceof PollError) {
      console.error('Task did not complete in time');
    }
    throw error;
  }
}

Health Check with Exponential Backoff

async function waitForServiceHealth(serviceUrl: string) {
  const { result } = await poll(
    async () => {
      const response = await fetch(`${serviceUrl}/health`);
      if (!response.ok) throw new Error('Service unhealthy');
      return response.json();
    },
    {
      interval: (attempt) => Math.min(1000 * Math.pow(2, attempt), 30000), // Max 30s
      retryCount: 10
    }
  );
  
  return result;
}

Form Submission with Retry

async function submitFormWithRetry(formData: FormData, abortSignal?: AbortSignal) {
  try {
    const { result, history } = await poll(
      async () => {
        const response = await fetch('/api/submit', {
          method: 'POST',
          body: formData
        });
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        
        return response.json();
      },
      {
        interval: 3000,
        retryCount: 3,
        signal: abortSignal
      }
    );
    
    console.log(`Submission succeeded after ${history.length} attempts`);
    return result;
  } catch (error) {
    if (error instanceof PollError) {
      // Show user-friendly error after all retries failed
      showError('Submission failed. Please try again later.');
    }
    throw error;
  }
}

WebSocket Connection Retry

async function connectWithRetry(url: string) {
  const { result } = await poll(
    () => new Promise((resolve, reject) => {
      const ws = new WebSocket(url);
      
      ws.onopen = () => resolve(ws);
      ws.onerror = (error) => reject(error);
      
      // Timeout after 5 seconds
      setTimeout(() => {
        if (ws.readyState !== WebSocket.OPEN) {
          ws.close();
          reject(new Error('Connection timeout'));
        }
      }, 5000);
    }),
    {
      interval: (attempt) => 1000 * (attempt + 1), // Linear backoff
      retryCount: 5
    }
  );
  
  return result;
}

Progressive Data Loading

async function loadAllPages(baseUrl: string) {
  const allData: any[] = [];
  let page = 1;
  
  await poll(
    async () => {
      const response = await fetch(`${baseUrl}?page=${page}`);
      const data = await response.json();
      
      allData.push(...data.items);
      page++;
      
      return data;
    },
    {
      interval: 500,
      retryCount: 3,
      shouldContinue: (data) => data.hasMore
    }
  );
  
  return allData;
}

License

MIT