An open API service indexing awesome lists of open source software.

https://github.com/ziflex/pinterval

Advanced setInterval
https://github.com/ziflex/pinterval

async interval typescript worker

Last synced: 5 months ago
JSON representation

Advanced setInterval

Awesome Lists containing this project

README

          

# pinterval

> Advanced interval management for JavaScript/TypeScript

[![npm version](https://badge.fury.io/js/pinterval.svg)](https://www.npmjs.com/package/pinterval)
[![Actions Status](https://github.com/ziflex/pinterval/workflows/Node%20CI/badge.svg)](https://github.com/ziflex/pinterval/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A powerful and flexible interval management library that goes beyond JavaScript's native `setInterval`. Perfect for background tasks, polling, retries, and complex scheduling scenarios with built-in support for async/await, error handling, and dynamic timing strategies.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Why pinterval?](#why-pinterval)
- [API Documentation](#api-documentation)
- [Core Concepts](#core-concepts)
- [Interval Class](#interval-class)
- [Start Modes](#start-modes)
- [Auto-Stop Mechanism](#auto-stop-mechanism)
- [Error Handling](#error-handling)
- [Helper Functions](#helper-functions)
- [poll](#poll)
- [until](#until)
- [retry](#retry)
- [times](#times)
- [pipeline](#pipeline)
- [sleep](#sleep)
- [Duration Functions](#duration-functions)
- [constant](#constant)
- [linear](#linear)
- [exponential](#exponential)
- [fibonacci](#fibonacci)
- [jittered](#jittered)
- [decorrelatedJitter](#decorrelatedjitter)
- [steps](#steps)
- [Real-World Examples](#real-world-examples)
- [TypeScript Support](#typescript-support)
- [Comparison with Native setInterval](#comparison-with-native-setinterval)
- [Best Practices](#best-practices)
- [Development](#development)
- [Contributing](#contributing)
- [License](#license)

## Features

- ✅ **Async/Await Support** - Native promise support for asynchronous operations
- ✅ **Graceful Error Handling** - Built-in error handling with customizable recovery strategies
- ✅ **Dynamic Intervals** - Calculate interval duration dynamically based on iteration count
- ✅ **Auto-Stop Mechanism** - Automatically stop intervals based on return values
- ✅ **Rich Helper Functions** - Pre-built utilities for common patterns (polling, retries, pipelines)
- ✅ **Backoff Strategies** - Multiple built-in duration functions for sophisticated retry logic
- ✅ **TypeScript First** - Full TypeScript support with comprehensive type definitions
- ✅ **Zero Dependencies** - Minimal footprint with only one tiny dependency
- ✅ **Production Ready** - Battle-tested and actively maintained

## Installation

Install using your preferred package manager:

```bash
# npm
npm install --save pinterval

# yarn
yarn add pinterval

# pnpm
pnpm add pinterval
```

## Quick Start

```typescript
import { Interval } from 'pinterval';

// Create a simple interval
const interval = new Interval({
func: () => console.log('Tick!'),
time: 1000
});

// Start the interval
interval.start();

// Stop when needed
setTimeout(() => interval.stop(), 5000);
```

## Why pinterval?

JavaScript's native `setInterval` has several limitations:

- No native async/await support
- No built-in error handling
- Fixed intervals only (no dynamic timing)
- No automatic cleanup on errors
- Callback-based API

`pinterval` solves all these problems with a modern, Promise-based API that's perfect for:

- **Polling APIs** - Check for updates with intelligent backoff
- **Background Tasks** - Run periodic maintenance with error recovery
- **Retry Logic** - Implement sophisticated retry strategies
- **Health Checks** - Monitor services with adaptive intervals
- **Rate Limiting** - Control execution frequency dynamically
- **Data Synchronization** - Sync data with automatic error handling

## API Documentation

Full API documentation is available at [http://ziflex.github.io/pinterval](http://ziflex.github.io/pinterval)

## Core Concepts

### Interval Class

The `Interval` class is the core building block of pinterval. It provides a flexible way to execute functions repeatedly with configurable timing and error handling.

#### Basic Usage

```typescript
import { Interval } from 'pinterval';

const interval = new Interval({
func: () => console.log('Tick!'),
time: 1000
});

interval.start();

// Stop when needed
interval.stop();
```

#### Constructor Parameters

```typescript
interface Params {
func: (() => boolean | void) | ((counter: number) => boolean | void);
time: number | ((counter: number) => number);
start?: 'immediate' | 'delayed';
onError?: (err: Error) => boolean | void;
}
```

- **func** - Function to execute on each interval. Can be sync or async (returns Promise)
- **time** - Interval duration in milliseconds or a function that calculates it dynamically
- **start** - When to execute the first tick: `'delayed'` (default) waits for first timeout, `'immediate'` executes immediately
- **onError** - Optional error handler. Return `true` to continue, `false` to stop

#### Methods

- **start()** - Starts the interval. Throws if already running.
- **stop()** - Stops the interval. Throws if already stopped.
- **isRunning** - Property that returns `true` if the interval is currently running.

### Start Modes

Control when your interval executes for the first time:

```typescript
// Delayed start (default): waits for timeout before first execution
const delayedInterval = new Interval({
func: () => console.log('First execution after 1 second'),
time: 1000,
start: 'delayed' // or omit this, it's the default
});

// Immediate start: executes immediately, then waits for timeout
const immediateInterval = new Interval({
func: () => console.log('Executes immediately!'),
time: 1000,
start: 'immediate'
});
```

### Auto-Stop Mechanism

If your function returns `false`, the interval automatically stops. This is useful for self-terminating intervals.

```typescript
import { Interval } from 'pinterval';

let counter = 0;
const interval = new Interval({
func: () => {
counter++;
console.log(`Tick ${counter}`);

// Stop after 10 ticks
return counter < 10;
},
time: 1000
});

interval.start();
// Will automatically stop after 10 executions
```

### Error Handling

Comprehensive error handling with both synchronous and asynchronous error handlers:

```typescript
import { Interval } from 'pinterval';

// Synchronous error handler
const interval = new Interval({
func: () => {
// This might throw
riskyOperation();
},
time: 1000,
onError: (err) => {
console.error('Error occurred:', err);

// Return false to stop, true to continue
if (err instanceof FatalError) {
return false; // Stop interval
}

return true; // Continue with next tick
}
});

// Asynchronous error handler
const asyncInterval = new Interval({
func: async () => {
const response = await fetch('https://api.example.com/data');
return response.ok;
},
time: 5000,
onError: async (err: Error) => {
// Log error to remote service
await fetch('https://logging-service.com/log', {
method: 'POST',
body: JSON.stringify({ error: err.message })
});

// Decide whether to continue
return err.message !== 'FATAL';
}
});
```

**Error Handler Return Values:**

- `true` - Continue interval execution (schedules next tick)
- `false` - Stop interval execution
- `undefined` or no return - Stops interval execution
- If error handler itself throws, the interval stops

### Async Support

Native support for asynchronous functions with proper race condition prevention:

```typescript
import { Interval } from 'pinterval';

const interval = new Interval({
func: async () => {
// The next tick won't start until this Promise resolves
const data = await fetch('https://api.example.com/status');
const json = await data.json();

console.log('Status:', json.status);

// Can return false to stop
return json.status !== 'completed';
},
time: 2000
});

interval.start();
```

**Key Points:**

- Each tick waits for the Promise to resolve before scheduling the next one
- No race conditions - async operations won't overlap
- Interval timing starts **after** async operation completes
- Return `false` from async function to stop the interval

### Dynamic Duration

Calculate interval duration dynamically based on the iteration count:

```typescript
import { Interval } from 'pinterval';

// Exponential backoff
const interval = new Interval({
func: () => console.log('Tick!'),
time: (counter) => {
const minTimeout = 500;
const maxTimeout = 10000;
const timeout = Math.round(minTimeout * Math.pow(2, counter - 1));

return Math.min(timeout, maxTimeout);
}
});

interval.start();
// Executions at: 500ms, 1000ms, 2000ms, 4000ms, 8000ms, 10000ms, 10000ms...
```

**Counter Parameter:**

- Starts at `1` for the first execution
- Increments with each tick
- Useful for implementing backoff strategies

For complex timing strategies, see the [Duration Functions](#duration-functions) section.

## Helper Functions

pinterval provides several high-level helper functions for common patterns. All helpers are Promise-based and work seamlessly with async/await.

### poll

Repeatedly checks a condition until it returns `true`. Perfect for waiting on asynchronous operations.

```typescript
import { poll } from 'pinterval';

// Wait for a condition to be true
await poll(async () => {
const status = await checkStatus();
return status === 'ready';
}, 1000);

console.log('Condition met!');
```

**Signature:**
```typescript
function poll(
predicate: () => boolean | Promise,
timeout: number | ((counter: number) => number),
start?: 'immediate' | 'delayed'
): Promise
```

**Parameters:**

- **predicate** - Function that returns `true` when condition is met
- **timeout** - Interval duration in milliseconds or duration function
- **start** - Start mode: `'delayed'` (default) or `'immediate'`

**Example with immediate start:**

```typescript
// Check immediately, then every 5 seconds
await poll(
async () => (await fetch('/api/status')).ok,
5000,
'immediate'
);
```

### until

Similar to `poll`, but returns the value from the predicate once it's defined (not `undefined`).

```typescript
import { until } from 'pinterval';

// Wait until we get actual data
const data = await until(async () => {
const response = await fetch('/api/data');
if (!response.ok) return undefined;

const json = await response.json();
return json.data; // Returns value once available
}, 2000);

console.log('Data received:', data);
```

**Signature:**
```typescript
function until(
predicate: () => T | undefined | Promise,
timeout: number | ((counter: number) => number),
start?: 'immediate' | 'delayed'
): Promise
```

**Key Difference from poll:**

- `poll` - Waits for `true`, returns `void`
- `until` - Waits for non-`undefined` value, returns that value

### retry

Executes a function with retry logic. Stops after reaching the maximum attempts or when a truthy value is returned.

```typescript
import { retry } from 'pinterval';

// Retry up to 5 times with 2 second intervals
const result = await retry(
async (attempt) => {
const response = await fetch('/api/resource');
if (response.ok) {
return await response.json();
}
return undefined; // Will retry
},
5, // max attempts
2000 // interval between attempts
);
```

**Signature:**
```typescript
function retry(
predicate: (attempt: number) => T | Promise,
attempts: number,
timeout: number | ((counter: number) => number),
start?: 'immediate' | 'delayed'
): Promise
```

**Parameters:**

- **predicate** - Function to retry that receives the current attempt number. Return `undefined` to retry, or a value to resolve
- **attempts** - Maximum number of retry attempts
- **timeout** - Interval between retries
- **start** - Start mode: `'immediate'` (default) or `'delayed'`

**With exponential backoff:**

```typescript
import { retry, duration } from 'pinterval';

const result = await retry(
async (attempt) => {
try {
return await fetchData();
} catch {
return undefined; // Retry on error
}
},
10,
duration.exponential(1000, 30000) // 1s, 2s, 4s, 8s, 16s, 30s, 30s...
);
```

### times

Executes a function a specific number of times with an interval between executions.

```typescript
import { times } from 'pinterval';

// Execute 5 times with 1 second between executions
await times(
async (counter) => {
console.log(`Execution ${counter}`);
await updateMetrics(counter);
},
5,
1000
);

console.log('All executions completed!');
```

**Signature:**
```typescript
function times(
predicate: (counter: number) => void | Promise,
amount: number,
timeout: number | ((counter: number) => number),
start?: 'immediate' | 'delayed'
): Promise
```

**Parameters:**

- **predicate** - Function to execute. Receives counter (1-based) as parameter
- **amount** - Number of times to execute
- **timeout** - Interval between executions
- **start** - Start mode: `'delayed'` (default) or `'immediate'`

### pipeline

Sequentially executes an array of functions with intervals between them. Each function receives the output of the previous one.

```typescript
import { pipeline } from 'pinterval';

const result = await pipeline([
() => 1,
(x) => x * 2, // receives 1, returns 2
(x) => x + 3, // receives 2, returns 5
(x) => x * 4 // receives 5, returns 20
], 100);

console.log(result); // 20
```

**Signature:**
```typescript
function pipeline(
predicates: Array<(data: any) => any | Promise>,
timeout: number | ((counter: number) => number),
start?: 'immediate' | 'delayed'
): Promise
```

**Important Notes:**

- First function executes with 0 timeout when `start: 'immediate'` (default)
- Each subsequent function waits for the timeout
- Output of each function is passed to the next
- Perfect for multi-stage data processing

**Async pipeline example:**

```typescript
import { pipeline } from 'pinterval';

const result = await pipeline([
async () => await fetch('/api/users'),
async (response) => await response.json(),
async (users) => users.filter(u => u.active),
async (activeUsers) => {
await saveToDatabase(activeUsers);
return activeUsers.length;
}
], 500);

console.log(`Processed ${result} active users`);
```

### sleep

Simple utility to pause execution for a specified duration.

```typescript
import { sleep } from 'pinterval';

console.log('Starting...');
await sleep(2000);
console.log('2 seconds later...');
```

**Signature:**
```typescript
function sleep(time: number): Promise
```

## Duration Functions

Starting with v3.7.0, pinterval includes a collection of duration calculation functions for dynamic interval scheduling. These are perfect for implementing sophisticated retry and backoff strategies.

All duration functions are available under the `duration` namespace and follow this signature:

```typescript
type DurationFunction = (counter: number) => number;
```

The `counter` parameter starts at 1 for the first execution and increments with each tick.

### constant

Returns the same duration for every execution. Useful for fixed intervals.

```typescript
import { Interval, duration } from 'pinterval';

const interval = new Interval({
func: () => console.log('Tick!'),
time: duration.constant(1000)
});

interval.start();
// Executes every 1000ms: 1000, 1000, 1000, 1000...
```

**Signature:**
```typescript
function constant(ms: number): DurationFunction
```

**Use Cases:**

- Fixed interval polling
- Regular health checks
- Consistent retry delays

### linear

Increases duration linearly by a fixed increment on each iteration.

```typescript
import { Interval, duration } from 'pinterval';

const interval = new Interval({
func: () => console.log('Tick!'),
time: duration.linear(100, 50)
});

interval.start();
// Executes at: 100ms, 150ms, 200ms, 250ms, 300ms...
```

**Signature:**
```typescript
function linear(initial: number, increment: number): DurationFunction
```

**Parameters:**

- **initial** - Starting duration in milliseconds
- **increment** - Amount to increase (or decrease if negative) per iteration

**Use Cases:**

- Gradual slowdown for polling
- Progressive backoff with predictable growth
- Testing and debugging scenarios

**Decreasing intervals:**

```typescript
// Start fast, get slower by 100ms each time
const decreasing = duration.linear(2000, -100);
// Executes at: 2000ms, 1900ms, 1800ms, 1700ms...
```

### exponential

Doubles the duration on each iteration with an optional maximum cap. This is the standard backoff strategy used in many systems.

```typescript
import { retry, duration } from 'pinterval';

// Exponential backoff for retries
const result = await retry(
async (attempt) => await fetchData(),
10,
duration.exponential(100, 10000)
);

// Executes at: 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 6400ms, 10000ms, 10000ms...
```

**Signature:**
```typescript
function exponential(initial: number, max?: number): DurationFunction
```

**Parameters:**

- **initial** - Starting duration in milliseconds
- **max** - Optional maximum duration cap

**Use Cases:**

- Standard retry backoff strategy
- Network request retries
- Database reconnection attempts
- API rate limiting

**Without cap:**

```typescript
// Unbounded exponential growth
const uncapped = duration.exponential(100);
// Executes at: 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 6400ms...
```

### fibonacci

Uses the Fibonacci sequence for duration calculation. Provides gentler growth than exponential backoff.

```typescript
import { Interval, duration } from 'pinterval';

const interval = new Interval({
func: () => console.log('Tick!'),
time: duration.fibonacci(100)
});

interval.start();
// Executes at: 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, 1300ms...
```

**Signature:**
```typescript
function fibonacci(initial: number): DurationFunction
```

**Parameters:**

- **initial** - Base duration in milliseconds (used for F(0) and F(1))

**Use Cases:**

- Gentler backoff than exponential
- Natural growth patterns
- Alternative retry strategy when exponential is too aggressive

### jittered

Adds randomness to exponential backoff to prevent the "thundering herd" problem where multiple clients retry simultaneously.

```typescript
import { retry, duration } from 'pinterval';

// Add ±10% randomness to prevent synchronized retries
const result = await retry(
async () => await fetchData(),
10,
duration.jittered(1000, 30000, 0.1)
);

// Example execution times (with ±10% jitter):
// ~1000ms (900-1100), ~2000ms (1800-2200), ~4000ms (3600-4400)...
```

**Signature:**
```typescript
function jittered(
initial: number,
max?: number,
jitterFactor?: number
): DurationFunction
```

**Parameters:**

- **initial** - Starting duration in milliseconds
- **max** - Optional maximum duration cap
- **jitterFactor** - Amount of randomness (default: 0.1 = ±10%)

**Use Cases:**

- Distributed system retries
- Preventing thundering herd problem
- Load distribution across time
- API rate limiting with multiple clients

**Custom jitter:**

```typescript
// ±25% randomness
const highJitter = duration.jittered(1000, 10000, 0.25);

// ±5% randomness
const lowJitter = duration.jittered(1000, 10000, 0.05);
```

### decorrelatedJitter

AWS-recommended jitter strategy where each delay is based on the previous delay, not the iteration count. This is a stateful function.

```typescript
import { retry, duration } from 'pinterval';

// AWS-style decorrelated jitter
const result = await retry(
async () => await fetchData(),
10,
duration.decorrelatedJitter(100, 10000)
);

// Each delay is random(0, previous_delay * 3), capped at max
// Provides excellent distribution for distributed systems
```

**Signature:**
```typescript
function decorrelatedJitter(initial: number, max: number): DurationFunction
```

**Parameters:**

- **initial** - Starting duration in milliseconds
- **max** - Maximum duration cap (required)

**Use Cases:**

- AWS SDK retry logic
- Best-practice distributed retries
- Optimal backoff with jitter
- Production-ready retry strategies

**Important Note:**

This function is stateful - each instance maintains internal state. Create a new instance for each interval:

```typescript
// ✅ Correct: new instance per interval
const interval1 = new Interval({
func: task1,
time: duration.decorrelatedJitter(100, 5000)
});

const interval2 = new Interval({
func: task2,
time: duration.decorrelatedJitter(100, 5000)
});

// ❌ Wrong: sharing instance causes unexpected behavior
const sharedDuration = duration.decorrelatedJitter(100, 5000);
const interval3 = new Interval({ func: task1, time: sharedDuration });
const interval4 = new Interval({ func: task2, time: sharedDuration });
```

### steps

Returns different durations based on counter thresholds. Perfect for phase-based intervals that change behavior over time.

```typescript
import { Interval, duration } from 'pinterval';

const interval = new Interval({
func: () => console.log('Tick!'),
time: duration.steps([
{ threshold: 0, duration: 100 }, // Fast for first 5
{ threshold: 5, duration: 500 }, // Medium for 5-10
{ threshold: 10, duration: 2000 } // Slow after 10
])
});

interval.start();
// Counter 1-4: 100ms
// Counter 5-9: 500ms
// Counter 10+: 2000ms
```

**Signature:**
```typescript
function steps(
thresholds: Array<{ threshold: number; duration: number }>
): DurationFunction
```

**Parameters:**

- **thresholds** - Array of threshold/duration pairs (order doesn't matter, will be sorted)

**Use Cases:**

- Phase-based intervals (fast → medium → slow)
- Polling that changes behavior over time
- Different retry strategies per attempt range
- Multi-stage backoff

**Complex example:**

```typescript
import { retry, duration } from 'pinterval';

// Aggressive at first, then back off
const result = await retry(
async (attempt) => await fetchData(),
20,
duration.steps([
{ threshold: 0, duration: 100 }, // First 3 attempts: fast (100ms)
{ threshold: 3, duration: 500 }, // Attempts 3-6: medium (500ms)
{ threshold: 6, duration: 2000 }, // Attempts 6-10: slow (2s)
{ threshold: 10, duration: 5000 } // Attempts 10+: very slow (5s)
])
);
```

## Real-World Examples

### Health Check with Exponential Backoff

Monitor a service health endpoint with intelligent backoff when failures occur:

```typescript
import { Interval, duration } from 'pinterval';

let consecutiveFailures = 0;

const healthCheck = new Interval({
func: async () => {
try {
const response = await fetch('https://api.example.com/health');

if (response.ok) {
consecutiveFailures = 0;
console.log('✓ Service is healthy');
return true;
}

consecutiveFailures++;
console.log(`✗ Service unhealthy (${consecutiveFailures} failures)`);
return true;
} catch (error) {
consecutiveFailures++;
console.error(`✗ Health check failed: ${error.message}`);
return consecutiveFailures < 10; // Stop after 10 failures
}
},
time: (counter) => {
// Normal polling: 5s, on failure: exponential backoff up to 60s
if (consecutiveFailures === 0) return 5000;
return Math.min(5000 * Math.pow(2, consecutiveFailures - 1), 60000);
},
start: 'immediate'
});

healthCheck.start();
```

### API Polling with Conditional Stop

Poll an API until a specific condition is met:

```typescript
import { poll } from 'pinterval';

async function waitForJobCompletion(jobId: string) {
console.log(`Waiting for job ${jobId} to complete...`);

await poll(async () => {
const response = await fetch(`/api/jobs/${jobId}`);
const job = await response.json();

console.log(`Job status: ${job.status}`);

if (job.status === 'completed') {
console.log('Job completed successfully!');
return true;
}

if (job.status === 'failed') {
throw new Error('Job failed!');
}

return false; // Keep polling
}, 2000, 'immediate');
}

// Usage
await waitForJobCompletion('job-123');
```

### Retry with Fallback Strategies

Implement sophisticated retry logic with multiple strategies:

```typescript
import { retry, duration } from 'pinterval';

async function fetchWithRetry(url: string) {
// Try primary endpoint with exponential backoff
try {
return await retry(
async (attempt) => {
const response = await fetch(url);
if (!response.ok) return undefined;
return await response.json();
},
5,
duration.exponential(1000, 10000),
'immediate'
);
} catch (primaryError) {
console.warn('Primary endpoint failed, trying backup...');

// Fall back to backup endpoint with linear backoff
return await retry(
async (attempt) => {
const response = await fetch(url.replace('api', 'api-backup'));
if (!response.ok) return undefined;
return await response.json();
},
3,
duration.linear(2000, 1000),
'immediate'
);
}
}
```

### Rate-Limited API Client

Implement a rate-limited API client that respects API limits:

```typescript
import { Interval } from 'pinterval';

class RateLimitedClient {
private queue: Array<() => Promise> = [];
private interval: Interval;

constructor(requestsPerSecond: number) {
const delay = 1000 / requestsPerSecond;

this.interval = new Interval({
func: async () => {
if (this.queue.length === 0) {
return true; // Keep running
}

const task = this.queue.shift();
if (task) {
await task();
}

return true;
},
time: delay,
start: 'immediate'
});

this.interval.start();
}

async request(url: string): Promise {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const response = await fetch(url);
resolve(response);
} catch (error) {
reject(error);
}
});
});
}

stop() {
this.interval.stop();
}
}

// Usage: max 10 requests per second
const client = new RateLimitedClient(10);

// All requests are automatically rate-limited
const responses = await Promise.all([
client.request('/api/users/1'),
client.request('/api/users/2'),
client.request('/api/users/3'),
// ... more requests
]);
```

### Database Connection Retry with Jitter

Prevent thundering herd when multiple services try to reconnect to a database:

```typescript
import { retry, duration } from 'pinterval';

async function connectToDatabase(config: DbConfig) {
console.log('Attempting to connect to database...');

return await retry(
async (attempt) => {
try {
const connection = await createConnection(config);
await connection.ping();
console.log('✓ Database connected');
return connection;
} catch (error) {
console.log(`✗ Connection failed (attempt ${attempt}): ${error.message}, retrying...`);
return undefined;
}
},
10,
duration.jittered(1000, 30000, 0.2), // ±20% jitter
'immediate'
);
}
```

### Multi-Stage Data Processing Pipeline

Process data through multiple stages with delays:

```typescript
import { pipeline } from 'pinterval';

async function processUserData(userId: string) {
const result = await pipeline([
// Stage 1: Fetch user data
async () => {
console.log('Stage 1: Fetching user data...');
const response = await fetch(`/api/users/${userId}`);
return await response.json();
},

// Stage 2: Enrich with additional data
async (user) => {
console.log('Stage 2: Enriching data...');
const orders = await fetch(`/api/users/${userId}/orders`);
return { ...user, orders: await orders.json() };
},

// Stage 3: Calculate analytics
async (userData) => {
console.log('Stage 3: Computing analytics...');
return {
...userData,
analytics: {
totalOrders: userData.orders.length,
totalSpent: userData.orders.reduce((sum, o) => sum + o.amount, 0)
}
};
},

// Stage 4: Save to cache
async (enrichedData) => {
console.log('Stage 4: Caching results...');
await saveToCache(`user:${userId}`, enrichedData);
return enrichedData;
}
], 500); // 500ms between stages

console.log('Pipeline completed!');
return result;
}
```

### Scheduled Background Task

Run a background cleanup task with dynamic timing:

```typescript
import { Interval, duration } from 'pinterval';

const cleanupTask = new Interval({
func: async (counter) => {
console.log(`Running cleanup task (iteration ${counter})...`);

try {
// Clean up old records
const deleted = await deleteOldRecords();
console.log(`✓ Cleaned up ${deleted} old records`);

// Clean up temporary files
await cleanupTempFiles();
console.log('✓ Temporary files cleaned');

return true; // Continue running
} catch (error) {
console.error(`✗ Cleanup failed: ${error.message}`);
return true; // Continue despite errors
}
},
time: duration.steps([
{ threshold: 0, duration: 60000 }, // First hour: every minute
{ threshold: 60, duration: 300000 }, // Hours 1-5: every 5 minutes
{ threshold: 300, duration: 3600000 } // After 5 hours: every hour
]),
start: 'delayed',
onError: async (err) => {
// Log error to monitoring service
await logError('cleanup-task', err);
return true; // Continue running
}
});

cleanupTask.start();
```

## TypeScript Support

pinterval is written in TypeScript and provides full type definitions out of the box. No need for `@types/*` packages!

### Type-Safe Intervals

```typescript
import { Interval, Params, IntervalFunction } from 'pinterval';

// Type-safe interval function
const myFunction: IntervalFunction = (counter) => {
console.log(`Tick ${counter}`);
return counter < 10;
};

// Type-safe parameters
const params: Params = {
func: myFunction,
time: 1000,
start: 'immediate',
onError: (err: Error) => {
console.error(err);
return false;
}
};

const interval = new Interval(params);
```

### Generic Return Types

Helper functions support generic types for type-safe return values:

```typescript
import { until, retry } from 'pinterval';

interface User {
id: string;
name: string;
email: string;
}

// Type-safe until
const user = await until(async () => {
const response = await fetch('/api/user');
if (!response.ok) return undefined;
return await response.json(); // Typed as User
}, 1000);

// user is typed as User
console.log(user.email);

// Type-safe retry
interface ApiResponse {
success: boolean;
data: any;
}

const result = await retry(
async (attempt) => {
const response = await fetch('/api/data');
if (!response.ok) return undefined;
return await response.json();
},
5,
2000
);
```

### Custom Duration Functions

Create type-safe duration functions:

```typescript
import { DurationFunction, Interval } from 'pinterval';

// Custom duration function with full type safety
const customDuration: DurationFunction = (counter: number): number => {
if (counter <= 3) return 1000;
if (counter <= 6) return 2000;
return 5000;
};

const interval = new Interval({
func: () => console.log('Tick!'),
time: customDuration
});
```

## Comparison with Native setInterval

Here's why you might choose pinterval over native `setInterval`:

| Feature | Native setInterval | pinterval |
|---------|-------------------|-----------|
| **Async/Await Support** | ❌ No native support | ✅ Built-in Promise support |
| **Error Handling** | ❌ Errors crash the interval | ✅ Graceful error handling with recovery |
| **Dynamic Intervals** | ❌ Fixed interval only | ✅ Calculate interval per iteration |
| **Auto-Stop** | ❌ Manual management only | ✅ Automatic stop on conditions |
| **Backoff Strategies** | ❌ Not supported | ✅ Multiple built-in strategies |
| **Race Conditions** | ❌ Can overlap with async code | ✅ Prevents overlapping execution |
| **Helper Functions** | ❌ Build your own | ✅ poll, retry, until, times, pipeline |
| **TypeScript** | ⚠️ Basic types only | ✅ Full TypeScript support |
| **API** | ⚠️ Callback-based | ✅ Modern Promise-based API |

### Migration Example

**Before (native setInterval):**

```javascript
let intervalId;
let attempts = 0;

intervalId = setInterval(async () => {
try {
attempts++;
const response = await fetch('/api/status');
const data = await response.json();

if (data.ready) {
clearInterval(intervalId);
console.log('Ready!');
}

if (attempts >= 10) {
clearInterval(intervalId);
throw new Error('Max attempts reached');
}
} catch (error) {
clearInterval(intervalId);
console.error('Error:', error);
}
}, 2000);
```

**After (pinterval):**

```typescript
import { retry } from 'pinterval';

try {
await retry(async (attempt) => {
const response = await fetch('/api/status');
const data = await response.json();
return data.ready ? data : undefined;
}, 10, 2000);

console.log('Ready!');
} catch (error) {
console.error('Error:', error);
}
```

## Best Practices

### 1. Choose the Right Helper Function

- Use `poll` when waiting for a boolean condition
- Use `until` when you need to return a value
- Use `retry` for operations with a maximum attempt limit
- Use `times` for a fixed number of executions
- Use `pipeline` for sequential multi-stage processing
- Use `Interval` class for complex custom scenarios

### 2. Handle Errors Appropriately

Always provide an error handler for production code:

```typescript
const interval = new Interval({
func: async () => {
await riskyOperation();
},
time: 5000,
onError: async (err) => {
// Log to monitoring service
await logError(err);

// Decide based on error type
if (err instanceof NetworkError) {
return true; // Retry on network errors
}

return false; // Stop on other errors
}
});
```

### 3. Use Appropriate Backoff Strategies

- **Constant**: Simple polling with no rate limiting concerns
- **Linear**: Gradually reduce load over time
- **Exponential**: Standard retry strategy, most commonly used
- **Fibonacci**: Gentler than exponential, good for user-facing features
- **Jittered**: Distributed systems with multiple clients
- **DecorrelatedJitter**: Production-grade distributed systems (AWS recommendation)
- **Steps**: Different strategies for different phases

### 4. Prevent Memory Leaks

Always stop intervals when they're no longer needed:

```typescript
class MyComponent {
private interval: Interval;

start() {
this.interval = new Interval({
func: () => this.updateData(),
time: 5000
});
this.interval.start();
}

// Clean up when component unmounts
cleanup() {
if (this.interval?.isRunning) {
this.interval.stop();
}
}
}
```

### 5. Use Immediate Start When Appropriate

For retries and initial checks, use `'immediate'` start mode:

```typescript
// ✅ Good: Check immediately, then retry
await retry(fetchData, 5, 2000, 'immediate');

// ❌ Less ideal: Unnecessary wait before first attempt
await retry(fetchData, 5, 2000, 'delayed');
```

### 6. Test with Shorter Intervals

Use shorter timeouts during testing:

```typescript
const timeout = process.env.NODE_ENV === 'test' ? 100 : 5000;

const interval = new Interval({
func: myFunction,
time: timeout
});
```

### 7. Combine Helpers for Complex Scenarios

```typescript
// Wait for service to be ready, then start processing
await poll(async () => await isServiceReady(), 1000);

// Now run the main task with retries
await times(async (counter) => {
await retry(async () => await processItem(counter), 3, 1000);
}, 10, 5000);
```

## Development

### Building the Project

```bash
# Install dependencies
npm install

# Build the project
npm run build

# The compiled JavaScript will be in the lib/ directory
```

### Running Tests

```bash
# Run all tests
npm test

# Run tests in watch mode
npm run test:watch
```

### Linting

```bash
# Lint the code
npm run lint

# Format code
npm run fmt
```

### Generating Documentation

```bash
# Generate TypeDoc documentation
npm run doc

# Documentation will be generated in docs/ directory
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

### Steps to Contribute

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

### Guidelines

- Follow the existing code style
- Add tests for new features
- Update documentation as needed
- Ensure all tests pass before submitting

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Credits

Created and maintained by [Tim Voronov](https://github.com/ziflex)

## Links

- [npm package](https://www.npmjs.com/package/pinterval)
- [API Documentation](http://ziflex.github.io/pinterval)
- [GitHub Repository](https://github.com/ziflex/pinterval)
- [Issue Tracker](https://github.com/ziflex/pinterval/issues)
- [Changelog](CHANGELOG.md)