@stevenleep/timewheel
v1.0.1
Published
A high-performance Time Wheel implementation in TypeScript with multiple design patterns
Maintainers
Readme
TimeWheel
A high-performance Timer Wheel implementation in TypeScript for efficient delayed task scheduling.
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/timewheelQuick 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); // 5TypeScript
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 insteadError 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.
