@evolu/common 7.4.0 → 8.0.0-next.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 +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +1064 -630
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -823
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +209 -282
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -529
- package/src/Instances.ts +0 -90
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -960
package/src/Schedule.ts
ADDED
|
@@ -0,0 +1,1471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable scheduling strategies.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { fibonacciAt, FibonacciIndex, increment } from "./Number.js";
|
|
8
|
+
import type { RandomDep } from "./Random.js";
|
|
9
|
+
import { done, err, type NextResult, ok } from "./Result.js";
|
|
10
|
+
import type { repeat, RepeatAttempt, retry, RetryAttempt } from "./Task.js";
|
|
11
|
+
import {
|
|
12
|
+
type Duration,
|
|
13
|
+
durationToMillis,
|
|
14
|
+
Millis,
|
|
15
|
+
minMillis,
|
|
16
|
+
type TimeDep,
|
|
17
|
+
} from "./Time.js";
|
|
18
|
+
import { onePositiveInt, PositiveInt } from "./Type.js";
|
|
19
|
+
import type { Predicate } from "./Types.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Composable scheduling strategies for {@link retry}, {@link repeat}, rate
|
|
23
|
+
* limiting, and more.
|
|
24
|
+
*
|
|
25
|
+
* A Schedule uses the State pattern: calling `schedule(deps)` creates a step
|
|
26
|
+
* function with internal state captured in closures. Each call to `step(input)`
|
|
27
|
+
* advances that state and returns `Ok([Output, Millis])` or `Err(Done<void>)`
|
|
28
|
+
* to stop. Multiple calls to `schedule(deps)` create independent state
|
|
29
|
+
* instances.
|
|
30
|
+
*
|
|
31
|
+
* ### Example
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* import {
|
|
35
|
+
* exponential,
|
|
36
|
+
* jitter,
|
|
37
|
+
* maxDelay,
|
|
38
|
+
* retry,
|
|
39
|
+
* take,
|
|
40
|
+
* } from "@evolu/common";
|
|
41
|
+
*
|
|
42
|
+
* const fetchWithRetry = retry(
|
|
43
|
+
* fetchData,
|
|
44
|
+
* // A jittered, capped, limited exponential backoff.
|
|
45
|
+
* jitter(1)(maxDelay("20s")(take(2)(exponential("100ms")))),
|
|
46
|
+
* );
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* Or use a preset:
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { retryStrategyAws, retry } from "@evolu/common";
|
|
53
|
+
*
|
|
54
|
+
* const fetchWithRetry = retry(fetchData, retryStrategyAws);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export type Schedule<out Output, in Input = unknown> = (
|
|
58
|
+
deps: ScheduleDeps,
|
|
59
|
+
) => (input: Input) => NextResult<readonly [Output, Millis]>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Dependencies provided to a {@link Schedule}.
|
|
63
|
+
*
|
|
64
|
+
* The executor provides these once, and the schedule uses what it needs.
|
|
65
|
+
*/
|
|
66
|
+
export type ScheduleDeps = TimeDep & RandomDep;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Base interface for schedule-based task helpers.
|
|
70
|
+
*
|
|
71
|
+
* Used by {@link RetryAttempt}, {@link RepeatAttempt}, and future schedule-driven
|
|
72
|
+
* helpers.
|
|
73
|
+
*
|
|
74
|
+
* @group Composition
|
|
75
|
+
*/
|
|
76
|
+
export interface ScheduleStep<Output> {
|
|
77
|
+
/** The current attempt. */
|
|
78
|
+
readonly attempt: PositiveInt;
|
|
79
|
+
|
|
80
|
+
/** Output from the {@link Schedule} step. */
|
|
81
|
+
readonly output: Output;
|
|
82
|
+
|
|
83
|
+
/** Delay before this step executes. */
|
|
84
|
+
readonly delay: Millis;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Internal per-step metrics computed from timestamps.
|
|
89
|
+
*
|
|
90
|
+
* The schedule computes this internally from deps.time.now().
|
|
91
|
+
*/
|
|
92
|
+
interface ScheduleStepMetrics {
|
|
93
|
+
/** Current attempt number. */
|
|
94
|
+
readonly attempt: PositiveInt;
|
|
95
|
+
/** Milliseconds elapsed since the schedule started. */
|
|
96
|
+
readonly elapsed: Millis;
|
|
97
|
+
/** Milliseconds since the previous step. On first step, this is 0. */
|
|
98
|
+
readonly elapsedSincePrevious: Millis;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates an internal per-step metrics tracker.
|
|
103
|
+
*
|
|
104
|
+
* Each call updates internal state and returns computed metrics.
|
|
105
|
+
*/
|
|
106
|
+
const createScheduleStepMetrics = (
|
|
107
|
+
deps: TimeDep,
|
|
108
|
+
): (() => ScheduleStepMetrics) => {
|
|
109
|
+
let attempt = onePositiveInt;
|
|
110
|
+
let start: Millis | null = null;
|
|
111
|
+
let previous: Millis | null = null;
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
const now = deps.time.now();
|
|
115
|
+
const currentAttempt = attempt;
|
|
116
|
+
attempt = increment(attempt) as PositiveInt;
|
|
117
|
+
start ??= now;
|
|
118
|
+
const elapsed = (now - start) as Millis;
|
|
119
|
+
const elapsedSincePrevious =
|
|
120
|
+
previous === null ? (0 as Millis) : ((now - previous) as Millis);
|
|
121
|
+
previous = now;
|
|
122
|
+
return { attempt: currentAttempt, elapsed, elapsedSincePrevious };
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A schedule that never stops and has no delay.
|
|
128
|
+
*
|
|
129
|
+
* Outputs the attempt count (0, 1, 2, ...). Useful as a base for composition or
|
|
130
|
+
* for immediate retry without backoff.
|
|
131
|
+
*
|
|
132
|
+
* ### Example
|
|
133
|
+
*
|
|
134
|
+
* ```ts
|
|
135
|
+
* // Retry immediately, up to 5 times
|
|
136
|
+
* const immediate = take(5)(forever);
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* @group Constructors
|
|
140
|
+
*/
|
|
141
|
+
export const forever: Schedule<number> = () => {
|
|
142
|
+
let attempt = 0;
|
|
143
|
+
return () => ok([attempt++, minMillis]);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A schedule that runs exactly once with no delay.
|
|
148
|
+
*
|
|
149
|
+
* Convenience for `take(1)(forever)`. Useful for one-shot operations.
|
|
150
|
+
*
|
|
151
|
+
* ### Example
|
|
152
|
+
*
|
|
153
|
+
* ```ts
|
|
154
|
+
* // Execute once, no retry
|
|
155
|
+
* const oneShot = once;
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @group Constructors
|
|
159
|
+
*/
|
|
160
|
+
export const once: Schedule<number> = () => {
|
|
161
|
+
let finished = false;
|
|
162
|
+
return () => {
|
|
163
|
+
if (finished) return err(done());
|
|
164
|
+
finished = true;
|
|
165
|
+
return ok([0, minMillis]);
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A schedule that recurs a fixed number of times.
|
|
171
|
+
*
|
|
172
|
+
* Convenience for `take(n)(forever)`. Outputs the current repetition count (0,
|
|
173
|
+
* 1, 2, ..., n-1).
|
|
174
|
+
*
|
|
175
|
+
* ### Example
|
|
176
|
+
*
|
|
177
|
+
* ```ts
|
|
178
|
+
* // Retry up to 3 times (4 total attempts including initial)
|
|
179
|
+
* const retry = recurs(3);
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @group Constructors
|
|
183
|
+
*/
|
|
184
|
+
export const recurs = (n: number): Schedule<number> => take(n)(forever);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Constant delay schedule.
|
|
188
|
+
*
|
|
189
|
+
* Always waits the same duration after each execution completes. Never stops —
|
|
190
|
+
* combine with {@link take} or {@link maxElapsed} to limit.
|
|
191
|
+
*
|
|
192
|
+
* ### Example
|
|
193
|
+
*
|
|
194
|
+
* ```ts
|
|
195
|
+
* // 1s, 1s, 1s, ... (polling)
|
|
196
|
+
* const poll = spaced("1s");
|
|
197
|
+
*
|
|
198
|
+
* // Retry 3 times with 500ms between each
|
|
199
|
+
* const retry = take(3)(spaced("500ms"));
|
|
200
|
+
*
|
|
201
|
+
* // Heartbeat schedule
|
|
202
|
+
* const heartbeat = spaced("30s");
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* @group Constructors
|
|
206
|
+
*/
|
|
207
|
+
export const spaced =
|
|
208
|
+
(duration: Duration): Schedule<Millis> =>
|
|
209
|
+
() => {
|
|
210
|
+
const ms = durationToMillis(duration);
|
|
211
|
+
return () => ok([ms, ms]);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Exponential backoff schedule.
|
|
216
|
+
*
|
|
217
|
+
* Computes delay as `base * factor^(attempt - 1)`:
|
|
218
|
+
*
|
|
219
|
+
* - Attempt 1: `base`
|
|
220
|
+
* - Attempt 2: `base * factor`
|
|
221
|
+
* - Attempt 3: `base * factor²`
|
|
222
|
+
* - ...
|
|
223
|
+
*
|
|
224
|
+
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
225
|
+
*
|
|
226
|
+
* ### Example
|
|
227
|
+
*
|
|
228
|
+
* ```ts
|
|
229
|
+
* // 100ms, 200ms, 400ms, 800ms, ...
|
|
230
|
+
* const exp = exponential("100ms");
|
|
231
|
+
*
|
|
232
|
+
* // 100ms, 150ms, 225ms, 338ms, ... (gentler growth)
|
|
233
|
+
* const gentle = exponential("100ms", 1.5);
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* @group Constructors
|
|
237
|
+
*/
|
|
238
|
+
export const exponential =
|
|
239
|
+
(base: Duration, factor = 2): Schedule<Millis> =>
|
|
240
|
+
() => {
|
|
241
|
+
const baseMs = durationToMillis(base);
|
|
242
|
+
let attempt = 0;
|
|
243
|
+
return () => {
|
|
244
|
+
attempt++;
|
|
245
|
+
const rawDelay = baseMs * Math.pow(factor, attempt - 1);
|
|
246
|
+
const delay = Millis.orThrow(Math.max(0, Math.round(rawDelay)));
|
|
247
|
+
return ok([delay, delay]);
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Linear backoff schedule.
|
|
253
|
+
*
|
|
254
|
+
* Delay increases linearly: `base * attempt`:
|
|
255
|
+
*
|
|
256
|
+
* - Attempt 1: `base`
|
|
257
|
+
* - Attempt 2: `base * 2`
|
|
258
|
+
* - Attempt 3: `base * 3`
|
|
259
|
+
* - ...
|
|
260
|
+
*
|
|
261
|
+
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
262
|
+
*
|
|
263
|
+
* ### Example
|
|
264
|
+
*
|
|
265
|
+
* ```ts
|
|
266
|
+
* // 100ms, 200ms, 300ms, 400ms, ...
|
|
267
|
+
* const lin = linear("100ms");
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* @group Constructors
|
|
271
|
+
*/
|
|
272
|
+
export const linear =
|
|
273
|
+
(base: Duration): Schedule<Millis> =>
|
|
274
|
+
() => {
|
|
275
|
+
const ms = durationToMillis(base);
|
|
276
|
+
let attempt = 0;
|
|
277
|
+
return () => {
|
|
278
|
+
attempt++;
|
|
279
|
+
const delay = Millis.orThrow(ms * attempt);
|
|
280
|
+
return ok([delay, delay]);
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Fibonacci backoff schedule.
|
|
286
|
+
*
|
|
287
|
+
* Delays follow the Fibonacci sequence, growing more slowly than exponential:
|
|
288
|
+
*
|
|
289
|
+
* - Attempt 1: `initial`
|
|
290
|
+
* - Attempt 2: `initial`
|
|
291
|
+
* - Attempt 3: `initial * 2`
|
|
292
|
+
* - Attempt 4: `initial * 3`
|
|
293
|
+
* - Attempt 5: `initial * 5`
|
|
294
|
+
* - ...
|
|
295
|
+
*
|
|
296
|
+
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
297
|
+
*
|
|
298
|
+
* ### Example
|
|
299
|
+
*
|
|
300
|
+
* ```ts
|
|
301
|
+
* // 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, ...
|
|
302
|
+
* const fib = fibonacci("100ms");
|
|
303
|
+
* ```
|
|
304
|
+
*
|
|
305
|
+
* @group Constructors
|
|
306
|
+
*/
|
|
307
|
+
export const fibonacci =
|
|
308
|
+
(initial: Duration): Schedule<Millis> =>
|
|
309
|
+
() => {
|
|
310
|
+
const ms = durationToMillis(initial);
|
|
311
|
+
let index = 1;
|
|
312
|
+
return () => {
|
|
313
|
+
const delay = Millis.orThrow(
|
|
314
|
+
ms * fibonacciAt(FibonacciIndex.orThrow(index)),
|
|
315
|
+
);
|
|
316
|
+
index++;
|
|
317
|
+
return ok([delay, delay]);
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Fixed interval schedule aligned to time windows.
|
|
323
|
+
*
|
|
324
|
+
* Recurs on a fixed interval, outputting the repetition count (0, 1, 2, ...).
|
|
325
|
+
* Unlike {@link spaced}, which waits a duration _after_ each execution, `fixed`
|
|
326
|
+
* maintains a consistent cadence from when the schedule started.
|
|
327
|
+
*
|
|
328
|
+
* If execution takes longer than the interval, the next execution happens
|
|
329
|
+
* immediately but subsequent runs still align to the original window
|
|
330
|
+
* boundaries. This prevents "pile-up" while maintaining predictable timing.
|
|
331
|
+
*
|
|
332
|
+
* ### Example
|
|
333
|
+
*
|
|
334
|
+
* ```ts
|
|
335
|
+
* // Health check every 5 seconds, aligned to windows
|
|
336
|
+
* const healthCheck = take(10)(fixed("5s"));
|
|
337
|
+
*
|
|
338
|
+
* // Cron-like behavior: run at consistent intervals
|
|
339
|
+
* const cronLike = fixed("1m");
|
|
340
|
+
* ```
|
|
341
|
+
*
|
|
342
|
+
* @group Constructors
|
|
343
|
+
*/
|
|
344
|
+
export const fixed =
|
|
345
|
+
(interval: Duration): Schedule<number> =>
|
|
346
|
+
(deps) => {
|
|
347
|
+
const intervalMs = durationToMillis(interval);
|
|
348
|
+
const metrics = createScheduleStepMetrics(deps);
|
|
349
|
+
let count = 0;
|
|
350
|
+
return () => {
|
|
351
|
+
const { elapsed } = metrics();
|
|
352
|
+
// Which window should we be in based on count?
|
|
353
|
+
const expectedWindowEnd = (count + 1) * intervalMs;
|
|
354
|
+
const runningBehind = intervalMs > 0 && elapsed >= expectedWindowEnd;
|
|
355
|
+
// Time until next window boundary
|
|
356
|
+
const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
|
|
357
|
+
const boundary = intervalMs - remainder;
|
|
358
|
+
const delay = runningBehind ? 0 : boundary;
|
|
359
|
+
return ok([count++, delay as Millis]);
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Divides the timeline into fixed windows and sleeps until the next boundary.
|
|
365
|
+
*
|
|
366
|
+
* Similar to {@link fixed}, but always sleeps until the next window boundary
|
|
367
|
+
* regardless of when the last execution started. Outputs the repetition count.
|
|
368
|
+
*
|
|
369
|
+
* Useful for aligning executions to regular intervals from the start time.
|
|
370
|
+
*
|
|
371
|
+
* ### Example
|
|
372
|
+
*
|
|
373
|
+
* ```ts
|
|
374
|
+
* // Execute at regular 5-second boundaries from start
|
|
375
|
+
* const aligned = windowed("5s");
|
|
376
|
+
* // If elapsed is 3s, waits 2s. If elapsed is 7s, waits 3s.
|
|
377
|
+
* ```
|
|
378
|
+
*
|
|
379
|
+
* @group Constructors
|
|
380
|
+
*/
|
|
381
|
+
export const windowed =
|
|
382
|
+
(interval: Duration): Schedule<number> =>
|
|
383
|
+
(deps) => {
|
|
384
|
+
const intervalMs = durationToMillis(interval);
|
|
385
|
+
const metrics = createScheduleStepMetrics(deps);
|
|
386
|
+
let count = 0;
|
|
387
|
+
return () => {
|
|
388
|
+
const { elapsed } = metrics();
|
|
389
|
+
const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
|
|
390
|
+
const delay = intervalMs === 0 ? 0 : intervalMs - remainder;
|
|
391
|
+
return ok([count++, delay as Millis]);
|
|
392
|
+
};
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* A schedule that runs once with a single delay.
|
|
397
|
+
*
|
|
398
|
+
* Convenience for `take(1)(spaced(delay))`. Useful for simple one-shot delays.
|
|
399
|
+
*
|
|
400
|
+
* ### Example
|
|
401
|
+
*
|
|
402
|
+
* ```ts
|
|
403
|
+
* // Wait 1 second then stop
|
|
404
|
+
* const oneShot = fromDelay("1s");
|
|
405
|
+
* ```
|
|
406
|
+
*
|
|
407
|
+
* @group Constructors
|
|
408
|
+
*/
|
|
409
|
+
export const fromDelay = (delay: Duration): Schedule<Millis> =>
|
|
410
|
+
take(1)(spaced(delay));
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* A schedule that runs through a sequence of delays.
|
|
414
|
+
*
|
|
415
|
+
* Convenience for sequencing single-delay schedules. Useful for predefined
|
|
416
|
+
* retry patterns.
|
|
417
|
+
*
|
|
418
|
+
* ### Example
|
|
419
|
+
*
|
|
420
|
+
* ```ts
|
|
421
|
+
* // Custom retry sequence: 100ms, 500ms, 2s
|
|
422
|
+
* const custom = fromDelays("100ms", "500ms", "2s");
|
|
423
|
+
* ```
|
|
424
|
+
*
|
|
425
|
+
* @group Constructors
|
|
426
|
+
*/
|
|
427
|
+
export const fromDelays = (
|
|
428
|
+
...delays: ReadonlyArray<Duration>
|
|
429
|
+
): Schedule<Millis> => sequenceSchedules(...delays.map((d) => fromDelay(d)));
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A schedule that outputs the total elapsed time since the schedule started.
|
|
433
|
+
*
|
|
434
|
+
* Never stops — combine with {@link take} or {@link maxElapsed} to limit. Useful
|
|
435
|
+
* for tracking how long a retry sequence has been running.
|
|
436
|
+
*
|
|
437
|
+
* ### Example
|
|
438
|
+
*
|
|
439
|
+
* ```ts
|
|
440
|
+
* // Track elapsed time alongside retries
|
|
441
|
+
* const withTiming = intersectSchedules(exponential("100ms"), elapsed);
|
|
442
|
+
* // Outputs: [[100, 0], [200, ~100], [400, ~300], ...]
|
|
443
|
+
*
|
|
444
|
+
* // Stop after 30 seconds of elapsed time
|
|
445
|
+
* const timeLimited = whileScheduleOutput((ms: Millis) => ms < 30000)(
|
|
446
|
+
* elapsed,
|
|
447
|
+
* );
|
|
448
|
+
* ```
|
|
449
|
+
*
|
|
450
|
+
* @group Constructors
|
|
451
|
+
*/
|
|
452
|
+
export const elapsed: Schedule<Millis> = (deps) => {
|
|
453
|
+
const metrics = createScheduleStepMetrics(deps);
|
|
454
|
+
return () => ok([metrics().elapsed, minMillis]);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* A schedule that runs for a specified duration then stops.
|
|
459
|
+
*
|
|
460
|
+
* Outputs the elapsed time. Useful for time-boxed operations or combining with
|
|
461
|
+
* other schedules to create time-limited variants.
|
|
462
|
+
*
|
|
463
|
+
* ### Example
|
|
464
|
+
*
|
|
465
|
+
* ```ts
|
|
466
|
+
* // Run for at most 30 seconds
|
|
467
|
+
* const timeLimited = during("30s");
|
|
468
|
+
*
|
|
469
|
+
* // Combine with exponential for time-boxed retry
|
|
470
|
+
* const timedRetry = intersectSchedules(
|
|
471
|
+
* exponential("100ms"),
|
|
472
|
+
* during("10s"),
|
|
473
|
+
* );
|
|
474
|
+
* ```
|
|
475
|
+
*
|
|
476
|
+
* @group Constructors
|
|
477
|
+
*/
|
|
478
|
+
export const during = (duration: Duration): Schedule<Millis> =>
|
|
479
|
+
whileScheduleOutput((ms: Millis) => ms <= durationToMillis(duration))(
|
|
480
|
+
elapsed,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* A schedule that always outputs a constant value.
|
|
485
|
+
*
|
|
486
|
+
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
487
|
+
*
|
|
488
|
+
* ### Example
|
|
489
|
+
*
|
|
490
|
+
* ```ts
|
|
491
|
+
* // Always output "retry"
|
|
492
|
+
* const labeled = always("retry");
|
|
493
|
+
*
|
|
494
|
+
* // Combine with timing
|
|
495
|
+
* const withLabel = intersectSchedules(
|
|
496
|
+
* exponential("100ms"),
|
|
497
|
+
* always("backoff"),
|
|
498
|
+
* );
|
|
499
|
+
* ```
|
|
500
|
+
*
|
|
501
|
+
* @group Constructors
|
|
502
|
+
*/
|
|
503
|
+
export const always = <A>(value: A): Schedule<A> =>
|
|
504
|
+
mapSchedule(() => value)(forever);
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Creates a schedule by unfolding a state.
|
|
508
|
+
*
|
|
509
|
+
* Each step outputs the current state and computes the next state using the
|
|
510
|
+
* provided function. Never stops — combine with {@link take} or
|
|
511
|
+
* {@link maxElapsed} to limit.
|
|
512
|
+
*
|
|
513
|
+
* ### Example
|
|
514
|
+
*
|
|
515
|
+
* ```ts
|
|
516
|
+
* // Counter: 0, 1, 2, 3, ...
|
|
517
|
+
* const counter = unfoldSchedule(0, (n) => n + 1);
|
|
518
|
+
*
|
|
519
|
+
* // Custom backoff: 100, 150, 225, 338, ... (×1.5 each time)
|
|
520
|
+
* const customBackoff = unfoldSchedule(100, (delay) =>
|
|
521
|
+
* Math.round(delay * 1.5),
|
|
522
|
+
* );
|
|
523
|
+
*
|
|
524
|
+
* // State machine
|
|
525
|
+
* type Phase = "init" | "warmup" | "active";
|
|
526
|
+
* const phases = unfoldSchedule<Phase>("init", (phase) => {
|
|
527
|
+
* switch (phase) {
|
|
528
|
+
* case "init":
|
|
529
|
+
* return "warmup";
|
|
530
|
+
* case "warmup":
|
|
531
|
+
* return "active";
|
|
532
|
+
* case "active":
|
|
533
|
+
* return "active";
|
|
534
|
+
* }
|
|
535
|
+
* });
|
|
536
|
+
* ```
|
|
537
|
+
*
|
|
538
|
+
* @group Constructors
|
|
539
|
+
*/
|
|
540
|
+
export const unfoldSchedule =
|
|
541
|
+
<State>(initial: State, next: (state: State) => State): Schedule<State> =>
|
|
542
|
+
() => {
|
|
543
|
+
let state = initial;
|
|
544
|
+
return () => {
|
|
545
|
+
const current = state;
|
|
546
|
+
state = next(state);
|
|
547
|
+
return ok([current, minMillis]);
|
|
548
|
+
};
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Limits a schedule to a maximum number of attempts.
|
|
553
|
+
*
|
|
554
|
+
* After `n` attempts, returns `Err(Done<void>)` (stop).
|
|
555
|
+
*
|
|
556
|
+
* ### Example
|
|
557
|
+
*
|
|
558
|
+
* ```ts
|
|
559
|
+
* // Exponential backoff, max 3 retries
|
|
560
|
+
* const limited = take(3)(exponential("100ms"));
|
|
561
|
+
* // Attempt 1: 100ms, Attempt 2: 200ms, Attempt 3: 400ms, Attempt 4: Err(Done<void>)
|
|
562
|
+
* ```
|
|
563
|
+
*
|
|
564
|
+
* @group Limiting
|
|
565
|
+
*/
|
|
566
|
+
export const take =
|
|
567
|
+
(n: number) =>
|
|
568
|
+
<Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
569
|
+
(deps) => {
|
|
570
|
+
const step = schedule(deps);
|
|
571
|
+
let attempt = 0;
|
|
572
|
+
return (input) => {
|
|
573
|
+
attempt++;
|
|
574
|
+
if (attempt > n) return err(done());
|
|
575
|
+
return step(input);
|
|
576
|
+
};
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Limits schedule execution to a maximum elapsed time.
|
|
581
|
+
*
|
|
582
|
+
* After `duration` has elapsed since the schedule started, returns
|
|
583
|
+
* `Err(Done<void>)`.
|
|
584
|
+
*
|
|
585
|
+
* ### Example
|
|
586
|
+
*
|
|
587
|
+
* ```ts
|
|
588
|
+
* // Retry for at most 30 seconds
|
|
589
|
+
* const timeLimited = maxElapsed("30s")(exponential("1s"));
|
|
590
|
+
* ```
|
|
591
|
+
*
|
|
592
|
+
* @group Limiting
|
|
593
|
+
*/
|
|
594
|
+
export const maxElapsed = (duration: Duration) => {
|
|
595
|
+
const maxMs = durationToMillis(duration);
|
|
596
|
+
return <Output, Input>(
|
|
597
|
+
schedule: Schedule<Output, Input>,
|
|
598
|
+
): Schedule<Output, Input> =>
|
|
599
|
+
(deps) => {
|
|
600
|
+
const step = schedule(deps);
|
|
601
|
+
const metrics = createScheduleStepMetrics(deps);
|
|
602
|
+
return (input) => {
|
|
603
|
+
const { elapsed } = metrics();
|
|
604
|
+
return elapsed >= maxMs ? err(done()) : step(input);
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Caps the delay to a maximum value.
|
|
611
|
+
*
|
|
612
|
+
* If the schedule returns a delay greater than `max`, returns `max` instead.
|
|
613
|
+
*
|
|
614
|
+
* ### Example
|
|
615
|
+
*
|
|
616
|
+
* ```ts
|
|
617
|
+
* // Exponential capped at 10 seconds
|
|
618
|
+
* const capped = maxDelay("10s")(exponential("1s"));
|
|
619
|
+
* // 1s, 2s, 4s, 8s, 10s, 10s, 10s, ...
|
|
620
|
+
* ```
|
|
621
|
+
*
|
|
622
|
+
* @group Limiting
|
|
623
|
+
*/
|
|
624
|
+
export const maxDelay = (max: Duration) => {
|
|
625
|
+
const maxMs = durationToMillis(max);
|
|
626
|
+
return <Output, Input>(
|
|
627
|
+
schedule: Schedule<Output, Input>,
|
|
628
|
+
): Schedule<Output, Input> =>
|
|
629
|
+
(deps) => {
|
|
630
|
+
const step = schedule(deps);
|
|
631
|
+
return (input) => {
|
|
632
|
+
const result = step(input);
|
|
633
|
+
if (!result.ok) return result;
|
|
634
|
+
const [output, delay] = result.value;
|
|
635
|
+
return ok([output, Millis.orThrow(Math.min(delay, maxMs))]);
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Adds randomized jitter to delays.
|
|
642
|
+
*
|
|
643
|
+
* Jitter helps prevent "thundering herd" when many clients retry simultaneously
|
|
644
|
+
* after a service recovers. The delay is randomized within a range:
|
|
645
|
+
*
|
|
646
|
+
* - `factor = 0` — no jitter (original delay)
|
|
647
|
+
* - `factor = 0.5` — delay varies ±50% (e.g., 1s becomes 500ms-1500ms)
|
|
648
|
+
* - `factor = 1` — full jitter, delay varies 0-200% (e.g., 1s becomes 0-2s)
|
|
649
|
+
*
|
|
650
|
+
* ### Example
|
|
651
|
+
*
|
|
652
|
+
* ```ts
|
|
653
|
+
* // AWS-style full jitter
|
|
654
|
+
* const awsStyle = jitter(1)(exponential("1s"));
|
|
655
|
+
*
|
|
656
|
+
* // Conservative jitter (±25%)
|
|
657
|
+
* const conservative = jitter(0.25)(exponential("1s"));
|
|
658
|
+
* ```
|
|
659
|
+
*
|
|
660
|
+
* @group Delay
|
|
661
|
+
*/
|
|
662
|
+
export const jitter =
|
|
663
|
+
(factor = 0.5) =>
|
|
664
|
+
<Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
665
|
+
(deps) => {
|
|
666
|
+
const step = schedule(deps);
|
|
667
|
+
return (input) => {
|
|
668
|
+
const result = step(input);
|
|
669
|
+
if (!result.ok) return result;
|
|
670
|
+
const [output, delay] = result.value;
|
|
671
|
+
const jittered = delay * (1 - factor + deps.random.next() * 2 * factor);
|
|
672
|
+
return ok([output, Millis.orThrow(Math.max(0, Math.round(jittered)))]);
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Adds an initial delay before the first attempt.
|
|
678
|
+
*
|
|
679
|
+
* Subsequent attempts use the schedule's normal delays.
|
|
680
|
+
*
|
|
681
|
+
* ### Example
|
|
682
|
+
*
|
|
683
|
+
* ```ts
|
|
684
|
+
* // Wait 1s before first attempt, then exponential backoff
|
|
685
|
+
* const withWarmup = delayed("1s")(exponential("100ms"));
|
|
686
|
+
* ```
|
|
687
|
+
*
|
|
688
|
+
* @group Delay
|
|
689
|
+
*/
|
|
690
|
+
export const delayed = (initialDelay: Duration) => {
|
|
691
|
+
const initialMs = durationToMillis(initialDelay);
|
|
692
|
+
return <Output, Input>(
|
|
693
|
+
schedule: Schedule<Output, Input>,
|
|
694
|
+
): Schedule<Output, Input> =>
|
|
695
|
+
(deps) => {
|
|
696
|
+
const step = schedule(deps);
|
|
697
|
+
let first = true;
|
|
698
|
+
return (input) => {
|
|
699
|
+
const result = step(input);
|
|
700
|
+
if (!result.ok) return result;
|
|
701
|
+
if (first) {
|
|
702
|
+
first = false;
|
|
703
|
+
return ok([result.value[0], initialMs]);
|
|
704
|
+
}
|
|
705
|
+
return result;
|
|
706
|
+
};
|
|
707
|
+
};
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Adds a fixed delay to the schedule's existing delay.
|
|
712
|
+
*
|
|
713
|
+
* ### Example
|
|
714
|
+
*
|
|
715
|
+
* ```ts
|
|
716
|
+
* // Add 500ms to each delay
|
|
717
|
+
* const slower = addDelay("500ms")(exponential("100ms"));
|
|
718
|
+
* // Delays: 600ms, 700ms, 900ms, 1300ms, ...
|
|
719
|
+
* ```
|
|
720
|
+
*
|
|
721
|
+
* @group Delay
|
|
722
|
+
*/
|
|
723
|
+
export const addDelay = (
|
|
724
|
+
extra: Duration,
|
|
725
|
+
): (<Output, Input>(
|
|
726
|
+
schedule: Schedule<Output, Input>,
|
|
727
|
+
) => Schedule<Output, Input>) => {
|
|
728
|
+
const extraMs = durationToMillis(extra);
|
|
729
|
+
return modifyDelay((d) => d + extraMs);
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Transforms the delay of a schedule.
|
|
734
|
+
*
|
|
735
|
+
* More flexible than {@link maxDelay} — can implement any delay transformation.
|
|
736
|
+
*
|
|
737
|
+
* ### Example
|
|
738
|
+
*
|
|
739
|
+
* ```ts
|
|
740
|
+
* // Double all delays
|
|
741
|
+
* const slower = modifyDelay((d) => d * 2)(exponential("100ms"));
|
|
742
|
+
*
|
|
743
|
+
* // Cap at 10s (equivalent to maxDelay)
|
|
744
|
+
* const capped = modifyDelay((d) => Math.min(d, 10000))(exponential("1s"));
|
|
745
|
+
* ```
|
|
746
|
+
*
|
|
747
|
+
* @group Delay
|
|
748
|
+
*/
|
|
749
|
+
export const modifyDelay =
|
|
750
|
+
(f: (delay: Millis) => number) =>
|
|
751
|
+
<Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
752
|
+
(deps) => {
|
|
753
|
+
const step = schedule(deps);
|
|
754
|
+
return (input) => {
|
|
755
|
+
const result = step(input);
|
|
756
|
+
if (!result.ok) return result;
|
|
757
|
+
const [output, delay] = result.value;
|
|
758
|
+
return ok([output, Millis.orThrow(Math.max(0, Math.round(f(delay))))]);
|
|
759
|
+
};
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Adjusts delay by subtracting execution time.
|
|
764
|
+
*
|
|
765
|
+
* A simple combinator that subtracts the previous execution time from the
|
|
766
|
+
* schedule's delay. If execution took longer than the delay, returns 0.
|
|
767
|
+
*
|
|
768
|
+
* For window-aligned scheduling, use {@link fixed} instead.
|
|
769
|
+
*
|
|
770
|
+
* ### Example
|
|
771
|
+
*
|
|
772
|
+
* ```ts
|
|
773
|
+
* // Poll every 5s, accounting for execution time
|
|
774
|
+
* const polling = compensate(spaced("5s"));
|
|
775
|
+
* // If poll takes 1s → wait 4s. If poll takes 6s → wait 0s.
|
|
776
|
+
* ```
|
|
777
|
+
*
|
|
778
|
+
* @group Delay
|
|
779
|
+
*/
|
|
780
|
+
export const compensate =
|
|
781
|
+
<Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
782
|
+
(deps) => {
|
|
783
|
+
const step = schedule(deps);
|
|
784
|
+
const metrics = createScheduleStepMetrics(deps);
|
|
785
|
+
return (input) => {
|
|
786
|
+
const { elapsedSincePrevious } = metrics();
|
|
787
|
+
const result = step(input);
|
|
788
|
+
if (!result.ok) return result;
|
|
789
|
+
const [output, delay] = result.value;
|
|
790
|
+
const adjusted = Math.max(0, delay - elapsedSincePrevious);
|
|
791
|
+
return ok([output, adjusted as Millis]);
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Continues while the input satisfies a predicate.
|
|
797
|
+
*
|
|
798
|
+
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `false`.
|
|
799
|
+
* Useful for input-aware retry strategies, e.g., only retry certain error
|
|
800
|
+
* types.
|
|
801
|
+
*
|
|
802
|
+
* ### Example
|
|
803
|
+
*
|
|
804
|
+
* ```ts
|
|
805
|
+
* interface MyError extends Typed<"Transient" | "Fatal"> {}
|
|
806
|
+
*
|
|
807
|
+
* // Only retry transient errors
|
|
808
|
+
* const retryTransient = whileScheduleInput(
|
|
809
|
+
* (error: MyError) => error.type === "Transient",
|
|
810
|
+
* )(exponential("100ms"));
|
|
811
|
+
* ```
|
|
812
|
+
*
|
|
813
|
+
* @group Filtering
|
|
814
|
+
*/
|
|
815
|
+
export const whileScheduleInput =
|
|
816
|
+
<Input>(predicate: Predicate<Input>) =>
|
|
817
|
+
<Output>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
818
|
+
(deps) => {
|
|
819
|
+
const step = schedule(deps);
|
|
820
|
+
return (input) => {
|
|
821
|
+
if (!predicate(input)) return err(done());
|
|
822
|
+
return step(input);
|
|
823
|
+
};
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Continues until the input satisfies a predicate.
|
|
828
|
+
*
|
|
829
|
+
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `true`.
|
|
830
|
+
* Useful for stopping retry on specific error conditions.
|
|
831
|
+
*
|
|
832
|
+
* ### Example
|
|
833
|
+
*
|
|
834
|
+
* ```ts
|
|
835
|
+
* interface MyError extends Typed<"Transient" | "Fatal"> {}
|
|
836
|
+
*
|
|
837
|
+
* // Stop retrying on fatal errors
|
|
838
|
+
* const stopOnFatal = untilScheduleInput(
|
|
839
|
+
* (error: MyError) => error.type === "Fatal",
|
|
840
|
+
* )(exponential("100ms"));
|
|
841
|
+
* ```
|
|
842
|
+
*
|
|
843
|
+
* @group Filtering
|
|
844
|
+
*/
|
|
845
|
+
export const untilScheduleInput =
|
|
846
|
+
<Input>(predicate: Predicate<Input>) =>
|
|
847
|
+
<Output>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
848
|
+
(deps) => {
|
|
849
|
+
const step = schedule(deps);
|
|
850
|
+
return (input) => {
|
|
851
|
+
if (predicate(input)) return err(done());
|
|
852
|
+
return step(input);
|
|
853
|
+
};
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Continues while the output satisfies a predicate.
|
|
858
|
+
*
|
|
859
|
+
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `false`.
|
|
860
|
+
*
|
|
861
|
+
* ### Example
|
|
862
|
+
*
|
|
863
|
+
* ```ts
|
|
864
|
+
* // Continue while delay is under 5 seconds
|
|
865
|
+
* const capped = whileScheduleOutput((delay: Millis) => delay < 5000)(
|
|
866
|
+
* exponential("1s"),
|
|
867
|
+
* );
|
|
868
|
+
* ```
|
|
869
|
+
*
|
|
870
|
+
* @group Filtering
|
|
871
|
+
*/
|
|
872
|
+
export const whileScheduleOutput =
|
|
873
|
+
<Output>(predicate: Predicate<Output>) =>
|
|
874
|
+
<Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
875
|
+
(deps) => {
|
|
876
|
+
const step = schedule(deps);
|
|
877
|
+
return (input) => {
|
|
878
|
+
const result = step(input);
|
|
879
|
+
if (!result.ok) return result;
|
|
880
|
+
if (!predicate(result.value[0])) return err(done());
|
|
881
|
+
return result;
|
|
882
|
+
};
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Continues until the output satisfies a predicate.
|
|
887
|
+
*
|
|
888
|
+
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `true`.
|
|
889
|
+
*
|
|
890
|
+
* ### Example
|
|
891
|
+
*
|
|
892
|
+
* ```ts
|
|
893
|
+
* // Stop when delay reaches 1 second
|
|
894
|
+
* const limited = untilScheduleOutput((delay: Millis) => delay >= 1000)(
|
|
895
|
+
* exponential("100ms"),
|
|
896
|
+
* );
|
|
897
|
+
* ```
|
|
898
|
+
*
|
|
899
|
+
* @group Filtering
|
|
900
|
+
*/
|
|
901
|
+
export const untilScheduleOutput =
|
|
902
|
+
<Output>(predicate: Predicate<Output>) =>
|
|
903
|
+
<Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
904
|
+
(deps) => {
|
|
905
|
+
const step = schedule(deps);
|
|
906
|
+
return (input) => {
|
|
907
|
+
const result = step(input);
|
|
908
|
+
if (!result.ok) return result;
|
|
909
|
+
if (predicate(result.value[0])) return err(done());
|
|
910
|
+
return result;
|
|
911
|
+
};
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Resets the schedule after a period of inactivity.
|
|
916
|
+
*
|
|
917
|
+
* If `elapsedSincePrevious` is greater than or equal to `duration`, creates a
|
|
918
|
+
* fresh state. Useful for circuit breakers that should "forget" failures after
|
|
919
|
+
* idle time.
|
|
920
|
+
*
|
|
921
|
+
* ### Example
|
|
922
|
+
*
|
|
923
|
+
* ```ts
|
|
924
|
+
* // Reset retry count after 1 minute of success
|
|
925
|
+
* const circuitBreaker = resetScheduleAfter("1m")(
|
|
926
|
+
* take(5)(exponential("1s")),
|
|
927
|
+
* );
|
|
928
|
+
* ```
|
|
929
|
+
*
|
|
930
|
+
* @group State
|
|
931
|
+
*/
|
|
932
|
+
export const resetScheduleAfter = (duration: Duration) => {
|
|
933
|
+
const resetMs = durationToMillis(duration);
|
|
934
|
+
return <Output, Input>(
|
|
935
|
+
schedule: Schedule<Output, Input>,
|
|
936
|
+
): Schedule<Output, Input> =>
|
|
937
|
+
(deps) => {
|
|
938
|
+
let step = schedule(deps);
|
|
939
|
+
const metrics = createScheduleStepMetrics(deps);
|
|
940
|
+
return (input) => {
|
|
941
|
+
const { elapsedSincePrevious } = metrics();
|
|
942
|
+
if (elapsedSincePrevious >= resetMs) {
|
|
943
|
+
step = schedule(deps);
|
|
944
|
+
}
|
|
945
|
+
return step(input);
|
|
946
|
+
};
|
|
947
|
+
};
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Transforms the output of a schedule.
|
|
952
|
+
*
|
|
953
|
+
* The delay (second tuple element) remains unchanged.
|
|
954
|
+
*
|
|
955
|
+
* ### Example
|
|
956
|
+
*
|
|
957
|
+
* ```ts
|
|
958
|
+
* import { exponential, mapSchedule } from "@evolu/common";
|
|
959
|
+
*
|
|
960
|
+
* const schedule = mapSchedule((delay) => ({
|
|
961
|
+
* delay,
|
|
962
|
+
* doubled: delay * 2,
|
|
963
|
+
* }))(exponential("100ms"));
|
|
964
|
+
* ```
|
|
965
|
+
*
|
|
966
|
+
* @group Transform
|
|
967
|
+
*/
|
|
968
|
+
export const mapSchedule =
|
|
969
|
+
<A, B>(f: (a: A) => B) =>
|
|
970
|
+
<Input>(schedule: Schedule<A, Input>): Schedule<B, Input> =>
|
|
971
|
+
(deps) => {
|
|
972
|
+
const step = schedule(deps);
|
|
973
|
+
return (input) => {
|
|
974
|
+
const result = step(input);
|
|
975
|
+
if (!result.ok) return result;
|
|
976
|
+
const [output, delay] = result.value;
|
|
977
|
+
return ok([f(output), delay]);
|
|
978
|
+
};
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Creates a schedule that outputs its input, or wraps an existing schedule to
|
|
983
|
+
* output input instead of the original output.
|
|
984
|
+
*
|
|
985
|
+
* When called with no arguments, creates a schedule that outputs its input
|
|
986
|
+
* directly (the "identity" schedule). When called with a schedule, wraps it to
|
|
987
|
+
* preserve timing behavior but replace output with input.
|
|
988
|
+
*
|
|
989
|
+
* ### Example
|
|
990
|
+
*
|
|
991
|
+
* ```ts
|
|
992
|
+
* import { exponential, passthrough } from "@evolu/common";
|
|
993
|
+
*
|
|
994
|
+
* interface MyError {
|
|
995
|
+
* readonly message: string;
|
|
996
|
+
* }
|
|
997
|
+
*
|
|
998
|
+
* // Constructor: output equals input
|
|
999
|
+
* const identity = passthrough<MyError>();
|
|
1000
|
+
*
|
|
1001
|
+
* // Combinator: preserve timing, replace output
|
|
1002
|
+
* const withInput = passthrough(exponential("100ms"));
|
|
1003
|
+
* ```
|
|
1004
|
+
*
|
|
1005
|
+
* @group Constructors
|
|
1006
|
+
*/
|
|
1007
|
+
export function passthrough<A>(): Schedule<A, A>;
|
|
1008
|
+
/** @group Transform */
|
|
1009
|
+
export function passthrough<Output, Input>(
|
|
1010
|
+
schedule: Schedule<Output, Input>,
|
|
1011
|
+
): Schedule<Input, Input>;
|
|
1012
|
+
export function passthrough<Output, Input>(
|
|
1013
|
+
schedule?: Schedule<Output, Input>,
|
|
1014
|
+
): Schedule<Input, Input> {
|
|
1015
|
+
if (schedule === undefined) {
|
|
1016
|
+
return () => (input) => ok([input, minMillis]);
|
|
1017
|
+
}
|
|
1018
|
+
return (deps) => {
|
|
1019
|
+
const step = schedule(deps);
|
|
1020
|
+
return (input) => {
|
|
1021
|
+
const result = step(input);
|
|
1022
|
+
if (!result.ok) return result;
|
|
1023
|
+
return ok([input, result.value[1]]);
|
|
1024
|
+
};
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Folds over the outputs of a schedule, accumulating state.
|
|
1030
|
+
*
|
|
1031
|
+
* Each step outputs the accumulated value. Useful for tracking totals,
|
|
1032
|
+
* collecting outputs, or building up metadata across attempts.
|
|
1033
|
+
*
|
|
1034
|
+
* ### Example
|
|
1035
|
+
*
|
|
1036
|
+
* ```ts
|
|
1037
|
+
* // Track total delay spent
|
|
1038
|
+
* const withTotal = foldSchedule(
|
|
1039
|
+
* 0,
|
|
1040
|
+
* (total: number, delay: Millis) => total + delay,
|
|
1041
|
+
* )(exponential("100ms"));
|
|
1042
|
+
* // Outputs: 100, 300, 700, 1500, ... (cumulative)
|
|
1043
|
+
*
|
|
1044
|
+
* // Collect all outputs
|
|
1045
|
+
* const collected = foldSchedule([] as Millis[], (acc, delay: Millis) => [
|
|
1046
|
+
* ...acc,
|
|
1047
|
+
* delay,
|
|
1048
|
+
* ])(take(3)(spaced("1s")));
|
|
1049
|
+
* // Outputs: [1000], [1000, 1000], [1000, 1000, 1000]
|
|
1050
|
+
*
|
|
1051
|
+
* // Count attempts with custom output
|
|
1052
|
+
* const counted = foldSchedule(
|
|
1053
|
+
* { attempts: 0, lastDelay: 0 as Millis },
|
|
1054
|
+
* (acc, delay: Millis) => ({
|
|
1055
|
+
* attempts: acc.attempts + 1,
|
|
1056
|
+
* lastDelay: delay,
|
|
1057
|
+
* }),
|
|
1058
|
+
* )(exponential("100ms"));
|
|
1059
|
+
* ```
|
|
1060
|
+
*
|
|
1061
|
+
* @group Transform
|
|
1062
|
+
*/
|
|
1063
|
+
export const foldSchedule =
|
|
1064
|
+
<Z, Output>(initial: Z, f: (acc: Z, output: Output) => Z) =>
|
|
1065
|
+
<Input>(schedule: Schedule<Output, Input>): Schedule<Z, Input> =>
|
|
1066
|
+
(deps) => {
|
|
1067
|
+
const step = schedule(deps);
|
|
1068
|
+
let acc = initial;
|
|
1069
|
+
return (input) => {
|
|
1070
|
+
const result = step(input);
|
|
1071
|
+
if (!result.ok) return result;
|
|
1072
|
+
const [output, delay] = result.value;
|
|
1073
|
+
acc = f(acc, output);
|
|
1074
|
+
return ok([acc, delay]);
|
|
1075
|
+
};
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Wraps a schedule to output the number of repetitions instead of original
|
|
1080
|
+
* output.
|
|
1081
|
+
*
|
|
1082
|
+
* Outputs 0, 1, 2, ... while preserving the underlying schedule's timing and
|
|
1083
|
+
* termination behavior.
|
|
1084
|
+
*
|
|
1085
|
+
* ### Example
|
|
1086
|
+
*
|
|
1087
|
+
* ```ts
|
|
1088
|
+
* // Track how many retries occurred
|
|
1089
|
+
* const counted = repetitions(exponential("100ms"));
|
|
1090
|
+
* // Outputs: 0, 1, 2, ... with exponential delays
|
|
1091
|
+
* ```
|
|
1092
|
+
*
|
|
1093
|
+
* @group Transform
|
|
1094
|
+
*/
|
|
1095
|
+
export const repetitions = <Output, Input>(
|
|
1096
|
+
schedule: Schedule<Output, Input>,
|
|
1097
|
+
): Schedule<number, Input> => foldSchedule(-1, (n) => n + 1)(schedule);
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Outputs the delay between recurrences.
|
|
1101
|
+
*
|
|
1102
|
+
* Wraps a schedule to output its delay (in milliseconds) instead of the
|
|
1103
|
+
* original output. Useful for monitoring or logging delay patterns.
|
|
1104
|
+
*
|
|
1105
|
+
* ### Example
|
|
1106
|
+
*
|
|
1107
|
+
* ```ts
|
|
1108
|
+
* // Monitor exponential delays
|
|
1109
|
+
* const monitorDelays = delays(exponential("100ms"));
|
|
1110
|
+
* // Outputs: 100, 200, 400, 800, ... (the delays themselves)
|
|
1111
|
+
*
|
|
1112
|
+
* // Log delays for debugging
|
|
1113
|
+
* const logged = tapScheduleOutput(console.log)(
|
|
1114
|
+
* delays(exponential("100ms")),
|
|
1115
|
+
* );
|
|
1116
|
+
* ```
|
|
1117
|
+
*
|
|
1118
|
+
* @group Transform
|
|
1119
|
+
*/
|
|
1120
|
+
export const delays =
|
|
1121
|
+
<Output, Input>(schedule: Schedule<Output, Input>): Schedule<Millis, Input> =>
|
|
1122
|
+
(deps) => {
|
|
1123
|
+
const step = schedule(deps);
|
|
1124
|
+
return (input) => {
|
|
1125
|
+
const result = step(input);
|
|
1126
|
+
if (!result.ok) return result;
|
|
1127
|
+
const [, delay] = result.value;
|
|
1128
|
+
return ok([delay, delay]);
|
|
1129
|
+
};
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Collects all outputs into an array.
|
|
1134
|
+
*
|
|
1135
|
+
* Each step outputs an array containing all outputs so far.
|
|
1136
|
+
*
|
|
1137
|
+
* ### Example
|
|
1138
|
+
*
|
|
1139
|
+
* ```ts
|
|
1140
|
+
* // Collect all delays
|
|
1141
|
+
* const collected = collectAllScheduleOutputs(take(3)(spaced("100ms")));
|
|
1142
|
+
* // Outputs: [100], [100, 100], [100, 100, 100]
|
|
1143
|
+
* ```
|
|
1144
|
+
*
|
|
1145
|
+
* @group Collection
|
|
1146
|
+
*/
|
|
1147
|
+
export const collectAllScheduleOutputs = <Output, Input>(
|
|
1148
|
+
schedule: Schedule<Output, Input>,
|
|
1149
|
+
): Schedule<ReadonlyArray<Output>, Input> =>
|
|
1150
|
+
foldSchedule<ReadonlyArray<Output>, Output>([], (acc, out) => [...acc, out])(
|
|
1151
|
+
schedule,
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Collects all inputs into an array.
|
|
1156
|
+
*
|
|
1157
|
+
* Each step outputs an array containing all inputs received so far. Mirror of
|
|
1158
|
+
* {@link collectAllScheduleOutputs} but for inputs.
|
|
1159
|
+
*
|
|
1160
|
+
* ### Example
|
|
1161
|
+
*
|
|
1162
|
+
* ```ts
|
|
1163
|
+
* // Collect all errors during retry
|
|
1164
|
+
* const errorHistory = collectScheduleInputs(
|
|
1165
|
+
* take(3)(exponential("100ms")),
|
|
1166
|
+
* );
|
|
1167
|
+
* // After 3 retries, outputs array of all error inputs
|
|
1168
|
+
* ```
|
|
1169
|
+
*
|
|
1170
|
+
* @group Collection
|
|
1171
|
+
*/
|
|
1172
|
+
export const collectScheduleInputs = <Output, Input>(
|
|
1173
|
+
schedule: Schedule<Output, Input>,
|
|
1174
|
+
): Schedule<ReadonlyArray<Input>, Input> =>
|
|
1175
|
+
collectAllScheduleOutputs(passthrough(schedule));
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Collects outputs while a predicate is true.
|
|
1179
|
+
*
|
|
1180
|
+
* More flexible than {@link collectAllScheduleOutputs} — stops collecting when
|
|
1181
|
+
* the predicate returns false.
|
|
1182
|
+
*
|
|
1183
|
+
* ### Example
|
|
1184
|
+
*
|
|
1185
|
+
* ```ts
|
|
1186
|
+
* // Collect delays while under 1 second
|
|
1187
|
+
* const smallDelays = collectWhileScheduleOutput(
|
|
1188
|
+
* (delay: Millis) => delay < 1000,
|
|
1189
|
+
* )(exponential("100ms"));
|
|
1190
|
+
* // Outputs: [100], [100, 200], [100, 200, 400], [100, 200, 400, 800], stops
|
|
1191
|
+
* ```
|
|
1192
|
+
*
|
|
1193
|
+
* @group Collection
|
|
1194
|
+
*/
|
|
1195
|
+
export const collectWhileScheduleOutput =
|
|
1196
|
+
<Output>(predicate: Predicate<Output>) =>
|
|
1197
|
+
<Input>(
|
|
1198
|
+
schedule: Schedule<Output, Input>,
|
|
1199
|
+
): Schedule<ReadonlyArray<Output>, Input> =>
|
|
1200
|
+
collectAllScheduleOutputs(whileScheduleOutput(predicate)(schedule));
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Collects outputs until a predicate becomes true.
|
|
1204
|
+
*
|
|
1205
|
+
* Mirror of {@link collectWhileScheduleOutput} — stops collecting when the
|
|
1206
|
+
* predicate returns true (inclusive of the matching output).
|
|
1207
|
+
*
|
|
1208
|
+
* ### Example
|
|
1209
|
+
*
|
|
1210
|
+
* ```ts
|
|
1211
|
+
* // Collect delays until reaching 1 second
|
|
1212
|
+
* const untilLarge = collectUntilScheduleOutput(
|
|
1213
|
+
* (delay: Millis) => delay >= 1000,
|
|
1214
|
+
* )(exponential("100ms"));
|
|
1215
|
+
* // Outputs: [100], [100, 200], [100, 200, 400], [100, 200, 400, 800], stops
|
|
1216
|
+
* ```
|
|
1217
|
+
*
|
|
1218
|
+
* @group Collection
|
|
1219
|
+
*/
|
|
1220
|
+
export const collectUntilScheduleOutput =
|
|
1221
|
+
<Output>(predicate: Predicate<Output>) =>
|
|
1222
|
+
<Input>(
|
|
1223
|
+
schedule: Schedule<Output, Input>,
|
|
1224
|
+
): Schedule<ReadonlyArray<Output>, Input> =>
|
|
1225
|
+
collectAllScheduleOutputs(untilScheduleOutput(predicate)(schedule));
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Sequences schedules: runs each until it stops, then continues with the next.
|
|
1229
|
+
*
|
|
1230
|
+
* Useful for adaptive strategies that start aggressive and become more
|
|
1231
|
+
* conservative over time.
|
|
1232
|
+
*
|
|
1233
|
+
* ### Example
|
|
1234
|
+
*
|
|
1235
|
+
* ```ts
|
|
1236
|
+
* // Fast retries first, then slower, then final fallback
|
|
1237
|
+
* const adaptive = sequenceSchedules(
|
|
1238
|
+
* take(3)(exponential("100ms")),
|
|
1239
|
+
* take(5)(fixed("500ms")),
|
|
1240
|
+
* fixed("1s"),
|
|
1241
|
+
* );
|
|
1242
|
+
* // Runs: 100ms, 200ms, 400ms, then 500ms×5, then 1s forever
|
|
1243
|
+
* ```
|
|
1244
|
+
*
|
|
1245
|
+
* @group Composition
|
|
1246
|
+
*/
|
|
1247
|
+
export const sequenceSchedules =
|
|
1248
|
+
<Output, Input>(
|
|
1249
|
+
...schedules: ReadonlyArray<Schedule<Output, Input>>
|
|
1250
|
+
): Schedule<Output, Input> =>
|
|
1251
|
+
(deps) => {
|
|
1252
|
+
let index = 0;
|
|
1253
|
+
type Step =
|
|
1254
|
+
| ((input: Input) => NextResult<readonly [Output, Millis]>)
|
|
1255
|
+
| null;
|
|
1256
|
+
let currentStep: Step = schedules.length > 0 ? schedules[0](deps) : null;
|
|
1257
|
+
return (input) => {
|
|
1258
|
+
while (currentStep !== null) {
|
|
1259
|
+
const result = currentStep(input);
|
|
1260
|
+
if (result.ok) return result;
|
|
1261
|
+
|
|
1262
|
+
// Current exhausted, try next.
|
|
1263
|
+
index++;
|
|
1264
|
+
currentStep = index < schedules.length ? schedules[index](deps) : null;
|
|
1265
|
+
}
|
|
1266
|
+
return err(done());
|
|
1267
|
+
};
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Combines two schedules with AND semantics.
|
|
1272
|
+
*
|
|
1273
|
+
* Continues only while both schedules want to continue. Uses the maximum delay.
|
|
1274
|
+
*
|
|
1275
|
+
* ### Example
|
|
1276
|
+
*
|
|
1277
|
+
* ```ts
|
|
1278
|
+
* // Retry up to 5 times AND within 30 seconds (both conditions must be met)
|
|
1279
|
+
* const both = intersectSchedules(
|
|
1280
|
+
* take(5)(exponential("1s")),
|
|
1281
|
+
* maxElapsed("30s")(forever),
|
|
1282
|
+
* );
|
|
1283
|
+
* ```
|
|
1284
|
+
*
|
|
1285
|
+
* @group Composition
|
|
1286
|
+
*/
|
|
1287
|
+
export const intersectSchedules =
|
|
1288
|
+
<OutputA, OutputB, Input>(
|
|
1289
|
+
a: Schedule<OutputA, Input>,
|
|
1290
|
+
b: Schedule<OutputB, Input>,
|
|
1291
|
+
): Schedule<[OutputA, OutputB], Input> =>
|
|
1292
|
+
(deps) => {
|
|
1293
|
+
const stepA = a(deps);
|
|
1294
|
+
const stepB = b(deps);
|
|
1295
|
+
return (input) => {
|
|
1296
|
+
const resultA = stepA(input);
|
|
1297
|
+
const resultB = stepB(input);
|
|
1298
|
+
if (!resultA.ok || !resultB.ok) return err(done());
|
|
1299
|
+
const [outputA, delayA] = resultA.value;
|
|
1300
|
+
const [outputB, delayB] = resultB.value;
|
|
1301
|
+
return ok([[outputA, outputB], Millis.orThrow(Math.max(delayA, delayB))]);
|
|
1302
|
+
};
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Combines two schedules with OR semantics.
|
|
1307
|
+
*
|
|
1308
|
+
* Continues while either schedule wants to continue. Uses the minimum delay.
|
|
1309
|
+
*
|
|
1310
|
+
* ### Example
|
|
1311
|
+
*
|
|
1312
|
+
* ```ts
|
|
1313
|
+
* // Retry up to 5 times OR up to 30 seconds, whichever is longer
|
|
1314
|
+
* const either = unionSchedules(
|
|
1315
|
+
* take(5)(exponential("1s")),
|
|
1316
|
+
* maxElapsed("30s")(forever),
|
|
1317
|
+
* );
|
|
1318
|
+
* ```
|
|
1319
|
+
*
|
|
1320
|
+
* @group Composition
|
|
1321
|
+
*/
|
|
1322
|
+
export const unionSchedules =
|
|
1323
|
+
<OutputA, OutputB, Input>(
|
|
1324
|
+
a: Schedule<OutputA, Input>,
|
|
1325
|
+
b: Schedule<OutputB, Input>,
|
|
1326
|
+
): Schedule<OutputA | OutputB, Input> =>
|
|
1327
|
+
(deps) => {
|
|
1328
|
+
const stepA = a(deps);
|
|
1329
|
+
const stepB = b(deps);
|
|
1330
|
+
return (input) => {
|
|
1331
|
+
const resultA = stepA(input);
|
|
1332
|
+
const resultB = stepB(input);
|
|
1333
|
+
|
|
1334
|
+
if (!resultA.ok && !resultB.ok) return err(done());
|
|
1335
|
+
if (!resultA.ok) return resultB;
|
|
1336
|
+
if (!resultB.ok) return resultA;
|
|
1337
|
+
|
|
1338
|
+
const [outputA, delayA] = resultA.value;
|
|
1339
|
+
const [outputB, delayB] = resultB.value;
|
|
1340
|
+
// Use minimum delay, output from the one with smaller delay
|
|
1341
|
+
const minDelay = Math.min(delayA, delayB) as Millis;
|
|
1342
|
+
return delayA <= delayB
|
|
1343
|
+
? ok([outputA, minDelay])
|
|
1344
|
+
: ok([outputB, minDelay]);
|
|
1345
|
+
};
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Selects between two schedules based on input.
|
|
1350
|
+
*
|
|
1351
|
+
* If {@link Predicate} returns `true`, uses `altSchedule`; otherwise uses the
|
|
1352
|
+
* base schedule. Useful for implementing error-aware backoff where certain
|
|
1353
|
+
* errors (e.g., throttling) use different delays.
|
|
1354
|
+
*
|
|
1355
|
+
* ### Example
|
|
1356
|
+
*
|
|
1357
|
+
* ```ts
|
|
1358
|
+
* interface MyError extends Typed<"Throttled" | "NetworkError"> {}
|
|
1359
|
+
*
|
|
1360
|
+
* const awsWithThrottling = whenInput<MyError, Millis>(
|
|
1361
|
+
* (error) => error.type === "Throttled",
|
|
1362
|
+
* exponential("1s"), // throttled: 1s base
|
|
1363
|
+
* )(exponential("100ms")); // normal: 100ms base
|
|
1364
|
+
* ```
|
|
1365
|
+
*
|
|
1366
|
+
* @group Composition
|
|
1367
|
+
*/
|
|
1368
|
+
export const whenInput =
|
|
1369
|
+
<Input, Output>(
|
|
1370
|
+
predicate: Predicate<Input>,
|
|
1371
|
+
altSchedule: Schedule<Output, Input>,
|
|
1372
|
+
) =>
|
|
1373
|
+
(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
1374
|
+
(deps) => {
|
|
1375
|
+
const normalStep = schedule(deps);
|
|
1376
|
+
const altStep = altSchedule(deps);
|
|
1377
|
+
return (input) => {
|
|
1378
|
+
if (predicate(input)) return altStep(input);
|
|
1379
|
+
return normalStep(input);
|
|
1380
|
+
};
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Executes a side effect for every output without altering the schedule.
|
|
1385
|
+
*
|
|
1386
|
+
* Useful for logging, monitoring, or debugging without changing schedule
|
|
1387
|
+
* behavior.
|
|
1388
|
+
*
|
|
1389
|
+
* ### Example
|
|
1390
|
+
*
|
|
1391
|
+
* ```ts
|
|
1392
|
+
* // Log each delay for debugging
|
|
1393
|
+
* const logged = tapScheduleOutput((delay: Millis) => {
|
|
1394
|
+
* console.log(`Next delay: ${delay}ms`);
|
|
1395
|
+
* })(exponential("100ms"));
|
|
1396
|
+
*
|
|
1397
|
+
* // Track metrics
|
|
1398
|
+
* const recorded: Array<Millis> = [];
|
|
1399
|
+
* const tracked = tapScheduleOutput((delay: Millis) => {
|
|
1400
|
+
* recorded.push(delay);
|
|
1401
|
+
* })(retryStrategyAws);
|
|
1402
|
+
* ```
|
|
1403
|
+
*
|
|
1404
|
+
* @group Side effects
|
|
1405
|
+
*/
|
|
1406
|
+
export const tapScheduleOutput =
|
|
1407
|
+
<Output>(f: (output: Output) => void) =>
|
|
1408
|
+
<Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
1409
|
+
(deps) => {
|
|
1410
|
+
const step = schedule(deps);
|
|
1411
|
+
return (input) => {
|
|
1412
|
+
const result = step(input);
|
|
1413
|
+
if (!result.ok) return result;
|
|
1414
|
+
f(result.value[0]);
|
|
1415
|
+
return result;
|
|
1416
|
+
};
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Executes a side effect for every input without altering the schedule.
|
|
1421
|
+
*
|
|
1422
|
+
* Useful for logging errors during retry or monitoring what values are being
|
|
1423
|
+
* processed.
|
|
1424
|
+
*
|
|
1425
|
+
* ### Example
|
|
1426
|
+
*
|
|
1427
|
+
* ```ts
|
|
1428
|
+
* interface MyError extends Typed<string> {}
|
|
1429
|
+
*
|
|
1430
|
+
* const retrySchedule: Schedule<Millis, MyError> = exponential("100ms");
|
|
1431
|
+
*
|
|
1432
|
+
* // Log each error during retry
|
|
1433
|
+
* const logged = tapScheduleInput((error: MyError) => {
|
|
1434
|
+
* console.log(`Retrying after error: ${error.type}`);
|
|
1435
|
+
* })(retrySchedule);
|
|
1436
|
+
*
|
|
1437
|
+
* // Track retry reasons
|
|
1438
|
+
* const reasons: Array<string> = [];
|
|
1439
|
+
* const tracked = tapScheduleInput((error: MyError) => {
|
|
1440
|
+
* reasons.push(error.type);
|
|
1441
|
+
* })(retrySchedule);
|
|
1442
|
+
* ```
|
|
1443
|
+
*
|
|
1444
|
+
* @group Side effects
|
|
1445
|
+
*/
|
|
1446
|
+
export const tapScheduleInput =
|
|
1447
|
+
<Input>(f: (input: Input) => void) =>
|
|
1448
|
+
<Output>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
1449
|
+
(deps) => {
|
|
1450
|
+
const step = schedule(deps);
|
|
1451
|
+
return (input) => {
|
|
1452
|
+
f(input);
|
|
1453
|
+
return step(input);
|
|
1454
|
+
};
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* AWS standard retry strategy.
|
|
1459
|
+
*
|
|
1460
|
+
* Exponential backoff (100ms base), max 2 retries (3 total attempts), 20s cap,
|
|
1461
|
+
* full jitter.
|
|
1462
|
+
*
|
|
1463
|
+
* @group Retry Strategies
|
|
1464
|
+
* @see https://github.com/aws/aws-sdk-java-v2/blob/master/core/retries/src/main/java/software/amazon/awssdk/retries/StandardRetryStrategy.java
|
|
1465
|
+
* @see https://github.com/aws/aws-sdk-java-v2/blob/master/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java
|
|
1466
|
+
*/
|
|
1467
|
+
export const retryStrategyAws: Schedule<Millis> = /*#__PURE__*/ jitter(1)(
|
|
1468
|
+
/*#__PURE__*/ maxDelay("20s")(
|
|
1469
|
+
/*#__PURE__*/ take(2)(/*#__PURE__*/ exponential("100ms")),
|
|
1470
|
+
),
|
|
1471
|
+
);
|