@dojocho/effect-ts 0.0.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/DOJO.md +22 -0
- package/dojo.json +50 -0
- package/katas/001-hello-effect/SENSEI.md +72 -0
- package/katas/001-hello-effect/solution.test.ts +35 -0
- package/katas/001-hello-effect/solution.ts +16 -0
- package/katas/002-transform-with-map/SENSEI.md +72 -0
- package/katas/002-transform-with-map/solution.test.ts +33 -0
- package/katas/002-transform-with-map/solution.ts +16 -0
- package/katas/003-generator-pipelines/SENSEI.md +72 -0
- package/katas/003-generator-pipelines/solution.test.ts +40 -0
- package/katas/003-generator-pipelines/solution.ts +29 -0
- package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
- package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
- package/katas/004-flatmap-and-chaining/solution.ts +18 -0
- package/katas/005-pipe-composition/SENSEI.md +81 -0
- package/katas/005-pipe-composition/solution.test.ts +41 -0
- package/katas/005-pipe-composition/solution.ts +19 -0
- package/katas/006-handle-errors/SENSEI.md +86 -0
- package/katas/006-handle-errors/solution.test.ts +53 -0
- package/katas/006-handle-errors/solution.ts +30 -0
- package/katas/007-tagged-errors/SENSEI.md +79 -0
- package/katas/007-tagged-errors/solution.test.ts +82 -0
- package/katas/007-tagged-errors/solution.ts +37 -0
- package/katas/008-error-patterns/SENSEI.md +89 -0
- package/katas/008-error-patterns/solution.test.ts +41 -0
- package/katas/008-error-patterns/solution.ts +38 -0
- package/katas/009-option-type/SENSEI.md +96 -0
- package/katas/009-option-type/solution.test.ts +49 -0
- package/katas/009-option-type/solution.ts +26 -0
- package/katas/010-either-and-exit/SENSEI.md +86 -0
- package/katas/010-either-and-exit/solution.test.ts +33 -0
- package/katas/010-either-and-exit/solution.ts +17 -0
- package/katas/011-services-and-context/SENSEI.md +82 -0
- package/katas/011-services-and-context/solution.test.ts +23 -0
- package/katas/011-services-and-context/solution.ts +17 -0
- package/katas/012-layers/SENSEI.md +73 -0
- package/katas/012-layers/solution.test.ts +23 -0
- package/katas/012-layers/solution.ts +26 -0
- package/katas/013-testing-effects/SENSEI.md +88 -0
- package/katas/013-testing-effects/solution.test.ts +41 -0
- package/katas/013-testing-effects/solution.ts +20 -0
- package/katas/014-schema-basics/SENSEI.md +81 -0
- package/katas/014-schema-basics/solution.test.ts +35 -0
- package/katas/014-schema-basics/solution.ts +25 -0
- package/katas/015-domain-modeling/SENSEI.md +85 -0
- package/katas/015-domain-modeling/solution.test.ts +46 -0
- package/katas/015-domain-modeling/solution.ts +42 -0
- package/katas/016-retry-and-schedule/SENSEI.md +72 -0
- package/katas/016-retry-and-schedule/solution.test.ts +26 -0
- package/katas/016-retry-and-schedule/solution.ts +23 -0
- package/katas/017-parallel-effects/SENSEI.md +70 -0
- package/katas/017-parallel-effects/solution.test.ts +33 -0
- package/katas/017-parallel-effects/solution.ts +17 -0
- package/katas/018-race-and-timeout/SENSEI.md +75 -0
- package/katas/018-race-and-timeout/solution.test.ts +30 -0
- package/katas/018-race-and-timeout/solution.ts +27 -0
- package/katas/019-ref-and-state/SENSEI.md +72 -0
- package/katas/019-ref-and-state/solution.test.ts +29 -0
- package/katas/019-ref-and-state/solution.ts +16 -0
- package/katas/020-fibers/SENSEI.md +80 -0
- package/katas/020-fibers/solution.test.ts +23 -0
- package/katas/020-fibers/solution.ts +23 -0
- package/katas/021-acquire-release/SENSEI.md +57 -0
- package/katas/021-acquire-release/solution.test.ts +23 -0
- package/katas/021-acquire-release/solution.ts +22 -0
- package/katas/022-scoped-layers/SENSEI.md +52 -0
- package/katas/022-scoped-layers/solution.test.ts +35 -0
- package/katas/022-scoped-layers/solution.ts +19 -0
- package/katas/023-resource-patterns/SENSEI.md +52 -0
- package/katas/023-resource-patterns/solution.test.ts +20 -0
- package/katas/023-resource-patterns/solution.ts +13 -0
- package/katas/024-streams-basics/SENSEI.md +61 -0
- package/katas/024-streams-basics/solution.test.ts +30 -0
- package/katas/024-streams-basics/solution.ts +16 -0
- package/katas/025-stream-operations/SENSEI.md +59 -0
- package/katas/025-stream-operations/solution.test.ts +26 -0
- package/katas/025-stream-operations/solution.ts +17 -0
- package/katas/026-combining-streams/SENSEI.md +54 -0
- package/katas/026-combining-streams/solution.test.ts +20 -0
- package/katas/026-combining-streams/solution.ts +16 -0
- package/katas/027-data-pipelines/SENSEI.md +58 -0
- package/katas/027-data-pipelines/solution.test.ts +22 -0
- package/katas/027-data-pipelines/solution.ts +16 -0
- package/katas/028-logging-and-spans/SENSEI.md +58 -0
- package/katas/028-logging-and-spans/solution.test.ts +50 -0
- package/katas/028-logging-and-spans/solution.ts +20 -0
- package/katas/029-http-client/SENSEI.md +59 -0
- package/katas/029-http-client/solution.test.ts +49 -0
- package/katas/029-http-client/solution.ts +24 -0
- package/katas/030-capstone/SENSEI.md +63 -0
- package/katas/030-capstone/solution.test.ts +67 -0
- package/katas/030-capstone/solution.ts +55 -0
- package/katas/031-config-and-environment/SENSEI.md +77 -0
- package/katas/031-config-and-environment/solution.test.ts +38 -0
- package/katas/031-config-and-environment/solution.ts +11 -0
- package/katas/032-cause-and-defects/SENSEI.md +90 -0
- package/katas/032-cause-and-defects/solution.test.ts +50 -0
- package/katas/032-cause-and-defects/solution.ts +23 -0
- package/katas/033-pattern-matching/SENSEI.md +86 -0
- package/katas/033-pattern-matching/solution.test.ts +36 -0
- package/katas/033-pattern-matching/solution.ts +28 -0
- package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
- package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
- package/katas/034-deferred-and-coordination/solution.ts +24 -0
- package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
- package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
- package/katas/035-queue-and-backpressure/solution.ts +21 -0
- package/katas/036-schema-advanced/SENSEI.md +81 -0
- package/katas/036-schema-advanced/solution.test.ts +55 -0
- package/katas/036-schema-advanced/solution.ts +19 -0
- package/katas/037-cache-and-memoization/SENSEI.md +73 -0
- package/katas/037-cache-and-memoization/solution.test.ts +47 -0
- package/katas/037-cache-and-memoization/solution.ts +24 -0
- package/katas/038-metrics/SENSEI.md +91 -0
- package/katas/038-metrics/solution.test.ts +39 -0
- package/katas/038-metrics/solution.ts +23 -0
- package/katas/039-managed-runtime/SENSEI.md +75 -0
- package/katas/039-managed-runtime/solution.test.ts +29 -0
- package/katas/039-managed-runtime/solution.ts +19 -0
- package/katas/040-request-batching/SENSEI.md +87 -0
- package/katas/040-request-batching/solution.test.ts +56 -0
- package/katas/040-request-batching/solution.ts +32 -0
- package/package.json +22 -0
- package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
- package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
- package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
- package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
- package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
- package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
- package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
- package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
- package/skills/effect-patterns-error-management/SKILL.md +1668 -0
- package/skills/effect-patterns-getting-started/SKILL.md +237 -0
- package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
- package/skills/effect-patterns-observability/SKILL.md +1586 -0
- package/skills/effect-patterns-platform/SKILL.md +1195 -0
- package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
- package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
- package/skills/effect-patterns-resource-management/SKILL.md +827 -0
- package/skills/effect-patterns-scheduling/SKILL.md +451 -0
- package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
- package/skills/effect-patterns-streams/SKILL.md +2052 -0
- package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
- package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
- package/skills/effect-patterns-testing/SKILL.md +1632 -0
- package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
- package/skills/effect-patterns-value-handling/SKILL.md +676 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-patterns-scheduling
|
|
3
|
+
description: Effect-TS patterns for Scheduling. Use when working with scheduling in Effect-TS applications.
|
|
4
|
+
---
|
|
5
|
+
# Effect-TS Patterns: Scheduling
|
|
6
|
+
This skill provides 3 curated Effect-TS patterns for scheduling.
|
|
7
|
+
Use this skill when working on tasks related to:
|
|
8
|
+
- scheduling
|
|
9
|
+
- Best practices in Effect-TS applications
|
|
10
|
+
- Real-world patterns and solutions
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🟢 Beginner Patterns
|
|
15
|
+
|
|
16
|
+
### Retry Failed Operations
|
|
17
|
+
|
|
18
|
+
**Rule:** Use Effect.retry with a Schedule to handle transient failures gracefully.
|
|
19
|
+
|
|
20
|
+
**Good Example:**
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Effect, Schedule, Data } from "effect"
|
|
24
|
+
|
|
25
|
+
// ============================================
|
|
26
|
+
// 1. Define error types
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
class NetworkError extends Data.TaggedError("NetworkError")<{
|
|
30
|
+
readonly message: string
|
|
31
|
+
}> {}
|
|
32
|
+
|
|
33
|
+
class RateLimitError extends Data.TaggedError("RateLimitError")<{
|
|
34
|
+
readonly retryAfter: number
|
|
35
|
+
}> {}
|
|
36
|
+
|
|
37
|
+
class NotFoundError extends Data.TaggedError("NotFoundError")<{
|
|
38
|
+
readonly resource: string
|
|
39
|
+
}> {}
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// 2. Simulate a flaky API call
|
|
43
|
+
// ============================================
|
|
44
|
+
|
|
45
|
+
let callCount = 0
|
|
46
|
+
const fetchData = Effect.gen(function* () {
|
|
47
|
+
callCount++
|
|
48
|
+
yield* Effect.log(`API call attempt ${callCount}`)
|
|
49
|
+
|
|
50
|
+
// Simulate intermittent failures
|
|
51
|
+
if (callCount < 3) {
|
|
52
|
+
return yield* Effect.fail(new NetworkError({ message: "Connection timeout" }))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { data: "Success!", attempts: callCount }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// 3. Basic retry - fixed attempts
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
const withBasicRetry = fetchData.pipe(
|
|
63
|
+
Effect.retry(Schedule.recurs(5)) // Retry up to 5 times
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// ============================================
|
|
67
|
+
// 4. Retry with delay
|
|
68
|
+
// ============================================
|
|
69
|
+
|
|
70
|
+
const withDelayedRetry = fetchData.pipe(
|
|
71
|
+
Effect.retry(
|
|
72
|
+
Schedule.spaced("500 millis").pipe(
|
|
73
|
+
Schedule.intersect(Schedule.recurs(5))
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// 5. Retry only specific errors
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
const fetchWithErrors = (shouldFail: boolean) =>
|
|
83
|
+
Effect.gen(function* () {
|
|
84
|
+
if (shouldFail) {
|
|
85
|
+
// Randomly fail with different errors
|
|
86
|
+
const random = Math.random()
|
|
87
|
+
if (random < 0.5) {
|
|
88
|
+
return yield* Effect.fail(new NetworkError({ message: "Timeout" }))
|
|
89
|
+
} else if (random < 0.8) {
|
|
90
|
+
return yield* Effect.fail(new RateLimitError({ retryAfter: 1000 }))
|
|
91
|
+
} else {
|
|
92
|
+
return yield* Effect.fail(new NotFoundError({ resource: "user:123" }))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return "Data fetched!"
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Only retry network and rate limit errors, not NotFoundError
|
|
99
|
+
const retryTransientOnly = fetchWithErrors(true).pipe(
|
|
100
|
+
Effect.retry({
|
|
101
|
+
schedule: Schedule.recurs(3),
|
|
102
|
+
while: (error) =>
|
|
103
|
+
error._tag === "NetworkError" || error._tag === "RateLimitError",
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// ============================================
|
|
108
|
+
// 6. Retry with exponential backoff
|
|
109
|
+
// ============================================
|
|
110
|
+
|
|
111
|
+
const withExponentialBackoff = fetchData.pipe(
|
|
112
|
+
Effect.retry(
|
|
113
|
+
Schedule.exponential("100 millis", 2).pipe( // 100ms, 200ms, 400ms...
|
|
114
|
+
Schedule.intersect(Schedule.recurs(5)) // Max 5 retries
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// ============================================
|
|
120
|
+
// 7. Run and observe
|
|
121
|
+
// ============================================
|
|
122
|
+
|
|
123
|
+
const program = Effect.gen(function* () {
|
|
124
|
+
yield* Effect.log("Starting retry demo...")
|
|
125
|
+
|
|
126
|
+
// Reset counter
|
|
127
|
+
callCount = 0
|
|
128
|
+
|
|
129
|
+
const result = yield* withBasicRetry
|
|
130
|
+
yield* Effect.log(`Final result: ${JSON.stringify(result)}`)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
Effect.runPromise(program)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Rationale:**
|
|
137
|
+
|
|
138
|
+
Use `Effect.retry` to automatically retry operations that fail due to transient errors like network timeouts.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
Many failures are temporary:
|
|
144
|
+
|
|
145
|
+
1. **Network issues** - Connection drops, timeouts
|
|
146
|
+
2. **Rate limits** - Too many requests
|
|
147
|
+
3. **Resource contention** - Database locks
|
|
148
|
+
4. **Service restarts** - Brief unavailability
|
|
149
|
+
|
|
150
|
+
Automatic retries handle these without manual intervention.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### Your First Schedule
|
|
157
|
+
|
|
158
|
+
**Rule:** Use Schedule to control when and how often effects run.
|
|
159
|
+
|
|
160
|
+
**Good Example:**
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { Effect, Schedule } from "effect"
|
|
164
|
+
|
|
165
|
+
// ============================================
|
|
166
|
+
// 1. Retry a failing operation
|
|
167
|
+
// ============================================
|
|
168
|
+
|
|
169
|
+
let attempts = 0
|
|
170
|
+
const flakyOperation = Effect.gen(function* () {
|
|
171
|
+
attempts++
|
|
172
|
+
if (attempts < 3) {
|
|
173
|
+
yield* Effect.log(`Attempt ${attempts} failed`)
|
|
174
|
+
return yield* Effect.fail(new Error("Temporary failure"))
|
|
175
|
+
}
|
|
176
|
+
return `Success on attempt ${attempts}`
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Retry up to 5 times
|
|
180
|
+
const withRetry = flakyOperation.pipe(
|
|
181
|
+
Effect.retry(Schedule.recurs(5))
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
// ============================================
|
|
185
|
+
// 2. Repeat a successful operation
|
|
186
|
+
// ============================================
|
|
187
|
+
|
|
188
|
+
const logTime = Effect.gen(function* () {
|
|
189
|
+
const now = new Date().toISOString()
|
|
190
|
+
yield* Effect.log(`Current time: ${now}`)
|
|
191
|
+
return now
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Repeat 3 times
|
|
195
|
+
const repeated = logTime.pipe(
|
|
196
|
+
Effect.repeat(Schedule.recurs(3))
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
// ============================================
|
|
200
|
+
// 3. Add delays between operations
|
|
201
|
+
// ============================================
|
|
202
|
+
|
|
203
|
+
// Repeat every second, 5 times
|
|
204
|
+
const polling = logTime.pipe(
|
|
205
|
+
Effect.repeat(
|
|
206
|
+
Schedule.spaced("1 second").pipe(
|
|
207
|
+
Schedule.intersect(Schedule.recurs(5))
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// ============================================
|
|
213
|
+
// 4. Common schedule patterns
|
|
214
|
+
// ============================================
|
|
215
|
+
|
|
216
|
+
// Fixed delay between attempts
|
|
217
|
+
const fixedDelay = Schedule.spaced("500 millis")
|
|
218
|
+
|
|
219
|
+
// Increasing delay (1s, 2s, 4s, 8s...)
|
|
220
|
+
const exponentialBackoff = Schedule.exponential("1 second")
|
|
221
|
+
|
|
222
|
+
// Maximum number of attempts
|
|
223
|
+
const limitedAttempts = Schedule.recurs(3)
|
|
224
|
+
|
|
225
|
+
// Combine: exponential backoff, max 5 attempts
|
|
226
|
+
const retryPolicy = Schedule.exponential("100 millis").pipe(
|
|
227
|
+
Schedule.intersect(Schedule.recurs(5))
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
// ============================================
|
|
231
|
+
// 5. Run examples
|
|
232
|
+
// ============================================
|
|
233
|
+
|
|
234
|
+
const program = Effect.gen(function* () {
|
|
235
|
+
yield* Effect.log("--- Retry Example ---")
|
|
236
|
+
const result = yield* withRetry
|
|
237
|
+
yield* Effect.log(`Result: ${result}`)
|
|
238
|
+
|
|
239
|
+
yield* Effect.log("\n--- Repeat Example ---")
|
|
240
|
+
yield* repeated
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
Effect.runPromise(program)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Rationale:**
|
|
247
|
+
|
|
248
|
+
Use `Schedule` to control timing in Effect programs - retrying failed operations, repeating successful ones, or adding delays.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
Schedules solve common timing problems:
|
|
254
|
+
|
|
255
|
+
1. **Retries** - Try again after failures
|
|
256
|
+
2. **Polling** - Check for updates periodically
|
|
257
|
+
3. **Rate limiting** - Control how fast things run
|
|
258
|
+
4. **Backoff** - Increase delays between attempts
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
## 🟡 Intermediate Patterns
|
|
266
|
+
|
|
267
|
+
### Scheduling Pattern 1: Repeat an Effect on a Fixed Interval
|
|
268
|
+
|
|
269
|
+
**Rule:** Repeat effects at fixed intervals using Schedule.fixed for steady-state operations and background tasks.
|
|
270
|
+
|
|
271
|
+
**Good Example:**
|
|
272
|
+
|
|
273
|
+
This example demonstrates a health check service that polls multiple service endpoints every 30 seconds and reports their status.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { Effect, Schedule, Duration } from "effect";
|
|
277
|
+
|
|
278
|
+
interface ServiceStatus {
|
|
279
|
+
readonly service: string;
|
|
280
|
+
readonly url: string;
|
|
281
|
+
readonly isHealthy: boolean;
|
|
282
|
+
readonly responseTime: number;
|
|
283
|
+
readonly lastChecked: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Mock health check that calls an endpoint
|
|
287
|
+
const checkServiceHealth = (
|
|
288
|
+
url: string,
|
|
289
|
+
service: string
|
|
290
|
+
): Effect.Effect<ServiceStatus> =>
|
|
291
|
+
Effect.gen(function* () {
|
|
292
|
+
const startTime = Date.now();
|
|
293
|
+
|
|
294
|
+
// Simulate HTTP call with occasional failures
|
|
295
|
+
const isHealthy = Math.random() > 0.1; // 90% success rate
|
|
296
|
+
const responseTime = Math.random() * 500; // 0-500ms
|
|
297
|
+
|
|
298
|
+
yield* Effect.sleep(Duration.millis(Math.round(responseTime)));
|
|
299
|
+
|
|
300
|
+
if (!isHealthy) {
|
|
301
|
+
yield* Effect.fail(new Error(`${service} is unhealthy`));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
service,
|
|
306
|
+
url,
|
|
307
|
+
isHealthy: true,
|
|
308
|
+
responseTime: Math.round(Date.now() - startTime),
|
|
309
|
+
lastChecked: Date.now(),
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Health check for multiple services
|
|
314
|
+
interface HealthCheckConfig {
|
|
315
|
+
readonly services: Array<{
|
|
316
|
+
readonly name: string;
|
|
317
|
+
readonly url: string;
|
|
318
|
+
}>;
|
|
319
|
+
readonly intervalSeconds: number;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Keep track of service status
|
|
323
|
+
const serviceStatuses = new Map<string, ServiceStatus>();
|
|
324
|
+
|
|
325
|
+
// Check all services and report status
|
|
326
|
+
const checkAllServices = (
|
|
327
|
+
config: HealthCheckConfig
|
|
328
|
+
): Effect.Effect<void> =>
|
|
329
|
+
Effect.gen(function* () {
|
|
330
|
+
for (const service of config.services) {
|
|
331
|
+
const status = yield* checkServiceHealth(service.url, service.name).pipe(
|
|
332
|
+
Effect.either
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (status._tag === "Right") {
|
|
336
|
+
serviceStatuses.set(service.name, status.right);
|
|
337
|
+
console.log(
|
|
338
|
+
`✓ ${service.name}: OK (${status.right.responseTime}ms)`
|
|
339
|
+
);
|
|
340
|
+
} else {
|
|
341
|
+
console.log(`✗ ${service.name}: FAILED`);
|
|
342
|
+
// Keep last known status if available
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Create the repeating health check
|
|
348
|
+
const createHealthCheckScheduler = (
|
|
349
|
+
config: HealthCheckConfig
|
|
350
|
+
): Effect.Effect<void> =>
|
|
351
|
+
checkAllServices(config).pipe(
|
|
352
|
+
// Schedule with fixed interval (fixed = ignore execution time)
|
|
353
|
+
Effect.repeat(
|
|
354
|
+
Schedule.fixed(Duration.seconds(config.intervalSeconds))
|
|
355
|
+
)
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Report current status
|
|
359
|
+
const reportStatus = (): Effect.Effect<void> =>
|
|
360
|
+
Effect.sync(() => {
|
|
361
|
+
if (serviceStatuses.size === 0) {
|
|
362
|
+
console.log("\n[STATUS] No services checked yet");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log("\n[STATUS REPORT]");
|
|
367
|
+
for (const [service, status] of serviceStatuses) {
|
|
368
|
+
const ago = Math.round((Date.now() - status.lastChecked) / 1000);
|
|
369
|
+
console.log(
|
|
370
|
+
` ${service}: ${status.isHealthy ? "✓" : "✗"} (checked ${ago}s ago)`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Run health checker in background and check status periodically
|
|
376
|
+
const program = Effect.gen(function* () {
|
|
377
|
+
const config: HealthCheckConfig = {
|
|
378
|
+
services: [
|
|
379
|
+
{ name: "API", url: "https://api.example.com/health" },
|
|
380
|
+
{ name: "Database", url: "https://db.example.com/health" },
|
|
381
|
+
{ name: "Cache", url: "https://cache.example.com/health" },
|
|
382
|
+
],
|
|
383
|
+
intervalSeconds: 5, // Check every 5 seconds
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Fork the health checker to run in background
|
|
387
|
+
const checker = yield* createHealthCheckScheduler(config).pipe(
|
|
388
|
+
Effect.fork
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Check and report status every 15 seconds for 60 seconds
|
|
392
|
+
yield* reportStatus().pipe(
|
|
393
|
+
Effect.repeat(
|
|
394
|
+
Schedule.addDelay(
|
|
395
|
+
Schedule.recurs(3), // 3 repetitions = 4 total (initial + 3)
|
|
396
|
+
() => Duration.seconds(15)
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Interrupt the background checker
|
|
402
|
+
yield* checker.interrupt();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
Effect.runPromise(program);
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
This pattern:
|
|
409
|
+
|
|
410
|
+
1. **Defines service health checks** that may fail
|
|
411
|
+
2. **Uses Schedule.fixed** to repeat every 5 seconds
|
|
412
|
+
3. **Handles failures gracefully** (keeps last known status)
|
|
413
|
+
4. **Runs in background** while main logic continues
|
|
414
|
+
5. **Reports current status** at intervals
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
**Rationale:**
|
|
419
|
+
|
|
420
|
+
When you need to run an effect repeatedly at regular intervals (e.g., every 5 seconds, every 30 minutes), use `Schedule.fixed` to specify the interval. This creates a schedule that repeats the effect indefinitely or until a condition stops it, with precise timing between executions.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
Many production systems need periodic operations:
|
|
426
|
+
|
|
427
|
+
- **Health checks**: Poll service availability every 30 seconds
|
|
428
|
+
- **Cache refresh**: Update cache every 5 minutes
|
|
429
|
+
- **Metrics collection**: Gather system metrics every 10 seconds
|
|
430
|
+
- **Data sync**: Sync data with remote service periodically
|
|
431
|
+
- **Cleanup tasks**: Remove stale data nightly
|
|
432
|
+
|
|
433
|
+
Without proper scheduling:
|
|
434
|
+
|
|
435
|
+
- Manual polling with `while` loops wastes CPU (busy-waiting)
|
|
436
|
+
- Thread.sleep blocks threads, preventing other work
|
|
437
|
+
- No automatic restart on failure
|
|
438
|
+
- Difficult to test deterministically
|
|
439
|
+
|
|
440
|
+
With `Schedule.fixed`:
|
|
441
|
+
|
|
442
|
+
- Efficient, non-blocking repetition
|
|
443
|
+
- Automatic failure handling and retry
|
|
444
|
+
- Testable with TestClock
|
|
445
|
+
- Clean, declarative syntax
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
|