jitterbug 1.0.0-alpha.4 → 1.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,17 @@
1
- # jitterbug
2
-
3
- Jitterbug is a lightweight library for building reliable retry behavior in distributed systems and API clients.
1
+ ![Jitterbug Logo](https://raw.githubusercontent.com/mecharmor/jitterbug/main/assets/logo.png)
4
2
 
5
3
  ![Build Status](https://github.com/mecharmor/jitterbug/actions/workflows/build.yml/badge.svg?branch=main)
6
4
  ![Tests](https://img.shields.io/github/actions/workflow/status/mecharmor/jitterbug/build.yml?branch=main&label=tests)
7
- ![Version](https://img.shields.io/npm/v/jitterbug?label=version)
5
+ [![npm version](https://img.shields.io/npm/v/jitterbug.svg)](https://www.npmjs.com/package/jitterbug)
8
6
  ![License](https://img.shields.io/badge/license-MIT-blue.svg)
7
+ ![Coverage](https://img.shields.io/badge/coverage-100.00%25-brightgreen)
8
+
9
+
10
+ Jitterbug is a modern, type‑safe retry engine for Node.js and browser environments. It provides predictable backoff behavior and a suite of configurable jitter strategies, all wrapped in a clean, minimal API. The library is intentionally lightweight and dependency‑free to keep integration simple and reduce risk for consumers.
11
+
12
+ Designed to be framework‑agnostic and easy to adopt, Jitterbug emphasizes clarity, reliability, and maintainability. Its implementation is backed by comprehensive automated testing to ensure consistent behavior and to support confident contributions from the community.
13
+
14
+
9
15
 
10
16
  ## Installation
11
17
 
@@ -35,7 +41,8 @@ const fetchWithRetry = retry(async (url) => {
35
41
  }, {
36
42
  maxAttempts: 5,
37
43
  delay: 1000,
38
- backoff: 'exponential', // 'exponential', 'linear', or 'fixed'
44
+ backoff: 'exponential',
45
+ jitterConfig: { type: 'equal' }, // optional jitter
39
46
  onRetry: (error, attempt, waitTime) => {
40
47
  console.log(`Retry attempt ${attempt} after ${waitTime}ms`);
41
48
  }
@@ -50,6 +57,43 @@ try {
50
57
  }
51
58
  ```
52
59
 
60
+ ### Jitter Utility Functions
61
+
62
+ Jitterbug also exports its jitter calculation helpers directly.
63
+ These functions are pure, deterministic (when `Math.random` is mocked), and can be used independently of the retry system.
64
+
65
+ ---
66
+
67
+ #### **`calculateEqualJitter(baseDelayMs: number): number`**
68
+ Produces a delay between **50% and 100%** of the base delay.
69
+ Useful for predictable but desynchronized retry timing.
70
+
71
+ ---
72
+
73
+ #### **`calculateFullJitter(minDelayMs: number, maxDelayMs: number): number`**
74
+ Returns a completely random delay between `minDelayMs` (inclusive) and `maxDelayMs` (exclusive).
75
+ Ideal for aggressively spreading retries under heavy load.
76
+
77
+ ---
78
+
79
+ #### **`calculateFixedJitter(baseDelayMs: number, jitterAmount: number): number`**
80
+ Subtracts a constant amount from the base delay, never below zero.
81
+ Useful when you want consistent desynchronization without randomness.
82
+
83
+ ---
84
+
85
+ #### **`calculateRandomJitter(baseDelayMs: number, jitterFraction: number): number`**
86
+ Applies a symmetric ±fraction jitter around the base delay.
87
+ Example: `fraction = 0.2` → delay may vary between 80% and 120% of the base.
88
+
89
+ ---
90
+
91
+ #### **`calculateDecorrelatedJitter(baseDelayMs: number, maxDelayMs: number, prevDelayMs?: number): number`**
92
+ Implements AWS‑style decorrelated jitter.
93
+ Each retry picks a random delay between the base delay and **3× the previous delay**, capped at `maxDelayMs`.
94
+ Excellent for large distributed systems where synchronized retries can overwhelm downstream services.
95
+
96
+
53
97
  ## API
54
98
 
55
99
  ### `retry<T>(fn: T, options?: RetryOptions): T`
@@ -62,6 +106,7 @@ Creates a retry wrapper function.
62
106
  - `maxAttempts` (number, default: 3): Maximum number of retry attempts
63
107
  - `delay` (number, default: 1000): Base delay in milliseconds
64
108
  - `backoff` ('exponential' | 'linear' | 'fixed', default: 'exponential'): Backoff strategy
109
+ - `jitterConfig` (`JitterConfig` | `undefined`): Optional jitter strategy applied on top of the base dela
65
110
  - `onRetry` ((error: Error, attempt: number, waitTime: number) => void, optional): Callback called before each retry
66
111
 
67
112
  **Returns:** A function that wraps the original function with retry logic
@@ -71,93 +116,26 @@ Creates a retry wrapper function.
71
116
  import type { RetryOptions, BackoffStrategy } from 'jitterbug';
72
117
  ```
73
118
 
74
- ## Development
119
+ ### Jitter Configuration Options
75
120
 
76
- ### Building
121
+ - **`{ type: 'none' }`**
122
+ No jitter applied. Retries use the exact backoff delay.
77
123
 
78
- ```bash
79
- # Build the project
80
- npm run build
124
+ - **`{ type: 'equal' }`**
125
+ Splits the delay into a fixed half and a random half. Produces predictable but desynchronized retry timing.
81
126
 
82
- # Build in watch mode
83
- npm run dev
127
+ - **`{ type: 'full', min, max }`**
128
+ Picks a completely random delay between `min` and `max`. Best for aggressively spreading retries under heavy load.
84
129
 
85
- # Type check without building
86
- npm run typecheck
87
- ```
130
+ - **`{ type: 'fixed', amount }`**
131
+ Subtracts a constant amount from the base delay (never below zero). Useful for consistent, predictable desynchronization.
88
132
 
89
- ### Running Tests
133
+ - **`{ type: 'random', fraction }`**
134
+ Applies a symmetric ±fraction jitter around the base delay (e.g., `0.2` = ±20%). Light, centered randomness.
90
135
 
91
- ```bash
92
- # Run tests in watch mode
93
- npm test
94
-
95
- # Run tests once
96
- npm run test:run
97
-
98
- # Run tests with coverage
99
- npm run test:coverage
100
- ```
101
-
102
- ### Publishing
103
-
104
- Publishing to npm is automated via GitHub Actions. To publish a new version:
105
-
106
- ```bash
107
- # Choose one based on the type of change:
108
- npm version patch # Bug fixes: 0.1.0 -> 0.1.1
109
- npm version minor # New features: 0.1.0 -> 0.2.0
110
- npm version major # Breaking changes: 0.1.0 -> 1.0.0
111
-
112
- # This automatically:
113
- # - Updates package.json version
114
- # - Builds the project (via version script)
115
- # - Creates a git commit
116
- # - Creates a git tag (v0.1.1, v0.2.0, etc.)
117
-
118
- # Then push both the commit and tags:
119
- git push && git push --tags
120
- ```
121
-
122
- **Publishing alpha/beta versions:**
123
-
124
- For pre-release versions (alpha, beta, rc):
136
+ - **`{ type: 'decorrelated', maxDelay }`**
137
+ AWS‑style decorrelated jitter. Each retry picks a random delay between the base delay and 3× the previous jittered delay, capped at `maxDelay`.
125
138
 
126
- ```bash
127
- # Create an alpha version
128
- npm version prepatch --preid=alpha # 0.1.0 -> 0.1.1-alpha.0
129
- npm version preminor --preid=alpha # 0.1.0 -> 0.2.0-alpha.0
130
- npm version premajor --preid=alpha # 0.1.0 -> 1.0.0-alpha.0
131
-
132
- # Increment alpha version
133
- npm version prerelease --preid=alpha # 0.1.1-alpha.0 -> 0.1.1-alpha.1
134
-
135
- # Push the commit and tag
136
- git push && git push --tags
137
- ```
138
-
139
- The CI/CD pipeline will automatically:
140
- - Run all tests
141
- - Build the project
142
- - Verify version matches the tag
143
- - Publish to npm (if tests pass)
144
-
145
- **Note:** `NPM_TOKEN` secret needs to be setupas a GitHub secret with your npm access token.
146
-
147
- ### Project Structure
148
-
149
- ```
150
- jitterbug/
151
- ├── src/ # TypeScript source code
152
- │ ├── retry.ts # Core retry implementation
153
- │ └── index.ts # Main entry point
154
- ├── dist/ # Built output (ESM + CJS + types)
155
- ├── test/ # Test files
156
- │ └── retry.test.js
157
- ├── tsconfig.json # TypeScript configuration
158
- ├── tsup.config.ts # Build configuration
159
- └── package.json
160
- ```
161
139
 
162
140
  ## Test Coverage
163
141
 
@@ -166,7 +144,7 @@ jitterbug/
166
144
  | Metric | Coverage |
167
145
  |--------|----------|
168
146
  | Statements | 100.00% |
169
- | Branches | 95.00% |
147
+ | Branches | 96.67% |
170
148
  | Functions | 100.00% |
171
149
  | Lines | 100.00% |
172
150
 
package/dist/index.cjs CHANGED
@@ -8,6 +8,35 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // src/jitter.ts
12
+ var jitter_exports = {};
13
+ __export(jitter_exports, {
14
+ calculateDecorrelatedJitter: () => calculateDecorrelatedJitter,
15
+ calculateEqualJitter: () => calculateEqualJitter,
16
+ calculateFixedJitter: () => calculateFixedJitter,
17
+ calculateFullJitter: () => calculateFullJitter,
18
+ calculateRandomJitter: () => calculateRandomJitter
19
+ });
20
+ function calculateFullJitter(minDelayMs, maxDelayMs) {
21
+ return Math.random() * (maxDelayMs - minDelayMs) + minDelayMs;
22
+ }
23
+ function calculateEqualJitter(baseDelayMs) {
24
+ const halfDelay = baseDelayMs / 2;
25
+ return halfDelay + Math.random() * halfDelay;
26
+ }
27
+ function calculateFixedJitter(maxDelayMs, jitterAmount) {
28
+ return Math.max(0, maxDelayMs - jitterAmount);
29
+ }
30
+ function calculateRandomJitter(baseDelayMs, jitterFraction) {
31
+ const randomNumberBetweenJitterFraction = (Math.random() * 2 - 1) * jitterFraction;
32
+ return Math.max(0, baseDelayMs * (1 + randomNumberBetweenJitterFraction));
33
+ }
34
+ function calculateDecorrelatedJitter(baseDelayMs, maxDelayMs, prevDelayMs = 0) {
35
+ const upperDelayMs = Math.max(baseDelayMs, prevDelayMs * 3);
36
+ const randomValueBetweenBaseDelayAnd3xPrevoiusDelay = baseDelayMs + Math.random() * (upperDelayMs - baseDelayMs);
37
+ return Math.min(maxDelayMs, randomValueBetweenBaseDelayAnd3xPrevoiusDelay);
38
+ }
39
+
11
40
  // src/retry.ts
12
41
  function sleep(ms) {
13
42
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -23,16 +52,37 @@ function calculateDelay(baseDelay, attempt, backoff) {
23
52
  return baseDelay;
24
53
  }
25
54
  }
55
+ function applyJitter(baseDelay, jitter, prevDelay) {
56
+ if (!jitter || jitter.type === "none") return 0;
57
+ switch (jitter.type) {
58
+ case "equal":
59
+ return calculateEqualJitter(baseDelay);
60
+ case "full":
61
+ return calculateFullJitter(jitter.min, jitter.max);
62
+ case "fixed":
63
+ return calculateFixedJitter(baseDelay, jitter.amount);
64
+ case "random":
65
+ return calculateRandomJitter(baseDelay, jitter.fraction);
66
+ case "decorrelated":
67
+ return calculateDecorrelatedJitter(
68
+ baseDelay,
69
+ jitter.maxDelay,
70
+ prevDelay
71
+ );
72
+ }
73
+ }
26
74
  function retry(fn, options = {}) {
27
75
  const {
28
76
  maxAttempts = 3,
29
77
  delay = 1e3,
30
78
  backoff = "exponential",
79
+ jitterConfig,
31
80
  onRetry = () => {
32
81
  }
33
82
  } = options;
34
83
  return (async function(...args) {
35
84
  let lastError;
85
+ let prevJitterDelay = 0;
36
86
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
37
87
  try {
38
88
  return await fn(...args);
@@ -40,8 +90,11 @@ function retry(fn, options = {}) {
40
90
  lastError = error instanceof Error ? error : new Error(String(error));
41
91
  if (attempt < maxAttempts) {
42
92
  const waitTime = calculateDelay(delay, attempt, backoff);
43
- onRetry(lastError, attempt, waitTime);
44
- await sleep(waitTime);
93
+ const jitterMs = applyJitter(waitTime, jitterConfig, prevJitterDelay);
94
+ prevJitterDelay = jitterMs;
95
+ const delayTimeMs = waitTime + jitterMs;
96
+ onRetry(lastError, attempt, delayTimeMs);
97
+ await sleep(delayTimeMs);
45
98
  }
46
99
  }
47
100
  }
@@ -49,35 +102,6 @@ function retry(fn, options = {}) {
49
102
  });
50
103
  }
51
104
 
52
- // src/jitter.ts
53
- var jitter_exports = {};
54
- __export(jitter_exports, {
55
- calculateDecorrelatedJitter: () => calculateDecorrelatedJitter,
56
- calculateEqualJitter: () => calculateEqualJitter,
57
- calculateFixedJitter: () => calculateFixedJitter,
58
- calculateFullJitter: () => calculateFullJitter,
59
- calculateRandomJitter: () => calculateRandomJitter
60
- });
61
- function calculateFullJitter(minDelayMs, maxDelayMs) {
62
- return Math.random() * (maxDelayMs - minDelayMs) + minDelayMs;
63
- }
64
- function calculateEqualJitter(baseDelayMs) {
65
- const halfDelay = baseDelayMs / 2;
66
- return halfDelay + Math.random() * halfDelay;
67
- }
68
- function calculateFixedJitter(maxDelayMs, jitterAmount) {
69
- return Math.max(0, maxDelayMs - jitterAmount);
70
- }
71
- function calculateRandomJitter(baseDelayMs, jitterFraction) {
72
- const randomNumberBetweenJitterFraction = (Math.random() * 2 - 1) * jitterFraction;
73
- return Math.max(0, baseDelayMs * (1 + randomNumberBetweenJitterFraction));
74
- }
75
- function calculateDecorrelatedJitter(baseDelayMs, maxDelayMs, prevDelayMs = 0) {
76
- const upperDelayMs = Math.max(baseDelayMs, prevDelayMs * 3);
77
- const randomValueBetweenBaseDelayAnd3xPrevoiusDelay = baseDelayMs + Math.random() * (upperDelayMs - baseDelayMs);
78
- return Math.min(maxDelayMs, randomValueBetweenBaseDelayAnd3xPrevoiusDelay);
79
- }
80
-
81
105
  // src/index.ts
82
106
  var index_default = {
83
107
  retry,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/retry.ts","../src/jitter.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Core retry logic implementation\n */\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction calculateDelay(baseDelay: number, attempt: number, backoff: BackoffStrategy): number {\n switch (backoff) {\n case 'exponential':\n return baseDelay * Math.pow(2, attempt - 1);\n case 'linear':\n return baseDelay * attempt;\n case 'fixed':\n default:\n return baseDelay;\n }\n}\n\nexport type BackoffStrategy = 'exponential' | 'linear' | 'fixed';\n\nexport interface RetryOptions {\n /** Maximum number of retry attempts (default: 3) */\n maxAttempts?: number;\n /** Base delay in milliseconds (default: 1000) */\n delay?: number;\n /** Backoff strategy: 'exponential', 'linear', or 'fixed' (default: 'exponential') */\n backoff?: BackoffStrategy;\n /** Callback called before each retry (error, attempt, waitTime) */\n onRetry?: (error: Error, attempt: number, waitTime: number) => void;\n}\n\n/**\n * Creates a retry wrapper function\n * @param fn - The async function to retry\n * @param options - Retry configuration\n * @returns A function that wraps the original function with retry logic\n */\n// @typescript-eslint/no-explicit-any\nexport function retry<T extends (...args: unknown[]) => Promise<unknown>>(\n fn: T,\n options: RetryOptions = {}\n): T {\n const {\n maxAttempts = 3,\n delay = 1000,\n backoff = 'exponential',\n onRetry = () => {}\n } = options;\n\n return (async function(...args: Parameters<T>): Promise<ReturnType<T>> {\n let lastError: Error;\n \n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(...args) as ReturnType<T>;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n \n if (attempt < maxAttempts) {\n const waitTime = calculateDelay(delay, attempt, backoff);\n onRetry(lastError, attempt, waitTime);\n await sleep(waitTime);\n }\n }\n }\n \n throw lastError!;\n }) as T;\n}\n\nexport { calculateDelay, sleep };\n\n","/**\n * Generates a full‑jitter delay within the given range.\n *\n * Full jitter selects a completely random delay between `minDelay` and\n * `maxDelay`, spreading numbers across the entire interval to create jitter.\n *\n * @param {number} minDelayMs - The minimum delay (inclusive).\n * @param {number} maxDelayMs - The maximum delay (exclusive).\n * @returns {number} A random delay between `minDelay` and `maxDelay`.\n * @description Full jitter is ideal when many clients may retry at the same time and you need to aggressively spread those retries out. It’s especially useful under high contention or bursty failure conditions, where a wide range of random delays helps prevent retry storms and reduces load on downstream services.\n */\nexport function calculateFullJitter(minDelayMs: number, maxDelayMs: number): number {\n return Math.random() * (maxDelayMs - minDelayMs) + minDelayMs;\n}\n\n/**\n * Calculates an equal‑jitter backoff delay.\n *\n * Equal jitter splits the delay into a fixed half and a random half.\n * This reduces synchronization between clients while avoiding the\n * extremely low delays produced by full jitter.\n *\n * @param {number} baseDelayMs - The original backoff delay before jitter.\n * @returns {number} A delay between half of `baseDelay` and the full `baseDelay`.\n * @description Equal jitter is useful when you want a smoother, more predictable retry pattern that still avoids synchronized retries. It guarantees at least half of the base delay while adding randomness to the other half, giving you a balanced backoff that reduces contention without the extreme variability of full jitter.\n */\nexport function calculateEqualJitter(baseDelayMs: number): number {\n const halfDelay = baseDelayMs / 2;\n return halfDelay + Math.random() * halfDelay;\n}\n\n\n/**\n * Calculates a fixed‑jitter delay by subtracting a constant amount from the base delay. Unlike random jitter strategies, fixed jitter provides predictable timing while still preventing perfectly aligned retries.\n * Use this when you want a small, consistent desynchronization between clients without introducing randomness — ideal for stable systems where timing consistency matters more than wide jitter distribution.\n * @param {number} maxDelayMs - The original delay before applying jitter.\n * @param {number} jitterAmount - The fixed amount to subtract.\n * @returns {number} The delay after subtracting `jitterAmount`, never below zero.\n */\nexport function calculateFixedJitter(maxDelayMs: number, jitterAmount: number): number {\n return Math.max(0, maxDelayMs - jitterAmount);\n}\n\n/**\n * Calculates a random‑jitter backoff delay by applying a symmetric\n * random variation around the base delay. The jitter fraction determines\n * how far the delay may swing above or below the original value.\n *\n * Random jitter is useful when you want to “wiggle” the delay without\n * dramatically changing its overall shape. It provides light\n * desynchronization between clients while keeping the delay centered\n * around the base value — ideal for systems that need some variability\n * but not the wide spread of full jitter.\n *\n * @param {number} baseDelayMs - The original delay before jitter is applied.\n * @param {number} jitterFraction - Maximum fractional deviation (e.g., 0.2 for ±20%).\n * @returns {number} A jittered delay, clamped to zero if the calculation goes negative.\n */\nexport function calculateRandomJitter(baseDelayMs: number, jitterFraction: number): number {\n // Math.random() gives us 0 to 1 so multiply by 2 and subtract 1 to get -1 to 1 range\n const randomNumberBetweenJitterFraction = (Math.random() * 2 - 1) * jitterFraction;\n return Math.max(0, baseDelayMs * (1 + randomNumberBetweenJitterFraction));\n}\n\n/**\n * Calculates a decorrelated jitter backoff delay using the algorithm\n * popularized by AWS. Each retry delay is chosen randomly between the\n * base delay and three times the previous delay, then capped at the\n * maximum allowed delay. This prevents synchronized retry spikes and\n * produces a smooth, adaptive backoff pattern.\n *\n * Use this when you need highly desynchronized retries across many\n * clients—especially in distributed systems or high‑traffic services\n * where coordinated retry storms can overwhelm downstream resources.\n *\n * AWS reference:\n * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/\n *\n * @param {number} baseDelayMs - The minimum delay for any retry attempt.\n * @param {number} maxDelayMs - The maximum allowed delay.\n * @param {number} [prevDelayMs=0] - The delay used for the previous retry.\n * @returns {number} A jittered delay capped at `maxDelayMs`.\n */\nexport function calculateDecorrelatedJitter(baseDelayMs: number, maxDelayMs: number, prevDelayMs: number = 0): number {\n const upperDelayMs = Math.max(baseDelayMs, prevDelayMs * 3);\n const randomValueBetweenBaseDelayAnd3xPrevoiusDelay = baseDelayMs + Math.random() * (upperDelayMs - baseDelayMs);\n return Math.min(maxDelayMs, randomValueBetweenBaseDelayAnd3xPrevoiusDelay)\n}","/**\n * Jitterbug - A lightweight library for building reliable retry behavior\n */\n\nexport { retry, calculateDelay, sleep, type RetryOptions, type BackoffStrategy } from './retry';\n\nimport * as jitterFunctions from './jitter';\nimport { retry } from './retry';\n\nexport default {\n retry,\n ...jitterFunctions\n};\n"]}
1
+ {"version":3,"sources":["../src/jitter.ts","../src/retry.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Generates a full‑jitter delay within the given range.\n *\n * Full jitter selects a completely random delay between `minDelay` and\n * `maxDelay`, spreading numbers across the entire interval to create jitter.\n *\n * @param {number} minDelayMs - The minimum delay (inclusive).\n * @param {number} maxDelayMs - The maximum delay (exclusive).\n * @returns {number} A random delay between `minDelay` and `maxDelay`.\n * @description Full jitter is ideal when many clients may retry at the same time and you need to aggressively spread those retries out. It’s especially useful under high contention or bursty failure conditions, where a wide range of random delays helps prevent retry storms and reduces load on downstream services.\n */\nexport function calculateFullJitter(minDelayMs: number, maxDelayMs: number): number {\n return Math.random() * (maxDelayMs - minDelayMs) + minDelayMs;\n}\n\n/**\n * Calculates an equal‑jitter backoff delay.\n *\n * Equal jitter splits the delay into a fixed half and a random half.\n * This reduces synchronization between clients while avoiding the\n * extremely low delays produced by full jitter.\n *\n * @param {number} baseDelayMs - The original backoff delay before jitter.\n * @returns {number} A delay between half of `baseDelay` and the full `baseDelay`.\n * @description Equal jitter is useful when you want a smoother, more predictable retry pattern that still avoids synchronized retries. It guarantees at least half of the base delay while adding randomness to the other half, giving you a balanced backoff that reduces contention without the extreme variability of full jitter.\n */\nexport function calculateEqualJitter(baseDelayMs: number): number {\n const halfDelay = baseDelayMs / 2;\n return halfDelay + Math.random() * halfDelay;\n}\n\n/**\n * Calculates a fixed‑jitter delay by subtracting a constant amount from the base delay. Unlike random jitter strategies, fixed jitter provides predictable timing while still preventing perfectly aligned retries.\n * Use this when you want a small, consistent desynchronization between clients without introducing randomness — ideal for stable systems where timing consistency matters more than wide jitter distribution.\n * @param {number} maxDelayMs - The original delay before applying jitter.\n * @param {number} jitterAmount - The fixed amount to subtract.\n * @returns {number} The delay after subtracting `jitterAmount`, never below zero.\n */\nexport function calculateFixedJitter(maxDelayMs: number, jitterAmount: number): number {\n return Math.max(0, maxDelayMs - jitterAmount);\n}\n\n/**\n * Calculates a random‑jitter backoff delay by applying a symmetric\n * random variation around the base delay. The jitter fraction determines\n * how far the delay may swing above or below the original value.\n *\n * Random jitter is useful when you want to “wiggle” the delay without\n * dramatically changing its overall shape. It provides light\n * desynchronization between clients while keeping the delay centered\n * around the base value — ideal for systems that need some variability\n * but not the wide spread of full jitter.\n *\n * @param {number} baseDelayMs - The original delay before jitter is applied.\n * @param {number} jitterFraction - Maximum fractional deviation (e.g., 0.2 for ±20%).\n * @returns {number} A jittered delay, clamped to zero if the calculation goes negative.\n */\nexport function calculateRandomJitter(baseDelayMs: number, jitterFraction: number): number {\n // Math.random() gives us 0 to 1 so multiply by 2 and subtract 1 to get -1 to 1 range\n const randomNumberBetweenJitterFraction = (Math.random() * 2 - 1) * jitterFraction;\n return Math.max(0, baseDelayMs * (1 + randomNumberBetweenJitterFraction));\n}\n\n/**\n * Calculates a decorrelated jitter backoff delay using the algorithm\n * popularized by AWS. Each retry delay is chosen randomly between the\n * base delay and three times the previous delay, then capped at the\n * maximum allowed delay. This prevents synchronized retry spikes and\n * produces a smooth, adaptive backoff pattern.\n *\n * Use this when you need highly desynchronized retries across many\n * clients—especially in distributed systems or high‑traffic services\n * where coordinated retry storms can overwhelm downstream resources.\n *\n * AWS reference:\n * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/\n *\n * @param {number} baseDelayMs - The minimum delay for any retry attempt.\n * @param {number} maxDelayMs - The maximum allowed delay.\n * @param {number} [prevDelayMs=0] - The delay used for the previous retry.\n * @returns {number} A jittered delay capped at `maxDelayMs`.\n */\nexport function calculateDecorrelatedJitter(baseDelayMs: number, maxDelayMs: number, prevDelayMs: number = 0): number {\n const upperDelayMs = Math.max(baseDelayMs, prevDelayMs * 3);\n const randomValueBetweenBaseDelayAnd3xPrevoiusDelay = baseDelayMs + Math.random() * (upperDelayMs - baseDelayMs);\n return Math.min(maxDelayMs, randomValueBetweenBaseDelayAnd3xPrevoiusDelay)\n}","/**\n * Core retry logic implementation\n */\n\nimport { calculateDecorrelatedJitter, calculateEqualJitter, calculateFixedJitter, calculateFullJitter, calculateRandomJitter } from \"./jitter\";\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nexport type BackoffStrategy = 'exponential' | 'linear' | 'fixed';\nfunction calculateDelay(baseDelay: number, attempt: number, backoff: BackoffStrategy): number {\n switch (backoff) {\n case 'exponential':\n return baseDelay * Math.pow(2, attempt - 1);\n case 'linear':\n return baseDelay * attempt;\n case 'fixed':\n default:\n return baseDelay;\n }\n}\n\nexport type JitterConfig =\n | { type: 'none' }\n | { type: 'equal' }\n | { type: 'full', min: number, max: number }\n | { type: 'fixed', amount: number }\n | { type: 'random', fraction: number }\n | { type: 'decorrelated', maxDelay: number };\n\n function applyJitter(\n baseDelay: number,\n jitter: JitterConfig | undefined,\n prevDelay: number\n ): number {\n if (!jitter || jitter.type === 'none') return 0;\n \n switch (jitter.type) {\n case 'equal':\n return calculateEqualJitter(baseDelay);\n \n case 'full':\n return calculateFullJitter(jitter.min, jitter.max);\n \n case 'fixed':\n return calculateFixedJitter(baseDelay, jitter.amount);\n \n case 'random':\n return calculateRandomJitter(baseDelay, jitter.fraction);\n \n case 'decorrelated':\n return calculateDecorrelatedJitter(\n baseDelay,\n jitter.maxDelay,\n prevDelay\n );\n }\n }\n \n\n\nexport interface RetryOptions {\n /** Maximum number of retry attempts (default: 3) */\n maxAttempts?: number;\n /** Base delay in milliseconds (default: 1000) */\n delay?: number;\n /** Backoff strategy: 'exponential', 'linear', or 'fixed' (default: 'exponential') */\n backoff?: BackoffStrategy;\n /** Callback called before each retry (error, attempt, waitTime) */\n onRetry?: (error: Error, attempt: number, waitTime: number) => void;\n /** Jitter settings */\n jitterConfig?: JitterConfig\n}\n\n/**\n * @param fn - The async function to retry\n * @param options - Retry configuration\n * @returns A function that wraps the original function with retry logic\n */\n// @typescript-eslint/no-explicit-any\nexport function retry<T extends (...args: unknown[]) => Promise<unknown>>(\n fn: T,\n options: RetryOptions = {}\n): T {\n const {\n maxAttempts = 3,\n delay = 1000,\n backoff = 'exponential',\n jitterConfig,\n onRetry = () => {}\n } = options;\n\n return (async function(...args: Parameters<T>): Promise<ReturnType<T>> {\n let lastError: Error;\n \n let prevJitterDelay = 0;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(...args) as ReturnType<T>;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n \n if (attempt < maxAttempts) {\n const waitTime = calculateDelay(delay, attempt, backoff);\n const jitterMs = applyJitter(waitTime, jitterConfig, prevJitterDelay)\n prevJitterDelay = jitterMs;\n const delayTimeMs = waitTime + jitterMs;\n onRetry(lastError, attempt, delayTimeMs);\n await sleep(delayTimeMs);\n }\n }\n }\n \n throw lastError!;\n }) as T;\n}\n\nexport { calculateDelay, sleep, applyJitter };\n\n","/**\n * Jitterbug - A lightweight library for building reliable retry behavior\n */\n\nexport { retry, calculateDelay, sleep, type RetryOptions, type BackoffStrategy } from './retry';\n\nimport * as jitterFunctions from './jitter';\nimport { retry } from './retry';\n\nexport default {\n retry,\n ...jitterFunctions\n};\n"]}
package/dist/index.d.cts CHANGED
@@ -2,8 +2,26 @@
2
2
  * Core retry logic implementation
3
3
  */
4
4
  declare function sleep(ms: number): Promise<void>;
5
- declare function calculateDelay(baseDelay: number, attempt: number, backoff: BackoffStrategy): number;
6
5
  type BackoffStrategy = 'exponential' | 'linear' | 'fixed';
6
+ declare function calculateDelay(baseDelay: number, attempt: number, backoff: BackoffStrategy): number;
7
+ type JitterConfig = {
8
+ type: 'none';
9
+ } | {
10
+ type: 'equal';
11
+ } | {
12
+ type: 'full';
13
+ min: number;
14
+ max: number;
15
+ } | {
16
+ type: 'fixed';
17
+ amount: number;
18
+ } | {
19
+ type: 'random';
20
+ fraction: number;
21
+ } | {
22
+ type: 'decorrelated';
23
+ maxDelay: number;
24
+ };
7
25
  interface RetryOptions {
8
26
  /** Maximum number of retry attempts (default: 3) */
9
27
  maxAttempts?: number;
@@ -13,9 +31,10 @@ interface RetryOptions {
13
31
  backoff?: BackoffStrategy;
14
32
  /** Callback called before each retry (error, attempt, waitTime) */
15
33
  onRetry?: (error: Error, attempt: number, waitTime: number) => void;
34
+ /** Jitter settings */
35
+ jitterConfig?: JitterConfig;
16
36
  }
17
37
  /**
18
- * Creates a retry wrapper function
19
38
  * @param fn - The async function to retry
20
39
  * @param options - Retry configuration
21
40
  * @returns A function that wraps the original function with retry logic
package/dist/index.d.ts CHANGED
@@ -2,8 +2,26 @@
2
2
  * Core retry logic implementation
3
3
  */
4
4
  declare function sleep(ms: number): Promise<void>;
5
- declare function calculateDelay(baseDelay: number, attempt: number, backoff: BackoffStrategy): number;
6
5
  type BackoffStrategy = 'exponential' | 'linear' | 'fixed';
6
+ declare function calculateDelay(baseDelay: number, attempt: number, backoff: BackoffStrategy): number;
7
+ type JitterConfig = {
8
+ type: 'none';
9
+ } | {
10
+ type: 'equal';
11
+ } | {
12
+ type: 'full';
13
+ min: number;
14
+ max: number;
15
+ } | {
16
+ type: 'fixed';
17
+ amount: number;
18
+ } | {
19
+ type: 'random';
20
+ fraction: number;
21
+ } | {
22
+ type: 'decorrelated';
23
+ maxDelay: number;
24
+ };
7
25
  interface RetryOptions {
8
26
  /** Maximum number of retry attempts (default: 3) */
9
27
  maxAttempts?: number;
@@ -13,9 +31,10 @@ interface RetryOptions {
13
31
  backoff?: BackoffStrategy;
14
32
  /** Callback called before each retry (error, attempt, waitTime) */
15
33
  onRetry?: (error: Error, attempt: number, waitTime: number) => void;
34
+ /** Jitter settings */
35
+ jitterConfig?: JitterConfig;
16
36
  }
17
37
  /**
18
- * Creates a retry wrapper function
19
38
  * @param fn - The async function to retry
20
39
  * @param options - Retry configuration
21
40
  * @returns A function that wraps the original function with retry logic
package/dist/index.js CHANGED
@@ -4,6 +4,35 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // src/jitter.ts
8
+ var jitter_exports = {};
9
+ __export(jitter_exports, {
10
+ calculateDecorrelatedJitter: () => calculateDecorrelatedJitter,
11
+ calculateEqualJitter: () => calculateEqualJitter,
12
+ calculateFixedJitter: () => calculateFixedJitter,
13
+ calculateFullJitter: () => calculateFullJitter,
14
+ calculateRandomJitter: () => calculateRandomJitter
15
+ });
16
+ function calculateFullJitter(minDelayMs, maxDelayMs) {
17
+ return Math.random() * (maxDelayMs - minDelayMs) + minDelayMs;
18
+ }
19
+ function calculateEqualJitter(baseDelayMs) {
20
+ const halfDelay = baseDelayMs / 2;
21
+ return halfDelay + Math.random() * halfDelay;
22
+ }
23
+ function calculateFixedJitter(maxDelayMs, jitterAmount) {
24
+ return Math.max(0, maxDelayMs - jitterAmount);
25
+ }
26
+ function calculateRandomJitter(baseDelayMs, jitterFraction) {
27
+ const randomNumberBetweenJitterFraction = (Math.random() * 2 - 1) * jitterFraction;
28
+ return Math.max(0, baseDelayMs * (1 + randomNumberBetweenJitterFraction));
29
+ }
30
+ function calculateDecorrelatedJitter(baseDelayMs, maxDelayMs, prevDelayMs = 0) {
31
+ const upperDelayMs = Math.max(baseDelayMs, prevDelayMs * 3);
32
+ const randomValueBetweenBaseDelayAnd3xPrevoiusDelay = baseDelayMs + Math.random() * (upperDelayMs - baseDelayMs);
33
+ return Math.min(maxDelayMs, randomValueBetweenBaseDelayAnd3xPrevoiusDelay);
34
+ }
35
+
7
36
  // src/retry.ts
8
37
  function sleep(ms) {
9
38
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -19,16 +48,37 @@ function calculateDelay(baseDelay, attempt, backoff) {
19
48
  return baseDelay;
20
49
  }
21
50
  }
51
+ function applyJitter(baseDelay, jitter, prevDelay) {
52
+ if (!jitter || jitter.type === "none") return 0;
53
+ switch (jitter.type) {
54
+ case "equal":
55
+ return calculateEqualJitter(baseDelay);
56
+ case "full":
57
+ return calculateFullJitter(jitter.min, jitter.max);
58
+ case "fixed":
59
+ return calculateFixedJitter(baseDelay, jitter.amount);
60
+ case "random":
61
+ return calculateRandomJitter(baseDelay, jitter.fraction);
62
+ case "decorrelated":
63
+ return calculateDecorrelatedJitter(
64
+ baseDelay,
65
+ jitter.maxDelay,
66
+ prevDelay
67
+ );
68
+ }
69
+ }
22
70
  function retry(fn, options = {}) {
23
71
  const {
24
72
  maxAttempts = 3,
25
73
  delay = 1e3,
26
74
  backoff = "exponential",
75
+ jitterConfig,
27
76
  onRetry = () => {
28
77
  }
29
78
  } = options;
30
79
  return (async function(...args) {
31
80
  let lastError;
81
+ let prevJitterDelay = 0;
32
82
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
33
83
  try {
34
84
  return await fn(...args);
@@ -36,8 +86,11 @@ function retry(fn, options = {}) {
36
86
  lastError = error instanceof Error ? error : new Error(String(error));
37
87
  if (attempt < maxAttempts) {
38
88
  const waitTime = calculateDelay(delay, attempt, backoff);
39
- onRetry(lastError, attempt, waitTime);
40
- await sleep(waitTime);
89
+ const jitterMs = applyJitter(waitTime, jitterConfig, prevJitterDelay);
90
+ prevJitterDelay = jitterMs;
91
+ const delayTimeMs = waitTime + jitterMs;
92
+ onRetry(lastError, attempt, delayTimeMs);
93
+ await sleep(delayTimeMs);
41
94
  }
42
95
  }
43
96
  }
@@ -45,35 +98,6 @@ function retry(fn, options = {}) {
45
98
  });
46
99
  }
47
100
 
48
- // src/jitter.ts
49
- var jitter_exports = {};
50
- __export(jitter_exports, {
51
- calculateDecorrelatedJitter: () => calculateDecorrelatedJitter,
52
- calculateEqualJitter: () => calculateEqualJitter,
53
- calculateFixedJitter: () => calculateFixedJitter,
54
- calculateFullJitter: () => calculateFullJitter,
55
- calculateRandomJitter: () => calculateRandomJitter
56
- });
57
- function calculateFullJitter(minDelayMs, maxDelayMs) {
58
- return Math.random() * (maxDelayMs - minDelayMs) + minDelayMs;
59
- }
60
- function calculateEqualJitter(baseDelayMs) {
61
- const halfDelay = baseDelayMs / 2;
62
- return halfDelay + Math.random() * halfDelay;
63
- }
64
- function calculateFixedJitter(maxDelayMs, jitterAmount) {
65
- return Math.max(0, maxDelayMs - jitterAmount);
66
- }
67
- function calculateRandomJitter(baseDelayMs, jitterFraction) {
68
- const randomNumberBetweenJitterFraction = (Math.random() * 2 - 1) * jitterFraction;
69
- return Math.max(0, baseDelayMs * (1 + randomNumberBetweenJitterFraction));
70
- }
71
- function calculateDecorrelatedJitter(baseDelayMs, maxDelayMs, prevDelayMs = 0) {
72
- const upperDelayMs = Math.max(baseDelayMs, prevDelayMs * 3);
73
- const randomValueBetweenBaseDelayAnd3xPrevoiusDelay = baseDelayMs + Math.random() * (upperDelayMs - baseDelayMs);
74
- return Math.min(maxDelayMs, randomValueBetweenBaseDelayAnd3xPrevoiusDelay);
75
- }
76
-
77
101
  // src/index.ts
78
102
  var index_default = {
79
103
  retry,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/retry.ts","../src/jitter.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Core retry logic implementation\n */\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction calculateDelay(baseDelay: number, attempt: number, backoff: BackoffStrategy): number {\n switch (backoff) {\n case 'exponential':\n return baseDelay * Math.pow(2, attempt - 1);\n case 'linear':\n return baseDelay * attempt;\n case 'fixed':\n default:\n return baseDelay;\n }\n}\n\nexport type BackoffStrategy = 'exponential' | 'linear' | 'fixed';\n\nexport interface RetryOptions {\n /** Maximum number of retry attempts (default: 3) */\n maxAttempts?: number;\n /** Base delay in milliseconds (default: 1000) */\n delay?: number;\n /** Backoff strategy: 'exponential', 'linear', or 'fixed' (default: 'exponential') */\n backoff?: BackoffStrategy;\n /** Callback called before each retry (error, attempt, waitTime) */\n onRetry?: (error: Error, attempt: number, waitTime: number) => void;\n}\n\n/**\n * Creates a retry wrapper function\n * @param fn - The async function to retry\n * @param options - Retry configuration\n * @returns A function that wraps the original function with retry logic\n */\n// @typescript-eslint/no-explicit-any\nexport function retry<T extends (...args: unknown[]) => Promise<unknown>>(\n fn: T,\n options: RetryOptions = {}\n): T {\n const {\n maxAttempts = 3,\n delay = 1000,\n backoff = 'exponential',\n onRetry = () => {}\n } = options;\n\n return (async function(...args: Parameters<T>): Promise<ReturnType<T>> {\n let lastError: Error;\n \n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(...args) as ReturnType<T>;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n \n if (attempt < maxAttempts) {\n const waitTime = calculateDelay(delay, attempt, backoff);\n onRetry(lastError, attempt, waitTime);\n await sleep(waitTime);\n }\n }\n }\n \n throw lastError!;\n }) as T;\n}\n\nexport { calculateDelay, sleep };\n\n","/**\n * Generates a full‑jitter delay within the given range.\n *\n * Full jitter selects a completely random delay between `minDelay` and\n * `maxDelay`, spreading numbers across the entire interval to create jitter.\n *\n * @param {number} minDelayMs - The minimum delay (inclusive).\n * @param {number} maxDelayMs - The maximum delay (exclusive).\n * @returns {number} A random delay between `minDelay` and `maxDelay`.\n * @description Full jitter is ideal when many clients may retry at the same time and you need to aggressively spread those retries out. It’s especially useful under high contention or bursty failure conditions, where a wide range of random delays helps prevent retry storms and reduces load on downstream services.\n */\nexport function calculateFullJitter(minDelayMs: number, maxDelayMs: number): number {\n return Math.random() * (maxDelayMs - minDelayMs) + minDelayMs;\n}\n\n/**\n * Calculates an equal‑jitter backoff delay.\n *\n * Equal jitter splits the delay into a fixed half and a random half.\n * This reduces synchronization between clients while avoiding the\n * extremely low delays produced by full jitter.\n *\n * @param {number} baseDelayMs - The original backoff delay before jitter.\n * @returns {number} A delay between half of `baseDelay` and the full `baseDelay`.\n * @description Equal jitter is useful when you want a smoother, more predictable retry pattern that still avoids synchronized retries. It guarantees at least half of the base delay while adding randomness to the other half, giving you a balanced backoff that reduces contention without the extreme variability of full jitter.\n */\nexport function calculateEqualJitter(baseDelayMs: number): number {\n const halfDelay = baseDelayMs / 2;\n return halfDelay + Math.random() * halfDelay;\n}\n\n\n/**\n * Calculates a fixed‑jitter delay by subtracting a constant amount from the base delay. Unlike random jitter strategies, fixed jitter provides predictable timing while still preventing perfectly aligned retries.\n * Use this when you want a small, consistent desynchronization between clients without introducing randomness — ideal for stable systems where timing consistency matters more than wide jitter distribution.\n * @param {number} maxDelayMs - The original delay before applying jitter.\n * @param {number} jitterAmount - The fixed amount to subtract.\n * @returns {number} The delay after subtracting `jitterAmount`, never below zero.\n */\nexport function calculateFixedJitter(maxDelayMs: number, jitterAmount: number): number {\n return Math.max(0, maxDelayMs - jitterAmount);\n}\n\n/**\n * Calculates a random‑jitter backoff delay by applying a symmetric\n * random variation around the base delay. The jitter fraction determines\n * how far the delay may swing above or below the original value.\n *\n * Random jitter is useful when you want to “wiggle” the delay without\n * dramatically changing its overall shape. It provides light\n * desynchronization between clients while keeping the delay centered\n * around the base value — ideal for systems that need some variability\n * but not the wide spread of full jitter.\n *\n * @param {number} baseDelayMs - The original delay before jitter is applied.\n * @param {number} jitterFraction - Maximum fractional deviation (e.g., 0.2 for ±20%).\n * @returns {number} A jittered delay, clamped to zero if the calculation goes negative.\n */\nexport function calculateRandomJitter(baseDelayMs: number, jitterFraction: number): number {\n // Math.random() gives us 0 to 1 so multiply by 2 and subtract 1 to get -1 to 1 range\n const randomNumberBetweenJitterFraction = (Math.random() * 2 - 1) * jitterFraction;\n return Math.max(0, baseDelayMs * (1 + randomNumberBetweenJitterFraction));\n}\n\n/**\n * Calculates a decorrelated jitter backoff delay using the algorithm\n * popularized by AWS. Each retry delay is chosen randomly between the\n * base delay and three times the previous delay, then capped at the\n * maximum allowed delay. This prevents synchronized retry spikes and\n * produces a smooth, adaptive backoff pattern.\n *\n * Use this when you need highly desynchronized retries across many\n * clients—especially in distributed systems or high‑traffic services\n * where coordinated retry storms can overwhelm downstream resources.\n *\n * AWS reference:\n * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/\n *\n * @param {number} baseDelayMs - The minimum delay for any retry attempt.\n * @param {number} maxDelayMs - The maximum allowed delay.\n * @param {number} [prevDelayMs=0] - The delay used for the previous retry.\n * @returns {number} A jittered delay capped at `maxDelayMs`.\n */\nexport function calculateDecorrelatedJitter(baseDelayMs: number, maxDelayMs: number, prevDelayMs: number = 0): number {\n const upperDelayMs = Math.max(baseDelayMs, prevDelayMs * 3);\n const randomValueBetweenBaseDelayAnd3xPrevoiusDelay = baseDelayMs + Math.random() * (upperDelayMs - baseDelayMs);\n return Math.min(maxDelayMs, randomValueBetweenBaseDelayAnd3xPrevoiusDelay)\n}","/**\n * Jitterbug - A lightweight library for building reliable retry behavior\n */\n\nexport { retry, calculateDelay, sleep, type RetryOptions, type BackoffStrategy } from './retry';\n\nimport * as jitterFunctions from './jitter';\nimport { retry } from './retry';\n\nexport default {\n retry,\n ...jitterFunctions\n};\n"]}
1
+ {"version":3,"sources":["../src/jitter.ts","../src/retry.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Generates a full‑jitter delay within the given range.\n *\n * Full jitter selects a completely random delay between `minDelay` and\n * `maxDelay`, spreading numbers across the entire interval to create jitter.\n *\n * @param {number} minDelayMs - The minimum delay (inclusive).\n * @param {number} maxDelayMs - The maximum delay (exclusive).\n * @returns {number} A random delay between `minDelay` and `maxDelay`.\n * @description Full jitter is ideal when many clients may retry at the same time and you need to aggressively spread those retries out. It’s especially useful under high contention or bursty failure conditions, where a wide range of random delays helps prevent retry storms and reduces load on downstream services.\n */\nexport function calculateFullJitter(minDelayMs: number, maxDelayMs: number): number {\n return Math.random() * (maxDelayMs - minDelayMs) + minDelayMs;\n}\n\n/**\n * Calculates an equal‑jitter backoff delay.\n *\n * Equal jitter splits the delay into a fixed half and a random half.\n * This reduces synchronization between clients while avoiding the\n * extremely low delays produced by full jitter.\n *\n * @param {number} baseDelayMs - The original backoff delay before jitter.\n * @returns {number} A delay between half of `baseDelay` and the full `baseDelay`.\n * @description Equal jitter is useful when you want a smoother, more predictable retry pattern that still avoids synchronized retries. It guarantees at least half of the base delay while adding randomness to the other half, giving you a balanced backoff that reduces contention without the extreme variability of full jitter.\n */\nexport function calculateEqualJitter(baseDelayMs: number): number {\n const halfDelay = baseDelayMs / 2;\n return halfDelay + Math.random() * halfDelay;\n}\n\n/**\n * Calculates a fixed‑jitter delay by subtracting a constant amount from the base delay. Unlike random jitter strategies, fixed jitter provides predictable timing while still preventing perfectly aligned retries.\n * Use this when you want a small, consistent desynchronization between clients without introducing randomness — ideal for stable systems where timing consistency matters more than wide jitter distribution.\n * @param {number} maxDelayMs - The original delay before applying jitter.\n * @param {number} jitterAmount - The fixed amount to subtract.\n * @returns {number} The delay after subtracting `jitterAmount`, never below zero.\n */\nexport function calculateFixedJitter(maxDelayMs: number, jitterAmount: number): number {\n return Math.max(0, maxDelayMs - jitterAmount);\n}\n\n/**\n * Calculates a random‑jitter backoff delay by applying a symmetric\n * random variation around the base delay. The jitter fraction determines\n * how far the delay may swing above or below the original value.\n *\n * Random jitter is useful when you want to “wiggle” the delay without\n * dramatically changing its overall shape. It provides light\n * desynchronization between clients while keeping the delay centered\n * around the base value — ideal for systems that need some variability\n * but not the wide spread of full jitter.\n *\n * @param {number} baseDelayMs - The original delay before jitter is applied.\n * @param {number} jitterFraction - Maximum fractional deviation (e.g., 0.2 for ±20%).\n * @returns {number} A jittered delay, clamped to zero if the calculation goes negative.\n */\nexport function calculateRandomJitter(baseDelayMs: number, jitterFraction: number): number {\n // Math.random() gives us 0 to 1 so multiply by 2 and subtract 1 to get -1 to 1 range\n const randomNumberBetweenJitterFraction = (Math.random() * 2 - 1) * jitterFraction;\n return Math.max(0, baseDelayMs * (1 + randomNumberBetweenJitterFraction));\n}\n\n/**\n * Calculates a decorrelated jitter backoff delay using the algorithm\n * popularized by AWS. Each retry delay is chosen randomly between the\n * base delay and three times the previous delay, then capped at the\n * maximum allowed delay. This prevents synchronized retry spikes and\n * produces a smooth, adaptive backoff pattern.\n *\n * Use this when you need highly desynchronized retries across many\n * clients—especially in distributed systems or high‑traffic services\n * where coordinated retry storms can overwhelm downstream resources.\n *\n * AWS reference:\n * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/\n *\n * @param {number} baseDelayMs - The minimum delay for any retry attempt.\n * @param {number} maxDelayMs - The maximum allowed delay.\n * @param {number} [prevDelayMs=0] - The delay used for the previous retry.\n * @returns {number} A jittered delay capped at `maxDelayMs`.\n */\nexport function calculateDecorrelatedJitter(baseDelayMs: number, maxDelayMs: number, prevDelayMs: number = 0): number {\n const upperDelayMs = Math.max(baseDelayMs, prevDelayMs * 3);\n const randomValueBetweenBaseDelayAnd3xPrevoiusDelay = baseDelayMs + Math.random() * (upperDelayMs - baseDelayMs);\n return Math.min(maxDelayMs, randomValueBetweenBaseDelayAnd3xPrevoiusDelay)\n}","/**\n * Core retry logic implementation\n */\n\nimport { calculateDecorrelatedJitter, calculateEqualJitter, calculateFixedJitter, calculateFullJitter, calculateRandomJitter } from \"./jitter\";\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nexport type BackoffStrategy = 'exponential' | 'linear' | 'fixed';\nfunction calculateDelay(baseDelay: number, attempt: number, backoff: BackoffStrategy): number {\n switch (backoff) {\n case 'exponential':\n return baseDelay * Math.pow(2, attempt - 1);\n case 'linear':\n return baseDelay * attempt;\n case 'fixed':\n default:\n return baseDelay;\n }\n}\n\nexport type JitterConfig =\n | { type: 'none' }\n | { type: 'equal' }\n | { type: 'full', min: number, max: number }\n | { type: 'fixed', amount: number }\n | { type: 'random', fraction: number }\n | { type: 'decorrelated', maxDelay: number };\n\n function applyJitter(\n baseDelay: number,\n jitter: JitterConfig | undefined,\n prevDelay: number\n ): number {\n if (!jitter || jitter.type === 'none') return 0;\n \n switch (jitter.type) {\n case 'equal':\n return calculateEqualJitter(baseDelay);\n \n case 'full':\n return calculateFullJitter(jitter.min, jitter.max);\n \n case 'fixed':\n return calculateFixedJitter(baseDelay, jitter.amount);\n \n case 'random':\n return calculateRandomJitter(baseDelay, jitter.fraction);\n \n case 'decorrelated':\n return calculateDecorrelatedJitter(\n baseDelay,\n jitter.maxDelay,\n prevDelay\n );\n }\n }\n \n\n\nexport interface RetryOptions {\n /** Maximum number of retry attempts (default: 3) */\n maxAttempts?: number;\n /** Base delay in milliseconds (default: 1000) */\n delay?: number;\n /** Backoff strategy: 'exponential', 'linear', or 'fixed' (default: 'exponential') */\n backoff?: BackoffStrategy;\n /** Callback called before each retry (error, attempt, waitTime) */\n onRetry?: (error: Error, attempt: number, waitTime: number) => void;\n /** Jitter settings */\n jitterConfig?: JitterConfig\n}\n\n/**\n * @param fn - The async function to retry\n * @param options - Retry configuration\n * @returns A function that wraps the original function with retry logic\n */\n// @typescript-eslint/no-explicit-any\nexport function retry<T extends (...args: unknown[]) => Promise<unknown>>(\n fn: T,\n options: RetryOptions = {}\n): T {\n const {\n maxAttempts = 3,\n delay = 1000,\n backoff = 'exponential',\n jitterConfig,\n onRetry = () => {}\n } = options;\n\n return (async function(...args: Parameters<T>): Promise<ReturnType<T>> {\n let lastError: Error;\n \n let prevJitterDelay = 0;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(...args) as ReturnType<T>;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n \n if (attempt < maxAttempts) {\n const waitTime = calculateDelay(delay, attempt, backoff);\n const jitterMs = applyJitter(waitTime, jitterConfig, prevJitterDelay)\n prevJitterDelay = jitterMs;\n const delayTimeMs = waitTime + jitterMs;\n onRetry(lastError, attempt, delayTimeMs);\n await sleep(delayTimeMs);\n }\n }\n }\n \n throw lastError!;\n }) as T;\n}\n\nexport { calculateDelay, sleep, applyJitter };\n\n","/**\n * Jitterbug - A lightweight library for building reliable retry behavior\n */\n\nexport { retry, calculateDelay, sleep, type RetryOptions, type BackoffStrategy } from './retry';\n\nimport * as jitterFunctions from './jitter';\nimport { retry } from './retry';\n\nexport default {\n retry,\n ...jitterFunctions\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jitterbug",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "A lightweight library for building reliable retry behavior in distributed systems and API clients",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",