Skip to content

Latest commit

 

History

History
249 lines (199 loc) · 6.63 KB

File metadata and controls

249 lines (199 loc) · 6.63 KB

@ismael3s/polly - JavaScript/Typescript Resilience Library

A JavaScript/TypeScript port of the popular .NET Polly library, built with RxJS and compatible with AsyncLocalStorage for context preservation.

Not production-ready yet. Use at your own risk, or just grab the rxjs code and use it directly.

Status

  • Timeout Strategy
  • Composition of Strategies
  • AsyncLocalStorage Context Preservation
  • Decorator Support
  • Abort Signal Support
  • Retry Strategy
  • Circuit Breaker Strategy
  • Telemetry Integration

...

Features

  • Timeout Strategy: Automatically timeout operations that take too long
  • Retry Strategy: Retry failed operations with configurable delay and exponential backoff
  • AbortController Support: Cancel operations using AbortController/AbortSignal
  • AsyncLocalStorage Compatibility: Preserve context across async operations
  • Fluent Builder API: Intuitive builder pattern similar to .NET Polly
  • RxJS Integration: Built on top of RxJS for powerful reactive programming
  • TypeScript Support: Full TypeScript support with proper type definitions
  • Decorator Support: Apply resilience strategies using decorators on class methods

Quick Start

import {
  ResiliencePipelineBuilder,
  TimeoutError,
  RetryExhaustedError,
  AbortError,
} from "./src/index";
import { AsyncLocalStorage } from "async_hooks";

// Create a simple pipeline with timeout and retry
const pipeline = new ResiliencePipelineBuilder()
  .withTimeout(2000) // 2 second timeout
  .withRetry(3, 500, 1.5) // 3 retries with exponential backoff
  .build();

// Execute a function that might fail
try {
  const result = await pipeline.execute(async () => {
    // Your potentially unreliable operation
    const response = await fetch("https://api.example.com/data");
    return await response.json();
  });
  console.log("Success:", result);
} catch (error) {
  if (error instanceof TimeoutError) {
    console.log("Operation timed out");
  } else if (error instanceof RetryExhaustedError) {
    console.log("All retry attempts failed");
  }
}

Examples

Basic Timeout

const timeoutPipeline = new ResiliencePipelineBuilder()
  .withTimeout(1000)
  .build();

try {
  await timeoutPipeline.execute(async () => {
    await new Promise((resolve) => setTimeout(resolve, 2000)); // Will timeout
    return "Success";
  });
} catch (error) {
  console.log(error.message); // "Operation timed out after 1000ms"
}

Retry with Exponential Backoff

const retryPipeline = new ResiliencePipelineBuilder()
  .withRetry(3, 100, 2) // 3 retries: 100ms, 200ms, 400ms delays
  .build();

let attempts = 0;
try {
  const result = await retryPipeline.execute(async () => {
    attempts++;
    if (attempts < 3) {
      throw new Error(`Attempt ${attempts} failed`);
    }
    return `Success on attempt ${attempts}`;
  });
  console.log(result); // "Success on attempt 3"
} catch (error) {
  console.log("All retries exhausted");
}

Cancellation with AbortController

const pipeline = new ResiliencePipelineBuilder()
  .withTimeout(5000)
  .withRetry(3)
  .build();

const controller = new AbortController();

// Start operation
const operation = pipeline.execute(async () => {
  const response = await fetch("https://api.example.com/data");
  return response.json();
}, controller.signal);

// Cancel after 2 seconds
setTimeout(() => controller.abort(), 2000);

try {
  const result = await operation;
  console.log("Success:", result);
} catch (error) {
  if (error instanceof AbortError) {
    console.log("Operation was cancelled");
  }
}

Strategy Composition

Strategies are applied in the order they are added to the builder:

  1. TimeoutRetry: Timeout applies to each retry attempt
  2. RetryTimeout: Timeout applies to the entire retry sequence

Choose the order based on your requirements:

// Timeout per retry attempt (recommended for most cases)
const pipeline1 = new ResiliencePipelineBuilder()
  .withTimeout(1000) // Each attempt times out after 1s
  .withRetry(3) // Up to 3 attempts
  .build();

// Timeout for entire operation
const pipeline2 = new ResiliencePipelineBuilder()
  .withRetry(3) // Up to 3 attempts
  .withTimeout(5000) // Entire operation times out after 5s
  .build();

Decorator Usage

You can also use the @Resilience decorator to apply resilience strategies directly to class methods:

Basic Decorator Usage

import { ResiliencePipelineBuilder, Resilience } from "@ismael3s/polly";

class ApiService {
  @Resilience(
    new ResiliencePipelineBuilder()
      .withRetry(2, 50, 1) // 2 retries, 50ms delay, no exponential growth
      .build()
  )
  async fetchUserData(userId: string): Promise<User> {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`Failed to fetch user: ${response.status}`);
    }
    return response.json();
  }

  @Resilience(
    new ResiliencePipelineBuilder()
      .withTimeout(1000) // 1 second timeout
      .build()
  )
  async quickOperation(): Promise<string> {
    // Fast operation that should complete within 1 second
    await new Promise((resolve) => setTimeout(resolve, 100));
    return "Operation completed";
  }

  @Resilience(
    new ResiliencePipelineBuilder()
      .withTimeout(500) // 500ms per-attempt timeout
      .withRetry(2, 100, 1.5) // 2 retries with exponential backoff
      .build()
  )
  async complexOperation(data: any): Promise<any> {
    // Complex operation with both timeout and retry
    return await this.processData(data);
  }

  // Works with static methods too
  @Resilience(new ResiliencePipelineBuilder().withRetry(1, 100, 1).build())
  static async staticOperation(): Promise<string> {
    return "Static operation completed";
  }
}

TypeScript Configuration for Decorators

To use decorators, make sure your tsconfig.json includes:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

TypeScript Support

This library is written in TypeScript and provides full type safety:

interface ApiResponse {
  id: number;
  name: string;
}

const result: ApiResponse = await pipeline.execute(
  async (): Promise<ApiResponse> => {
    const response = await fetch("/api/user");
    return await response.json();
  }
);

Dependencies

  • RxJS: For reactive programming and strategy composition

Inspiration

This library is inspired by the excellent Polly library for .NET, bringing similar resilience patterns to the JavaScript/TypeScript ecosystem.