@zap-studio/retry 1.0.0 → 1.1.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.1.0]
8
+
9
+ ### Added
10
+
11
+ `exponentialBackoff(...)` and `linearBackoff(...)` gain an optional `jitter?: "full" | "equal" | JitterOptions` option, applied to the computed delay after it's capped at `maxDelayMs`. `"full"` randomizes across `[0, delayMs]`; `"equal"` keeps a floor at half the delay, randomizing across `[delayMs/2, delayMs]`. Pass `{ mode, random }` to override the random source, useful for deterministic tests.
12
+
13
+ New export: `applyJitter(delayMs, jitter?)`, also available from the `./jitter` subpath. See [Jitter](https://www.zapstudio.dev/retry/jitter).
14
+
7
15
  ## [1.0.0]
8
16
 
9
17
  ### Added
package/README.md CHANGED
@@ -13,6 +13,7 @@ npm install @zap-studio/retry
13
13
  ## Features
14
14
 
15
15
  - **Built-in policies**: `fixedDelay(...)`, `linearBackoff(...)`, and `exponentialBackoff(...)`.
16
+ - **Jitter**: `"full"` or `"equal"` jitter on `exponentialBackoff`/`linearBackoff`, to avoid synchronized retries against a shared upstream.
16
17
  - **A shared runner** via `runRetryPolicy(policy, execute, options?)` with attempt-aware callbacks and custom sleep injection.
17
18
  - **Structured terminal errors**: `RetryError` on exhaustion, `AbortError` on cancellation.
18
19
  - **Non-throw mode** (`throwOnExhausted: false`) returns a `RetryRunResult` instead of throwing.
@@ -65,6 +66,19 @@ const linear = linearBackoff({
65
66
  const fixed = fixedDelay({ maxAttempts: 4, delayMs: 300 });
66
67
  ```
67
68
 
69
+ ## Jitter
70
+
71
+ `"full"` or `"equal"` jitter on `exponentialBackoff`/`linearBackoff`, applied to the delay after it's capped, to avoid synchronized retries against a shared upstream.
72
+
73
+ ```ts
74
+ const policy = exponentialBackoff({
75
+ maxAttempts: 5,
76
+ baseDelayMs: 100,
77
+ maxDelayMs: 2_000,
78
+ jitter: "full",
79
+ });
80
+ ```
81
+
68
82
  ## Shared Runner
69
83
 
70
84
  Via `runRetryPolicy(policy, execute, options?)` with attempt-aware callbacks and custom sleep injection.
@@ -1,4 +1,5 @@
1
1
  import { RetryPolicy } from "./types.js";
2
+ import { JitterMode, JitterOptions } from "./jitter.js";
2
3
  //#region src/exponential-backoff.d.ts
3
4
  /**
4
5
  * Configuration for `exponentialBackoff(...)`.
@@ -8,6 +9,7 @@ import { RetryPolicy } from "./types.js";
8
9
  * maxAttempts: 5,
9
10
  * baseDelayMs: 100,
10
11
  * maxDelayMs: 2_000,
12
+ * jitter: "full",
11
13
  * };
12
14
  */
13
15
  interface ExponentialBackoffOptions {
@@ -23,6 +25,11 @@ interface ExponentialBackoffOptions {
23
25
  * Hard upper bound in milliseconds for computed exponential delay.
24
26
  */
25
27
  maxDelayMs: number;
28
+ /**
29
+ * Optional jitter applied to the computed delay, after capping at
30
+ * `maxDelayMs`.
31
+ */
32
+ jitter?: JitterMode | JitterOptions;
26
33
  }
27
34
  /**
28
35
  * Creates a retry policy with exponential delay growth up to a max cap.
@@ -1 +1 @@
1
- {"version":3,"file":"exponential-backoff.d.ts","names":[],"sources":["../src/exponential-backoff.ts"],"mappings":";;;;;;;;;;;;UAsBiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;;;;;;;;;cAaW,qBACX,SAAS,8BACR"}
1
+ {"version":3,"file":"exponential-backoff.d.ts","names":[],"sources":["../src/exponential-backoff.ts"],"mappings":";;;;;;;;;;;;;;UAyBiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;;EAKA,SAAS,aAAa;;;;;;;;;;;;cAaX,qBACX,SAAS,8BACR"}
@@ -1,5 +1,11 @@
1
+ import { applyJitter } from "./jitter.js";
1
2
  //#region src/exponential-backoff.ts
2
3
  /**
4
+ * Exponential backoff retry strategy.
5
+ *
6
+ * @module @zap-studio/retry/exponential-backoff
7
+ */
8
+ /**
3
9
  * Creates a retry policy with exponential delay growth up to a max cap.
4
10
  *
5
11
  * @example
@@ -10,7 +16,7 @@
10
16
  * });
11
17
  */
12
18
  const exponentialBackoff = (options) => {
13
- const { maxAttempts, baseDelayMs, maxDelayMs } = options;
19
+ const { maxAttempts, baseDelayMs, maxDelayMs, jitter } = options;
14
20
  return {
15
21
  /**
16
22
  * Computes retry decision for the current attempt.
@@ -22,8 +28,9 @@ next(input) {
22
28
  shouldRetry: false
23
29
  };
24
30
  const exponent = Math.max(0, input.attempt - 1);
31
+ const cappedDelayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** exponent);
25
32
  return {
26
- delayMs: Math.min(maxDelayMs, baseDelayMs * 2 ** exponent),
33
+ delayMs: applyJitter(cappedDelayMs, jitter),
27
34
  reason: "retry",
28
35
  shouldRetry: true
29
36
  };
@@ -1 +1 @@
1
- {"version":3,"file":"exponential-backoff.js","names":[],"sources":["../src/exponential-backoff.ts"],"sourcesContent":["/**\n * Exponential backoff retry strategy.\n *\n * @module @zap-studio/retry/exponential-backoff\n */\n\nimport type {\n RetryDecision,\n RetryDecisionInput,\n RetryPolicy,\n} from \"./types.js\";\n\n/**\n * Configuration for `exponentialBackoff(...)`.\n *\n * @example\n * const options: ExponentialBackoffOptions = {\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * maxDelayMs: 2_000,\n * };\n */\nexport interface ExponentialBackoffOptions {\n /**\n * Maximum number of attempts (including the first) before giving up.\n */\n maxAttempts: number;\n /**\n * Initial delay in milliseconds, doubled each retry until capped.\n */\n baseDelayMs: number;\n /**\n * Hard upper bound in milliseconds for computed exponential delay.\n */\n maxDelayMs: number;\n}\n\n/**\n * Creates a retry policy with exponential delay growth up to a max cap.\n *\n * @example\n * const policy = exponentialBackoff({\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * maxDelayMs: 2_000,\n * });\n */\nexport const exponentialBackoff = (\n options: ExponentialBackoffOptions\n): RetryPolicy => {\n const { maxAttempts, baseDelayMs, maxDelayMs } = options;\n\n return {\n /**\n * Computes retry decision for the current attempt.\n */\n next(input: RetryDecisionInput): RetryDecision {\n if (input.attempt >= maxAttempts) {\n return {\n delayMs: 0,\n reason: \"max-attempts-reached\",\n shouldRetry: false,\n };\n }\n\n const exponent = Math.max(0, input.attempt - 1);\n const delayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** exponent);\n\n return { delayMs, reason: \"retry\", shouldRetry: true };\n },\n };\n};\n"],"mappings":";;;;;;;;;;;AA+CA,MAAa,sBACX,YACgB;CAChB,MAAM,EAAE,aAAa,aAAa,eAAe;CAEjD,OAAO;;;;AAIL,KAAK,OAA0C;EAC7C,IAAI,MAAM,WAAW,aACnB,OAAO;GACL,SAAS;GACT,QAAQ;GACR,aAAa;EACf;EAGF,MAAM,WAAW,KAAK,IAAI,GAAG,MAAM,UAAU,CAAC;EAG9C,OAAO;GAAE,SAFO,KAAK,IAAI,YAAY,cAAc,KAAK,QAEzC;GAAG,QAAQ;GAAS,aAAa;EAAK;CACvD,EACF;AACF"}
1
+ {"version":3,"file":"exponential-backoff.js","names":[],"sources":["../src/exponential-backoff.ts"],"sourcesContent":["/**\n * Exponential backoff retry strategy.\n *\n * @module @zap-studio/retry/exponential-backoff\n */\n\nimport { applyJitter } from \"./jitter.js\";\nimport type { JitterMode, JitterOptions } from \"./jitter.js\";\nimport type {\n RetryDecision,\n RetryDecisionInput,\n RetryPolicy,\n} from \"./types.js\";\n\n/**\n * Configuration for `exponentialBackoff(...)`.\n *\n * @example\n * const options: ExponentialBackoffOptions = {\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * maxDelayMs: 2_000,\n * jitter: \"full\",\n * };\n */\nexport interface ExponentialBackoffOptions {\n /**\n * Maximum number of attempts (including the first) before giving up.\n */\n maxAttempts: number;\n /**\n * Initial delay in milliseconds, doubled each retry until capped.\n */\n baseDelayMs: number;\n /**\n * Hard upper bound in milliseconds for computed exponential delay.\n */\n maxDelayMs: number;\n /**\n * Optional jitter applied to the computed delay, after capping at\n * `maxDelayMs`.\n */\n jitter?: JitterMode | JitterOptions;\n}\n\n/**\n * Creates a retry policy with exponential delay growth up to a max cap.\n *\n * @example\n * const policy = exponentialBackoff({\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * maxDelayMs: 2_000,\n * });\n */\nexport const exponentialBackoff = (\n options: ExponentialBackoffOptions\n): RetryPolicy => {\n const { maxAttempts, baseDelayMs, maxDelayMs, jitter } = options;\n\n return {\n /**\n * Computes retry decision for the current attempt.\n */\n next(input: RetryDecisionInput): RetryDecision {\n if (input.attempt >= maxAttempts) {\n return {\n delayMs: 0,\n reason: \"max-attempts-reached\",\n shouldRetry: false,\n };\n }\n\n const exponent = Math.max(0, input.attempt - 1);\n const cappedDelayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** exponent);\n const delayMs = applyJitter(cappedDelayMs, jitter);\n\n return { delayMs, reason: \"retry\", shouldRetry: true };\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAuDA,MAAa,sBACX,YACgB;CAChB,MAAM,EAAE,aAAa,aAAa,YAAY,WAAW;CAEzD,OAAO;;;;AAIL,KAAK,OAA0C;EAC7C,IAAI,MAAM,WAAW,aACnB,OAAO;GACL,SAAS;GACT,QAAQ;GACR,aAAa;EACf;EAGF,MAAM,WAAW,KAAK,IAAI,GAAG,MAAM,UAAU,CAAC;EAC9C,MAAM,gBAAgB,KAAK,IAAI,YAAY,cAAc,KAAK,QAAQ;EAGtE,OAAO;GAAE,SAFO,YAAY,eAAe,MAE5B;GAAG,QAAQ;GAAS,aAAa;EAAK;CACvD,EACF;AACF"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { i as RetryErrorContext, n as AbortErrorContext, r as RetryError, t as AbortError } from "./errors-CS5UPJWs.js";
2
2
  import { RetryDecision, RetryDecisionInput, RetryExhaustedInput, RetryPolicy, RetryRunOptions, RetryRunResult } from "./types.js";
3
3
  import { defaultSleep, runRetryPolicy } from "./base-policy.js";
4
+ import { JitterMode, JitterOptions, applyJitter } from "./jitter.js";
4
5
  import { ExponentialBackoffOptions, exponentialBackoff } from "./exponential-backoff.js";
5
6
  import { FixedDelayOptions, fixedDelay } from "./fixed-delay.js";
6
7
  import { LinearBackoffOptions, linearBackoff } from "./linear-backoff.js";
7
- export { AbortError, type AbortErrorContext, type ExponentialBackoffOptions, type FixedDelayOptions, type LinearBackoffOptions, type RetryDecision, type RetryDecisionInput, RetryError, type RetryErrorContext, type RetryExhaustedInput, type RetryPolicy, type RetryRunOptions, type RetryRunResult, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy };
8
+ export { AbortError, type AbortErrorContext, type ExponentialBackoffOptions, type FixedDelayOptions, type JitterMode, type JitterOptions, type LinearBackoffOptions, type RetryDecision, type RetryDecisionInput, RetryError, type RetryErrorContext, type RetryExhaustedInput, type RetryPolicy, type RetryRunOptions, type RetryRunResult, applyJitter, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AbortError, RetryError } from "./errors.js";
2
2
  import { defaultSleep, runRetryPolicy } from "./base-policy.js";
3
+ import { applyJitter } from "./jitter.js";
3
4
  import { exponentialBackoff } from "./exponential-backoff.js";
4
5
  import { fixedDelay } from "./fixed-delay.js";
5
6
  import { linearBackoff } from "./linear-backoff.js";
6
- export { AbortError, RetryError, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy };
7
+ export { AbortError, RetryError, applyJitter, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy };
@@ -0,0 +1,48 @@
1
+ //#region src/jitter.d.ts
2
+ /**
3
+ * Jitter strategies applied to a computed backoff delay.
4
+ *
5
+ * @module @zap-studio/retry/jitter
6
+ */
7
+ /**
8
+ * Supported jitter strategies.
9
+ *
10
+ * - `"full"`: `random(0, delayMs)` — max spread, best thundering-herd
11
+ * protection.
12
+ * - `"equal"`: `delayMs/2 + random(0, delayMs/2)` — keeps a floor at half
13
+ * the computed delay, less spread than full jitter.
14
+ */
15
+ type JitterMode = "equal" | "full";
16
+ /**
17
+ * Configuration for jitter application.
18
+ *
19
+ * @example
20
+ * const jitter: JitterOptions = { mode: "full" };
21
+ */
22
+ interface JitterOptions {
23
+ /**
24
+ * Jitter strategy to apply.
25
+ */
26
+ mode: JitterMode;
27
+ /**
28
+ * Random source in `[0, 1)`, overridable for deterministic tests.
29
+ *
30
+ * @default Math.random
31
+ */
32
+ random?: () => number;
33
+ }
34
+ /**
35
+ * Applies a jitter strategy to a computed delay.
36
+ *
37
+ * @param delayMs - Delay in milliseconds before jitter.
38
+ * @param jitter - Jitter mode shorthand, full `JitterOptions`, or `undefined`
39
+ * to leave `delayMs` untouched.
40
+ * @returns Jittered delay in milliseconds, rounded to the nearest integer.
41
+ *
42
+ * @example
43
+ * const delayMs = applyJitter(1000, "full"); // 0-1000
44
+ */
45
+ declare const applyJitter: (delayMs: number, jitter?: JitterMode | JitterOptions) => number;
46
+ //#endregion
47
+ export { JitterMode, JitterOptions, applyJitter };
48
+ //# sourceMappingURL=jitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jitter.d.ts","names":[],"sources":["../src/jitter.ts"],"mappings":";;;;;;;;;;;;;;KAcY;;;;;;;UAQK;;;;EAIf,MAAM;;;;;;EAMN;;;;;;;;;;;;;cAcW,cACX,iBACA,SAAS,aAAa"}
package/dist/jitter.js ADDED
@@ -0,0 +1,24 @@
1
+ //#region src/jitter.ts
2
+ /**
3
+ * Applies a jitter strategy to a computed delay.
4
+ *
5
+ * @param delayMs - Delay in milliseconds before jitter.
6
+ * @param jitter - Jitter mode shorthand, full `JitterOptions`, or `undefined`
7
+ * to leave `delayMs` untouched.
8
+ * @returns Jittered delay in milliseconds, rounded to the nearest integer.
9
+ *
10
+ * @example
11
+ * const delayMs = applyJitter(1000, "full"); // 0-1000
12
+ */
13
+ const applyJitter = (delayMs, jitter) => {
14
+ if (jitter === void 0) return delayMs;
15
+ const mode = typeof jitter === "string" ? jitter : jitter.mode;
16
+ const random = (typeof jitter === "string" ? void 0 : jitter.random) ?? Math.random;
17
+ if (mode === "full") return Math.round(random() * delayMs);
18
+ const half = delayMs / 2;
19
+ return Math.round(half + random() * half);
20
+ };
21
+ //#endregion
22
+ export { applyJitter };
23
+
24
+ //# sourceMappingURL=jitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jitter.js","names":[],"sources":["../src/jitter.ts"],"sourcesContent":["/**\n * Jitter strategies applied to a computed backoff delay.\n *\n * @module @zap-studio/retry/jitter\n */\n\n/**\n * Supported jitter strategies.\n *\n * - `\"full\"`: `random(0, delayMs)` — max spread, best thundering-herd\n * protection.\n * - `\"equal\"`: `delayMs/2 + random(0, delayMs/2)` — keeps a floor at half\n * the computed delay, less spread than full jitter.\n */\nexport type JitterMode = \"equal\" | \"full\";\n\n/**\n * Configuration for jitter application.\n *\n * @example\n * const jitter: JitterOptions = { mode: \"full\" };\n */\nexport interface JitterOptions {\n /**\n * Jitter strategy to apply.\n */\n mode: JitterMode;\n /**\n * Random source in `[0, 1)`, overridable for deterministic tests.\n *\n * @default Math.random\n */\n random?: () => number;\n}\n\n/**\n * Applies a jitter strategy to a computed delay.\n *\n * @param delayMs - Delay in milliseconds before jitter.\n * @param jitter - Jitter mode shorthand, full `JitterOptions`, or `undefined`\n * to leave `delayMs` untouched.\n * @returns Jittered delay in milliseconds, rounded to the nearest integer.\n *\n * @example\n * const delayMs = applyJitter(1000, \"full\"); // 0-1000\n */\nexport const applyJitter = (\n delayMs: number,\n jitter?: JitterMode | JitterOptions\n): number => {\n if (jitter === undefined) {\n return delayMs;\n }\n\n const mode = typeof jitter === \"string\" ? jitter : jitter.mode;\n const random =\n (typeof jitter === \"string\" ? undefined : jitter.random) ?? Math.random;\n\n if (mode === \"full\") {\n return Math.round(random() * delayMs);\n }\n\n const half = delayMs / 2;\n return Math.round(half + random() * half);\n};\n"],"mappings":";;;;;;;;;;;;AA8CA,MAAa,eACX,SACA,WACW;CACX,IAAI,WAAW,KAAA,GACb,OAAO;CAGT,MAAM,OAAO,OAAO,WAAW,WAAW,SAAS,OAAO;CAC1D,MAAM,UACH,OAAO,WAAW,WAAW,KAAA,IAAY,OAAO,WAAW,KAAK;CAEnE,IAAI,SAAS,QACX,OAAO,KAAK,MAAM,OAAO,IAAI,OAAO;CAGtC,MAAM,OAAO,UAAU;CACvB,OAAO,KAAK,MAAM,OAAO,OAAO,IAAI,IAAI;AAC1C"}
@@ -1,4 +1,5 @@
1
1
  import { RetryPolicy } from "./types.js";
2
+ import { JitterMode, JitterOptions } from "./jitter.js";
2
3
  //#region src/linear-backoff.d.ts
3
4
  /**
4
5
  * Configuration for `linearBackoff(...)`.
@@ -9,6 +10,7 @@ import { RetryPolicy } from "./types.js";
9
10
  * baseDelayMs: 100,
10
11
  * incrementMs: 100,
11
12
  * maxDelayMs: 2_000,
13
+ * jitter: "equal",
12
14
  * };
13
15
  */
14
16
  interface LinearBackoffOptions {
@@ -28,6 +30,11 @@ interface LinearBackoffOptions {
28
30
  * Hard upper bound in milliseconds for computed linear delay.
29
31
  */
30
32
  maxDelayMs: number;
33
+ /**
34
+ * Optional jitter applied to the computed delay, after capping at
35
+ * `maxDelayMs`.
36
+ */
37
+ jitter?: JitterMode | JitterOptions;
31
38
  }
32
39
  /**
33
40
  * Creates a retry policy with linear delay growth up to a max cap.
@@ -1 +1 @@
1
- {"version":3,"file":"linear-backoff.d.ts","names":[],"sources":["../src/linear-backoff.ts"],"mappings":";;;;;;;;;;;;;UAuBiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;EAIA;;;;;;;;;;;;;cAcW,gBAAiB,SAAS,yBAAuB"}
1
+ {"version":3,"file":"linear-backoff.d.ts","names":[],"sources":["../src/linear-backoff.ts"],"mappings":";;;;;;;;;;;;;;;UA0BiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;EAIA;;;;;EAKA,SAAS,aAAa;;;;;;;;;;;;;cAcX,gBAAiB,SAAS,yBAAuB"}
@@ -1,5 +1,11 @@
1
+ import { applyJitter } from "./jitter.js";
1
2
  //#region src/linear-backoff.ts
2
3
  /**
4
+ * Linear backoff retry strategy.
5
+ *
6
+ * @module @zap-studio/retry/linear-backoff
7
+ */
8
+ /**
3
9
  * Creates a retry policy with linear delay growth up to a max cap.
4
10
  *
5
11
  * @example
@@ -11,7 +17,7 @@
11
17
  * });
12
18
  */
13
19
  const linearBackoff = (options) => {
14
- const { maxAttempts, baseDelayMs, incrementMs, maxDelayMs } = options;
20
+ const { maxAttempts, baseDelayMs, incrementMs, maxDelayMs, jitter } = options;
15
21
  return {
16
22
  /**
17
23
  * Computes retry decision for the current attempt.
@@ -22,8 +28,9 @@ next(input) {
22
28
  reason: "max-attempts-reached",
23
29
  shouldRetry: false
24
30
  };
31
+ const cappedDelayMs = Math.min(maxDelayMs, baseDelayMs + incrementMs * (input.attempt - 1));
25
32
  return {
26
- delayMs: Math.min(maxDelayMs, baseDelayMs + incrementMs * (input.attempt - 1)),
33
+ delayMs: applyJitter(cappedDelayMs, jitter),
27
34
  reason: "retry",
28
35
  shouldRetry: true
29
36
  };
@@ -1 +1 @@
1
- {"version":3,"file":"linear-backoff.js","names":[],"sources":["../src/linear-backoff.ts"],"sourcesContent":["/**\n * Linear backoff retry strategy.\n *\n * @module @zap-studio/retry/linear-backoff\n */\n\nimport type {\n RetryDecision,\n RetryDecisionInput,\n RetryPolicy,\n} from \"./types.js\";\n\n/**\n * Configuration for `linearBackoff(...)`.\n *\n * @example\n * const options: LinearBackoffOptions = {\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * incrementMs: 100,\n * maxDelayMs: 2_000,\n * };\n */\nexport interface LinearBackoffOptions {\n /**\n * Maximum number of attempts (including the first) before giving up.\n */\n maxAttempts: number;\n /**\n * Delay in milliseconds after the first failed attempt.\n */\n baseDelayMs: number;\n /**\n * Amount added to the delay for each subsequent retry.\n */\n incrementMs: number;\n /**\n * Hard upper bound in milliseconds for computed linear delay.\n */\n maxDelayMs: number;\n}\n\n/**\n * Creates a retry policy with linear delay growth up to a max cap.\n *\n * @example\n * const policy = linearBackoff({\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * incrementMs: 100,\n * maxDelayMs: 2_000,\n * });\n */\nexport const linearBackoff = (options: LinearBackoffOptions): RetryPolicy => {\n const { maxAttempts, baseDelayMs, incrementMs, maxDelayMs } = options;\n\n return {\n /**\n * Computes retry decision for the current attempt.\n */\n next(input: RetryDecisionInput): RetryDecision {\n if (input.attempt >= maxAttempts) {\n return {\n delayMs: 0,\n reason: \"max-attempts-reached\",\n shouldRetry: false,\n };\n }\n\n const delayMs = Math.min(\n maxDelayMs,\n baseDelayMs + incrementMs * (input.attempt - 1)\n );\n\n return { delayMs, reason: \"retry\", shouldRetry: true };\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;AAqDA,MAAa,iBAAiB,YAA+C;CAC3E,MAAM,EAAE,aAAa,aAAa,aAAa,eAAe;CAE9D,OAAO;;;;AAIL,KAAK,OAA0C;EAC7C,IAAI,MAAM,WAAW,aACnB,OAAO;GACL,SAAS;GACT,QAAQ;GACR,aAAa;EACf;EAQF,OAAO;GAAE,SALO,KAAK,IACnB,YACA,cAAc,eAAe,MAAM,UAAU,EAGhC;GAAG,QAAQ;GAAS,aAAa;EAAK;CACvD,EACF;AACF"}
1
+ {"version":3,"file":"linear-backoff.js","names":[],"sources":["../src/linear-backoff.ts"],"sourcesContent":["/**\n * Linear backoff retry strategy.\n *\n * @module @zap-studio/retry/linear-backoff\n */\n\nimport { applyJitter } from \"./jitter.js\";\nimport type { JitterMode, JitterOptions } from \"./jitter.js\";\nimport type {\n RetryDecision,\n RetryDecisionInput,\n RetryPolicy,\n} from \"./types.js\";\n\n/**\n * Configuration for `linearBackoff(...)`.\n *\n * @example\n * const options: LinearBackoffOptions = {\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * incrementMs: 100,\n * maxDelayMs: 2_000,\n * jitter: \"equal\",\n * };\n */\nexport interface LinearBackoffOptions {\n /**\n * Maximum number of attempts (including the first) before giving up.\n */\n maxAttempts: number;\n /**\n * Delay in milliseconds after the first failed attempt.\n */\n baseDelayMs: number;\n /**\n * Amount added to the delay for each subsequent retry.\n */\n incrementMs: number;\n /**\n * Hard upper bound in milliseconds for computed linear delay.\n */\n maxDelayMs: number;\n /**\n * Optional jitter applied to the computed delay, after capping at\n * `maxDelayMs`.\n */\n jitter?: JitterMode | JitterOptions;\n}\n\n/**\n * Creates a retry policy with linear delay growth up to a max cap.\n *\n * @example\n * const policy = linearBackoff({\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * incrementMs: 100,\n * maxDelayMs: 2_000,\n * });\n */\nexport const linearBackoff = (options: LinearBackoffOptions): RetryPolicy => {\n const { maxAttempts, baseDelayMs, incrementMs, maxDelayMs, jitter } = options;\n\n return {\n /**\n * Computes retry decision for the current attempt.\n */\n next(input: RetryDecisionInput): RetryDecision {\n if (input.attempt >= maxAttempts) {\n return {\n delayMs: 0,\n reason: \"max-attempts-reached\",\n shouldRetry: false,\n };\n }\n\n const cappedDelayMs = Math.min(\n maxDelayMs,\n baseDelayMs + incrementMs * (input.attempt - 1)\n );\n const delayMs = applyJitter(cappedDelayMs, jitter);\n\n return { delayMs, reason: \"retry\", shouldRetry: true };\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6DA,MAAa,iBAAiB,YAA+C;CAC3E,MAAM,EAAE,aAAa,aAAa,aAAa,YAAY,WAAW;CAEtE,OAAO;;;;AAIL,KAAK,OAA0C;EAC7C,IAAI,MAAM,WAAW,aACnB,OAAO;GACL,SAAS;GACT,QAAQ;GACR,aAAa;EACf;EAGF,MAAM,gBAAgB,KAAK,IACzB,YACA,cAAc,eAAe,MAAM,UAAU,EAC/C;EAGA,OAAO;GAAE,SAFO,YAAY,eAAe,MAE5B;GAAG,QAAQ;GAAS,aAAa;EAAK;CACvD,EACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-studio/retry",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Composable, tree-shakeable retry policies for resilient async operations.",
6
6
  "keywords": [
@@ -35,6 +35,7 @@
35
35
  "./errors": "./dist/errors.js",
36
36
  "./exponential-backoff": "./dist/exponential-backoff.js",
37
37
  "./fixed-delay": "./dist/fixed-delay.js",
38
+ "./jitter": "./dist/jitter.js",
38
39
  "./linear-backoff": "./dist/linear-backoff.js",
39
40
  "./types": "./dist/types.js",
40
41
  "./package.json": "./package.json"