@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.
Files changed (149) hide show
  1. package/DOJO.md +22 -0
  2. package/dojo.json +50 -0
  3. package/katas/001-hello-effect/SENSEI.md +72 -0
  4. package/katas/001-hello-effect/solution.test.ts +35 -0
  5. package/katas/001-hello-effect/solution.ts +16 -0
  6. package/katas/002-transform-with-map/SENSEI.md +72 -0
  7. package/katas/002-transform-with-map/solution.test.ts +33 -0
  8. package/katas/002-transform-with-map/solution.ts +16 -0
  9. package/katas/003-generator-pipelines/SENSEI.md +72 -0
  10. package/katas/003-generator-pipelines/solution.test.ts +40 -0
  11. package/katas/003-generator-pipelines/solution.ts +29 -0
  12. package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
  13. package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
  14. package/katas/004-flatmap-and-chaining/solution.ts +18 -0
  15. package/katas/005-pipe-composition/SENSEI.md +81 -0
  16. package/katas/005-pipe-composition/solution.test.ts +41 -0
  17. package/katas/005-pipe-composition/solution.ts +19 -0
  18. package/katas/006-handle-errors/SENSEI.md +86 -0
  19. package/katas/006-handle-errors/solution.test.ts +53 -0
  20. package/katas/006-handle-errors/solution.ts +30 -0
  21. package/katas/007-tagged-errors/SENSEI.md +79 -0
  22. package/katas/007-tagged-errors/solution.test.ts +82 -0
  23. package/katas/007-tagged-errors/solution.ts +37 -0
  24. package/katas/008-error-patterns/SENSEI.md +89 -0
  25. package/katas/008-error-patterns/solution.test.ts +41 -0
  26. package/katas/008-error-patterns/solution.ts +38 -0
  27. package/katas/009-option-type/SENSEI.md +96 -0
  28. package/katas/009-option-type/solution.test.ts +49 -0
  29. package/katas/009-option-type/solution.ts +26 -0
  30. package/katas/010-either-and-exit/SENSEI.md +86 -0
  31. package/katas/010-either-and-exit/solution.test.ts +33 -0
  32. package/katas/010-either-and-exit/solution.ts +17 -0
  33. package/katas/011-services-and-context/SENSEI.md +82 -0
  34. package/katas/011-services-and-context/solution.test.ts +23 -0
  35. package/katas/011-services-and-context/solution.ts +17 -0
  36. package/katas/012-layers/SENSEI.md +73 -0
  37. package/katas/012-layers/solution.test.ts +23 -0
  38. package/katas/012-layers/solution.ts +26 -0
  39. package/katas/013-testing-effects/SENSEI.md +88 -0
  40. package/katas/013-testing-effects/solution.test.ts +41 -0
  41. package/katas/013-testing-effects/solution.ts +20 -0
  42. package/katas/014-schema-basics/SENSEI.md +81 -0
  43. package/katas/014-schema-basics/solution.test.ts +35 -0
  44. package/katas/014-schema-basics/solution.ts +25 -0
  45. package/katas/015-domain-modeling/SENSEI.md +85 -0
  46. package/katas/015-domain-modeling/solution.test.ts +46 -0
  47. package/katas/015-domain-modeling/solution.ts +42 -0
  48. package/katas/016-retry-and-schedule/SENSEI.md +72 -0
  49. package/katas/016-retry-and-schedule/solution.test.ts +26 -0
  50. package/katas/016-retry-and-schedule/solution.ts +23 -0
  51. package/katas/017-parallel-effects/SENSEI.md +70 -0
  52. package/katas/017-parallel-effects/solution.test.ts +33 -0
  53. package/katas/017-parallel-effects/solution.ts +17 -0
  54. package/katas/018-race-and-timeout/SENSEI.md +75 -0
  55. package/katas/018-race-and-timeout/solution.test.ts +30 -0
  56. package/katas/018-race-and-timeout/solution.ts +27 -0
  57. package/katas/019-ref-and-state/SENSEI.md +72 -0
  58. package/katas/019-ref-and-state/solution.test.ts +29 -0
  59. package/katas/019-ref-and-state/solution.ts +16 -0
  60. package/katas/020-fibers/SENSEI.md +80 -0
  61. package/katas/020-fibers/solution.test.ts +23 -0
  62. package/katas/020-fibers/solution.ts +23 -0
  63. package/katas/021-acquire-release/SENSEI.md +57 -0
  64. package/katas/021-acquire-release/solution.test.ts +23 -0
  65. package/katas/021-acquire-release/solution.ts +22 -0
  66. package/katas/022-scoped-layers/SENSEI.md +52 -0
  67. package/katas/022-scoped-layers/solution.test.ts +35 -0
  68. package/katas/022-scoped-layers/solution.ts +19 -0
  69. package/katas/023-resource-patterns/SENSEI.md +52 -0
  70. package/katas/023-resource-patterns/solution.test.ts +20 -0
  71. package/katas/023-resource-patterns/solution.ts +13 -0
  72. package/katas/024-streams-basics/SENSEI.md +61 -0
  73. package/katas/024-streams-basics/solution.test.ts +30 -0
  74. package/katas/024-streams-basics/solution.ts +16 -0
  75. package/katas/025-stream-operations/SENSEI.md +59 -0
  76. package/katas/025-stream-operations/solution.test.ts +26 -0
  77. package/katas/025-stream-operations/solution.ts +17 -0
  78. package/katas/026-combining-streams/SENSEI.md +54 -0
  79. package/katas/026-combining-streams/solution.test.ts +20 -0
  80. package/katas/026-combining-streams/solution.ts +16 -0
  81. package/katas/027-data-pipelines/SENSEI.md +58 -0
  82. package/katas/027-data-pipelines/solution.test.ts +22 -0
  83. package/katas/027-data-pipelines/solution.ts +16 -0
  84. package/katas/028-logging-and-spans/SENSEI.md +58 -0
  85. package/katas/028-logging-and-spans/solution.test.ts +50 -0
  86. package/katas/028-logging-and-spans/solution.ts +20 -0
  87. package/katas/029-http-client/SENSEI.md +59 -0
  88. package/katas/029-http-client/solution.test.ts +49 -0
  89. package/katas/029-http-client/solution.ts +24 -0
  90. package/katas/030-capstone/SENSEI.md +63 -0
  91. package/katas/030-capstone/solution.test.ts +67 -0
  92. package/katas/030-capstone/solution.ts +55 -0
  93. package/katas/031-config-and-environment/SENSEI.md +77 -0
  94. package/katas/031-config-and-environment/solution.test.ts +38 -0
  95. package/katas/031-config-and-environment/solution.ts +11 -0
  96. package/katas/032-cause-and-defects/SENSEI.md +90 -0
  97. package/katas/032-cause-and-defects/solution.test.ts +50 -0
  98. package/katas/032-cause-and-defects/solution.ts +23 -0
  99. package/katas/033-pattern-matching/SENSEI.md +86 -0
  100. package/katas/033-pattern-matching/solution.test.ts +36 -0
  101. package/katas/033-pattern-matching/solution.ts +28 -0
  102. package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
  103. package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
  104. package/katas/034-deferred-and-coordination/solution.ts +24 -0
  105. package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
  106. package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
  107. package/katas/035-queue-and-backpressure/solution.ts +21 -0
  108. package/katas/036-schema-advanced/SENSEI.md +81 -0
  109. package/katas/036-schema-advanced/solution.test.ts +55 -0
  110. package/katas/036-schema-advanced/solution.ts +19 -0
  111. package/katas/037-cache-and-memoization/SENSEI.md +73 -0
  112. package/katas/037-cache-and-memoization/solution.test.ts +47 -0
  113. package/katas/037-cache-and-memoization/solution.ts +24 -0
  114. package/katas/038-metrics/SENSEI.md +91 -0
  115. package/katas/038-metrics/solution.test.ts +39 -0
  116. package/katas/038-metrics/solution.ts +23 -0
  117. package/katas/039-managed-runtime/SENSEI.md +75 -0
  118. package/katas/039-managed-runtime/solution.test.ts +29 -0
  119. package/katas/039-managed-runtime/solution.ts +19 -0
  120. package/katas/040-request-batching/SENSEI.md +87 -0
  121. package/katas/040-request-batching/solution.test.ts +56 -0
  122. package/katas/040-request-batching/solution.ts +32 -0
  123. package/package.json +22 -0
  124. package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
  125. package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
  126. package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
  127. package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
  128. package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
  129. package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
  130. package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
  131. package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
  132. package/skills/effect-patterns-error-management/SKILL.md +1668 -0
  133. package/skills/effect-patterns-getting-started/SKILL.md +237 -0
  134. package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
  135. package/skills/effect-patterns-observability/SKILL.md +1586 -0
  136. package/skills/effect-patterns-platform/SKILL.md +1195 -0
  137. package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
  138. package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
  139. package/skills/effect-patterns-resource-management/SKILL.md +827 -0
  140. package/skills/effect-patterns-scheduling/SKILL.md +451 -0
  141. package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
  142. package/skills/effect-patterns-streams/SKILL.md +2052 -0
  143. package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
  144. package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
  145. package/skills/effect-patterns-testing/SKILL.md +1632 -0
  146. package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
  147. package/skills/effect-patterns-value-handling/SKILL.md +676 -0
  148. package/tsconfig.json +20 -0
  149. package/vitest.config.ts +3 -0
