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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@stevenleep/timewheel

v1.0.1

Published

A high-performance Time Wheel implementation in TypeScript with multiple design patterns

Readme

TimeWheel

A high-performance Timer Wheel implementation in TypeScript for efficient delayed task scheduling.

npm version License: MIT

Features

  • High Performance - O(1) task insertion and deletion
  • 🎯 Precise Timing - Configurable tick duration for precision control
  • 🔄 Repeating Tasks - Built-in support for recurring tasks with limits
  • 🏗️ Hierarchical Wheels - Multi-level time wheels for long delays (seconds → minutes → hours → days)
  • 🔌 Adapters - Easy conversion between polling, cron, promises, and time wheel tasks
  • 🎨 Extensible - Strategy pattern for custom execution behaviors
  • 📊 Observable - Event system for monitoring and statistics
  • 🛡️ Type Safe - Full TypeScript support with strict types

Installation

npm install @stevenleep/timewheel

Quick Start

import { createTimeWheel } from '@stevenleep/timewheel';

// Create a time wheel with 60 buckets, 1 second per tick
const wheel = createTimeWheel({
  bucketCount: 60,
  tickDuration: 1000,
  autoStart: true,
});

// Schedule a task to run after 5 seconds
wheel.addTask(() => {
  console.log('Hello after 5 seconds!');
}, { delay: 5000 });

// Schedule a repeating task
wheel.addTask(() => {
  console.log('Runs every 2 seconds');
}, {
  delay: 2000,
  repeat: true,
  repeatInterval: 2000,
  maxRepeatCount: 10, // Stop after 10 executions
});

// Cancel a task
const task = wheel.addTask(() => {}, { delay: 10000 });
wheel.removeTask(task.id);

// Stop the wheel when done
wheel.stop();

Core Concepts

How Time Wheel Works

A time wheel is a circular buffer of "buckets", where each bucket holds tasks scheduled for that time slot. A pointer advances through the buckets at regular intervals (ticks), executing tasks in the current bucket.

     [0] [1] [2] [3] [4] [5] ... [59]
      ↑
   pointer (advances every tick)

Advantages over setTimeout/setInterval:

  • Constant time O(1) for adding/removing tasks
  • Efficient memory usage for large numbers of timers
  • Predictable execution timing

Configuration

| Option | Type | Description | |--------|------|-------------| | bucketCount | number | Number of slots in the wheel | | tickDuration | number | Time per slot in milliseconds | | autoStart | boolean | Start immediately on creation | | maxTaskCount | number | Maximum concurrent tasks | | name | string | Wheel identifier |

Maximum delay = bucketCount × tickDuration

Example: 60 buckets × 1000ms = 60 seconds max delay

For longer delays, use Hierarchical Time Wheel.

API Reference

TimeWheel

import { TimeWheel } from '@stevenleep/timewheel';

const wheel = new TimeWheel({
  bucketCount: 60,
  tickDuration: 1000,
});

// Lifecycle
wheel.start();
wheel.pause();
wheel.resume();
wheel.stop();
wheel.destroy();

// Task management
const task = wheel.addTask(callback, options);
wheel.removeTask(taskId);
wheel.getTask(taskId);
wheel.clearTasks();

// Statistics
const stats = wheel.getStats();
// { totalTasks, completedTasks, failedTasks, pendingTasks, uptime, tickCount }

Task Options

interface TaskOptions {
  delay: number;              // Required: delay in milliseconds
  id?: string;                // Custom task ID
  name?: string;              // Task name for debugging
  priority?: TaskPriority;    // LOW, NORMAL, HIGH, CRITICAL
  repeat?: boolean;           // Enable repeating
  repeatInterval?: number;    // Interval between repeats
  maxRepeatCount?: number;    // Max repeats (-1 for infinite)
  timeout?: number;           // Execution timeout
  retryCount?: number;        // Retry on failure
  retryDelay?: number;        // Delay between retries
  metadata?: object;          // Custom data
}

Hierarchical Time Wheel

For delays longer than a single wheel can handle:

import { 
  createHierarchicalTimeWheel,
  HierarchicalTimeWheelBuilder 
} from '@stevenleep/timewheel';

// Quick setup: seconds + minutes + hours (supports ~25 hours)
const wheel = createHierarchicalTimeWheel();
wheel.start();

// Schedule task for 1 hour later
wheel.addTask(() => console.log('1 hour passed'), { 
  delay: 60 * 60 * 1000 
});

// Custom configuration
const customWheel = new HierarchicalTimeWheelBuilder()
  .addLevel(60, 1000)      // 60 seconds
  .addLevel(60, 60000)     // 60 minutes  
  .addLevel(24, 3600000)   // 24 hours
  .addLevel(30, 86400000)  // 30 days
  .withAutoStart(true)
  .withName('LongTermScheduler')
  .build();

Adapters

Polling Adapter

Convert between polling loops and time wheel tasks:

import { createPollingAdapter } from '@stevenleep/timewheel';

