# TimeWheel

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

[![npm version](https://img.shields.io/npm/v/@stevenleep/timewheel.svg)](https://www.npmjs.com/package/@stevenleep/timewheel)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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

```bash
npm install @stevenleep/timewheel
```

## Quick Start

```typescript
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](#hierarchical-time-wheel).

## API Reference

### TimeWheel

```typescript
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

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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:

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

## Best Practices

### Choosing Configuration

```typescript
// 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

```typescript
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

```typescript
// 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.
