@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,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
|
+
|