const polling = createPollingAdapter(wheel);

// Convert polling to time wheel task
const task = polling.toTimeWheelTask(
  async () => {
    const status = await checkServerHealth();
    console.log('Server status:', status);
  },
  { 
    interval: 5000,
    immediate: true,      // Execute immediately first
    maxExecutions: 100    // Stop after 100 checks
  }
);

// Convert back to native polling
const handle = polling.fromTimeWheelTask(
  () => console.log('polling...'),
  { interval: 1000 }
);
handle.stop();

Scheduler Adapter

Convenient scheduling methods:

import { createScheduler } from '@stevenleep/timewheel';

const scheduler = createScheduler(wheel);

// Schedule at specific time
scheduler.at(new Date('2024-12-31 23:59:59'), () => {
  console.log('Happy New Year!');
});

// Schedule after delay
scheduler.after(5000, () => console.log('5 seconds later'));

// Repeating with options
scheduler.every(1000, () => console.log('tick'), { 
  maxCount: 10,
  startImmediately: true 
});

// Batch scheduling
const { tasks, cancelAll } = scheduler.batch([
  { callback: () => console.log('A'), delay: 1000 },
  { callback: () => console.log('B'), delay: 2000 },
  { callback: () => console.log('C'), delay: 3000 },
]);

// Sequential execution
scheduler.sequence([
  () => step1(),
  () => step2(),
  () => step3(),
], 1000); // 1 second between each

// Debounce and throttle
const debouncedSave = scheduler.debounce(() => saveData(), 500);
const throttledScroll = scheduler.throttle(() => onScroll(), 100);

// Retry with backoff
const result = await scheduler.retry(
  () => unstableApiCall(),
  { maxAttempts: 5, delay: 1000, backoff: 2 }
);

Promise Adapter

Promise-based async utilities:

import { createPromiseAdapter } from '@stevenleep/timewheel';

const promise = createPromiseAdapter(wheel);

// Simple delay
await promise.delay(1000);
await promise.sleep(2000);

// Timeout wrapper
try {
  const result = await promise.timeout(
    fetch('/api/slow-endpoint'),
    5000
  );
} catch (e) {
  console.log('Request timed out');
}

// Poll until condition
const data = await promise.poll(
  () => fetch('/api/job-status').then(r => r.json()),
  {
    interval: 1000,
    maxAttempts: 30,
    until: (result) => result.status === 'complete'
  }
);

// Retry with exponential backoff
const result = await promise.retryWithBackoff(
  () => unreliableOperation(),
  {
    maxAttempts: 5,
    initialDelay: 100,
    maxDelay: 10000,
    factor: 2
  }
);

// Wait for condition
await promise.waitFor(
  () => document.querySelector('#element') !== null,
  { interval: 100, timeout: 5000 }
);

Cron Adapter

Cron-style scheduling:

import { createCronAdapter } from '@stevenleep/timewheel';

const cron = createCronAdapter(wheel);

// Cron expression (minute hour dayOfMonth month dayOfWeek)
const handle = cron.schedule('0 * * * *', () => {
  console.log('Runs every hour');
});

// Object syntax
cron.schedule(
  { minute: '30', hour: '9' },
  () => console.log('Daily at 9:30 AM')
);

// Cancel
handle.cancel();

// Cancel all
cron.cancelAll();

Execution Strategies

Customize how tasks are executed:

import {
  SyncExecutionStrategy,
  ParallelExecutionStrategy,
  RetryExecutionStrategy,
  CircuitBreakerStrategy,
  BatchExecutionStrategy,
} from '@stevenleep/timewheel';

// Default: sequential execution
wheel.setExecutionStrategy(new SyncExecutionStrategy());

// Parallel with concurrency limit
wheel.setExecutionStrategy(new ParallelExecutionStrategy(10));

// Auto-retry failed tasks
wheel.setExecutionStrategy(
  new RetryExecutionStrategy(3, 1000, 2) // 3 retries, 1s delay, 2x backoff
);

// Circuit breaker pattern
const circuitBreaker = new CircuitBreakerStrategy(
  new SyncExecutionStrategy(),
  5,     // Open after 5 failures
  3,     // Close after 3 successes
  30000  // Reset timeout
);
wheel.setExecutionStrategy(circuitBreaker);

console.log(circuitBreaker.getState()); // 'CLOSED' | 'OPEN' | 'HALF_OPEN'

Task Decorators

Enhance tasks with additional behaviors:

import { 
  TimerTask,
  TaskDecoratorBuilder,
  LoggingDecorator,
  RetryDecorator,
  TimeoutDecorator,
  CachingDecorator,
  TimingDecorator,
} from '@stevenleep/timewheel';

const task = new TimerTask(() => riskyOperation(), { delay: 1000 });

// Using builder pattern
const enhanced = new TaskDecoratorBuilder(task)
  .withLogging()                    // Log start/end
  .withTiming((ms) => {             // Measure duration
    console.log(`Took ${ms}ms`);
  })
  .withRetry(3, 1000)              // Retry 3 times
  .withTimeout(5000)               // 5 second timeout
  .withCaching(60000)              // Cache result for 1 minute
  .build();

