Skip to content

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

Notifications You must be signed in to change notification settings

stevenleep/TimeWheel

Repository files navigation

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.

About

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

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published