@jagreehal/workflow 1.12.0 → 1.13.0
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/README.md +1197 -20
- package/dist/duration.cjs +2 -0
- package/dist/duration.cjs.map +1 -0
- package/dist/duration.d.cts +246 -0
- package/dist/duration.d.ts +246 -0
- package/dist/duration.js +2 -0
- package/dist/duration.js.map +1 -0
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/match.cjs +2 -0
- package/dist/match.cjs.map +1 -0
- package/dist/match.d.cts +216 -0
- package/dist/match.d.ts +216 -0
- package/dist/match.js +2 -0
- package/dist/match.js.map +1 -0
- package/dist/schedule.cjs +2 -0
- package/dist/schedule.cjs.map +1 -0
- package/dist/schedule.d.cts +387 -0
- package/dist/schedule.d.ts +387 -0
- package/dist/schedule.js +2 -0
- package/dist/schedule.js.map +1 -0
- package/docs/api.md +30 -0
- package/docs/coming-from-neverthrow.md +103 -10
- package/docs/effect-features-to-port.md +210 -0
- package/docs/match-examples.test.ts +558 -0
- package/docs/match.md +417 -0
- package/docs/policies-examples.test.ts +750 -0
- package/docs/policies.md +508 -0
- package/docs/resource-management-examples.test.ts +729 -0
- package/docs/resource-management.md +509 -0
- package/docs/schedule-examples.test.ts +736 -0
- package/docs/schedule.md +467 -0
- package/docs/tagged-error-examples.test.ts +494 -0
- package/docs/tagged-error.md +730 -0
- package/docs/visualization-examples.test.ts +663 -0
- package/docs/visualization.md +395 -0
- package/docs/visualize-examples.md +1 -1
- package/package.json +17 -2
package/docs/schedule.md
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# Schedule
|
|
2
|
+
|
|
3
|
+
Composable scheduling primitives for retry and polling strategies. Build complex schedules by combining simple building blocks.
|
|
4
|
+
|
|
5
|
+
## The Problem: Hard-Coded Retry Logic
|
|
6
|
+
|
|
7
|
+
Retry strategies end up scattered and inflexible:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// ❌ Hard-coded, not reusable
|
|
11
|
+
async function fetchWithRetry(url: string) {
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
try {
|
|
14
|
+
return await fetch(url);
|
|
15
|
+
} catch {
|
|
16
|
+
await sleep(100 * Math.pow(2, i)); // Exponential backoff
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw new Error('Failed after 3 attempts');
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When you need different strategies for different operations, you copy-paste and tweak.
|
|
24
|
+
|
|
25
|
+
## The Solution: Composable Schedules
|
|
26
|
+
|
|
27
|
+
Define scheduling strategies as composable values:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
31
|
+
|
|
32
|
+
// ✓ Composable, reusable
|
|
33
|
+
const retryStrategy = Schedule.exponential(Duration.millis(100))
|
|
34
|
+
.pipe(Schedule.jittered(0.2))
|
|
35
|
+
.pipe(Schedule.upTo(5))
|
|
36
|
+
.pipe(Schedule.andThen(Schedule.spaced(Duration.minutes(1))));
|
|
37
|
+
|
|
38
|
+
// Use with step.retry or other retry utilities
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Base Schedules
|
|
42
|
+
|
|
43
|
+
### Forever & Recurs
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { Schedule } from '@jagreehal/workflow';
|
|
47
|
+
|
|
48
|
+
Schedule.forever() // Repeats indefinitely with no delay
|
|
49
|
+
Schedule.recurs(5) // Repeats exactly 5 times
|
|
50
|
+
Schedule.once() // Repeats exactly once
|
|
51
|
+
Schedule.stop() // Never runs (immediately done)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Delay-Based Schedules
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
58
|
+
|
|
59
|
+
Schedule.spaced(Duration.seconds(1)) // Fixed 1s interval
|
|
60
|
+
Schedule.exponential(Duration.millis(100)) // 100ms, 200ms, 400ms, 800ms...
|
|
61
|
+
Schedule.exponential(Duration.millis(100), 3) // Custom factor: 100ms, 300ms, 900ms...
|
|
62
|
+
Schedule.linear(Duration.millis(100)) // 100ms, 200ms, 300ms, 400ms...
|
|
63
|
+
Schedule.fibonacci(Duration.millis(100)) // 100ms, 100ms, 200ms, 300ms, 500ms...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Combinators
|
|
67
|
+
|
|
68
|
+
### Limits
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Limit by count
|
|
72
|
+
const limited = Schedule.exponential(Duration.millis(100))
|
|
73
|
+
.pipe(Schedule.upTo(5)); // Max 5 iterations
|
|
74
|
+
|
|
75
|
+
// Limit by elapsed time
|
|
76
|
+
const timeLimited = Schedule.spaced(Duration.seconds(1))
|
|
77
|
+
.pipe(Schedule.upToElapsed(Duration.minutes(5))); // Stop after 5 minutes
|
|
78
|
+
|
|
79
|
+
// Cap delays
|
|
80
|
+
const capped = Schedule.exponential(Duration.millis(100))
|
|
81
|
+
.pipe(Schedule.maxDelay(Duration.seconds(30))) // Never exceed 30s
|
|
82
|
+
.pipe(Schedule.minDelay(Duration.millis(50))); // Never below 50ms
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Jitter
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Add randomness to prevent thundering herd
|
|
89
|
+
const jittered = Schedule.exponential(Duration.millis(100))
|
|
90
|
+
.pipe(Schedule.jittered(0.2)); // ±20% random variation
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Composition Combinators
|
|
94
|
+
|
|
95
|
+
### `andThen` - Sequential Composition
|
|
96
|
+
|
|
97
|
+
Chain two schedules: run the first until done, then run the second.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
101
|
+
|
|
102
|
+
// 5 fast retries, then switch to slow polling
|
|
103
|
+
const strategy = Schedule.exponential(Duration.millis(100))
|
|
104
|
+
.pipe(Schedule.upTo(5))
|
|
105
|
+
.pipe(Schedule.andThen(Schedule.spaced(Duration.minutes(1))));
|
|
106
|
+
|
|
107
|
+
// Run the schedule
|
|
108
|
+
const runner = Schedule.run(strategy);
|
|
109
|
+
runner.next(undefined); // 100ms (exponential phase)
|
|
110
|
+
runner.next(undefined); // 200ms
|
|
111
|
+
runner.next(undefined); // 400ms
|
|
112
|
+
runner.next(undefined); // 800ms
|
|
113
|
+
runner.next(undefined); // 1600ms (last of 5)
|
|
114
|
+
runner.next(undefined); // 60000ms (switched to spaced)
|
|
115
|
+
runner.next(undefined); // 60000ms (continues forever)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Key Feature: Reusability**
|
|
119
|
+
|
|
120
|
+
Schedules are reusable - each `run()` call starts fresh:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const schedule = Schedule.recurs(2)
|
|
124
|
+
.pipe(Schedule.andThen(Schedule.recurs(2)));
|
|
125
|
+
|
|
126
|
+
// First run
|
|
127
|
+
const delays1 = Schedule.delays(schedule, 10); // [0ms, 0ms, 0ms, 0ms]
|
|
128
|
+
|
|
129
|
+
// Second run on SAME instance - works correctly
|
|
130
|
+
const delays2 = Schedule.delays(schedule, 10); // [0ms, 0ms, 0ms, 0ms]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Composability**
|
|
134
|
+
|
|
135
|
+
`andThen` works with any schedule, including nested compositions:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// union → andThen
|
|
139
|
+
const strategy1 = Schedule.union(
|
|
140
|
+
Schedule.recurs(2),
|
|
141
|
+
Schedule.recurs(3)
|
|
142
|
+
).pipe(Schedule.andThen(Schedule.recurs(1)));
|
|
143
|
+
|
|
144
|
+
// andThen → andThen (triple chain)
|
|
145
|
+
const strategy2 = Schedule.recurs(1)
|
|
146
|
+
.pipe(Schedule.andThen(Schedule.recurs(1)))
|
|
147
|
+
.pipe(Schedule.andThen(Schedule.recurs(1)));
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `union` - Parallel with Shorter Delay
|
|
151
|
+
|
|
152
|
+
Run two schedules in parallel, taking the shorter delay at each step. Continues while either schedule continues.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
156
|
+
|
|
157
|
+
// Use whichever delay is shorter
|
|
158
|
+
const aggressive = Schedule.union(
|
|
159
|
+
Schedule.exponential(Duration.millis(100)), // Fast start
|
|
160
|
+
Schedule.spaced(Duration.seconds(1)) // Steady fallback
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const runner = Schedule.run(aggressive);
|
|
164
|
+
runner.next(undefined); // { delay: 100ms, output: [0, 0] }
|
|
165
|
+
runner.next(undefined); // { delay: 200ms, output: [1, 1] }
|
|
166
|
+
runner.next(undefined); // { delay: 400ms, output: [2, 2] }
|
|
167
|
+
runner.next(undefined); // { delay: 800ms, output: [3, 3] }
|
|
168
|
+
runner.next(undefined); // { delay: 1000ms, output: [4, 4] } // spaced wins
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Use Case: Adaptive Backoff**
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Start aggressive, fall back to steady polling
|
|
175
|
+
const adaptiveRetry = Schedule.union(
|
|
176
|
+
Schedule.exponential(Duration.millis(50)).pipe(Schedule.upTo(3)), // Fast initial
|
|
177
|
+
Schedule.spaced(Duration.seconds(5)) // Steady fallback
|
|
178
|
+
);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `intersect` - Parallel with Longer Delay
|
|
182
|
+
|
|
183
|
+
Run two schedules in parallel, taking the longer delay at each step. Stops when either schedule stops.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
187
|
+
|
|
188
|
+
// Use whichever delay is longer (more conservative)
|
|
189
|
+
const conservative = Schedule.intersect(
|
|
190
|
+
Schedule.exponential(Duration.millis(100)),
|
|
191
|
+
Schedule.spaced(Duration.seconds(1))
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const runner = Schedule.run(conservative);
|
|
195
|
+
runner.next(undefined); // { delay: 1000ms, output: [0, 0] } // spaced wins
|
|
196
|
+
runner.next(undefined); // { delay: 1000ms, output: [1, 1] }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Use Case: Rate Limiting**
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
// Never faster than 1 request per second, but back off on failures
|
|
203
|
+
const rateLimited = Schedule.intersect(
|
|
204
|
+
Schedule.spaced(Duration.seconds(1)), // Minimum 1s between requests
|
|
205
|
+
Schedule.exponential(Duration.millis(100)) // Back off on repeated failures
|
|
206
|
+
);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Nested Composition
|
|
210
|
+
|
|
211
|
+
All combinators compose freely:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// union(andThen(...), andThen(...))
|
|
215
|
+
const complex = Schedule.union(
|
|
216
|
+
Schedule.recurs(1).pipe(Schedule.andThen(Schedule.recurs(1))),
|
|
217
|
+
Schedule.recurs(2).pipe(Schedule.andThen(Schedule.recurs(1)))
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// intersect(andThen(...), recurs)
|
|
221
|
+
const nested = Schedule.intersect(
|
|
222
|
+
Schedule.recurs(2).pipe(Schedule.andThen(Schedule.recurs(2))),
|
|
223
|
+
Schedule.recurs(5)
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// andThen(union(...), union(...))
|
|
227
|
+
const chained = Schedule.union(Schedule.recurs(1), Schedule.recurs(1))
|
|
228
|
+
.pipe(Schedule.andThen(Schedule.union(Schedule.recurs(1), Schedule.recurs(1))));
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Running Schedules
|
|
232
|
+
|
|
233
|
+
### `Schedule.run` - Step-by-Step Iterator
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const schedule = Schedule.exponential(Duration.millis(100))
|
|
237
|
+
.pipe(Schedule.upTo(3));
|
|
238
|
+
|
|
239
|
+
const runner = Schedule.run(schedule);
|
|
240
|
+
|
|
241
|
+
const step1 = runner.next(undefined);
|
|
242
|
+
// { done: false, value: { delay: Duration(100ms), output: 0 } }
|
|
243
|
+
|
|
244
|
+
const step2 = runner.next(undefined);
|
|
245
|
+
// { done: false, value: { delay: Duration(200ms), output: 1 } }
|
|
246
|
+
|
|
247
|
+
const step3 = runner.next(undefined);
|
|
248
|
+
// { done: false, value: { delay: Duration(400ms), output: 2 } }
|
|
249
|
+
|
|
250
|
+
const step4 = runner.next(undefined);
|
|
251
|
+
// { done: true }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `Schedule.delays` - Get All Delays (Testing)
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
const schedule = Schedule.fibonacci(Duration.millis(100))
|
|
258
|
+
.pipe(Schedule.upTo(5));
|
|
259
|
+
|
|
260
|
+
const delays = Schedule.delays(schedule);
|
|
261
|
+
// [100ms, 100ms, 200ms, 300ms, 500ms]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Transformations
|
|
265
|
+
|
|
266
|
+
### `map` - Transform Output
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
const withAttemptNumber = Schedule.recurs(3)
|
|
270
|
+
.pipe(Schedule.map((n) => ({ attempt: n + 1 })));
|
|
271
|
+
|
|
272
|
+
const runner = Schedule.run(withAttemptNumber);
|
|
273
|
+
runner.next(undefined); // { output: { attempt: 1 } }
|
|
274
|
+
runner.next(undefined); // { output: { attempt: 2 } }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### `tap` - Side Effects
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
const withLogging = Schedule.exponential(Duration.millis(100))
|
|
281
|
+
.pipe(Schedule.tap((iteration) => console.log(`Attempt ${iteration + 1}`)));
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### `modifyDelay` - Transform Delays
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
const doubled = Schedule.spaced(Duration.seconds(1))
|
|
288
|
+
.pipe(Schedule.modifyDelay((d) => Duration.multiply(d, 2)));
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Real-World Scenarios
|
|
292
|
+
|
|
293
|
+
### Database Connection Pool: Health Check and Reconnect
|
|
294
|
+
|
|
295
|
+
Your app loses database connectivity occasionally. You want aggressive reconnection attempts initially, then back off to avoid hammering a potentially overloaded server.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
299
|
+
|
|
300
|
+
// Quick retries for blips, then slower checks to let the DB recover
|
|
301
|
+
const dbReconnect = Schedule.exponential(Duration.millis(100))
|
|
302
|
+
.pipe(Schedule.upTo(5)) // 5 quick attempts: 100, 200, 400, 800, 1600ms
|
|
303
|
+
.pipe(Schedule.andThen(
|
|
304
|
+
Schedule.spaced(Duration.seconds(10)) // Then check every 10s
|
|
305
|
+
.pipe(Schedule.upTo(30)) // Give up after 5 minutes
|
|
306
|
+
));
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Why this works: Most connection drops are brief (network hiccup, connection pool exhaustion). Fast retries catch these. If the problem persists, slower retries prevent your app from contributing to the overload.
|
|
310
|
+
|
|
311
|
+
### Email Service: Handle Transient Failures
|
|
312
|
+
|
|
313
|
+
Your email provider has rate limits and occasional 429s. You need to respect limits while still retrying transient failures.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
317
|
+
|
|
318
|
+
// Never faster than rate limit, but back off on repeated failures
|
|
319
|
+
const emailRetry = Schedule.intersect(
|
|
320
|
+
Schedule.spaced(Duration.millis(200)), // Rate limit: 5 emails/second
|
|
321
|
+
Schedule.exponential(Duration.millis(100)) // Back off: 100, 200, 400...
|
|
322
|
+
.pipe(Schedule.maxDelay(Duration.seconds(60))) // Cap at 1 minute
|
|
323
|
+
).pipe(Schedule.upTo(10)); // Max 10 attempts
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Why `intersect`: Takes the *longer* delay, so you never exceed rate limits even when backing off rapidly. The exponential only kicks in once it exceeds the rate limit baseline.
|
|
327
|
+
|
|
328
|
+
### Webhook Delivery: Escalating Retry with Deadline
|
|
329
|
+
|
|
330
|
+
You're sending webhooks to customer endpoints. Some endpoints are slow or unreliable. You want quick retries for transient issues, slower retries for persistent issues, but a hard deadline.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
334
|
+
|
|
335
|
+
// Three phases: immediate, patient, very patient
|
|
336
|
+
const webhookDelivery = Schedule.exponential(Duration.seconds(1))
|
|
337
|
+
.pipe(Schedule.upTo(3)) // Phase 1: 1s, 2s, 4s
|
|
338
|
+
.pipe(Schedule.andThen(
|
|
339
|
+
Schedule.spaced(Duration.minutes(5))
|
|
340
|
+
.pipe(Schedule.upTo(6)) // Phase 2: every 5min for 30min
|
|
341
|
+
))
|
|
342
|
+
.pipe(Schedule.andThen(
|
|
343
|
+
Schedule.spaced(Duration.hours(1))
|
|
344
|
+
.pipe(Schedule.upTo(24)) // Phase 3: hourly for 24h
|
|
345
|
+
));
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Why three phases: Fast retries catch brief outages. Medium retries handle maintenance windows. Slow retries give endpoints 24 hours to come back online before you mark the delivery as failed.
|
|
349
|
+
|
|
350
|
+
### Background Job Processing: Throttled with Backpressure
|
|
351
|
+
|
|
352
|
+
You're processing jobs from a queue. You want steady throughput, but if jobs start failing, slow down to avoid overwhelming downstream services.
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
356
|
+
|
|
357
|
+
// Steady pace normally, but back off when things go wrong
|
|
358
|
+
const jobProcessing = Schedule.union(
|
|
359
|
+
Schedule.spaced(Duration.millis(100)), // Normal: 10 jobs/second
|
|
360
|
+
Schedule.exponential(Duration.millis(50)) // Failure: back off quickly
|
|
361
|
+
.pipe(Schedule.maxDelay(Duration.seconds(5))) // But never more than 5s
|
|
362
|
+
).pipe(Schedule.jittered(0.1)); // Add jitter to prevent thundering herd
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Why `union`: Takes the *shorter* delay. When healthy, processes at steady 100ms. When failing, exponential grows but union caps effective delay at 100ms until failures compound enough to exceed that baseline.
|
|
366
|
+
|
|
367
|
+
## Real-World Patterns
|
|
368
|
+
|
|
369
|
+
### Checkout Retries You Can Explain to a PM
|
|
370
|
+
|
|
371
|
+
Your payment provider flakes 1–2% of the time. You want a few fast retries to catch brief blips, then slow down so you don't hammer the gateway.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
375
|
+
|
|
376
|
+
// Fast retries for transient blips, then slower follow-ups
|
|
377
|
+
const paymentRetry = Schedule.exponential(Duration.millis(200))
|
|
378
|
+
.pipe(Schedule.upTo(4)) // 4 quick attempts
|
|
379
|
+
.pipe(Schedule.andThen(
|
|
380
|
+
Schedule.spaced(Duration.seconds(30)) // Then check every 30s
|
|
381
|
+
.pipe(Schedule.upTo(6)) // Give up after 3 minutes
|
|
382
|
+
));
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Shipment Tracking: "Check Every 5 Minutes, Stop After a Day"
|
|
386
|
+
|
|
387
|
+
Your operations team wants predictable polling that doesn't run forever.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
391
|
+
|
|
392
|
+
const trackShipment = Schedule.spaced(Duration.minutes(5))
|
|
393
|
+
.pipe(Schedule.upToElapsed(Duration.hours(24)));
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Public API Safety: Respect Rate Limits While Backing Off
|
|
397
|
+
|
|
398
|
+
If you get throttled, back off, but never exceed the platform's baseline rate limit.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { Schedule, Duration } from '@jagreehal/workflow';
|
|
402
|
+
|
|
403
|
+
const safeApiCalls = Schedule.intersect(
|
|
404
|
+
Schedule.spaced(Duration.seconds(1)), // Never faster than 1 req/s
|
|
405
|
+
Schedule.exponential(Duration.millis(200)) // Back off on failures
|
|
406
|
+
.pipe(Schedule.maxDelay(Duration.seconds(30)))
|
|
407
|
+
);
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Exponential Backoff with Jitter and Cap
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
const httpRetry = Schedule.exponential(Duration.millis(100))
|
|
414
|
+
.pipe(Schedule.jittered(0.2)) // Add randomness
|
|
415
|
+
.pipe(Schedule.maxDelay(Duration.seconds(30))) // Cap at 30s
|
|
416
|
+
.pipe(Schedule.upTo(5)); // Max 5 attempts
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Two-Phase Retry Strategy
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
// Aggressive retries first, then patient polling
|
|
423
|
+
const twoPhase = Schedule.exponential(Duration.millis(50))
|
|
424
|
+
.pipe(Schedule.upTo(3))
|
|
425
|
+
.pipe(Schedule.andThen(
|
|
426
|
+
Schedule.spaced(Duration.minutes(1)).pipe(Schedule.upTo(10))
|
|
427
|
+
));
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Adaptive Rate Limiting
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
// Take the more conservative delay between rate limit and backoff
|
|
434
|
+
const adaptive = Schedule.intersect(
|
|
435
|
+
Schedule.spaced(Duration.millis(100)), // Rate limit: 10 req/s
|
|
436
|
+
Schedule.exponential(Duration.millis(50)) // Back off on failures
|
|
437
|
+
).pipe(Schedule.upTo(10));
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Aggressive Retry with Fallback
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
// Take the shorter delay between exponential and fixed ceiling
|
|
444
|
+
const aggressive = Schedule.union(
|
|
445
|
+
Schedule.exponential(Duration.millis(100)),
|
|
446
|
+
Schedule.spaced(Duration.seconds(5)) // Never wait more than 5s
|
|
447
|
+
).pipe(Schedule.upTo(10));
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Summary
|
|
451
|
+
|
|
452
|
+
| Function | Purpose |
|
|
453
|
+
| -------- | ------- |
|
|
454
|
+
| `Schedule.forever()` | Repeat indefinitely |
|
|
455
|
+
| `Schedule.recurs(n)` | Repeat exactly n times |
|
|
456
|
+
| `Schedule.spaced(d)` | Fixed delay interval |
|
|
457
|
+
| `Schedule.exponential(d)` | Exponential backoff |
|
|
458
|
+
| `Schedule.andThen(s)` | Sequential: first → second |
|
|
459
|
+
| `Schedule.union(a, b)` | Parallel: shorter delay, either continues |
|
|
460
|
+
| `Schedule.intersect(a, b)` | Parallel: longer delay, both must continue |
|
|
461
|
+
| `Schedule.upTo(n)` | Limit iterations |
|
|
462
|
+
| `Schedule.maxDelay(d)` | Cap maximum delay |
|
|
463
|
+
| `Schedule.jittered(f)` | Add random variation |
|
|
464
|
+
| `Schedule.run(s)` | Create iterator |
|
|
465
|
+
| `Schedule.delays(s)` | Get all delays (testing) |
|
|
466
|
+
|
|
467
|
+
**The key insight:** Build complex retry strategies by composing simple primitives. Schedules are reusable and safe to run multiple times.
|