// Or manually compose
const decorated = new TimeoutDecorator(
  new RetryDecorator(
    new LoggingDecorator(task),
    3, 1000
  ),
  5000
);

Observers

Monitor time wheel events:

import {
  StatisticsObserver,
  LoggingObserver,
  CallbackObserver,
  TimeWheelEvent,
} from '@stevenleep/timewheel';

// Statistics collection
const stats = new StatisticsObserver();
wheel.addObserver(stats);

console.log(stats.getReport());
console.log(stats.getAverageTaskDuration());
console.log(stats.getEventCount(TimeWheelEvent.TASK_COMPLETED));

// Logging
wheel.addObserver(new LoggingObserver());

// Custom callbacks
const callback = new CallbackObserver();
callback.on(TimeWheelEvent.TASK_COMPLETED, (data) => {
  console.log(`Task ${data.task?.id} completed`);
});
callback.on(TimeWheelEvent.TASK_FAILED, (data) => {
  console.error(`Task failed: ${data.error?.message}`);
});
wheel.addObserver(callback);

// Filter specific events
const tickObserver = new LoggingObserver(undefined, [TimeWheelEvent.TICK]);

Available Events

| Event | Description | |-------|-------------| | STARTED | Wheel started | | STOPPED | Wheel stopped | | PAUSED | Wheel paused | | RESUMED | Wheel resumed | | TICK | Pointer advanced | | TASK_ADDED | Task scheduled | | TASK_REMOVED | Task removed | | TASK_STARTED | Task execution began | | TASK_COMPLETED | Task succeeded | | TASK_FAILED | Task threw error | | TASK_CANCELLED | Task cancelled |

Global Manager

Manage multiple time wheels:

import { TimeWheelManager } from '@stevenleep/timewheel';

const manager = TimeWheelManager.getInstance();

// Create named wheels
const fastWheel = manager.createTimeWheel({
  bucketCount: 60,
  tickDuration: 100,
  name: 'fast',
});

const slowWheel = manager.createTimeWheel({
  bucketCount: 60,
  tickDuration: 60000,
  name: 'slow',
});

// Retrieve by name
const wheel = manager.getTimeWheel('fast');

// Batch operations
manager.startAll();
manager.stopAll();
manager.destroyAll();

// Global statistics
const globalStats = manager.getGlobalStats();
// { wheelCount, totalTasks, totalCompleted, totalFailed }

// Cleanup
TimeWheelManager.resetInstance();

Utility Functions

import {
  // ID generation
  generateId,
  generateShortId,
  
  // Time utilities
  delay,
  parseTimeString,
  formatDuration,
  toSeconds,
  toMinutes,
  fromSeconds,
  fromMinutes,
  
  // Function utilities
  throttle,
  debounce,
  once,
  memoize,
} from '@stevenleep/timewheel';

// Parse time strings
parseTimeString('5m');   // 300000
parseTimeString('2h');   // 7200000
parseTimeString('100ms'); // 100

// Format durations
formatDuration(3600000); // "1.00h"
formatDuration(150000);  // "2.50m"

// Time conversions
fromMinutes(5);  // 300000
toSeconds(5000); // 5

TypeScript

Full type definitions included:

import type {
  ITask,
  ITimeWheel,
  IBucket,
  IObserver,
  IExecutionStrategy,
  TaskOptions,
  TaskStatus,
  TaskPriority,
  TimeWheelConfig,
  TimeWheelStatus,
  TimeWheelEvent,
  TimeWheelEventData,
  TimeWheelStats,
} from '@stevenleep/timewheel';

Best Practices

Choosing Configuration

// High precision, short delays (< 1 minute)
{ bucketCount: 1000, tickDuration: 10 }  // 10ms precision, 10s max

// General purpose (< 1 hour)
{ bucketCount: 3600, tickDuration: 1000 } // 1s precision, 1h max

// Long running (< 24 hours)
// Use HierarchicalTimeWheel instead

Error Handling

wheel.addTask(async () => {
  try {
    await riskyOperation();
  } catch (error) {
    // Handle error - task won't be marked as failed
    console.error(error);
  }
}, { delay: 1000 });

// Or use retry options
wheel.addTask(() => riskyOperation(), {
  delay: 1000,
  retryCount: 3,
  retryDelay: 1000,
});

// Or use observer for centralized handling
const observer = new CallbackObserver();
observer.on(TimeWheelEvent.TASK_FAILED, (data) => {
  reportError(data.error);
});
wheel.addObserver(observer);

Cleanup

// Always clean up when done
wheel.stop();       // Stop ticking
wheel.clearTasks(); // Remove pending tasks
wheel.destroy();    // Full cleanup

// Or use manager
const manager = TimeWheelManager.getInstance();
// ... use wheels ...
manager.destroyAll();

License

MIT

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting PRs.