From ec4c3a4d8801043cd9674802acda2f2279e30a40 Mon Sep 17 00:00:00 2001 From: vabatta <2137077+vabatta@users.noreply.github.com> Date: Sun, 30 Nov 2025 01:59:18 +0100 Subject: [PATCH 1/2] first draft --- packages/retry-strategies/README.md | 288 ++++++------------ .../src/backoff/constant-backoff.test.ts | 27 +- .../src/backoff/constant-backoff.ts | 11 + .../decorrelated-jitter-backoff.test.ts | 26 +- .../backoff/decorrelated-jitter-backoff.ts | 21 ++ .../src/backoff/equal-jitter-backoff.test.ts | 23 +- .../src/backoff/equal-jitter-backoff.ts | 20 ++ .../src/backoff/exponential-backoff.test.ts | 27 +- .../src/backoff/exponential-backoff.ts | 22 ++ .../src/backoff/fibonacci-backoff.test.ts | 27 +- .../src/backoff/fibonacci-backoff.ts | 21 ++ .../src/backoff/full-jitter-backoff.test.ts | 23 +- .../src/backoff/full-jitter-backoff.ts | 20 ++ .../src/backoff/linear-backoff.test.ts | 27 +- .../src/backoff/linear-backoff.ts | 17 ++ .../src/backoff/stop-backoff.test.ts | 25 +- .../src/backoff/stop-backoff.ts | 8 + .../src/backoff/zero-backoff.test.ts | 27 +- .../src/backoff/zero-backoff.ts | 8 + .../retry-strategies/src/utils/upto.test.ts | 31 +- packages/retry-strategies/src/utils/upto.ts | 97 ++++-- 21 files changed, 553 insertions(+), 243 deletions(-) diff --git a/packages/retry-strategies/README.md b/packages/retry-strategies/README.md index 977a442..8a42344 100644 --- a/packages/retry-strategies/README.md +++ b/packages/retry-strategies/README.md @@ -12,55 +12,31 @@ npm install @proventuslabs/retry-strategies ## Usage -### Basic Usage +### API Styles -```typescript -import { retry, ExponentialBackoff } from "@proventuslabs/retry-strategies"; - -// Retry a failing operation with exponential backoff -const result = await retry( - () => fetch("/api/data").then(res => res.json()), - { strategy: new ExponentialBackoff(100, 5000) } -); -``` - -### Using Abort Signals +This package supports both **class-based** and **functional** API styles. Factory functions (lowercase) are convenience wrappers that create identical instances: ```typescript -import { retry, LinearBackoff } from "@proventuslabs/retry-strategies"; - -const controller = new AbortController(); - -// Abort after 10 seconds -setTimeout(() => controller.abort(), 10000); +// Class-based style +import { ExponentialBackoff, UptoBackoff } from "@proventuslabs/retry-strategies"; +const strategy = new ExponentialBackoff(100, 5000); +const limited = new UptoBackoff(3, strategy); -try { - const result = await retry( - () => fetchData(), - { - strategy: new LinearBackoff(1000), - signal: controller.signal - } - ); -} catch (error) { - // Handle abort or failure -} +// Functional style (equivalent) +import { exponential, upto } from "@proventuslabs/retry-strategies"; +const strategy = exponential(100, 5000); +const limited = upto(3, strategy); ``` -### Custom Stop Condition +### Basic Usage ```typescript -import { retry, FibonacciBackoff } from "@proventuslabs/retry-strategies"; +import { retry, exponential } from "@proventuslabs/retry-strategies"; +// Retry a failing operation with exponential backoff const result = await retry( - () => fetch("/api/resource"), - { - strategy: new FibonacciBackoff(100, 10000), - stop: (error, attempt) => { - // Stop retrying on 404 or after 5 attempts - return error.status === 404 || attempt >= 5; - } - } + () => fetch("/api/data").then(res => res.json()), + { strategy: exponential(100, 5000) } ); ``` @@ -106,6 +82,23 @@ Waits for the specified amount of time or until an optional AbortSignal is trigg - **`unknown`** - The reason of the AbortSignal if the operation is aborted (generally `DOMException` `AbortError`) - **`RangeError`** - If the delay exceeds INT32_MAX (2147483647ms, approximately 24.8 days) +### `upto(retries, strategy)` + +Limits a backoff strategy to a maximum number of retry attempts. Once the limit is reached, `nextBackoff()` returns `NaN` to stop retrying. + +#### Parameters + +- **`retries`** `number` - Maximum number of retry attempts allowed (must be >= 0 and an integer) +- **`strategy`** `BackoffStrategy` - The underlying backoff strategy to wrap + +#### Returns + +`UptoBackoff` - A new UptoBackoff instance that stops after the specified number of retries + +#### Throws + +- **`RangeError`** - If retries is NaN, not an integer, or less than 0 + ### `BackoffStrategy` Interface All backoff strategies implement this interface: @@ -127,170 +120,117 @@ interface BackoffStrategy { ## Backoff Strategies -### `ExponentialBackoff` +All strategies are available in both class-based and functional styles. Examples below show the functional style for brevity. + +### `ExponentialBackoff` / `exponential()` Increases the delay exponentially using the AWS algorithm. **Formula:** `min(cap, base * 2^n)` ```typescript -const strategy = new ExponentialBackoff( - 100, // base delay in ms - 5000 // cap (maximum delay) in ms (optional, defaults to Infinity) -); +const strategy = exponential(100, 5000); +// Parameters: +// base: 100 - base delay in ms +// cap: 5000 - cap (maximum delay) in ms (optional, defaults to Infinity) // Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 5000ms, 5000ms... ``` -### `LinearBackoff` +### `LinearBackoff` / `linear()` Increases the delay linearly by a fixed increment on each retry. **Formula:** `min(cap, initialDelay + (increment * n))` ```typescript -const strategy = new LinearBackoff( - 1000, // increment in ms - 500, // initial delay in ms (optional, default: 0) - 10000 // cap (maximum delay) in ms (optional, defaults to Infinity) -); +const strategy = linear(1000, 500, 10000); +// Parameters: +// increment: 1000 - increment in ms +// initialDelay: 500 - initial delay in ms (optional, default: 0) +// cap: 10000 - cap (maximum delay) in ms (optional, defaults to Infinity) // Delays: 500ms, 1500ms, 2500ms, 3500ms, 4500ms... ``` -### `FibonacciBackoff` +### `FibonacciBackoff` / `fibonacci()` Increases the delay following the Fibonacci sequence. **Formula:** `min(cap, base * fib(n))` ```typescript -const strategy = new FibonacciBackoff( - 100, // base delay in ms - 10000 // cap (maximum delay) in ms (optional, defaults to Infinity) -); +const strategy = fibonacci(100, 10000); +// Parameters: +// base: 100 - base delay in ms +// cap: 10000 - cap (maximum delay) in ms (optional, defaults to Infinity) // Delays: 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, 1300ms, 2100ms... ``` -### `FullJitterBackoff` +### `FullJitterBackoff` / `fullJitter()` Uses the AWS FullJitter algorithm to add randomness to exponential backoff, preventing thundering herd problems. **Formula:** `random(0, min(cap, base * 2^n))` ```typescript -const strategy = new FullJitterBackoff( - 100, // base delay in ms - 5000 // cap (maximum delay) in ms (optional, defaults to Infinity) -); +const strategy = fullJitter(100, 5000); +// Parameters: +// base: 100 - base delay in ms +// cap: 5000 - cap (maximum delay) in ms (optional, defaults to Infinity) // Delays: random values between 0 and the exponential cap ``` -### `EqualJitterBackoff` +### `EqualJitterBackoff` / `equalJitter()` Uses the AWS EqualJitter algorithm, providing a balanced approach between exponential backoff and full jitter. **Formula:** `(min(cap, base * 2^n) / 2) + random(0, min(cap, base * 2^n) / 2)` ```typescript -const strategy = new EqualJitterBackoff( - 100, // base delay in ms - 5000 // cap (maximum delay) in ms (optional, defaults to Infinity) -); +const strategy = equalJitter(100, 5000); +// Parameters: +// base: 100 - base delay in ms +// cap: 5000 - cap (maximum delay) in ms (optional, defaults to Infinity) ``` -### `DecorrelatedJitterBackoff` +### `DecorrelatedJitterBackoff` / `decorrelatedJitter()` Uses the AWS Decorrelated Jitter algorithm, where each delay is based on the previous delay rather than attempt count. **Formula:** `min(cap, random(base, previousDelay * 3))` ```typescript -const strategy = new DecorrelatedJitterBackoff( - 100, // base delay in ms - 10000 // cap (maximum delay) in ms (optional, defaults to Infinity) -); +const strategy = decorrelatedJitter(100, 10000); +// Parameters: +// base: 100 - base delay in ms +// cap: 10000 - cap (maximum delay) in ms (optional, defaults to Infinity) ``` -### `ConstantBackoff` +### `ConstantBackoff` / `constant()` Always returns the same backoff delay, useful for fixed-interval polling. ```typescript -const strategy = new ConstantBackoff(1000); // Always 1000ms +const strategy = constant(1000); // Always 1000ms ``` -### `ZeroBackoff` +### `ZeroBackoff` / `zero()` Always returns zero delay, useful for immediate retries without waiting. ```typescript -const strategy = new ZeroBackoff(); // Always 0ms +const strategy = zero(); // Always 0ms ``` -### `StopBackoff` +### `StopBackoff` / `stop()` Always returns `NaN`, indicating that no retries should be made. ```typescript -const strategy = new StopBackoff(); // Never retries -``` - -## Utility Functions - -### `upto(retries, strategy)` - -Limits a backoff strategy to a maximum number of retry attempts. Once the limit is reached, `nextBackoff()` returns `NaN` to stop retrying. - -#### Parameters - -- **`retries`** `number` - Maximum number of retry attempts allowed (must be >= 0 and an integer) -- **`strategy`** `BackoffStrategy` - The underlying backoff strategy to wrap - -#### Returns - -`BackoffStrategy` - A new BackoffStrategy that stops after the specified number of retries - -#### Throws - -- **`RangeError`** - If retries is NaN, not an integer, or less than 0 - -#### Example - -```typescript -import { retry, ExponentialBackoff, upto } from "@proventuslabs/retry-strategies"; - -// Limit exponential backoff to exactly 3 retry attempts -const strategy = upto(3, new ExponentialBackoff(100, 5000)); - -await retry( - () => fetchData(), - { strategy } -); -// Will attempt the operation at most 4 times (initial + 3 retries) -``` - -You can also combine `upto` with any other backoff strategy: - -```typescript -import { upto, FullJitterBackoff } from "@proventuslabs/retry-strategies"; - -// Limit jitter backoff to 5 attempts -const limitedJitter = upto(5, new FullJitterBackoff(100, 5000)); - -// Limit constant backoff to 10 attempts -const limitedPolling = upto(10, new ConstantBackoff(1000)); +const strategy = stop(); // Never retries ``` ## Behavior -### Key Features - -- **Composable strategies**: All strategies implement the same `BackoffStrategy` interface -- **AbortSignal support**: Cancel retry operations at any time -- **Custom stop conditions**: Define custom logic for when to stop retrying -- **Type-safe**: Full TypeScript support with comprehensive type definitions -- **Zero dependencies**: Pure JavaScript implementation with no external dependencies -- **Standards-based**: Uses native JavaScript APIs (`setTimeout`, `AbortSignal`, `Promise`) - ### Retry Loop Behavior The retry loop continues indefinitely until one of these conditions is met: @@ -305,8 +245,6 @@ The retry loop continues indefinitely until one of these conditions is met: - **Negative delays**: Treated as zero (no wait) - **NaN from strategy**: Immediately stops retrying and throws the last error - **Delays exceeding INT32_MAX**: Throws `RangeError` before attempting the wait -- **Synchronous functions**: Work seamlessly alongside asynchronous functions -- **Multiple retry instances**: Each `retry` call resets the strategy at the start ### Concurrency Safety @@ -314,7 +252,7 @@ The retry loop continues indefinitely until one of these conditions is met: ```typescript // ❌ Not safe - shared strategy -const strategy = new ExponentialBackoff(100, 5000); +const strategy = exponential(100, 5000); await Promise.all([ retry(operation1, { strategy }), retry(operation2, { strategy }) @@ -322,17 +260,17 @@ await Promise.all([ // ✅ Safe - separate strategies await Promise.all([ - retry(operation1, { strategy: new ExponentialBackoff(100, 5000) }), - retry(operation2, { strategy: new ExponentialBackoff(100, 5000) }) + retry(operation1, { strategy: exponential(100, 5000) }), + retry(operation2, { strategy: exponential(100, 5000) }) ]); ``` ## Examples -### Example 1: API Request with Exponential Backoff +### API Request with Exponential Backoff ```typescript -import { retry, ExponentialBackoff, upto } from "@proventuslabs/retry-strategies"; +import { retry, exponential, upto } from "@proventuslabs/retry-strategies"; async function fetchUserData(userId: string) { return retry( @@ -344,67 +282,37 @@ async function fetchUserData(userId: string) { return response.json(); }, { - // Limit to 5 retry attempts with exponential backoff - strategy: upto(5, new ExponentialBackoff(100, 5000)), - stop: (error) => { - // Stop on client errors (4xx) - return error.message.includes("HTTP 4"); - } - } - ); -} -``` - -### Example 2: Polling with Linear Backoff - -```typescript -import { retry, LinearBackoff, upto } from "@proventuslabs/retry-strategies"; - -async function pollForJobCompletion(jobId: string) { - return retry( - async () => { - const job = await fetchJob(jobId); - if (job.status === "pending") { - throw new Error("Job not ready"); - } - return job; - }, - { - // Limit to 20 attempts: 500ms, 1500ms, 2500ms... - strategy: upto(20, new LinearBackoff(1000, 500)) + strategy: upto(5, exponential(100, 5000)), + stop: (error) => error.message.includes("HTTP 4") // Stop on client errors } ); } ``` -### Example 3: Rate-Limited API with Jitter +### Rate-Limited API with Jitter ```typescript -import { retry, FullJitterBackoff } from "@proventuslabs/retry-strategies"; +import { retry, fullJitter } from "@proventuslabs/retry-strategies"; async function callRateLimitedAPI(endpoint: string) { return retry( async () => { const response = await fetch(endpoint); - if (response.status === 429) { - // Too Many Requests - throw new Error("Rate limited"); - } + if (response.status === 429) throw new Error("Rate limited"); return response.json(); }, { - // Jitter helps prevent thundering herd when multiple clients retry - strategy: new FullJitterBackoff(1000, 30000), - stop: (error, attempt) => !error.message.includes("Rate limited") + strategy: fullJitter(1000, 30000), // Jitter prevents thundering herd + stop: (error) => !error.message.includes("Rate limited") } ); } ``` -### Example 4: Fibonacci Backoff with Timeout +### Timeout with AbortSignal ```typescript -import { retry, FibonacciBackoff } from "@proventuslabs/retry-strategies"; +import { retry, fibonacci } from "@proventuslabs/retry-strategies"; async function fetchWithTimeout(url: string, timeoutMs: number) { const controller = new AbortController(); @@ -413,10 +321,7 @@ async function fetchWithTimeout(url: string, timeoutMs: number) { try { return await retry( () => fetch(url, { signal: controller.signal }), - { - strategy: new FibonacciBackoff(100, 5000), - signal: controller.signal - } + { strategy: fibonacci(100, 5000), signal: controller.signal } ); } finally { clearTimeout(timeout); @@ -424,27 +329,6 @@ async function fetchWithTimeout(url: string, timeoutMs: number) { } ``` -### Example 5: Immediate Retries with Custom Logic - -```typescript -import { retry, ZeroBackoff } from "@proventuslabs/retry-strategies"; - -async function fetchWithTransientErrorRetry() { - return retry( - () => performOperation(), - { - strategy: new ZeroBackoff(), // No delay between retries - stop: (error, attempt) => { - // Only retry transient network errors, max 3 times - const isTransient = error.code === "ECONNRESET" || - error.code === "ETIMEDOUT"; - return !isTransient || attempt >= 3; - } - } - ); -} -``` - ## Limitations - **Maximum delay**: Delays cannot exceed INT32_MAX (2147483647ms, approximately 24.8 days) due to `setTimeout` limitations diff --git a/packages/retry-strategies/src/backoff/constant-backoff.test.ts b/packages/retry-strategies/src/backoff/constant-backoff.test.ts index 1495d59..3afe2bb 100644 --- a/packages/retry-strategies/src/backoff/constant-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/constant-backoff.test.ts @@ -1,6 +1,6 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { ConstantBackoff } from "./constant-backoff.ts"; +import { ConstantBackoff, constant } from "./constant-backoff.ts"; /* node:coverage disable */ suite("Constant backoff strategy (Unit)", () => { @@ -180,4 +180,29 @@ suite("Constant backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates ConstantBackoff instance", (ctx: TestContext) => { + ctx.plan(3); + + // Arrange & Act + const strategy = constant(1000); + + // Assert + ctx.assert.ok( + strategy instanceof ConstantBackoff, + "should return ConstantBackoff instance", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 1000, + "should work correctly", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 1000, + "should maintain constant delay", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/constant-backoff.ts b/packages/retry-strategies/src/backoff/constant-backoff.ts index 4e86b5a..024773d 100644 --- a/packages/retry-strategies/src/backoff/constant-backoff.ts +++ b/packages/retry-strategies/src/backoff/constant-backoff.ts @@ -40,3 +40,14 @@ export class ConstantBackoff implements BackoffStrategy { // No-op: ConstantBackoff has no mutable state to reset } } + +/** + * A backoff policy that always returns the same backoff delay. + * + * @param delay - The constant delay in milliseconds to return for each backoff (must be >= 0) + * @returns A new ConstantBackoff instance + * + * @throws {RangeError} If delay is NaN or less than 0 + */ +export const constant = (delay: number): ConstantBackoff => + new ConstantBackoff(delay); diff --git a/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.test.ts b/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.test.ts index eebbb2f..451fd36 100644 --- a/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.test.ts @@ -1,6 +1,9 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { DecorrelatedJitterBackoff } from "./decorrelated-jitter-backoff.ts"; +import { + DecorrelatedJitterBackoff, + decorrelatedJitter, +} from "./decorrelated-jitter-backoff.ts"; /* node:coverage disable */ suite("Decorrelated jitter backoff strategy (Unit)", () => { @@ -472,4 +475,25 @@ suite("Decorrelated jitter backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates DecorrelatedJitterBackoff instance", (ctx: TestContext) => { + ctx.plan(2); + + // Arrange & Act + const strategy = decorrelatedJitter(100, 5000); + + // Assert + ctx.assert.ok( + strategy instanceof DecorrelatedJitterBackoff, + "should return DecorrelatedJitterBackoff instance", + ); + // Just verify it returns a number (jitter is random) + const delay = strategy.nextBackoff(); + ctx.assert.ok( + typeof delay === "number" && delay >= 0, + "should work correctly", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.ts b/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.ts index 7da5b4a..d799482 100644 --- a/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.ts +++ b/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.ts @@ -69,3 +69,24 @@ export class DecorrelatedJitterBackoff implements BackoffStrategy { this.previousDelay = this.base; } } + +/** + * A backoff policy that uses the AWS DecorrelatedJitter algorithm. + * The delay for attempt n is: min(cap, random(base, previous_delay * 3)) + * + * This strategy decorrelates the retry attempts from each other, making the delays + * unpredictable and helping to avoid synchronization between multiple clients. + * It generally results in shorter overall wait times compared to other strategies. + * + * @param base - The base delay in milliseconds (must be >= 0) + * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) + * @returns A new DecorrelatedJitterBackoff instance + * + * @throws {RangeError} If base or cap is NaN or invalid + * + * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} + */ +export const decorrelatedJitter = ( + base: number, + cap: number = Number.POSITIVE_INFINITY, +): DecorrelatedJitterBackoff => new DecorrelatedJitterBackoff(base, cap); diff --git a/packages/retry-strategies/src/backoff/equal-jitter-backoff.test.ts b/packages/retry-strategies/src/backoff/equal-jitter-backoff.test.ts index 7d31a31..3f129ae 100644 --- a/packages/retry-strategies/src/backoff/equal-jitter-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/equal-jitter-backoff.test.ts @@ -1,6 +1,6 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { EqualJitterBackoff } from "./equal-jitter-backoff.ts"; +import { EqualJitterBackoff, equalJitter } from "./equal-jitter-backoff.ts"; /* node:coverage disable */ suite("Equal jitter backoff strategy (Unit)", () => { @@ -404,4 +404,25 @@ suite("Equal jitter backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates EqualJitterBackoff instance", (ctx: TestContext) => { + ctx.plan(2); + + // Arrange & Act + const strategy = equalJitter(100, 5000); + + // Assert + ctx.assert.ok( + strategy instanceof EqualJitterBackoff, + "should return EqualJitterBackoff instance", + ); + // Just verify it returns a number (jitter is random) + const delay = strategy.nextBackoff(); + ctx.assert.ok( + typeof delay === "number" && delay >= 0, + "should work correctly", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/equal-jitter-backoff.ts b/packages/retry-strategies/src/backoff/equal-jitter-backoff.ts index 2d6c74a..8b16d7c 100644 --- a/packages/retry-strategies/src/backoff/equal-jitter-backoff.ts +++ b/packages/retry-strategies/src/backoff/equal-jitter-backoff.ts @@ -66,3 +66,23 @@ export class EqualJitterBackoff implements BackoffStrategy { this.attemptCount = 0; } } + +/** + * A backoff policy that uses the AWS EqualJitter algorithm. + * The delay for attempt n is: temp = min(cap, base * 2 ** n), then delay = temp / 2 + random(0, temp / 2) + * + * This strategy balances between consistent delays and randomness to prevent thundering herd problems + * while maintaining more predictable timing than FullJitter. + * + * @param base - The base delay in milliseconds (must be >= 0) + * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) + * @returns A new EqualJitterBackoff instance + * + * @throws {RangeError} If base or cap is NaN or invalid + * + * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} + */ +export const equalJitter = ( + base: number, + cap: number = Number.POSITIVE_INFINITY, +): EqualJitterBackoff => new EqualJitterBackoff(base, cap); diff --git a/packages/retry-strategies/src/backoff/exponential-backoff.test.ts b/packages/retry-strategies/src/backoff/exponential-backoff.test.ts index b376101..c04d478 100644 --- a/packages/retry-strategies/src/backoff/exponential-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/exponential-backoff.test.ts @@ -1,6 +1,6 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { ExponentialBackoff } from "./exponential-backoff.ts"; +import { ExponentialBackoff, exponential } from "./exponential-backoff.ts"; /* node:coverage disable */ suite("Exponential backoff strategy (Unit)", () => { @@ -327,4 +327,29 @@ suite("Exponential backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates ExponentialBackoff instance", (ctx: TestContext) => { + ctx.plan(3); + + // Arrange & Act + const strategy = exponential(100, 5000); + + // Assert + ctx.assert.ok( + strategy instanceof ExponentialBackoff, + "should return ExponentialBackoff instance", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 100, + "should work correctly", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 200, + "should maintain state correctly", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/exponential-backoff.ts b/packages/retry-strategies/src/backoff/exponential-backoff.ts index 8be1ec4..13773f5 100644 --- a/packages/retry-strategies/src/backoff/exponential-backoff.ts +++ b/packages/retry-strategies/src/backoff/exponential-backoff.ts @@ -66,3 +66,25 @@ export class ExponentialBackoff implements BackoffStrategy { this.attemptCount = 0; } } + +/** + * A backoff policy that increases the delay exponentially using the AWS algorithm. + * The delay for attempt n is: min(cap, base * 2 ** n) + * + * @param base - The base delay in milliseconds (must be >= 0) + * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) + * @returns A new ExponentialBackoff instance + * + * @throws {RangeError} If base or cap is NaN or invalid + * + * @remarks + * After an extremely large number of retry attempts (50+ for exponential strategies), + * floating-point precision may be lost in delay calculations. In practice, this is not + * a concern as the cap will have been reached long before precision loss occurs. + * + * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} + */ +export const exponential = ( + base: number, + cap: number = Number.POSITIVE_INFINITY, +): ExponentialBackoff => new ExponentialBackoff(base, cap); diff --git a/packages/retry-strategies/src/backoff/fibonacci-backoff.test.ts b/packages/retry-strategies/src/backoff/fibonacci-backoff.test.ts index 0fb6f1b..cf0071f 100644 --- a/packages/retry-strategies/src/backoff/fibonacci-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/fibonacci-backoff.test.ts @@ -1,6 +1,6 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { FibonacciBackoff } from "./fibonacci-backoff.ts"; +import { FibonacciBackoff, fibonacci } from "./fibonacci-backoff.ts"; /* node:coverage disable */ suite("Fibonacci backoff strategy (Unit)", () => { @@ -392,4 +392,29 @@ suite("Fibonacci backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates FibonacciBackoff instance", (ctx: TestContext) => { + ctx.plan(3); + + // Arrange & Act + const strategy = fibonacci(100, 5000); + + // Assert + ctx.assert.ok( + strategy instanceof FibonacciBackoff, + "should return FibonacciBackoff instance", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 100, + "should work correctly", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 100, + "should maintain state correctly", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/fibonacci-backoff.ts b/packages/retry-strategies/src/backoff/fibonacci-backoff.ts index 11543ef..aeb600e 100644 --- a/packages/retry-strategies/src/backoff/fibonacci-backoff.ts +++ b/packages/retry-strategies/src/backoff/fibonacci-backoff.ts @@ -73,3 +73,24 @@ export class FibonacciBackoff implements BackoffStrategy { this.currentDelay = this.base; } } + +/** + * A backoff policy that increases the delay following the Fibonacci sequence. + * The delay for each attempt follows: base, base, 2*base, 3*base, 5*base, 8*base, 13*base... + * The sequence is capped at a maximum delay value. + * + * @param base - The base delay in milliseconds (must be >= 0) + * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) + * @returns A new FibonacciBackoff instance + * + * @throws {RangeError} If base or cap is NaN or invalid + * + * @remarks + * After an extremely large number of retry attempts (~90+ iterations), + * floating-point precision may be lost in Fibonacci calculations. In practice, this is not + * a concern as the cap will have been reached long before precision loss occurs. + */ +export const fibonacci = ( + base: number, + cap: number = Number.POSITIVE_INFINITY, +): FibonacciBackoff => new FibonacciBackoff(base, cap); diff --git a/packages/retry-strategies/src/backoff/full-jitter-backoff.test.ts b/packages/retry-strategies/src/backoff/full-jitter-backoff.test.ts index 0a0c9fa..9d6627b 100644 --- a/packages/retry-strategies/src/backoff/full-jitter-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/full-jitter-backoff.test.ts @@ -1,6 +1,6 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { FullJitterBackoff } from "./full-jitter-backoff.ts"; +import { FullJitterBackoff, fullJitter } from "./full-jitter-backoff.ts"; /* node:coverage disable */ suite("Full jitter backoff strategy (Unit)", () => { @@ -400,4 +400,25 @@ suite("Full jitter backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates FullJitterBackoff instance", (ctx: TestContext) => { + ctx.plan(2); + + // Arrange & Act + const strategy = fullJitter(100, 5000); + + // Assert + ctx.assert.ok( + strategy instanceof FullJitterBackoff, + "should return FullJitterBackoff instance", + ); + // Just verify it returns a number (jitter is random) + const delay = strategy.nextBackoff(); + ctx.assert.ok( + typeof delay === "number" && delay >= 0, + "should work correctly", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/full-jitter-backoff.ts b/packages/retry-strategies/src/backoff/full-jitter-backoff.ts index 60a4a42..f4e78b3 100644 --- a/packages/retry-strategies/src/backoff/full-jitter-backoff.ts +++ b/packages/retry-strategies/src/backoff/full-jitter-backoff.ts @@ -65,3 +65,23 @@ export class FullJitterBackoff implements BackoffStrategy { this.attemptCount = 0; } } + +/** + * A backoff policy that uses the AWS FullJitter algorithm. + * The delay for attempt n is: random(0, min(cap, base * 2 ** n)) + * + * This strategy adds randomness to exponential backoff to prevent thundering herd problems + * where multiple clients retry at the same time. + * + * @param base - The base delay in milliseconds (must be >= 0) + * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) + * @returns A new FullJitterBackoff instance + * + * @throws {RangeError} If base or cap is NaN or invalid + * + * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} + */ +export const fullJitter = ( + base: number, + cap: number = Number.POSITIVE_INFINITY, +): FullJitterBackoff => new FullJitterBackoff(base, cap); diff --git a/packages/retry-strategies/src/backoff/linear-backoff.test.ts b/packages/retry-strategies/src/backoff/linear-backoff.test.ts index 01b2e86..d71f3ae 100644 --- a/packages/retry-strategies/src/backoff/linear-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/linear-backoff.test.ts @@ -1,6 +1,6 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { LinearBackoff } from "./linear-backoff.ts"; +import { LinearBackoff, linear } from "./linear-backoff.ts"; /* node:coverage disable */ suite("Linear backoff strategy (Unit)", () => { @@ -462,4 +462,29 @@ suite("Linear backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates LinearBackoff instance", (ctx: TestContext) => { + ctx.plan(3); + + // Arrange & Act + const strategy = linear(100, 500); + + // Assert + ctx.assert.ok( + strategy instanceof LinearBackoff, + "should return LinearBackoff instance", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 500, + "should work correctly", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 600, + "should maintain state correctly", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/linear-backoff.ts b/packages/retry-strategies/src/backoff/linear-backoff.ts index 9260f5d..e455ed1 100644 --- a/packages/retry-strategies/src/backoff/linear-backoff.ts +++ b/packages/retry-strategies/src/backoff/linear-backoff.ts @@ -80,3 +80,20 @@ export class LinearBackoff implements BackoffStrategy { this.attemptCount = 0; } } + +/** + * A backoff policy that increases the delay linearly by a fixed increment on each retry. + * The delay for attempt n is: min(cap, initialDelay + (increment * n)) + * + * @param increment - The amount to increase the delay by on each retry (must be >= 0) + * @param initialDelay - The initial delay in milliseconds before any increments (must be >= 0, defaults to 0) + * @param cap - The maximum delay in milliseconds (must be >= initialDelay, defaults to Infinity) + * @returns A new LinearBackoff instance + * + * @throws {RangeError} If increment, initialDelay, or cap is NaN or invalid + */ +export const linear = ( + increment: number, + initialDelay = 0, + cap: number = Number.POSITIVE_INFINITY, +): LinearBackoff => new LinearBackoff(increment, initialDelay, cap); diff --git a/packages/retry-strategies/src/backoff/stop-backoff.test.ts b/packages/retry-strategies/src/backoff/stop-backoff.test.ts index a55678f..202d7d6 100644 --- a/packages/retry-strategies/src/backoff/stop-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/stop-backoff.test.ts @@ -1,6 +1,6 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { StopBackoff } from "./stop-backoff.ts"; +import { StopBackoff, stop } from "./stop-backoff.ts"; /* node:coverage disable */ suite("Stop backoff strategy (Unit)", () => { @@ -87,4 +87,27 @@ suite("Stop backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates StopBackoff instance", (ctx: TestContext) => { + ctx.plan(3); + + // Arrange & Act + const strategy = stop(); + + // Assert + ctx.assert.ok( + strategy instanceof StopBackoff, + "should return StopBackoff instance", + ); + ctx.assert.ok( + Number.isNaN(strategy.nextBackoff()), + "should work correctly", + ); + ctx.assert.ok( + Number.isNaN(strategy.nextBackoff()), + "should always return NaN", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/stop-backoff.ts b/packages/retry-strategies/src/backoff/stop-backoff.ts index 27a4907..8ebd355 100644 --- a/packages/retry-strategies/src/backoff/stop-backoff.ts +++ b/packages/retry-strategies/src/backoff/stop-backoff.ts @@ -23,3 +23,11 @@ export class StopBackoff implements BackoffStrategy { // No-op: StopBackoff has no state to reset } } + +/** + * A fixed backoff policy that always returns NaN for nextBackoff(), + * meaning that the operation should never be retried. + * + * @returns A new StopBackoff instance + */ +export const stop = (): StopBackoff => new StopBackoff(); diff --git a/packages/retry-strategies/src/backoff/zero-backoff.test.ts b/packages/retry-strategies/src/backoff/zero-backoff.test.ts index 80028a0..ef3c313 100644 --- a/packages/retry-strategies/src/backoff/zero-backoff.test.ts +++ b/packages/retry-strategies/src/backoff/zero-backoff.test.ts @@ -1,6 +1,6 @@ import { describe, suite, type TestContext, test } from "node:test"; -import { ZeroBackoff } from "./zero-backoff.ts"; +import { ZeroBackoff, zero } from "./zero-backoff.ts"; /* node:coverage disable */ suite("Zero backoff strategy (Unit)", () => { @@ -98,4 +98,29 @@ suite("Zero backoff strategy (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates ZeroBackoff instance", (ctx: TestContext) => { + ctx.plan(3); + + // Arrange & Act + const strategy = zero(); + + // Assert + ctx.assert.ok( + strategy instanceof ZeroBackoff, + "should return ZeroBackoff instance", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 0, + "should work correctly", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 0, + "should always return zero", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/backoff/zero-backoff.ts b/packages/retry-strategies/src/backoff/zero-backoff.ts index 971f43a..5a1fc88 100644 --- a/packages/retry-strategies/src/backoff/zero-backoff.ts +++ b/packages/retry-strategies/src/backoff/zero-backoff.ts @@ -23,3 +23,11 @@ export class ZeroBackoff implements BackoffStrategy { // No-op: ZeroBackoff has no state to reset } } + +/** + * A fixed backoff policy whose backoff time is always zero, + * meaning that the operation is retried immediately without waiting, indefinitely. + * + * @returns A new ZeroBackoff instance + */ +export const zero = (): ZeroBackoff => new ZeroBackoff(); diff --git a/packages/retry-strategies/src/utils/upto.test.ts b/packages/retry-strategies/src/utils/upto.test.ts index f6775c1..a9f3813 100644 --- a/packages/retry-strategies/src/utils/upto.test.ts +++ b/packages/retry-strategies/src/utils/upto.test.ts @@ -1,7 +1,8 @@ import { describe, suite, type TestContext, test } from "node:test"; +import { ExponentialBackoff } from "../backoff/exponential-backoff.ts"; import type { BackoffStrategy } from "../backoff/interface.ts"; -import { upto } from "./upto.ts"; +import { UptoBackoff, upto } from "./upto.ts"; /* node:coverage disable */ @@ -459,4 +460,32 @@ suite("Up to retry limiter (Unit)", () => { ); }); }); + + describe("factory function", () => { + test("creates UptoBackoff instance", (ctx: TestContext) => { + ctx.plan(3); + + // Arrange + const exponential = new ExponentialBackoff(100, 5000); + + // Act + const strategy = upto(3, exponential); + + // Assert + ctx.assert.ok( + strategy instanceof UptoBackoff, + "should return UptoBackoff instance", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 100, + "should work correctly", + ); + ctx.assert.strictEqual( + strategy.nextBackoff(), + 200, + "should maintain state correctly", + ); + }); + }); }); diff --git a/packages/retry-strategies/src/utils/upto.ts b/packages/retry-strategies/src/utils/upto.ts index c981be6..af6ce40 100644 --- a/packages/retry-strategies/src/utils/upto.ts +++ b/packages/retry-strategies/src/utils/upto.ts @@ -4,16 +4,12 @@ import type { BackoffStrategy } from "../backoff/interface.ts"; * Limits a backoff strategy to a maximum number of retry attempts. * Once the limit is reached, `nextBackoff()` returns `NaN` to stop retrying. * - * @param retries - Maximum number of retry attempts allowed (must be >= 0 and an integer) - * @param strategy - The underlying backoff strategy to wrap - * @returns A new BackoffStrategy that stops after the specified number of retries - * - * @throws {RangeError} If retries is NaN, not an integer, or less than 0 - * * @example * ```ts + * import { ExponentialBackoff, UptoBackoff } from '@proventuslabs/retry-strategies'; + * * const exponential = new ExponentialBackoff(100, 5000); - * const limited = upto(3, exponential); // Only allow 3 retries + * const limited = new UptoBackoff(3, exponential); * * limited.nextBackoff(); // Returns delay from exponential * limited.nextBackoff(); // Returns delay from exponential @@ -21,31 +17,70 @@ import type { BackoffStrategy } from "../backoff/interface.ts"; * limited.nextBackoff(); // Returns NaN - no more retries * ``` */ -export const upto = ( - retries: number, - strategy: BackoffStrategy, -): BackoffStrategy => { - if (Number.isNaN(retries)) { - throw new RangeError(`Retries must not be NaN`); - } - if (!Number.isInteger(retries)) { - throw new RangeError(`Retries must be an integer, received: ${retries}`); - } - if (retries < 0) { - throw new RangeError(`Retries must be 0 or greater, received: ${retries}`); +export class UptoBackoff implements BackoffStrategy { + private attemptsLeft: number; + private readonly retries: number; + private readonly strategy: T; + + /** + * Creates a new UptoBackoff instance. + * + * @param retries - Maximum number of retry attempts allowed (must be >= 0 and an integer) + * @param strategy - The underlying backoff strategy to wrap + * @throws {RangeError} If retries is NaN, not an integer, or less than 0 + */ + public constructor(retries: number, strategy: T) { + if (Number.isNaN(retries)) { + throw new RangeError(`Retries must not be NaN`); + } + if (!Number.isInteger(retries)) { + throw new RangeError(`Retries must be an integer, received: ${retries}`); + } + if (retries < 0) { + throw new RangeError( + `Retries must be 0 or greater, received: ${retries}`, + ); + } + + this.attemptsLeft = retries; + this.retries = retries; + this.strategy = strategy; } - let attemptsLeft = retries; + /** + * Calculate the next backoff delay. + * Returns the delay from the underlying strategy until the retry limit is reached, + * then returns NaN to stop retrying. + * + * @returns The next delay in milliseconds from the underlying strategy, or NaN if retries exhausted + */ + nextBackoff(): number { + if (this.attemptsLeft-- <= 0) return NaN; + + return this.strategy.nextBackoff(); + } - return { - nextBackoff() { - if (attemptsLeft-- <= 0) return NaN; + /** + * Reset to the initial state. + * Resets both the retry counter and the underlying strategy. + */ + resetBackoff(): void { + this.attemptsLeft = this.retries; + this.strategy.resetBackoff(); + } +} - return strategy.nextBackoff(); - }, - resetBackoff() { - attemptsLeft = retries; - strategy.resetBackoff(); - }, - }; -}; +/** + * Limits a backoff strategy to a maximum number of retry attempts. + * Once the limit is reached, `nextBackoff()` returns `NaN` to stop retrying. + * + * @param retries - Maximum number of retry attempts allowed (must be >= 0 and an integer) + * @param strategy - The underlying backoff strategy to wrap + * @returns A new UptoBackoff instance that stops after the specified number of retries + * + * @throws {RangeError} If retries is NaN, not an integer, or less than 0 + */ +export const upto = ( + retries: number, + strategy: T, +): UptoBackoff => new UptoBackoff(retries, strategy); From 206656d37f22d249fa9e63b9b70a53dc780c44aa Mon Sep 17 00:00:00 2001 From: vabatta <2137077+vabatta@users.noreply.github.com> Date: Sun, 30 Nov 2025 11:09:03 +0100 Subject: [PATCH 2/2] docs alignment --- packages/retry-strategies/README.md | 119 +++++++----------- .../src/backoff/constant-backoff.ts | 19 ++- .../backoff/decorrelated-jitter-backoff.ts | 38 +++--- .../src/backoff/equal-jitter-backoff.ts | 34 +++-- .../src/backoff/exponential-backoff.ts | 37 ++---- .../src/backoff/fibonacci-backoff.ts | 39 ++---- .../src/backoff/full-jitter-backoff.ts | 34 +++-- .../src/backoff/linear-backoff.ts | 31 +++-- .../src/backoff/stop-backoff.ts | 12 +- .../src/backoff/zero-backoff.ts | 12 +- packages/retry-strategies/src/retry/retry.ts | 54 ++++---- packages/retry-strategies/src/utils/upto.ts | 35 +++--- .../retry-strategies/src/utils/wait-for.ts | 12 +- 13 files changed, 190 insertions(+), 286 deletions(-) diff --git a/packages/retry-strategies/README.md b/packages/retry-strategies/README.md index 8a42344..8e632a8 100644 --- a/packages/retry-strategies/README.md +++ b/packages/retry-strategies/README.md @@ -44,60 +44,59 @@ const result = await retry( ### `retry(fn, options)` -Attempts to execute a function repeatedly according to a backoff strategy until it succeeds, a provided stop condition is met, or an optional AbortSignal is triggered. +Executes a function repeatedly according to a backoff strategy until it succeeds, stops, or is aborted. #### Parameters -- **`fn`** `() => T | Promise` - The function to retry. Can be synchronous or return a promise. -- **`options`** `RetryOptions` - Configuration options: - - **`strategy`** `BackoffStrategy` - Strategy for calculating delays between retries (required) - - **`stop`** `(error: unknown, attempt: number) => boolean` - Optional function to determine whether to stop retrying. Return `true` to stop. (default: `() => false`) - - **`signal`** `AbortSignal` - Optional AbortSignal to cancel the retry operation (default: `undefined`) +- **`fn`** `() => T | Promise` - The function to retry +- **`options`** `RetryOptions` - Configuration: + - **`strategy`** `BackoffStrategy` - Delay calculation strategy (required) + - **`stop`** `(error: unknown, attempt: number) => boolean` - Stop condition (default: `() => false`) + - **`signal`** `AbortSignal` - Cancellation signal (optional) #### Returns -`Promise` - A promise that resolves with the function's result if it eventually succeeds. +`Promise` - Resolves with the function's result on success #### Throws -- **`unknown`** - The last encountered error if retries are exhausted or if the stop function returns `true` -- **`unknown`** - The reason of the AbortSignal if the operation is aborted (generally `DOMException` `AbortError`) -- **`RangeError`** - If the backoff strategy returns a delay exceeding INT32_MAX (2147483647ms, approximately 24.8 days) +- **`unknown`** - Last error if retries exhausted, stop condition met, or aborted +- **`RangeError`** - If delay exceeds INT32_MAX (2147483647ms) ### `waitFor(delay, signal?)` -Waits for the specified amount of time or until an optional AbortSignal is triggered. +Waits for a specified duration or until aborted. #### Parameters -- **`delay`** `number` - Duration to wait in milliseconds. Negative values are treated as zero. -- **`signal`** `AbortSignal` - Optional AbortSignal to cancel the wait. +- **`delay`** `number` - Wait duration in milliseconds (negative treated as zero) +- **`signal`** `AbortSignal` - Cancellation signal (optional) #### Returns -`Promise` - A promise that resolves after the delay has elapsed or rejects if the signal is aborted. +`Promise` - Resolves after delay or rejects if aborted #### Throws -- **`unknown`** - The reason of the AbortSignal if the operation is aborted (generally `DOMException` `AbortError`) -- **`RangeError`** - If the delay exceeds INT32_MAX (2147483647ms, approximately 24.8 days) +- **`unknown`** - Abort reason if cancelled +- **`RangeError`** - If delay exceeds INT32_MAX (2147483647ms) ### `upto(retries, strategy)` -Limits a backoff strategy to a maximum number of retry attempts. Once the limit is reached, `nextBackoff()` returns `NaN` to stop retrying. +Limits a strategy to a maximum number of retry attempts. #### Parameters -- **`retries`** `number` - Maximum number of retry attempts allowed (must be >= 0 and an integer) -- **`strategy`** `BackoffStrategy` - The underlying backoff strategy to wrap +- **`retries`** `number` - Maximum retry attempts (integer >= 0) +- **`strategy`** `BackoffStrategy` - Strategy to wrap #### Returns -`UptoBackoff` - A new UptoBackoff instance that stops after the specified number of retries +`UptoBackoff` - Strategy that stops after specified retries #### Throws -- **`RangeError`** - If retries is NaN, not an integer, or less than 0 +- **`RangeError`** - If retries is invalid (NaN, non-integer, or < 0) ### `BackoffStrategy` Interface @@ -125,108 +124,89 @@ All strategies are available in both class-based and functional styles. Examples ### `ExponentialBackoff` / `exponential()` Increases the delay exponentially using the AWS algorithm. - **Formula:** `min(cap, base * 2^n)` ```typescript const strategy = exponential(100, 5000); -// Parameters: -// base: 100 - base delay in ms -// cap: 5000 - cap (maximum delay) in ms (optional, defaults to Infinity) -// Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 5000ms, 5000ms... +// base: 100, cap: 5000 (optional, default: Infinity) +// Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 5000ms... ``` ### `LinearBackoff` / `linear()` -Increases the delay linearly by a fixed increment on each retry. - +Increases the delay linearly by a fixed increment. **Formula:** `min(cap, initialDelay + (increment * n))` ```typescript const strategy = linear(1000, 500, 10000); -// Parameters: -// increment: 1000 - increment in ms -// initialDelay: 500 - initial delay in ms (optional, default: 0) -// cap: 10000 - cap (maximum delay) in ms (optional, defaults to Infinity) +// increment: 1000, initialDelay: 500 (default: 0), cap: 10000 (default: Infinity) // Delays: 500ms, 1500ms, 2500ms, 3500ms, 4500ms... ``` ### `FibonacciBackoff` / `fibonacci()` Increases the delay following the Fibonacci sequence. - **Formula:** `min(cap, base * fib(n))` ```typescript const strategy = fibonacci(100, 10000); -// Parameters: -// base: 100 - base delay in ms -// cap: 10000 - cap (maximum delay) in ms (optional, defaults to Infinity) +// base: 100, cap: 10000 (default: Infinity) // Delays: 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, 1300ms, 2100ms... ``` ### `FullJitterBackoff` / `fullJitter()` -Uses the AWS FullJitter algorithm to add randomness to exponential backoff, preventing thundering herd problems. - +AWS FullJitter algorithm - adds randomness to exponential backoff. **Formula:** `random(0, min(cap, base * 2^n))` ```typescript const strategy = fullJitter(100, 5000); -// Parameters: -// base: 100 - base delay in ms -// cap: 5000 - cap (maximum delay) in ms (optional, defaults to Infinity) -// Delays: random values between 0 and the exponential cap +// base: 100, cap: 5000 (default: Infinity) +// Delays: random values between 0 and exponential cap ``` ### `EqualJitterBackoff` / `equalJitter()` -Uses the AWS EqualJitter algorithm, providing a balanced approach between exponential backoff and full jitter. - +AWS EqualJitter algorithm - balances consistency and randomness. **Formula:** `(min(cap, base * 2^n) / 2) + random(0, min(cap, base * 2^n) / 2)` ```typescript const strategy = equalJitter(100, 5000); -// Parameters: -// base: 100 - base delay in ms -// cap: 5000 - cap (maximum delay) in ms (optional, defaults to Infinity) +// base: 100, cap: 5000 (default: Infinity) ``` ### `DecorrelatedJitterBackoff` / `decorrelatedJitter()` -Uses the AWS Decorrelated Jitter algorithm, where each delay is based on the previous delay rather than attempt count. - +AWS DecorrelatedJitter algorithm - each delay based on previous delay. **Formula:** `min(cap, random(base, previousDelay * 3))` ```typescript const strategy = decorrelatedJitter(100, 10000); -// Parameters: -// base: 100 - base delay in ms -// cap: 10000 - cap (maximum delay) in ms (optional, defaults to Infinity) +// base: 100, cap: 10000 (default: Infinity) ``` ### `ConstantBackoff` / `constant()` -Always returns the same backoff delay, useful for fixed-interval polling. +Always returns the same delay. ```typescript -const strategy = constant(1000); // Always 1000ms +const strategy = constant(1000); ``` ### `ZeroBackoff` / `zero()` -Always returns zero delay, useful for immediate retries without waiting. +Always returns zero delay for immediate retries. ```typescript -const strategy = zero(); // Always 0ms +const strategy = zero(); ``` ### `StopBackoff` / `stop()` -Always returns `NaN`, indicating that no retries should be made. +Always returns `NaN` to prevent retries. ```typescript -const strategy = stop(); // Never retries +const strategy = stop(); ``` ## Behavior @@ -242,13 +222,13 @@ The retry loop continues indefinitely until one of these conditions is met: ### Edge Cases -- **Negative delays**: Treated as zero (no wait) -- **NaN from strategy**: Immediately stops retrying and throws the last error -- **Delays exceeding INT32_MAX**: Throws `RangeError` before attempting the wait +- **Negative delays**: Treated as zero +- **NaN from strategy**: Stops retrying and throws last error +- **Delays exceeding INT32_MAX**: Throws `RangeError` ### Concurrency Safety -**Important:** The `retry` function is not concurrently safe when using stateful strategies. If you need to retry multiple operations in parallel, create separate strategy instances for each retry operation: +**Important:** Not safe to share stateful strategies across concurrent `retry` operations. Create separate instances: ```typescript // ❌ Not safe - shared strategy @@ -276,14 +256,12 @@ async function fetchUserData(userId: string) { return retry( async () => { const response = await fetch(`/api/users/${userId}`); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } + if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }, { strategy: upto(5, exponential(100, 5000)), - stop: (error) => error.message.includes("HTTP 4") // Stop on client errors + stop: (error) => error.message.includes("HTTP 4") } ); } @@ -302,7 +280,7 @@ async function callRateLimitedAPI(endpoint: string) { return response.json(); }, { - strategy: fullJitter(1000, 30000), // Jitter prevents thundering herd + strategy: fullJitter(1000, 30000), stop: (error) => !error.message.includes("Rate limited") } ); @@ -332,10 +310,9 @@ async function fetchWithTimeout(url: string, timeoutMs: number) { ## Limitations - **Maximum delay**: Delays cannot exceed INT32_MAX (2147483647ms, approximately 24.8 days) due to `setTimeout` limitations -- **Not concurrency-safe**: Stateful strategies should not be shared across concurrent `retry` operations -- **No built-in attempt limit**: The `retry` function will continue indefinitely unless the strategy exhausts, the stop function returns `true`, or an abort signal is triggered. Use the `upto()` utility to limit attempts, provide a stop condition, or use strategies that eventually return `NaN`. -- **Randomness**: Jitter-based strategies use `Math.random()`, which is not cryptographically secure -- **Timing precision**: Actual delays may vary slightly due to JavaScript event loop timing +- **Not concurrency-safe**: Don't share stateful strategies across concurrent operations +- **No built-in attempt limit**: Use the `upto()` utility to limit attempts, provide a `retry` stop condition, or use strategies that eventually return `NaN` +- **Randomness**: Jitter-based strategies use `Math.random()` ## Standards References diff --git a/packages/retry-strategies/src/backoff/constant-backoff.ts b/packages/retry-strategies/src/backoff/constant-backoff.ts index 024773d..9e3b67f 100644 --- a/packages/retry-strategies/src/backoff/constant-backoff.ts +++ b/packages/retry-strategies/src/backoff/constant-backoff.ts @@ -1,7 +1,7 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A backoff policy that always returns the same backoff delay. + * Always returns the same delay. */ export class ConstantBackoff implements BackoffStrategy { private readonly delay: number; @@ -9,8 +9,8 @@ export class ConstantBackoff implements BackoffStrategy { /** * Creates a new ConstantBackoff instance. * - * @param delay - The constant delay in milliseconds to return for each backoff (must be >= 0) - * @throws {RangeError} If delay is NaN or less than 0 + * @param delay - Constant delay in milliseconds (>= 0) + * @throws {RangeError} If delay is invalid */ public constructor(delay: number) { if (Number.isNaN(delay)) { @@ -24,9 +24,8 @@ export class ConstantBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Always returns the configured constant delay. * - * @returns The constant delay in milliseconds + * @returns Constant delay in milliseconds */ public nextBackoff(): number { return this.delay; @@ -34,7 +33,6 @@ export class ConstantBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Since ConstantBackoff has no mutable state, this is a no-op. */ public resetBackoff(): void { // No-op: ConstantBackoff has no mutable state to reset @@ -42,12 +40,11 @@ export class ConstantBackoff implements BackoffStrategy { } /** - * A backoff policy that always returns the same backoff delay. + * Always returns the same delay. * - * @param delay - The constant delay in milliseconds to return for each backoff (must be >= 0) - * @returns A new ConstantBackoff instance - * - * @throws {RangeError} If delay is NaN or less than 0 + * @param delay - Constant delay in milliseconds (>= 0) + * @returns ConstantBackoff instance + * @throws {RangeError} If delay is invalid */ export const constant = (delay: number): ConstantBackoff => new ConstantBackoff(delay); diff --git a/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.ts b/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.ts index d799482..0bcc0af 100644 --- a/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.ts +++ b/packages/retry-strategies/src/backoff/decorrelated-jitter-backoff.ts @@ -1,13 +1,11 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A backoff policy that uses the AWS DecorrelatedJitter algorithm. + * AWS DecorrelatedJitter algorithm - each delay based on previous delay. + * Decorrelates retry attempts to avoid synchronization between clients. + * Generally results in shorter overall wait times. * - * The delay for attempt n is: min(cap, random(base, previous_delay * 3)) - * - * This strategy decorrelates the retry attempts from each other, making the delays - * unpredictable and helping to avoid synchronization between multiple clients. - * It generally results in shorter overall wait times compared to other strategies. + * Formula: `min(cap, random(base, previousDelay * 3))` * * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} */ @@ -19,9 +17,9 @@ export class DecorrelatedJitterBackoff implements BackoffStrategy { /** * Creates a new DecorrelatedJitterBackoff instance. * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @throws {RangeError} If base or cap is NaN or invalid + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @throws {RangeError} If base or cap is invalid */ public constructor(base: number, cap: number = Number.POSITIVE_INFINITY) { if (Number.isNaN(base)) { @@ -47,9 +45,8 @@ export class DecorrelatedJitterBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Returns a random delay between base and triple the previous delay, capped at maximum. * - * @returns The next delay in milliseconds: min(cap, random(base, previous_delay * 3)) + * @returns Random delay in milliseconds: `min(cap, random(base, previousDelay * 3))` */ public nextBackoff(): number { const upperBound = this.previousDelay * 3; @@ -63,7 +60,6 @@ export class DecorrelatedJitterBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Resets the previous delay to the base delay. */ public resetBackoff(): void { this.previousDelay = this.base; @@ -71,18 +67,16 @@ export class DecorrelatedJitterBackoff implements BackoffStrategy { } /** - * A backoff policy that uses the AWS DecorrelatedJitter algorithm. - * The delay for attempt n is: min(cap, random(base, previous_delay * 3)) - * - * This strategy decorrelates the retry attempts from each other, making the delays - * unpredictable and helping to avoid synchronization between multiple clients. - * It generally results in shorter overall wait times compared to other strategies. + * AWS DecorrelatedJitter algorithm - each delay based on previous delay. + * Formula: `min(cap, random(base, previousDelay * 3))` * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @returns A new DecorrelatedJitterBackoff instance + * Decorrelates retry attempts to avoid synchronization between clients. + * Generally results in shorter overall wait times. * - * @throws {RangeError} If base or cap is NaN or invalid + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @returns DecorrelatedJitterBackoff instance + * @throws {RangeError} If base or cap is invalid * * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} */ diff --git a/packages/retry-strategies/src/backoff/equal-jitter-backoff.ts b/packages/retry-strategies/src/backoff/equal-jitter-backoff.ts index 8b16d7c..91f9acb 100644 --- a/packages/retry-strategies/src/backoff/equal-jitter-backoff.ts +++ b/packages/retry-strategies/src/backoff/equal-jitter-backoff.ts @@ -1,12 +1,10 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A backoff policy that uses the AWS EqualJitter algorithm. + * AWS EqualJitter algorithm - balances consistency and randomness. + * Provides more predictable timing than FullJitter while still preventing thundering herd. * - * The delay for attempt n is: temp = min(cap, base * 2 ** n), then delay = temp / 2 + random(0, temp / 2) - * - * This strategy balances between consistent delays and randomness to prevent thundering herd problems - * while maintaining more predictable timing than FullJitter. + * Formula: `(min(cap, base * 2^n) / 2) + random(0, min(cap, base * 2^n) / 2)` * * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} */ @@ -18,9 +16,9 @@ export class EqualJitterBackoff implements BackoffStrategy { /** * Creates a new EqualJitterBackoff instance. * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @throws {RangeError} If base or cap is NaN or invalid + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @throws {RangeError} If base or cap is invalid */ public constructor(base: number, cap: number = Number.POSITIVE_INFINITY) { if (Number.isNaN(base)) { @@ -46,9 +44,8 @@ export class EqualJitterBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Returns a delay that is half deterministic and half random. * - * @returns The next delay in milliseconds: temp / 2 + random(0, temp / 2), where temp = min(cap, base * 2 ** attemptCount) + * @returns Delay in milliseconds: `(temp / 2) + random(0, temp / 2)` where `temp = min(cap, base * 2^n)` */ public nextBackoff(): number { const temp = Math.min(this.cap, this.base * 2 ** this.attemptCount); @@ -60,7 +57,6 @@ export class EqualJitterBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Resets the attempt counter to 0, so the next call to nextBackoff will use attempt 0. */ public resetBackoff(): void { this.attemptCount = 0; @@ -68,17 +64,15 @@ export class EqualJitterBackoff implements BackoffStrategy { } /** - * A backoff policy that uses the AWS EqualJitter algorithm. - * The delay for attempt n is: temp = min(cap, base * 2 ** n), then delay = temp / 2 + random(0, temp / 2) - * - * This strategy balances between consistent delays and randomness to prevent thundering herd problems - * while maintaining more predictable timing than FullJitter. + * AWS EqualJitter algorithm - balances consistency and randomness. + * Formula: `(min(cap, base * 2^n) / 2) + random(0, min(cap, base * 2^n) / 2)` * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @returns A new EqualJitterBackoff instance + * Provides more predictable timing than FullJitter while still preventing thundering herd. * - * @throws {RangeError} If base or cap is NaN or invalid + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @returns EqualJitterBackoff instance + * @throws {RangeError} If base or cap is invalid * * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} */ diff --git a/packages/retry-strategies/src/backoff/exponential-backoff.ts b/packages/retry-strategies/src/backoff/exponential-backoff.ts index 13773f5..1c6a379 100644 --- a/packages/retry-strategies/src/backoff/exponential-backoff.ts +++ b/packages/retry-strategies/src/backoff/exponential-backoff.ts @@ -1,9 +1,9 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A backoff policy that increases the delay exponentially using the AWS algorithm. + * Increases the delay exponentially using the AWS algorithm. * - * The delay for attempt n is: min(cap, base * 2 ** n) + * Formula: `min(cap, base * 2^n)` * * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} */ @@ -15,14 +15,9 @@ export class ExponentialBackoff implements BackoffStrategy { /** * Creates a new ExponentialBackoff instance. * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @throws {RangeError} If base or cap is NaN or invalid - * - * @remarks - * After an extremely large number of retry attempts (50+ for exponential strategies), - * floating-point precision may be lost in delay calculations. In practice, this is not - * a concern as the cap will have been reached long before precision loss occurs. + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @throws {RangeError} If base or cap is invalid */ public constructor(base: number, cap: number = Number.POSITIVE_INFINITY) { if (Number.isNaN(base)) { @@ -48,9 +43,8 @@ export class ExponentialBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Returns a delay that increases exponentially with each call, capped at the maximum. * - * @returns The next delay in milliseconds: min(cap, base * 2 ** attemptCount) + * @returns Delay in milliseconds: `min(cap, base * 2^n)` */ public nextBackoff(): number { const delay = Math.min(this.cap, this.base * 2 ** this.attemptCount); @@ -60,7 +54,6 @@ export class ExponentialBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Resets the attempt counter to 0, so the next call to nextBackoff will return the base delay. */ public resetBackoff(): void { this.attemptCount = 0; @@ -68,19 +61,13 @@ export class ExponentialBackoff implements BackoffStrategy { } /** - * A backoff policy that increases the delay exponentially using the AWS algorithm. - * The delay for attempt n is: min(cap, base * 2 ** n) - * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @returns A new ExponentialBackoff instance - * - * @throws {RangeError} If base or cap is NaN or invalid + * Increases the delay exponentially using the AWS algorithm. + * Formula: `min(cap, base * 2^n)` * - * @remarks - * After an extremely large number of retry attempts (50+ for exponential strategies), - * floating-point precision may be lost in delay calculations. In practice, this is not - * a concern as the cap will have been reached long before precision loss occurs. + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @returns ExponentialBackoff instance + * @throws {RangeError} If base or cap is invalid * * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} */ diff --git a/packages/retry-strategies/src/backoff/fibonacci-backoff.ts b/packages/retry-strategies/src/backoff/fibonacci-backoff.ts index aeb600e..c9552e5 100644 --- a/packages/retry-strategies/src/backoff/fibonacci-backoff.ts +++ b/packages/retry-strategies/src/backoff/fibonacci-backoff.ts @@ -1,10 +1,9 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A backoff policy that increases the delay following the Fibonacci sequence. + * Increases the delay following the Fibonacci sequence. * - * The delay for each attempt follows: base, base, 2*base, 3*base, 5*base, 8*base, 13*base... - * The sequence is capped at a maximum delay value. + * Formula: `min(cap, base * fib(n))` */ export class FibonacciBackoff implements BackoffStrategy { private readonly base: number; @@ -15,14 +14,9 @@ export class FibonacciBackoff implements BackoffStrategy { /** * Creates a new FibonacciBackoff instance. * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @throws {RangeError} If base or cap is NaN or invalid - * - * @remarks - * After an extremely large number of retry attempts (~90+ iterations), - * floating-point precision may be lost in Fibonacci calculations. In practice, this is not - * a concern as the cap will have been reached long before precision loss occurs. + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @throws {RangeError} If base or cap is invalid */ public constructor(base: number, cap: number = Number.POSITIVE_INFINITY) { if (Number.isNaN(base)) { @@ -49,9 +43,8 @@ export class FibonacciBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Returns a delay that follows the Fibonacci sequence, capped at the maximum. * - * @returns The next delay in milliseconds following the Fibonacci sequence + * @returns Delay in milliseconds: `min(cap, base * fib(n))` */ public nextBackoff(): number { const delay = Math.min(this.cap, this.currentDelay); @@ -66,7 +59,6 @@ export class FibonacciBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Resets the Fibonacci sequence to start from the beginning. */ public resetBackoff(): void { this.previousDelay = 0; @@ -75,20 +67,13 @@ export class FibonacciBackoff implements BackoffStrategy { } /** - * A backoff policy that increases the delay following the Fibonacci sequence. - * The delay for each attempt follows: base, base, 2*base, 3*base, 5*base, 8*base, 13*base... - * The sequence is capped at a maximum delay value. - * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @returns A new FibonacciBackoff instance - * - * @throws {RangeError} If base or cap is NaN or invalid + * Increases the delay following the Fibonacci sequence. + * Formula: `min(cap, base * fib(n))` * - * @remarks - * After an extremely large number of retry attempts (~90+ iterations), - * floating-point precision may be lost in Fibonacci calculations. In practice, this is not - * a concern as the cap will have been reached long before precision loss occurs. + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @returns FibonacciBackoff instance + * @throws {RangeError} If base or cap is invalid */ export const fibonacci = ( base: number, diff --git a/packages/retry-strategies/src/backoff/full-jitter-backoff.ts b/packages/retry-strategies/src/backoff/full-jitter-backoff.ts index f4e78b3..4b76751 100644 --- a/packages/retry-strategies/src/backoff/full-jitter-backoff.ts +++ b/packages/retry-strategies/src/backoff/full-jitter-backoff.ts @@ -1,12 +1,10 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A backoff policy that uses the AWS FullJitter algorithm. + * AWS FullJitter algorithm - adds randomness to exponential backoff. + * Prevents thundering herd problems where multiple clients retry simultaneously. * - * The delay for attempt n is: random(0, min(cap, base * 2 ** n)) - * - * This strategy adds randomness to exponential backoff to prevent thundering herd problems - * where multiple clients retry at the same time. + * Formula: `random(0, min(cap, base * 2^n))` * * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} */ @@ -18,9 +16,9 @@ export class FullJitterBackoff implements BackoffStrategy { /** * Creates a new FullJitterBackoff instance. * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @throws {RangeError} If base or cap is NaN or invalid + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @throws {RangeError} If base or cap is invalid */ public constructor(base: number, cap: number = Number.POSITIVE_INFINITY) { if (Number.isNaN(base)) { @@ -46,9 +44,8 @@ export class FullJitterBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Returns a random delay between 0 and the exponentially increasing maximum. * - * @returns The next delay in milliseconds: random(0, min(cap, base * 2 ** attemptCount)) + * @returns Random delay in milliseconds: `random(0, min(cap, base * 2^n))` */ public nextBackoff(): number { const maxDelay = Math.min(this.cap, this.base * 2 ** this.attemptCount); @@ -59,7 +56,6 @@ export class FullJitterBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Resets the attempt counter to 0, so the next call to nextBackoff will use attempt 0. */ public resetBackoff(): void { this.attemptCount = 0; @@ -67,17 +63,15 @@ export class FullJitterBackoff implements BackoffStrategy { } /** - * A backoff policy that uses the AWS FullJitter algorithm. - * The delay for attempt n is: random(0, min(cap, base * 2 ** n)) - * - * This strategy adds randomness to exponential backoff to prevent thundering herd problems - * where multiple clients retry at the same time. + * AWS FullJitter algorithm - adds randomness to exponential backoff. + * Formula: `random(0, min(cap, base * 2^n))` * - * @param base - The base delay in milliseconds (must be >= 0) - * @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity) - * @returns A new FullJitterBackoff instance + * Prevents thundering herd problems where multiple clients retry simultaneously. * - * @throws {RangeError} If base or cap is NaN or invalid + * @param base - Base delay in milliseconds (>= 0) + * @param cap - Maximum delay in milliseconds (>= base, default: Infinity) + * @returns FullJitterBackoff instance + * @throws {RangeError} If base or cap is invalid * * @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter} */ diff --git a/packages/retry-strategies/src/backoff/linear-backoff.ts b/packages/retry-strategies/src/backoff/linear-backoff.ts index e455ed1..c22cfee 100644 --- a/packages/retry-strategies/src/backoff/linear-backoff.ts +++ b/packages/retry-strategies/src/backoff/linear-backoff.ts @@ -1,9 +1,9 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A backoff policy that increases the delay linearly by a fixed increment on each retry. + * Increases the delay linearly by a fixed increment. * - * The delay for attempt n is: min(cap, initialDelay + (increment * n)) + * Formula: `min(cap, initialDelay + (increment * n))` */ export class LinearBackoff implements BackoffStrategy { private readonly initialDelay: number; @@ -14,10 +14,10 @@ export class LinearBackoff implements BackoffStrategy { /** * Creates a new LinearBackoff instance. * - * @param increment - The amount to increase the delay by on each retry (must be >= 0) - * @param initialDelay - The initial delay in milliseconds before any increments (must be >= 0, defaults to 0) - * @param cap - The maximum delay in milliseconds (must be >= initialDelay, defaults to Infinity) - * @throws {RangeError} If increment, initialDelay, or cap is NaN or invalid + * @param increment - Delay increment per retry (>= 0) + * @param initialDelay - Initial delay (>= 0, default: 0) + * @param cap - Maximum delay (>= initialDelay, default: Infinity) + * @throws {RangeError} If parameters are invalid */ public constructor( increment: number, @@ -59,9 +59,8 @@ export class LinearBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Returns a delay that increases linearly with each call, capped at the maximum. * - * @returns The next delay in milliseconds: min(cap, initialDelay + (increment * attemptCount)) + * @returns Delay in milliseconds: `min(cap, initialDelay + (increment * n))` */ public nextBackoff(): number { const delay = Math.min( @@ -74,7 +73,6 @@ export class LinearBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Resets the attempt counter to 0, so the next call to nextBackoff will return the initial delay. */ public resetBackoff(): void { this.attemptCount = 0; @@ -82,15 +80,14 @@ export class LinearBackoff implements BackoffStrategy { } /** - * A backoff policy that increases the delay linearly by a fixed increment on each retry. - * The delay for attempt n is: min(cap, initialDelay + (increment * n)) + * Increases the delay linearly by a fixed increment. + * Formula: `min(cap, initialDelay + (increment * n))` * - * @param increment - The amount to increase the delay by on each retry (must be >= 0) - * @param initialDelay - The initial delay in milliseconds before any increments (must be >= 0, defaults to 0) - * @param cap - The maximum delay in milliseconds (must be >= initialDelay, defaults to Infinity) - * @returns A new LinearBackoff instance - * - * @throws {RangeError} If increment, initialDelay, or cap is NaN or invalid + * @param increment - Delay increment per retry (>= 0) + * @param initialDelay - Initial delay (>= 0, default: 0) + * @param cap - Maximum delay (>= initialDelay, default: Infinity) + * @returns LinearBackoff instance + * @throws {RangeError} If parameters are invalid */ export const linear = ( increment: number, diff --git a/packages/retry-strategies/src/backoff/stop-backoff.ts b/packages/retry-strategies/src/backoff/stop-backoff.ts index 8ebd355..f4077c9 100644 --- a/packages/retry-strategies/src/backoff/stop-backoff.ts +++ b/packages/retry-strategies/src/backoff/stop-backoff.ts @@ -1,15 +1,13 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A fixed backoff policy that always returns NaN for nextBackoff(), - * meaning that the operation should never be retried. + * Always returns `NaN` to prevent retries. */ export class StopBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Always returns NaN to indicate no retries should be made. * - * @returns Always returns NaN to signal no retry + * @returns Always `NaN` */ public nextBackoff(): number { return Number.NaN; @@ -17,7 +15,6 @@ export class StopBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Since StopBackoff has no state, this is a no-op. */ public resetBackoff(): void { // No-op: StopBackoff has no state to reset @@ -25,9 +22,8 @@ export class StopBackoff implements BackoffStrategy { } /** - * A fixed backoff policy that always returns NaN for nextBackoff(), - * meaning that the operation should never be retried. + * Always returns `NaN` to prevent retries. * - * @returns A new StopBackoff instance + * @returns StopBackoff instance */ export const stop = (): StopBackoff => new StopBackoff(); diff --git a/packages/retry-strategies/src/backoff/zero-backoff.ts b/packages/retry-strategies/src/backoff/zero-backoff.ts index 5a1fc88..6bc8856 100644 --- a/packages/retry-strategies/src/backoff/zero-backoff.ts +++ b/packages/retry-strategies/src/backoff/zero-backoff.ts @@ -1,15 +1,13 @@ import type { BackoffStrategy } from "./interface.ts"; /** - * A fixed backoff policy whose backoff time is always zero, - * meaning that the operation is retried immediately without waiting, indefinitely. + * Always returns zero delay for immediate retries. */ export class ZeroBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Always returns 0 to retry immediately. * - * @returns Always returns 0 milliseconds + * @returns Always 0 milliseconds */ public nextBackoff(): number { return 0; @@ -17,7 +15,6 @@ export class ZeroBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Since ZeroBackoff has no state, this is a no-op. */ public resetBackoff(): void { // No-op: ZeroBackoff has no state to reset @@ -25,9 +22,8 @@ export class ZeroBackoff implements BackoffStrategy { } /** - * A fixed backoff policy whose backoff time is always zero, - * meaning that the operation is retried immediately without waiting, indefinitely. + * Always returns zero delay for immediate retries. * - * @returns A new ZeroBackoff instance + * @returns ZeroBackoff instance */ export const zero = (): ZeroBackoff => new ZeroBackoff(); diff --git a/packages/retry-strategies/src/retry/retry.ts b/packages/retry-strategies/src/retry/retry.ts index c26f5ce..12741db 100644 --- a/packages/retry-strategies/src/retry/retry.ts +++ b/packages/retry-strategies/src/retry/retry.ts @@ -2,32 +2,28 @@ import type { BackoffStrategy } from "../backoff/interface.ts"; import { waitFor } from "../utils/wait-for.ts"; /** - * Options for configuring the behavior of the `retry` function. + * Configuration options for the `retry` function. */ export type RetryOptions = { /** - * Strategy for calculating delays between retries. - * Should implement `BackoffStrategy`, which typically provides: - * - `resetBackoff()`: Resets the backoff sequence. - * - `nextBackoff()`: Returns the delay in milliseconds for the next retry. + * Delay calculation strategy (required). + * Implements `BackoffStrategy` with `nextBackoff()` and `resetBackoff()` methods. */ strategy: BackoffStrategy; /** - * Optional function to determine whether to stop retrying based on the encountered error. - * - * @param error - The error thrown by the function being retried. - * @param attempt - The attempt index for the current retry (0-based, positive integer). - * @returns `true` to stop retries, anything else to continue. + * Stop condition to determine whether to stop retrying. * + * @param error - The error thrown by the function being retried + * @param attempt - The attempt index (0-based) + * @returns `true` to stop retries, otherwise continues * @default () => false */ stop?: (error: unknown, attempt: number) => boolean; /** - * Optional AbortSignal to cancel the retry operation. - * If the signal is aborted, the `retry` function will immediately reject - * with `signal.reason`. + * Cancellation signal to abort the retry operation. + * If aborted, `retry` rejects with `signal.reason`. * * @default undefined */ @@ -35,30 +31,22 @@ export type RetryOptions = { }; /** - * Attempts to execute a function repeatedly according to a backoff strategy until it succeeds, - * a provided stop condition is met, or an optional AbortSignal is triggered. - * - * @note This method is not concurrently safe as *stateful* strategies might be shared across them. + * Executes a function repeatedly according to a backoff strategy until it succeeds, stops, or is aborted. * - * ## Behavior + * The retry loop continues until: (1) function succeeds, (2) strategy returns `NaN`, (3) stop function returns `true`, or (4) abort signal triggers. * - * The retry loop continues indefinitely until one of these conditions is met: - * - The function succeeds - * - The backoff strategy exhausts its retries (returns `NaN`) - * - The stop function returns `true` - * - The abort signal is triggered + * @note Not concurrently safe - don't share stateful strategies across concurrent operations. * - * @template T - The return type of the function being retried. - * @param fn - The function to retry. Can be synchronous or return a promise. - * @param options - Configuration options for retrying. - * @param options.strategy - Backoff strategy used to determine delays between retries. - * @param options.stop - Optional function called with the error to determine if retrying should stop. Return `true` to stop. - * @param options.signal - Optional AbortSignal to cancel retries. If aborted, the returned promise is rejected with `signal.reason`. - * @returns A promise that resolves with the function's result if it eventually succeeds. + * @template T - The return type of the function being retried + * @param fn - The function to retry + * @param options - Configuration + * @param options.strategy - Delay calculation strategy (required) + * @param options.stop - Stop condition (default: `() => false`) + * @param options.signal - Cancellation signal (optional) + * @returns Resolves with the function's result on success * - * @throws {unknown} The last encountered error if retries are exhausted or if the stop function returns `true`. - * @throws {unknown} The reason of the AbortSignal if the operation is aborted (generally {@link DOMException} `AbortError`). - * @throws {RangeError} If the backoff strategy returns a delay exceeding INT32_MAX (2147483647ms, approximately 24.8 days). + * @throws {unknown} Last error if retries exhausted, stop condition met, or aborted + * @throws {RangeError} If delay exceeds INT32_MAX (2147483647ms) * * @example * ```ts diff --git a/packages/retry-strategies/src/utils/upto.ts b/packages/retry-strategies/src/utils/upto.ts index af6ce40..afb5077 100644 --- a/packages/retry-strategies/src/utils/upto.ts +++ b/packages/retry-strategies/src/utils/upto.ts @@ -1,15 +1,14 @@ import type { BackoffStrategy } from "../backoff/interface.ts"; /** - * Limits a backoff strategy to a maximum number of retry attempts. - * Once the limit is reached, `nextBackoff()` returns `NaN` to stop retrying. + * Limits a strategy to a maximum number of retry attempts. + * Returns `NaN` once the limit is reached to stop retrying. * * @example * ```ts - * import { ExponentialBackoff, UptoBackoff } from '@proventuslabs/retry-strategies'; + * import { exponential, upto } from '@proventuslabs/retry-strategies'; * - * const exponential = new ExponentialBackoff(100, 5000); - * const limited = new UptoBackoff(3, exponential); + * const limited = upto(3, exponential(100, 5000)); * * limited.nextBackoff(); // Returns delay from exponential * limited.nextBackoff(); // Returns delay from exponential @@ -25,9 +24,9 @@ export class UptoBackoff implements BackoffStrategy { /** * Creates a new UptoBackoff instance. * - * @param retries - Maximum number of retry attempts allowed (must be >= 0 and an integer) - * @param strategy - The underlying backoff strategy to wrap - * @throws {RangeError} If retries is NaN, not an integer, or less than 0 + * @param retries - Maximum retry attempts (integer >= 0) + * @param strategy - Strategy to wrap + * @throws {RangeError} If retries is invalid (NaN, non-integer, or < 0) */ public constructor(retries: number, strategy: T) { if (Number.isNaN(retries)) { @@ -49,10 +48,9 @@ export class UptoBackoff implements BackoffStrategy { /** * Calculate the next backoff delay. - * Returns the delay from the underlying strategy until the retry limit is reached, - * then returns NaN to stop retrying. + * Delegates to the underlying strategy until the limit is reached, then returns `NaN`. * - * @returns The next delay in milliseconds from the underlying strategy, or NaN if retries exhausted + * @returns Delay from underlying strategy, or `NaN` if retries exhausted */ nextBackoff(): number { if (this.attemptsLeft-- <= 0) return NaN; @@ -62,7 +60,7 @@ export class UptoBackoff implements BackoffStrategy { /** * Reset to the initial state. - * Resets both the retry counter and the underlying strategy. + * Resets the retry counter and the underlying strategy. */ resetBackoff(): void { this.attemptsLeft = this.retries; @@ -71,14 +69,15 @@ export class UptoBackoff implements BackoffStrategy { } /** - * Limits a backoff strategy to a maximum number of retry attempts. - * Once the limit is reached, `nextBackoff()` returns `NaN` to stop retrying. + * Limits a strategy to a maximum number of retry attempts. + * Returns `NaN` once the limit is reached to stop retrying. * - * @param retries - Maximum number of retry attempts allowed (must be >= 0 and an integer) - * @param strategy - The underlying backoff strategy to wrap - * @returns A new UptoBackoff instance that stops after the specified number of retries + * @template T - Wrapped strategy + * @param retries - Maximum retry attempts (integer >= 0) + * @param strategy - Strategy to wrap + * @returns Strategy that stops after specified retries * - * @throws {RangeError} If retries is NaN, not an integer, or less than 0 + * @throws {RangeError} If retries is invalid (NaN, non-integer, or < 0) */ export const upto = ( retries: number, diff --git a/packages/retry-strategies/src/utils/wait-for.ts b/packages/retry-strategies/src/utils/wait-for.ts index 40e7b6c..af4a7ff 100644 --- a/packages/retry-strategies/src/utils/wait-for.ts +++ b/packages/retry-strategies/src/utils/wait-for.ts @@ -5,14 +5,14 @@ export const INT32_MAX = 0x7fffffff; /** - * Waits for the specified amount of time or until an optional AbortSignal is triggered. + * Waits for a specified duration or until aborted. * - * @param delay - Duration to wait in milliseconds. Negative values are treated as zero. - * @param signal - Optional AbortSignal to cancel the wait. If the signal is aborted, the returned promise is rejected with `signal.reason`. - * @returns A promise that resolves after the delay has elapsed or rejects if the signal is aborted. + * @param delay - Wait duration in milliseconds (negative treated as zero) + * @param signal - Cancellation signal (optional) + * @returns Resolves after delay or rejects if aborted * - * @throws {unknown} The reason of the AbortSignal if the operation is aborted (generally {@link DOMException} `AbortError`). - * @throws {RangeError} If the delay exceeds INT32_MAX (2147483647ms, approximately 24.8 days). + * @throws {unknown} Abort reason if cancelled + * @throws {RangeError} If delay exceeds INT32_MAX (2147483647ms) * * @example * ```ts