| 1 | /** |
| 2 | * @title Working with the Schedule module |
| 3 | * |
| 4 | * Build schedules, compose them, and use them with `Effect.retry` and `Effect.repeat`. |
| 5 | */ |
| 6 | import { Duration, Effect, Random, Schedule, Schema } from "effect" |
| 7 | |
| 8 | // Production pattern: capped exponential backoff with jitter and max attempts. |
| 9 | // Delays start at 250ms, grow exponentially with jitter, and are capped at 10s. |
| 10 | export const productionRetrySchedule = Schedule.min([ |
| 11 | Schedule.exponential("250 millis"), |
| 12 | // Cap the delay at 10 seconds to avoid excessively long waits. |
| 13 | Schedule.spaced("10 seconds") |
| 14 | ]).pipe( |
| 15 | Schedule.jittered, |
| 16 | Schedule.setInputType<HttpError>(), |
| 17 | Schedule.while(({ input }) => input.retryable) |
| 18 | ) |
| 19 | |
| 20 | export const fetchUserProfile = Effect.fn("fetchUserProfile")( |
| 21 | function*(userId: string) { |
| 22 | const random = yield* Random.next |
| 23 | const status = random > 0.7 |
| 24 | ? 200 |
| 25 | : random > 0.3 |
| 26 | ? 503 |
| 27 | : 401 |
| 28 | |
| 29 | if (status !== 200) { |
| 30 | return yield* new HttpError({ |
| 31 | message: `Request for ${userId} failed`, |
| 32 | status, |
| 33 | retryable: status >= 500 |
| 34 | }) |
| 35 | } |
| 36 | |
| 37 | return { |
| 38 | id: userId, |
| 39 | name: "Ada Lovelace" |
| 40 | } as const |
| 41 | } |
| 42 | ) |
| 43 | |
| 44 | // Use the schedule with `Effect.retry` to retry failures. |
| 45 | export const loadUserWithRetry = fetchUserProfile("user-123").pipe( |
| 46 | Effect.retry(productionRetrySchedule), |
| 47 | // If the effect still fails after exhausting the schedule, turn the error |
| 48 | // into a fatal one. |
| 49 | Effect.orDie |
| 50 | ) |
| 51 | |
| 52 | export const loadUserWithInferredInput = fetchUserProfile("user-123").pipe( |
| 53 | // You can also pass a schedule builder function that assists with inferring |
| 54 | // the input type. This is especially useful when the schedule needs to |
| 55 | // inspect the error to determine retryability. |
| 56 | Effect.retry(($) => |
| 57 | $(Schedule.spaced("1 seconds")).pipe( |
| 58 | Schedule.while(({ input }) => input.retryable) |
| 59 | ) |
| 60 | ), |
| 61 | Effect.orDie |
| 62 | ) |
| 63 | |
| 64 | export class HttpError extends Schema.TaggedError<HttpError>()("HttpError", { |
| 65 | message: Schema.String, |
| 66 | status: Schema.Int, |
| 67 | retryable: Schema.Boolean |
| 68 | }) {} |
| 69 | |
| 70 | // Start with a few schedule constructors. |
| 71 | export const maxRetries = Schedule.recurs(5) |
| 72 | export const spacedPolling = Schedule.spaced("30 seconds") |
| 73 | export const exponentialBackoff = Schedule.exponential("200 millis") |
| 74 | |
| 75 | // `Schedule.max` continues only while all schedules continue and outputs |
| 76 | // the slowest delay. It is useful for combining a delay pattern with a hard |
| 77 | // attempt cap. |
| 78 | export const retryBackoffWithLimit = Schedule.max([ |
| 79 | Schedule.exponential("250 millis"), |
| 80 | Schedule.recurs(6) |
| 81 | ]) |
| 82 | |
| 83 | // `Schedule.min` continues while any schedule continues and outputs |
| 84 | // the fastest delay. It is useful for fallback behavior. |
| 85 | // |
| 86 | // This schedule will start at 500ms, exponentially increase, and will be capped |
| 87 | // at 2 seconds. |
| 88 | export const keepTryingUpToTwoSeconds = Schedule.min([ |
| 89 | Schedule.exponential("500 millis"), |
| 90 | Schedule.spaced("2 seconds") |
| 91 | ]) |
| 92 | |
| 93 | // Use `Schedule.while` to continue only for retryable failures. |
| 94 | // This lets non-retryable errors fail fast, even if attempts remain. |
| 95 | export const retryableOnly = Schedule.exponential("200 millis").pipe( |
| 96 | // You can use `setInputType` to specify the type of input the schedule will |
| 97 | // receive. |
| 98 | Schedule.setInputType<HttpError>(), |
| 99 | Schedule.while(({ input }) => input.retryable) |
| 100 | ) |
| 101 | |
| 102 | // `tap` is useful for performing side effects like logging or metrics. |
| 103 | export const instrumentedRetrySchedule = retryableOnly.pipe( |
| 104 | Schedule.setInputType<HttpError>(), |
| 105 | Schedule.tap((meta) => |
| 106 | Effect.logDebug( |
| 107 | `Retrying after ${meta.input.status}: ${meta.input.message}. Next retry in ${Duration.toMillis(meta.duration)}ms` |
| 108 | ) |
| 109 | ) |
| 110 | ) |