@@ -0,0 +1,763 @@
1
+ ---
2
+ name: effect-patterns-scheduling-periodic-tasks
3
+ description: Effect-TS patterns for Scheduling Periodic Tasks. Use when working with scheduling periodic tasks in Effect-TS applications.
4
+ ---
5
+ # Effect-TS Patterns: Scheduling Periodic Tasks
6
+ This skill provides 3 curated Effect-TS patterns for scheduling periodic tasks.
7
+ Use this skill when working on tasks related to:
8
+ - scheduling periodic tasks
9
+ - Best practices in Effect-TS applications
10
+ - Real-world patterns and solutions
11
+
12
+ ---
13
+
14
+ ## 🟡 Intermediate Patterns
15
+
16
+ ### Scheduling Pattern 4: Debounce and Throttle Execution
17
+
18
+ **Rule:** Use debounce to wait for silence before executing, and throttle to limit execution frequency, both critical for handling rapid events.
19
+
20
+ **Good Example:**
21
+
22
+ This example demonstrates debouncing and throttling for common scenarios.
23
+
24
+ ```typescript
25
+ import { Effect, Schedule, Ref } from "effect";
26
+
27
+ interface SearchQuery {
28
+ readonly query: string;
29
+ readonly timestamp: Date;
30
+ }
31
+
32
+ // Simulate API search
33
+ const performSearch = (query: string): Effect.Effect<string[]> =>
34
+ Effect.gen(function* () {
35
+ yield* Effect.log(`[API] Searching for: "${query}"`);
36
+
37
+ yield* Effect.sleep("100 millis"); // Simulate API delay
38
+
39
+ return [
40
+ `Result 1 for ${query}`,
41
+ `Result 2 for ${query}`,
42
+ `Result 3 for ${query}`,
43
+ ];
44
+ });
45
+
46
+ // Main: demonstrate debounce and throttle
47
+ const program = Effect.gen(function* () {
48
+ console.log(`\n[DEBOUNCE/THROTTLE] Handling rapid events\n`);
49
+
50
+ // Example 1: Debounce search input
51
+ console.log(`[1] Debounced search (wait for silence):\n`);
52
+
53
+ const searchQueries = ["h", "he", "hel", "hell", "hello"];
54
+
55
+ const debouncedSearches = yield* Ref.make<Effect.Effect<string[]>[]>([]);
56
+
57
+ for (const query of searchQueries) {
58
+ yield* Effect.log(`[INPUT] User typed: "${query}"`);
59
+
60
+ // In real app, this would be debounced
61
+ yield* Effect.sleep("150 millis"); // User typing
62
+ }
63
+
64
+ // After user stops, execute search
65
+ yield* Effect.log(`[DEBOUNCE] User silent for 200ms, executing search`);
66
+
67
+ const searchResults = yield* performSearch("hello");
68
+
69
+ yield* Effect.log(`[RESULTS] ${searchResults.length} results found\n`);
70
+
71
+ // Example 2: Throttle scroll events
72
+ console.log(`[2] Throttled scroll handler (max 10/sec):\n`);
73
+
74
+ const scrollEventCount = yield* Ref.make(0);
75
+ const updateCount = yield* Ref.make(0);
76
+
77
+ // Simulate 100 rapid scroll events
78
+ for (let i = 0; i < 100; i++) {
79
+ yield* Ref.update(scrollEventCount, (c) => c + 1);
80
+
81
+ // In real app, scroll handler would be throttled
82
+ if (i % 10 === 0) {
83
+ // Simulate throttled update (max 10 per second)
84
+ yield* Ref.update(updateCount, (c) => c + 1);
85
+ }
86
+ }
87
+
88
+ const events = yield* Ref.get(scrollEventCount);
89
+ const updates = yield* Ref.get(updateCount);
90
+
91
+ yield* Effect.log(
92
+ `[THROTTLE] ${events} scroll events → ${updates} updates (${(updates / events * 100).toFixed(1)}% update rate)\n`
93
+ );
94
+
95
+ // Example 3: Deduplication
96
+ console.log(`[3] Deduplicating rapid events:\n`);
97
+
98
+ const userClicks = ["click", "click", "click", "dblclick", "click"];
99
+
100
+ const lastClick = yield* Ref.make<string | null>(null);
101
+ const clickCount = yield* Ref.make(0);
102
+
103
+ for (const click of userClicks) {
104
+ const prev = yield* Ref.get(lastClick);
105
+
106
+ if (click !== prev) {
107
+ yield* Effect.log(`[CLICK] Processing: ${click}`);
108
+ yield* Ref.update(clickCount, (c) => c + 1);
109
+ yield* Ref.set(lastClick, click);
110
+ } else {
111
+ yield* Effect.log(`[CLICK] Duplicate: ${click} (skipped)`);
112
+ }
113
+ }
114
+
115
+ const processed = yield* Ref.get(clickCount);
116
+
117
+ yield* Effect.log(
118
+ `\n[DEDUPE] ${userClicks.length} clicks → ${processed} processed\n`
119
+ );
120
+
121
+ // Example 4: Exponential backoff on repeated errors
122
+ console.log(`[4] Throttled retry on errors:\n`);
123
+
124
+ let retryCount = 0;
125
+
126
+ const operation = Effect.gen(function* () {
127
+ retryCount++;
128
+
129
+ if (retryCount < 3) {
130
+ yield* Effect.fail(new Error("Still failing"));
131
+ }
132
+
133
+ yield* Effect.log(`[SUCCESS] Succeeded on attempt ${retryCount}`);
134
+
135
+ return "done";
136
+ }).pipe(
137
+ Effect.retry(
138
+ Schedule.exponential("100 millis").pipe(
139
+ Schedule.upTo("1 second"),
140
+ Schedule.recurs(5)
141
+ )
142
+ )
143
+ );
144
+
145
+ yield* operation;
146
+ });
147
+
148
+ Effect.runPromise(program);
149
+ ```
150
+
151
+ ---
152
+
153
+ **Rationale:**
154
+
155
+ Debounce and throttle manage rapid events:
156
+
157
+ - **Debounce**: Wait for silence (delay after last event), then execute once
158
+ - **Throttle**: Execute at most once per interval
159
+ - **Deduplication**: Skip duplicate events
160
+ - **Rate limiting**: Limit events per second
161
+
162
+ Pattern: `Schedule.debounce(duration)` or `Schedule.throttle(maxEvents, duration)`
163
+
164
+ ---
165
+
166
+
167
+ Rapid events without debounce/throttle cause problems:
168
+
169
+ **Debounce example**: Search input
170
+ - User types "hello" character by character
171
+ - Without debounce: 5 API calls (one per character)
172
+ - With debounce: 1 API call after user stops typing
173
+
174
+ **Throttle example**: Scroll events
175
+ - Scroll fires 100+ times per second
176
+ - Without throttle: Updates lag, GC pressure
177
+ - With throttle: Update max 60 times per second
178
+
179
+ Real-world issues:
180
+ - **API overload**: Search queries hammer backend
181
+ - **Rendering lag**: Too many DOM updates
182
+ - **Resource exhaustion**: Event handlers never catch up
183
+
184
+ Debounce/throttle enable:
185
+ - **Efficiency**: Fewer operations
186
+ - **Responsiveness**: UI stays smooth
187
+ - **Resource safety**: Prevent exhaustion
188
+ - **Sanity**: Predictable execution
189
+
190
+ ---
191
+
192
+ ---
193
+
194
+ ### Scheduling Pattern 3: Schedule Tasks with Cron Expressions
195
+
196
+ **Rule:** Use cron expressions to schedule periodic tasks at specific calendar times, enabling flexible scheduling beyond simple fixed intervals.
197
+
198
+ **Good Example:**
199
+
200
+ This example demonstrates scheduling a daily report generation using cron, with timezone support.
201
+
202
+ ```typescript
203
+ import { Effect, Schedule, Console } from "effect";
204
+ import { DateTime } from "luxon"; // For timezone handling
205
+
206
+ interface ReportConfig {
207
+ readonly cronExpression: string;
208
+ readonly timezone?: string;
209
+ readonly jobName: string;
210
+ }
211
+
212
+ interface ScheduledReport {
213
+ readonly timestamp: Date;
214
+ readonly jobName: string;
215
+ readonly result: string;
216
+ }
217
+
218
+ // Simple cron parser (in production, use a library like cron-parser)
219
+ const parseCronExpression = (
220
+ expression: string
221
+ ): {
222
+ minute: number[];
223
+ hour: number[];
224
+ dayOfMonth: number[];
225
+ month: number[];
226
+ dayOfWeek: number[];
227
+ } => {
228
+ const parts = expression.split(" ");
229
+
230
+ const parseField = (field: string, max: number): number[] => {
231
+ if (field === "*") {
232
+ return Array.from({ length: max + 1 }, (_, i) => i);
233
+ }
234
+
235
+ if (field.includes(",")) {
236
+ return field.split(",").flatMap((part) => parseField(part, max));
237
+ }
238
+
239
+ if (field.includes("-")) {
240
+ const [start, end] = field.split("-").map(Number);
241
+ return Array.from({ length: end - start + 1 }, (_, i) => start + i);
242
+ }
243
+
244
+ return [Number(field)];
245
+ };
246
+
247
+ return {
248
+ minute: parseField(parts[0], 59),
249
+ hour: parseField(parts[1], 23),
250
+ dayOfMonth: parseField(parts[2], 31),
251
+ month: parseField(parts[3], 12),
252
+ dayOfWeek: parseField(parts[4], 6),
253
+ };
254
+ };
255
+
256
+ // Check if current time matches cron expression
257
+ const shouldRunNow = (parsed: ReturnType<typeof parseCronExpression>): boolean => {
258
+ const now = new Date();
259
+
260
+ return (
261
+ parsed.minute.includes(now.getUTCMinutes()) &&
262
+ parsed.hour.includes(now.getUTCHours()) &&
263
+ parsed.dayOfMonth.includes(now.getUTCDate()) &&
264
+ parsed.month.includes(now.getUTCMonth() + 1) &&
265
+ parsed.dayOfWeek.includes(now.getUTCDay())
266
+ );
267
+ };
268
+
269
+ // Generate a report
270
+ const generateReport = (jobName: string): Effect.Effect<ScheduledReport> =>
271
+ Effect.gen(function* () {
272
+ yield* Console.log(`[REPORT] Generating ${jobName}...`);
273
+
274
+ // Simulate report generation
275
+ yield* Effect.sleep("100 millis");
276
+
277
+ return {
278
+ timestamp: new Date(),
279
+ jobName,
280
+ result: `Report generated at ${new Date().toISOString()}`,
281
+ };
282
+ });
283
+
284
+ // Schedule with cron expression
285
+ const scheduleWithCron = (config: ReportConfig) =>
286
+ Effect.gen(function* () {
287
+ const parsed = parseCronExpression(config.cronExpression);
288
+
289
+ yield* Console.log(
290
+ `[SCHEDULER] Scheduling job: ${config.jobName}`
291
+ );
292
+ yield* Console.log(`[SCHEDULER] Cron: ${config.cronExpression}`);
293
+ yield* Console.log(`[SCHEDULER] Timezone: ${config.timezone || "UTC"}\n`);
294
+
295
+ // Create schedule that checks every minute
296
+ const schedule = Schedule.fixed("1 minute").pipe(
297
+ Schedule.untilInputEffect((report: ScheduledReport) =>
298
+ Effect.gen(function* () {
299
+ const isPastTime = shouldRunNow(parsed);
300
+
301
+ if (isPastTime) {
302
+ yield* Console.log(
303
+ `[SCHEDULED] ✓ Running at ${report.timestamp.toISOString()}`
304
+ );
305
+ return true; // Stop scheduling
306
+ }
307
+
308
+ return false; // Continue scheduling
309
+ })
310
+ )
311
+ );
312
+
313
+ // Generate report with cron schedule
314
+ yield* generateReport(config.jobName).pipe(
315
+ Effect.repeat(schedule)
316
+ );
317
+ });
318
+
319
+ // Demonstrate multiple cron schedules
320
+ const program = Effect.gen(function* () {
321
+ console.log(
322
+ `\n[START] Scheduling multiple jobs with cron expressions\n`
323
+ );
324
+
325
+ // Schedule examples (note: in real app, these would run at actual times)
326
+ const jobs = [
327
+ {
328
+ cronExpression: "0 9 * * 1-5", // 9 AM weekdays
329
+ jobName: "Daily Standup Report",
330
+ timezone: "America/New_York",
331
+ },
332
+ {
333
+ cronExpression: "0 0 * * *", // Midnight daily
334
+ jobName: "Nightly Backup",
335
+ timezone: "UTC",
336
+ },
337
+ {
338
+ cronExpression: "0 0 1 * *", // Midnight on 1st of month
339
+ jobName: "Monthly Summary",
340
+ timezone: "Europe/London",
341
+ },
342
+ ];
343
+
344
+ yield* Console.log("[JOBS] Scheduled:");
345
+ jobs.forEach((job) => {
346
+ console.log(
347
+ ` - ${job.jobName}: ${job.cronExpression} (${job.timezone})`
348
+ );
349
+ });
350
+ });
351
+
352
+ Effect.runPromise(program);
353
+ ```
354
+
355
+ ---
356
+
357
+ **Rationale:**
358
+
359
+ Use cron expressions for scheduling that aligns with business calendars:
360
+
361
+ - **Hourly backups**: `0 * * * *` (at :00 every hour)
362
+ - **Daily reports**: `0 9 * * 1-5` (9 AM weekdays)
363
+ - **Monthly cleanup**: `0 0 1 * *` (midnight on 1st of month)
364
+ - **Business hours**: `0 9-17 * * 1-5` (9 AM-5 PM, Mon-Fri)
365
+
366
+ Format: `minute hour day month weekday`
367
+
368
+ ---
369
+
370
+
371
+ Fixed intervals don't align with business needs:
372
+
373
+ **Fixed interval** (every 24 hours):
374
+ - If task takes 2 hours, next run is 26 hours later
375
+ - Drifts over time
376
+ - No alignment with calendar
377
+ - Fails during daylight saving time changes
378
+
379
+ **Cron expressions**:
380
+ - Specific calendar times (e.g., always 9 AM)
381
+ - Independent of execution duration
382
+ - Aligns with business hours
383
+ - Natural DST handling (clock adjusts, cron resyncs)
384
+ - Human-readable vs. milliseconds
385
+
386
+ Real-world example: Daily report at 9 AM
387
+ - **Fixed interval**: Scheduled at 9:00, takes 1 hour → next at 10:00 → drift until 5 PM
388
+ - **Cron `0 9 * * *`**: Always runs at 9:00 regardless of duration or previous delays
389
+
390
+ ---
391
+
392
+ ---
393
+
394
+
395
+ ## 🟠 Advanced Patterns
396
+
397
+ ### Scheduling Pattern 5: Advanced Retry Chains and Circuit Breakers
398
+
399
+ **Rule:** Use retry chains with circuit breakers to handle complex failure scenarios, detect cascade failures early, and prevent resource exhaustion.
400
+
401
+ **Good Example:**
402
+
403
+ This example demonstrates circuit breaker and fallback chain patterns.
404
+
405
+ ```typescript
406
+ import { Effect, Schedule, Ref, Data } from "effect";
407
+
408
+ // Error classification
409
+ class RetryableError extends Data.TaggedError("RetryableError")<{
410
+ message: string;
411
+ code: string;
412
+ }> {}
413
+
414
+ class NonRetryableError extends Data.TaggedError("NonRetryableError")<{
415
+ message: string;
416
+ code: string;
417
+ }> {}
418
+
419
+ class CircuitBreakerOpenError extends Data.TaggedError("CircuitBreakerOpenError")<{
420
+ message: string;
421
+ }> {}
422
+
423
+ // Circuit breaker state
424
+ interface CircuitBreakerState {
425
+ status: "closed" | "open" | "half-open";
426
+ failureCount: number;
427
+ lastFailureTime: Date | null;
428
+ successCount: number;
429
+ }
430
+
431
+ // Create circuit breaker
432
+ const createCircuitBreaker = (config: {
433
+ failureThreshold: number;
434
+ resetTimeoutMs: number;
435
+ halfOpenRequests: number;
436
+ }) =>
437
+ Effect.gen(function* () {
438
+ const state = yield* Ref.make<CircuitBreakerState>({
439
+ status: "closed",
440
+ failureCount: 0,
441
+ lastFailureTime: null,
442
+ successCount: 0,
443
+ });
444
+
445
+ const recordSuccess = Effect.gen(function* () {
446
+ yield* Ref.modify(state, (s) => {
447
+ if (s.status === "half-open") {
448
+ return [
449
+ undefined,
450
+ {
451
+ ...s,
452
+ successCount: s.successCount + 1,
453
+ status: s.successCount + 1 >= config.halfOpenRequests
454
+ ? "closed"
455
+ : "half-open",
456
+ failureCount: 0,
457
+ },
458
+ ];
459
+ }
460
+ return [undefined, s];
461
+ });
462
+ });
463
+
464
+ const recordFailure = Effect.gen(function* () {
465
+ yield* Ref.modify(state, (s) => {
466
+ const newFailureCount = s.failureCount + 1;
467
+ const newStatus = newFailureCount >= config.failureThreshold
468
+ ? "open"
469
+ : s.status;
470
+
471
+ return [
472
+ undefined,
473
+ {
474
+ ...s,
475
+ failureCount: newFailureCount,
476
+ lastFailureTime: new Date(),
477
+ status: newStatus,
478
+ },
479
+ ];
480
+ });
481
+ });
482
+
483
+ const canExecute = Effect.gen(function* () {
484
+ const current = yield* Ref.get(state);
485
+
486
+ if (current.status === "closed") {
487
+ return true;
488
+ }
489
+
490
+ if (current.status === "open") {
491
+ const timeSinceFailure = Date.now() - (current.lastFailureTime?.getTime() ?? 0);
492
+
493
+ if (timeSinceFailure > config.resetTimeoutMs) {
494
+ yield* Ref.modify(state, (s) => [
495
+ undefined,
496
+ {
497
+ ...s,
498
+ status: "half-open",
499
+ failureCount: 0,
500
+ successCount: 0,
501
+ },
502
+ ]);
503
+ return true;
504
+ }
505
+
506
+ return false;
507
+ }
508
+
509
+ // half-open: allow limited requests
510
+ return true;
511
+ });
512
+
513
+ return { recordSuccess, recordFailure, canExecute, state };
514
+ });
515
+
516
+ // Main example
517
+ const program = Effect.gen(function* () {
518
+ console.log(`\n[ADVANCED RETRY] Circuit breaker and fallback chains\n`);
519
+
520
+ // Create circuit breaker
521
+ const cb = yield* createCircuitBreaker({
522
+ failureThreshold: 3,
523
+ resetTimeoutMs: 1000,
524
+ halfOpenRequests: 2,
525
+ });
526
+
527
+ // Example 1: Circuit breaker in action
528
+ console.log(`[1] Circuit breaker state transitions:\n`);
529
+
530
+ let requestCount = 0;
531
+
532
+ const callWithCircuitBreaker = (shouldFail: boolean) =>
533
+ Effect.gen(function* () {
534
+ const canExecute = yield* cb.canExecute;
535
+
536
+ if (!canExecute) {
537
+ yield* Effect.fail(
538
+ new CircuitBreakerOpenError({
539
+ message: "Circuit breaker is open",
540
+ })
541
+ );
542
+ }
543
+
544
+ requestCount++;
545
+
546
+ if (shouldFail) {
547
+ yield* cb.recordFailure;
548
+ yield* Effect.log(
549
+ `[REQUEST ${requestCount}] FAILED (Circuit: ${(yield* Ref.get(cb.state)).status})`
550
+ );
551
+ yield* Effect.fail(
552
+ new RetryableError({
553
+ message: "Service error",
554
+ code: "500",
555
+ })
556
+ );
557
+ } else {
558
+ yield* cb.recordSuccess;
559
+ yield* Effect.log(
560
+ `[REQUEST ${requestCount}] SUCCESS (Circuit: ${(yield* Ref.get(cb.state)).status})`
561
+ );
562
+ return "success";
563
+ }
564
+ });
565
+
566
+ // Simulate failures then recovery
567
+ const failSequence = [true, true, true, false, false, false];
568
+
569
+ for (const shouldFail of failSequence) {
570
+ yield* callWithCircuitBreaker(shouldFail).pipe(
571
+ Effect.catchAll((error) =>
572
+ Effect.gen(function* () {
573
+ if (error._tag === "CircuitBreakerOpenError") {
574
+ yield* Effect.log(
575
+ `[REQUEST ${requestCount + 1}] REJECTED (Circuit open)`
576
+ );
577
+ } else {
578
+ yield* Effect.log(
579
+ `[REQUEST ${requestCount + 1}] ERROR caught`
580
+ );
581
+ }
582
+ })
583
+ )
584
+ );
585
+
586
+ // Add delay between requests
587
+ yield* Effect.sleep("100 millis");
588
+ }
589
+
590
+ // Example 2: Fallback chain
591
+ console.log(`\n[2] Fallback chain (primary → secondary → cache):\n`);
592
+
593
+ const endpoints = {
594
+ primary: "https://api.primary.com/data",
595
+ secondary: "https://api.secondary.com/data",
596
+ cache: "cached-data",
597
+ };
598
+
599
+ const callEndpoint = (name: string, shouldFail: boolean) =>
600
+ Effect.gen(function* () {
601
+ yield* Effect.log(`[CALL] Trying ${name}`);
602
+
603
+ if (shouldFail) {
604
+ yield* Effect.sleep("50 millis");
605
+ yield* Effect.fail(
606
+ new RetryableError({
607
+ message: `${name} failed`,
608
+ code: "500",
609
+ })
610
+ );
611
+ }
612
+
613
+ yield* Effect.sleep("50 millis");
614
+ return `data-from-${name}`;
615
+ });
616
+
617
+ const fallbackChain = callEndpoint("primary", true).pipe(
618
+ Effect.orElse(() => callEndpoint("secondary", false)),
619
+ Effect.orElse(() => {
620
+ yield* Effect.log(`[FALLBACK] Using cached data`);
621
+ return Effect.succeed(endpoints.cache);
622
+ })
623
+ );
624
+
625
+ const result = yield* fallbackChain;
626
+
627
+ yield* Effect.log(`[RESULT] Got: ${result}\n`);
628
+
629
+ // Example 3: Error-specific retry strategy
630
+ console.log(`[3] Error classification and adaptive retry:\n`);
631
+
632
+ const classifyError = (code: string) => {
633
+ if (["502", "503", "504"].includes(code)) {
634
+ return "retryable-service-error";
635
+ }
636
+ if (["408", "429"].includes(code)) {
637
+ return "retryable-rate-limit";
638
+ }
639
+ if (["404", "401", "403"].includes(code)) {
640
+ return "non-retryable";
641
+ }
642
+ if (code === "timeout") {
643
+ return "retryable-network";
644
+ }
645
+ return "unknown";
646
+ };
647
+
648
+ const errorCodes = ["500", "404", "429", "503", "timeout"];
649
+
650
+ for (const code of errorCodes) {
651
+ const classification = classifyError(code);
652
+ const shouldRetry = !classification.startsWith("non-retryable");
653
+
654
+ yield* Effect.log(
655
+ `[ERROR ${code}] → ${classification} (Retry: ${shouldRetry})`
656
+ );
657
+ }
658
+
659
+ // Example 4: Bulkhead pattern
660
+ console.log(`\n[4] Bulkhead isolation (limit concurrency per endpoint):\n`);
661
+
662
+ const bulkheads = {
663
+ "primary-api": { maxConcurrent: 5, currentCount: 0 },
664
+ "secondary-api": { maxConcurrent: 3, currentCount: 0 },
665
+ };
666
+
667
+ const acquirePermit = (endpoint: string) =>
668
+ Effect.gen(function* () {
669
+ const bulkhead = bulkheads[endpoint as keyof typeof bulkheads];
670
+
671
+ if (!bulkhead) {
672
+ return false;
673
+ }
674
+
675
+ if (bulkhead.currentCount < bulkhead.maxConcurrent) {
676
+ bulkhead.currentCount++;
677
+ return true;
678
+ }
679
+
680
+ yield* Effect.log(
681
+ `[BULKHEAD] ${endpoint} at capacity (${bulkhead.currentCount}/${bulkhead.maxConcurrent})`
682
+ );
683
+
684
+ return false;
685
+ });
686
+
687
+ // Simulate requests
688
+ for (let i = 0; i < 10; i++) {
689
+ const endpoint = i < 6 ? "primary-api" : "secondary-api";
690
+ const acquired = yield* acquirePermit(endpoint);
691
+
692
+ if (acquired) {
693
+ yield* Effect.log(
694
+ `[REQUEST] Acquired permit for ${endpoint}`
695
+ );
696
+ }
697
+ }
698
+ });
699
+
700
+ Effect.runPromise(program);
701
+ ```
702
+
703
+ ---
704
+
705
+ **Rationale:**
706
+
707
+ Advanced retry strategies handle multiple failure types:
708
+
709
+ - **Circuit breaker**: Stop retrying when error rate is high
710
+ - **Bulkhead**: Limit concurrency per operation
711
+ - **Fallback chain**: Try multiple approaches in order
712
+ - **Adaptive retry**: Adjust strategy based on failure pattern
713
+ - **Health checks**: Verify recovery before resuming
714
+
715
+ Pattern: Combine `Schedule.retry`, `Ref` state, and error classification
716
+
717
+ ---
718
+
719
+
720
+ Simple retry fails in production:
721
+
722
+ **Scenario 1: Cascade Failure**
723
+ - Service A calls Service B (down)
724
+ - Retries pile up, consuming resources
725
+ - A gets overloaded trying to recover B
726
+ - System collapses
727
+
728
+ **Scenario 2: Mixed Failures**
729
+ - 404 (not found) - retrying won't help
730
+ - 500 (server error) - retrying might help
731
+ - Network timeout - retrying might help
732
+ - Same retry strategy for all = inefficient
733
+
734
+ **Scenario 3: Thundering Herd**
735
+ - 10,000 clients all retrying at once
736
+ - Server recovers, gets hammered again
737
+ - Needs coordinated backoff + jitter
738
+
739
+ Solutions:
740
+
741
+ **Circuit breaker**:
742
+ - Monitor error rate
743
+ - Stop requests when high
744
+ - Resume gradually
745
+ - Prevent cascade failures
746
+
747
+ **Fallback chain**:
748
+ - Try primary endpoint
749
+ - Try secondary endpoint
750
+ - Use cache
751
+ - Return degraded result
752
+
753
+ **Adaptive retry**:
754
+ - Classify error type
755
+ - Use appropriate strategy
756
+ - Skip unretryable errors
757
+ - Adjust backoff dynamically
758
+
759
+ ---
760
+
761
+ ---
762
+
763
+