denoland / std

The Deno Standard Library
https://jsr.io/@std
MIT License
3.22k stars 621 forks source link

feat(async/unstable): add `isRetriable` option to `retry` (#6196) #6197

Closed lionel-rowe closed 4 days ago

lionel-rowe commented 6 days ago

Closes #6196

lionel-rowe commented 6 days ago

CI failing on unrelated unstable_spinner test:

Spinner.color can set each color => ./cli/unstable_spinner_test.ts:142:6
error: AssertionError: Expected actual: "
⠋ 
" to contain: "
⠙ ".
  throw new AssertionError(msg);
        ^
    at assertStringIncludes (file:///D:/a/std/std/assert/string_includes.ts:29:9)
    at file:///D:/a/std/std/cli/unstable_spinner_test.ts:146:3

 FAILURES 

Spinner.color can set each color => ./cli/unstable_spinner_test.ts:142:6
kt3k commented 6 days ago

Because we'd like to accept every new feature as unstable feature (ref), can you copy the existing retry.ts/retry_test.ts to unstable_retry.ts/unstable_retry_test.ts and move these changes to there?

lionel-rowe commented 6 days ago

Because we'd like to accept every new feature as unstable feature (ref), can you copy the existing retry.ts/retry_test.ts to unstable_retry.ts/unstable_retry_test.ts and move these changes to there?

Done. Current output for git diff --no-index async/retry.ts async/unstable_retry.ts:

diff --git a/async/retry.ts b/async/unstable_retry.ts
index d2f0250c..0dc80c86 100644
--- a/async/retry.ts
+++ b/async/unstable_retry.ts
@@ -2,33 +2,13 @@
 // This module is browser compatible.

 import { exponentialBackoffWithJitter } from "./_util.ts";
+import { RetryError } from "./retry.ts";

 /**
- * Error thrown in {@linkcode retry} once the maximum number of failed attempts
- * has been reached.
+ * Options for {@linkcode retry}.
  *
- * @example Usage
- * ```ts no-assert ignore
- * import { RetryError } from "@std/async/retry";
- *
- * throw new RetryError({ foo: "bar" }, 3);
- * ```
+ * @experimental **UNSTABLE**: New API, yet to be vetted.
  */
-export class RetryError extends Error {
-  /**
-   * Constructs a new {@linkcode RetryError} instance.
-   *
-   * @param cause the cause for this error.
-   * @param attempts the number of retry attempts made.
-   */
-  constructor(cause: unknown, attempts: number) {
-    super(`Retrying exceeded the maxAttempts (${attempts}).`);
-    this.name = "RetryError";
-    this.cause = cause;
-  }
-}
-
-/** Options for {@linkcode retry}. */
 export interface RetryOptions {
   /**
    * How much to backoff after each retry.
@@ -61,6 +41,16 @@ export interface RetryOptions {
    * @default {1}
    */
   jitter?: number;
+
+  /**
+   * Callback to determine if an error or other thrown value is retriable.
+   *
+   * @default {() => true}
+   *
+   * @param err The thrown error or other value.
+   * @returns `true` if the error is retriable, `false` otherwise.
+   */
+  isRetriable?: (err: unknown) => boolean;
 }

 /**
@@ -68,6 +58,8 @@ export interface RetryOptions {
  * Retries as long as the given function throws. If the attempts are exhausted,
  * throws a {@linkcode RetryError} with `cause` set to the inner exception.
  *
+ * @experimental **UNSTABLE**: New API, yet to be vetted.
+ *
  * The backoff is calculated by multiplying `minTimeout` with `multiplier` to the power of the current attempt counter (starting at 0 up to `maxAttempts - 1`). It is capped at `maxTimeout` however.
  * How long the actual delay is, depends on `jitter`.
  *
@@ -127,6 +119,7 @@ export async function retry<T>(
     maxAttempts = 5,
     minTimeout = 1000,
     jitter = 1,
+    isRetriable = () => true,
   } = options ?? {};

   if (maxTimeout <= 0) {
@@ -150,6 +143,10 @@ export async function retry<T>(
     try {
       return await fn();
     } catch (error) {
+      if (!isRetriable(error)) {
+        throw error;
+      }
+
       if (attempt + 1 >= maxAttempts) {
         throw new RetryError(error, maxAttempts);
       }