@spoosh/plugin-retry 0.3.0 → 0.3.1

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
@@ -27,18 +27,59 @@ useRead((api) => api("posts").GET(), { retries: 5, retryDelay: 2000 });
27
27
  useRead((api) => api("posts").GET(), { retries: false });
28
28
  ```
29
29
 
30
+ ## Retry Behavior
31
+
32
+ By default, the plugin retries on:
33
+
34
+ - **Network errors** - Always retried (cannot be disabled via `shouldRetry`)
35
+ - **Status codes** - `408`, `429`, `500`, `502`, `503`, `504`
36
+
37
+ ### Custom Retry Logic
38
+
39
+ Use the `shouldRetry` callback for custom retry conditions:
40
+
41
+ ```typescript
42
+ retryPlugin({
43
+ retries: 3,
44
+ shouldRetry: ({ status, error, attempt, maxRetries }) => {
45
+ // Only retry on 503 Service Unavailable
46
+ return status === 503;
47
+ },
48
+ });
49
+
50
+ // Per-request override
51
+ useRead((api) => api("posts").GET(), {
52
+ shouldRetry: ({ status }) => status === 429,
53
+ });
54
+ ```
55
+
56
+ > **Note:** Network errors are always retried regardless of the `shouldRetry` callback return value.
57
+
30
58
  ## Options
31
59
 
32
60
  ### Plugin Config
33
61
 
34
- | Option | Type | Default | Description |
35
- | ------------ | ----------------- | ------- | ------------------------------------------------------------ |
36
- | `retries` | `number \| false` | `3` | Number of retry attempts. Set to `false` to disable retries. |
37
- | `retryDelay` | `number` | `1000` | Delay between retries in milliseconds |
62
+ | Option | Type | Default | Description |
63
+ | ------------- | --------------------- | --------------------------------------- | ---------------------------------------------------------------- |
64
+ | `retries` | `number \| false` | `3` | Number of retry attempts. Set to `false` to disable retries. |
65
+ | `retryDelay` | `number` | `1000` | Delay between retries in milliseconds (uses exponential backoff) |
66
+ | `shouldRetry` | `ShouldRetryCallback` | Retries on 408, 429, 500, 502, 503, 504 | Custom callback to determine if a request should be retried |
38
67
 
39
68
  ### Per-Request Options
40
69
 
41
- | Option | Type | Description |
42
- | ------------ | ----------------- | ---------------------------------------- |
43
- | `retries` | `number \| false` | Override retry attempts for this request |
44
- | `retryDelay` | `number` | Override retry delay for this request |
70
+ | Option | Type | Description |
71
+ | ------------- | --------------------- | ---------------------------------------- |
72
+ | `retries` | `number \| false` | Override retry attempts for this request |
73
+ | `retryDelay` | `number` | Override retry delay for this request |
74
+ | `shouldRetry` | `ShouldRetryCallback` | Override retry logic for this request |
75
+
76
+ ### ShouldRetryContext
77
+
78
+ The `shouldRetry` callback receives a context object:
79
+
80
+ | Property | Type | Description |
81
+ | ------------ | --------- | ------------------------------------ |
82
+ | `status` | `number?` | HTTP status code from the response |
83
+ | `error` | `unknown` | The error that occurred |
84
+ | `attempt` | `number` | Current attempt number (0-indexed) |
85
+ | `maxRetries` | `number` | Maximum number of retries configured |
package/dist/index.d.mts CHANGED
@@ -1,22 +1,62 @@
1
1
  import { SpooshPlugin } from '@spoosh/core';
2
2
 
3
+ /**
4
+ * Context passed to the shouldRetry callback.
5
+ */
6
+ interface ShouldRetryContext {
7
+ /** HTTP status code from the response, if available */
8
+ status?: number;
9
+ /** The error that occurred, if any */
10
+ error: unknown;
11
+ /** Current attempt number (0-indexed) */
12
+ attempt: number;
13
+ /** Maximum number of retries configured */
14
+ maxRetries: number;
15
+ }
16
+ /**
17
+ * Callback to determine if a request should be retried.
18
+ * Network errors are always retried regardless of this callback's return value.
19
+ *
20
+ * @returns `true` to retry, `false` to stop retrying
21
+ */
22
+ type ShouldRetryCallback = (context: ShouldRetryContext) => boolean;
23
+ /** Status codes that are retried by default: 408, 429, 500, 502, 503, 504 */
24
+ declare const DEFAULT_RETRY_STATUS_CODES: readonly [408, 429, 500, 502, 503, 504];
3
25
  interface RetryPluginConfig {
4
26
  /** Number of retry attempts. Set to `false` to disable retries. Defaults to 3. */
5
27
  retries?: number | false;
6
28
  /** Delay between retries in milliseconds. Defaults to 1000. */
7
29
  retryDelay?: number;
30
+ /**
31
+ * Custom callback to determine if a request should be retried.
32
+ * Network errors are always retried regardless of this callback.
33
+ * Defaults to retrying on status codes: 408, 429, 500, 502, 503, 504.
34
+ */
35
+ shouldRetry?: ShouldRetryCallback;
8
36
  }
9
37
  interface RetryReadOptions {
10
38
  /** Number of retry attempts. Set to `false` to disable retries. Overrides plugin default. */
11
39
  retries?: number | false;
12
40
  /** Delay between retries in milliseconds. Overrides plugin default. */
13
41
  retryDelay?: number;
42
+ /**
43
+ * Custom callback to determine if a request should be retried.
44
+ * Network errors are always retried regardless of this callback.
45
+ * Overrides plugin default.
46
+ */
47
+ shouldRetry?: ShouldRetryCallback;
14
48
  }
15
49
  interface RetryWriteOptions {
16
50
  /** Number of retry attempts. Set to `false` to disable retries. Overrides plugin default. */
17
51
  retries?: number | false;
18
52
  /** Delay between retries in milliseconds. Overrides plugin default. */
19
53
  retryDelay?: number;
54
+ /**
55
+ * Custom callback to determine if a request should be retried.
56
+ * Network errors are always retried regardless of this callback.
57
+ * Overrides plugin default.
58
+ */
59
+ shouldRetry?: ShouldRetryCallback;
20
60
  }
21
61
  type RetryInfiniteReadOptions = RetryReadOptions;
22
62
  type RetryReadResult = object;
@@ -56,4 +96,4 @@ declare function retryPlugin(config?: RetryPluginConfig): SpooshPlugin<{
56
96
  writeResult: RetryWriteResult;
57
97
  }>;
58
98
 
59
- export { type RetryInfiniteReadOptions, type RetryPluginConfig, type RetryReadOptions, type RetryReadResult, type RetryWriteOptions, type RetryWriteResult, retryPlugin };
99
+ export { DEFAULT_RETRY_STATUS_CODES, type RetryInfiniteReadOptions, type RetryPluginConfig, type RetryReadOptions, type RetryReadResult, type RetryWriteOptions, type RetryWriteResult, type ShouldRetryCallback, type ShouldRetryContext, retryPlugin };
package/dist/index.d.ts CHANGED
@@ -1,22 +1,62 @@
1
1
  import { SpooshPlugin } from '@spoosh/core';
2
2
 
3
+ /**
4
+ * Context passed to the shouldRetry callback.
5
+ */
6
+ interface ShouldRetryContext {
7
+ /** HTTP status code from the response, if available */
8
+ status?: number;
9
+ /** The error that occurred, if any */
10
+ error: unknown;
11
+ /** Current attempt number (0-indexed) */
12
+ attempt: number;
13
+ /** Maximum number of retries configured */
14
+ maxRetries: number;
15
+ }
16
+ /**
17
+ * Callback to determine if a request should be retried.
18
+ * Network errors are always retried regardless of this callback's return value.
19
+ *
20
+ * @returns `true` to retry, `false` to stop retrying
21
+ */
22
+ type ShouldRetryCallback = (context: ShouldRetryContext) => boolean;
23
+ /** Status codes that are retried by default: 408, 429, 500, 502, 503, 504 */
24
+ declare const DEFAULT_RETRY_STATUS_CODES: readonly [408, 429, 500, 502, 503, 504];
3
25
  interface RetryPluginConfig {
4
26
  /** Number of retry attempts. Set to `false` to disable retries. Defaults to 3. */
5
27
  retries?: number | false;
6
28
  /** Delay between retries in milliseconds. Defaults to 1000. */
7
29
  retryDelay?: number;
30
+ /**
31
+ * Custom callback to determine if a request should be retried.
32
+ * Network errors are always retried regardless of this callback.
33
+ * Defaults to retrying on status codes: 408, 429, 500, 502, 503, 504.
34
+ */
35
+ shouldRetry?: ShouldRetryCallback;
8
36
  }
9
37
  interface RetryReadOptions {
10
38
  /** Number of retry attempts. Set to `false` to disable retries. Overrides plugin default. */
11
39
  retries?: number | false;
12
40
  /** Delay between retries in milliseconds. Overrides plugin default. */
13
41
  retryDelay?: number;
42
+ /**
43
+ * Custom callback to determine if a request should be retried.
44
+ * Network errors are always retried regardless of this callback.
45
+ * Overrides plugin default.
46
+ */
47
+ shouldRetry?: ShouldRetryCallback;
14
48
  }
15
49
  interface RetryWriteOptions {
16
50
  /** Number of retry attempts. Set to `false` to disable retries. Overrides plugin default. */
17
51
  retries?: number | false;
18
52
  /** Delay between retries in milliseconds. Overrides plugin default. */
19
53
  retryDelay?: number;
54
+ /**
55
+ * Custom callback to determine if a request should be retried.
56
+ * Network errors are always retried regardless of this callback.
57
+ * Overrides plugin default.
58
+ */
59
+ shouldRetry?: ShouldRetryCallback;
20
60
  }
21
61
  type RetryInfiniteReadOptions = RetryReadOptions;
22
62
  type RetryReadResult = object;
@@ -56,4 +96,4 @@ declare function retryPlugin(config?: RetryPluginConfig): SpooshPlugin<{
56
96
  writeResult: RetryWriteResult;
57
97
  }>;
58
98
 
59
- export { type RetryInfiniteReadOptions, type RetryPluginConfig, type RetryReadOptions, type RetryReadResult, type RetryWriteOptions, type RetryWriteResult, retryPlugin };
99
+ export { DEFAULT_RETRY_STATUS_CODES, type RetryInfiniteReadOptions, type RetryPluginConfig, type RetryReadOptions, type RetryReadResult, type RetryWriteOptions, type RetryWriteResult, type ShouldRetryCallback, type ShouldRetryContext, retryPlugin };
package/dist/index.js CHANGED
@@ -20,16 +20,35 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ DEFAULT_RETRY_STATUS_CODES: () => DEFAULT_RETRY_STATUS_CODES,
23
24
  retryPlugin: () => retryPlugin
24
25
  });
25
26
  module.exports = __toCommonJS(src_exports);
26
27
 
28
+ // src/types.ts
29
+ var DEFAULT_RETRY_STATUS_CODES = [
30
+ 408,
31
+ 429,
32
+ 500,
33
+ 502,
34
+ 503,
35
+ 504
36
+ ];
37
+
27
38
  // src/plugin.ts
28
39
  var import_core = require("@spoosh/core");
29
40
  var PLUGIN_NAME = "spoosh:retry";
30
41
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
42
+ var defaultShouldRetry = ({ status }) => {
43
+ if (status === void 0) return false;
44
+ return DEFAULT_RETRY_STATUS_CODES.includes(status);
45
+ };
31
46
  function retryPlugin(config = {}) {
32
- const { retries: defaultRetries = 3, retryDelay: defaultRetryDelay = 1e3 } = config;
47
+ const {
48
+ retries: defaultRetries = 3,
49
+ retryDelay: defaultRetryDelay = 1e3,
50
+ shouldRetry: defaultShouldRetryFn = defaultShouldRetry
51
+ } = config;
33
52
  return {
34
53
  name: PLUGIN_NAME,
35
54
  operations: ["read", "write", "infiniteRead"],
@@ -39,6 +58,7 @@ function retryPlugin(config = {}) {
39
58
  const pluginOptions = context.pluginOptions;
40
59
  const retriesConfig = pluginOptions?.retries ?? defaultRetries;
41
60
  const retryDelayConfig = pluginOptions?.retryDelay ?? defaultRetryDelay;
61
+ const shouldRetryFn = pluginOptions?.shouldRetry ?? defaultShouldRetryFn;
42
62
  const maxRetries = retriesConfig === false ? 0 : retriesConfig;
43
63
  if (!maxRetries || maxRetries < 0) {
44
64
  t?.skip("Disabled");
@@ -62,19 +82,38 @@ function retryPlugin(config = {}) {
62
82
  t?.log("Aborted", { color: "muted" });
63
83
  return res;
64
84
  }
65
- if ((0, import_core.isNetworkError)(res.error) && attempt < maxRetries) {
85
+ const isLastAttempt = attempt >= maxRetries;
86
+ if ((0, import_core.isNetworkError)(res.error)) {
87
+ if (isLastAttempt) {
88
+ t?.log("Max retries reached (network error)", { color: "error" });
89
+ return res;
90
+ }
66
91
  const delayMs = retryDelayConfig * Math.pow(2, attempt);
67
92
  await delay(delayMs);
68
93
  continue;
69
94
  }
70
- if (attempt > 0) {
71
- if ((0, import_core.isNetworkError)(res.error)) {
95
+ if (res.error) {
96
+ const shouldRetryResult = shouldRetryFn({
97
+ status: res.status,
98
+ error: res.error,
99
+ attempt,
100
+ maxRetries
101
+ });
102
+ if (shouldRetryResult && !isLastAttempt) {
103
+ t?.log(`Status ${res.status} - will retry`, { color: "warning" });
104
+ const delayMs = retryDelayConfig * Math.pow(2, attempt);
105
+ await delay(delayMs);
106
+ continue;
107
+ }
108
+ if (attempt > 0) {
72
109
  t?.log("Max retries reached or non-retryable error", {
73
110
  color: "error"
74
111
  });
75
- } else {
76
- t?.log("Retry succeeded", { color: "success" });
77
112
  }
113
+ return res;
114
+ }
115
+ if (attempt > 0) {
116
+ t?.log("Retry succeeded", { color: "success" });
78
117
  }
79
118
  return res;
80
119
  }
package/dist/index.mjs CHANGED
@@ -1,9 +1,27 @@
1
+ // src/types.ts
2
+ var DEFAULT_RETRY_STATUS_CODES = [
3
+ 408,
4
+ 429,
5
+ 500,
6
+ 502,
7
+ 503,
8
+ 504
9
+ ];
10
+
1
11
  // src/plugin.ts
2
12
  import { isNetworkError, isAbortError, clone } from "@spoosh/core";
3
13
  var PLUGIN_NAME = "spoosh:retry";
4
14
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
+ var defaultShouldRetry = ({ status }) => {
16
+ if (status === void 0) return false;
17
+ return DEFAULT_RETRY_STATUS_CODES.includes(status);
18
+ };
5
19
  function retryPlugin(config = {}) {
6
- const { retries: defaultRetries = 3, retryDelay: defaultRetryDelay = 1e3 } = config;
20
+ const {
21
+ retries: defaultRetries = 3,
22
+ retryDelay: defaultRetryDelay = 1e3,
23
+ shouldRetry: defaultShouldRetryFn = defaultShouldRetry
24
+ } = config;
7
25
  return {
8
26
  name: PLUGIN_NAME,
9
27
  operations: ["read", "write", "infiniteRead"],
@@ -13,6 +31,7 @@ function retryPlugin(config = {}) {
13
31
  const pluginOptions = context.pluginOptions;
14
32
  const retriesConfig = pluginOptions?.retries ?? defaultRetries;
15
33
  const retryDelayConfig = pluginOptions?.retryDelay ?? defaultRetryDelay;
34
+ const shouldRetryFn = pluginOptions?.shouldRetry ?? defaultShouldRetryFn;
16
35
  const maxRetries = retriesConfig === false ? 0 : retriesConfig;
17
36
  if (!maxRetries || maxRetries < 0) {
18
37
  t?.skip("Disabled");
@@ -36,19 +55,38 @@ function retryPlugin(config = {}) {
36
55
  t?.log("Aborted", { color: "muted" });
37
56
  return res;
38
57
  }
39
- if (isNetworkError(res.error) && attempt < maxRetries) {
58
+ const isLastAttempt = attempt >= maxRetries;
59
+ if (isNetworkError(res.error)) {
60
+ if (isLastAttempt) {
61
+ t?.log("Max retries reached (network error)", { color: "error" });
62
+ return res;
63
+ }
40
64
  const delayMs = retryDelayConfig * Math.pow(2, attempt);
41
65
  await delay(delayMs);
42
66
  continue;
43
67
  }
44
- if (attempt > 0) {
45
- if (isNetworkError(res.error)) {
68
+ if (res.error) {
69
+ const shouldRetryResult = shouldRetryFn({
70
+ status: res.status,
71
+ error: res.error,
72
+ attempt,
73
+ maxRetries
74
+ });
75
+ if (shouldRetryResult && !isLastAttempt) {
76
+ t?.log(`Status ${res.status} - will retry`, { color: "warning" });
77
+ const delayMs = retryDelayConfig * Math.pow(2, attempt);
78
+ await delay(delayMs);
79
+ continue;
80
+ }
81
+ if (attempt > 0) {
46
82
  t?.log("Max retries reached or non-retryable error", {
47
83
  color: "error"
48
84
  });
49
- } else {
50
- t?.log("Retry succeeded", { color: "success" });
51
85
  }
86
+ return res;
87
+ }
88
+ if (attempt > 0) {
89
+ t?.log("Retry succeeded", { color: "success" });
52
90
  }
53
91
  return res;
54
92
  }
@@ -57,5 +95,6 @@ function retryPlugin(config = {}) {
57
95
  };
58
96
  }
59
97
  export {
98
+ DEFAULT_RETRY_STATUS_CODES,
60
99
  retryPlugin
61
100
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/plugin-retry",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Automatic retry plugin for Spoosh with configurable attempts and delay",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,8 +36,8 @@
36
36
  "@spoosh/core": ">=0.13.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@spoosh/test-utils": "0.2.0",
40
- "@spoosh/core": "0.13.0"
39
+ "@spoosh/core": "0.13.0",
40
+ "@spoosh/test-utils": "0.2.0"
41
41
  },
42
42
  "scripts": {
43
43
  "dev": "tsup --watch",