1/**
2 * @title Working with the Schedule module
3 *
4 * Build schedules, compose them, and use them with `Effect.retry` and `Effect.repeat`.
5 */
6import { 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.
10export 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
20export 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.
45export 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
52export 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
64export 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.
71export const maxRetries = Schedule.recurs(5)
72export const spacedPolling = Schedule.spaced("30 seconds")
73export 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.
78export 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.
88export 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.
95export 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.
103export 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)