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
- Host: GitHub
- URL: https://github.com/ziflex/pinterval
- Owner: ziflex
- License: mit
- Created: 2017-01-23T16:52:57.000Z (over 9 years ago)
- Default Branch: master
- Last Pushed: 2023-03-01T00:40:00.000Z (over 3 years ago)
- Last Synced: 2025-02-24T04:37:42.527Z (over 1 year ago)
- Topics: async, interval, typescript, worker
- Language: TypeScript
- Homepage:
- Size: 1.22 MB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# pinterval
> Advanced interval management for JavaScript/TypeScript
[](https://www.npmjs.com/package/pinterval)
[](https://github.com/ziflex/pinterval/actions)
[](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)