@evolu/common 8.0.0-next.4 → 8.0.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/dist/src/Array.d.ts +279 -173
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +105 -101
- package/dist/src/Assert.d.ts +44 -44
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +44 -32
- package/dist/src/BigInt.d.ts +6 -3
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -2
- package/dist/src/Brand.d.ts +27 -40
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Buffer.d.ts +27 -11
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Cache.d.ts +9 -4
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +8 -3
- package/dist/src/Callbacks.d.ts +30 -19
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Console.d.ts +145 -66
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +91 -43
- package/dist/src/Crypto.d.ts +85 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +44 -20
- package/dist/src/Eq.d.ts +87 -21
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +67 -19
- package/dist/src/Error.d.ts +5 -3
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +16 -0
- package/dist/src/Function.d.ts +119 -58
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +86 -43
- package/dist/src/Http.d.ts +394 -0
- package/dist/src/Http.d.ts.map +1 -0
- package/dist/src/Http.js +102 -0
- package/dist/src/Identicon.d.ts +16 -7
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +14 -5
- package/dist/src/LeakDetector.d.ts +88 -0
- package/dist/src/LeakDetector.d.ts.map +1 -0
- package/dist/src/LeakDetector.js +86 -0
- package/dist/src/LockManager.d.ts +18 -9
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +7 -8
- package/dist/src/Lookup.d.ts +43 -13
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +28 -8
- package/dist/src/Microtask.d.ts +1 -1
- package/dist/src/Microtask.d.ts.map +1 -1
- package/dist/src/Microtask.js +3 -7
- package/dist/src/Number.d.ts +49 -13
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +34 -5
- package/dist/src/Object.d.ts +160 -40
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +92 -46
- package/dist/src/Option.d.ts +10 -7
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +9 -6
- package/dist/src/Order.d.ts +21 -21
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +18 -21
- package/dist/src/Platform.d.ts +31 -0
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +98 -0
- package/dist/src/Random.d.ts +32 -17
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +12 -8
- package/dist/src/Redacted.d.ts +43 -29
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +14 -3
- package/dist/src/Ref.d.ts +11 -3
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +2 -2
- package/dist/src/RefCount.d.ts +2 -6
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/RefCount.js +33 -90
- package/dist/src/Relation.d.ts +70 -5
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +77 -2
- package/dist/src/Resource.d.ts +590 -155
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +592 -343
- package/dist/src/Result.d.ts +838 -191
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +163 -102
- package/dist/src/Schedule.d.ts +684 -231
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +766 -295
- package/dist/src/Set.d.ts +89 -40
- package/dist/src/Set.d.ts.map +1 -1
- package/dist/src/Set.js +50 -29
- package/dist/src/Sqlite.d.ts +29 -72
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +41 -46
- package/dist/src/StackTrace.d.ts +40 -0
- package/dist/src/StackTrace.d.ts.map +1 -0
- package/dist/src/StackTrace.js +140 -0
- package/dist/src/Store.d.ts +5 -4
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +5 -4
- package/dist/src/String.d.ts +7 -1
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +20 -7
- package/dist/src/Task.d.ts +3572 -1770
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1879 -1274
- package/dist/src/Test.d.ts +17 -125
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +17 -68
- package/dist/src/Time.d.ts +108 -49
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +172 -73
- package/dist/src/Type.d.ts +3542 -1926
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +3070 -1784
- package/dist/src/Types.d.ts +200 -108
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +39 -8
- package/dist/src/WebSocket.d.ts +48 -23
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +15 -6
- package/dist/src/Worker.d.ts +94 -36
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +58 -24
- package/dist/src/index.d.ts +55 -52
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/intl/cs.d.ts +94 -0
- package/dist/src/intl/cs.d.ts.map +1 -0
- package/dist/src/intl/cs.js +220 -0
- package/dist/src/intl/index.d.ts +7 -0
- package/dist/src/intl/index.d.ts.map +1 -0
- package/dist/src/intl/index.js +6 -0
- package/dist/src/local-first/Db.d.ts +12 -8
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +109 -132
- package/dist/src/local-first/Error.d.ts +4 -4
- package/dist/src/local-first/Evolu.d.ts +338 -92
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +47 -55
- package/dist/src/local-first/LocalAuth.d.ts +4 -4
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/Owner.d.ts +61 -24
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +45 -8
- package/dist/src/local-first/Protocol.d.ts +44 -31
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +36 -13
- package/dist/src/local-first/Query.d.ts +147 -62
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +112 -58
- package/dist/src/local-first/Relay.d.ts +44 -14
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +92 -41
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +25 -8
- package/dist/src/local-first/Shared.d.ts +23 -21
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +100 -57
- package/dist/src/local-first/Storage.d.ts +53 -32
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +194 -234
- package/dist/src/local-first/Timestamp.d.ts +21 -21
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +2 -2
- package/dist/src/local-first/index.d.ts +11 -11
- package/package.json +14 -13
- package/src/Array.ts +306 -205
- package/src/Assert.ts +54 -69
- package/src/BigInt.ts +6 -3
- package/src/Brand.ts +27 -40
- package/src/Buffer.ts +28 -12
- package/src/Cache.ts +9 -4
- package/src/Callbacks.ts +30 -19
- package/src/Console.ts +156 -84
- package/src/Crypto.ts +83 -41
- package/src/Eq.ts +95 -22
- package/src/Error.ts +8 -4
- package/src/Function.ts +167 -59
- package/src/Http.ts +549 -0
- package/src/Identicon.ts +16 -7
- package/src/LeakDetector.ts +182 -0
- package/src/LockManager.ts +43 -39
- package/src/Lookup.ts +62 -28
- package/src/Microtask.ts +19 -23
- package/src/Number.ts +64 -12
- package/src/Object.ts +173 -49
- package/src/Option.ts +10 -7
- package/src/Order.ts +23 -21
- package/src/Platform.ts +136 -0
- package/src/Polyfills.ts +6 -12
- package/src/Random.ts +32 -19
- package/src/Redacted.ts +44 -30
- package/src/Ref.ts +14 -6
- package/src/RefCount.ts +38 -80
- package/src/Relation.ts +202 -10
- package/src/Resource.ts +1199 -591
- package/src/Result.ts +930 -271
- package/src/Schedule.ts +859 -343
- package/src/Set.ts +89 -40
- package/src/Sqlite.ts +144 -138
- package/src/StackTrace.ts +230 -0
- package/src/Store.ts +8 -7
- package/src/String.ts +22 -5
- package/src/Task.ts +5101 -2832
- package/src/Test.ts +20 -168
- package/src/Time.ts +293 -90
- package/src/Type.ts +10034 -4117
- package/src/Types.ts +227 -110
- package/src/WebSocket.ts +64 -36
- package/src/Worker.ts +180 -72
- package/src/index.ts +55 -52
- package/src/intl/cs.ts +388 -0
- package/src/intl/index.ts +7 -0
- package/src/local-first/Db.ts +152 -179
- package/src/local-first/Error.ts +4 -4
- package/src/local-first/Evolu.ts +495 -250
- package/src/local-first/LocalAuth.ts +6 -12
- package/src/local-first/Owner.ts +58 -21
- package/src/local-first/Protocol.ts +49 -31
- package/src/local-first/Query.ts +146 -61
- package/src/local-first/Relay.ts +51 -21
- package/src/local-first/Schema.ts +109 -51
- package/src/local-first/Shared.ts +266 -226
- package/src/local-first/Storage.ts +266 -264
- package/src/local-first/Timestamp.ts +25 -18
- package/src/local-first/index.ts +11 -11
- package/dist/src/Skiplist.d.ts +0 -28
- package/dist/src/Skiplist.d.ts.map +0 -1
- package/dist/src/Skiplist.js +0 -64
- package/dist/src/Tracer.d.ts +0 -48
- package/dist/src/Tracer.d.ts.map +0 -1
- package/dist/src/Tracer.js +0 -6
- package/src/Skiplist.ts +0 -108
- package/src/Tracer.ts +0 -54
package/src/Schedule.ts
CHANGED
|
@@ -4,26 +4,38 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import {
|
|
8
|
+
fibonacciAt,
|
|
9
|
+
FibonacciIndex,
|
|
10
|
+
increment,
|
|
11
|
+
max,
|
|
12
|
+
min,
|
|
13
|
+
type Percentage,
|
|
14
|
+
percentageToRatio,
|
|
15
|
+
} from "./Number.ts";
|
|
16
|
+
import type { RandomDep } from "./Random.ts";
|
|
17
|
+
import { done, err, type NextResult, ok } from "./Result.ts";
|
|
18
|
+
import type { repeat, RepeatAttempt, retry, RetryAttempt } from "./Task.ts";
|
|
12
19
|
import {
|
|
13
20
|
type Duration,
|
|
21
|
+
type DurationLiteral,
|
|
14
22
|
durationToMillis,
|
|
15
23
|
Millis,
|
|
16
24
|
minMillis,
|
|
25
|
+
PositiveMillis,
|
|
17
26
|
saturateMillis,
|
|
18
27
|
type TimeDep,
|
|
19
|
-
} from "./Time.
|
|
28
|
+
} from "./Time.ts";
|
|
20
29
|
import {
|
|
30
|
+
assertType,
|
|
31
|
+
type Int0To100OrNonNegativeInt,
|
|
32
|
+
NonNaNNumber,
|
|
21
33
|
NonNegativeFiniteNumber,
|
|
22
|
-
NonNegativeInt,
|
|
23
|
-
onePositiveInt,
|
|
34
|
+
type NonNegativeInt,
|
|
24
35
|
PositiveInt,
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
type Ratio,
|
|
37
|
+
} from "./Type.ts";
|
|
38
|
+
import type { Predicate } from "./Types.ts";
|
|
27
39
|
|
|
28
40
|
/**
|
|
29
41
|
* Composable scheduling strategies for {@link retry}, {@link repeat}, rate
|
|
@@ -35,30 +47,58 @@ import type { Predicate } from "./Types.js";
|
|
|
35
47
|
* to stop. Multiple calls to `schedule(deps)` create independent state
|
|
36
48
|
* instances.
|
|
37
49
|
*
|
|
38
|
-
*
|
|
50
|
+
* `Err(Done<void>)` is terminal. After a step returns it, every subsequent call
|
|
51
|
+
* to that step must also return `Err(Done<void>)`.
|
|
52
|
+
*
|
|
53
|
+
* With {@link retry} and {@link repeat}, the initial Task execution happens
|
|
54
|
+
* before the first schedule step. Schedule outputs therefore describe
|
|
55
|
+
* recurrences, not the initial execution. Time-based schedules establish their
|
|
56
|
+
* time origin on the first step call, not when `schedule(deps)` creates the
|
|
57
|
+
* step.
|
|
58
|
+
*
|
|
59
|
+
* ### Composing a retry policy
|
|
39
60
|
*
|
|
40
61
|
* ```ts
|
|
41
62
|
* import {
|
|
63
|
+
* err,
|
|
42
64
|
* exponential,
|
|
43
65
|
* jitter,
|
|
44
66
|
* maxDelay,
|
|
67
|
+
* ok,
|
|
45
68
|
* retry,
|
|
46
69
|
* take,
|
|
70
|
+
* testCreateRun,
|
|
71
|
+
* type RandomNumber,
|
|
72
|
+
* type Task,
|
|
47
73
|
* } from "@evolu/common";
|
|
48
74
|
*
|
|
75
|
+
* let attempts = 0;
|
|
76
|
+
* const fetchData: Task<string, { readonly type: "FetchError" }> = () => {
|
|
77
|
+
* attempts++;
|
|
78
|
+
* return attempts < 2 ? err({ type: "FetchError" }) : ok("data");
|
|
79
|
+
* };
|
|
80
|
+
*
|
|
49
81
|
* const fetchWithRetry = retry(
|
|
50
82
|
* fetchData,
|
|
51
83
|
* // A jittered, capped, limited exponential backoff.
|
|
52
|
-
* jitter(
|
|
84
|
+
* jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
|
|
53
85
|
* );
|
|
86
|
+
*
|
|
87
|
+
* await using run = testCreateRun({
|
|
88
|
+
* random: { next: () => 0 as RandomNumber },
|
|
89
|
+
* });
|
|
90
|
+
* expectOk(await run(fetchWithRetry), "data");
|
|
54
91
|
* ```
|
|
55
92
|
*
|
|
56
93
|
* Or use a preset:
|
|
57
94
|
*
|
|
58
95
|
* ```ts
|
|
59
|
-
* import {
|
|
96
|
+
* import { ok, retry, retryStrategyAws, type Task } from "@evolu/common";
|
|
60
97
|
*
|
|
98
|
+
* const fetchData: Task<string> = () => ok("data");
|
|
61
99
|
* const fetchWithRetry = retry(fetchData, retryStrategyAws);
|
|
100
|
+
*
|
|
101
|
+
* expect(fetchWithRetry).toBeTypeOf("function");
|
|
62
102
|
* ```
|
|
63
103
|
*/
|
|
64
104
|
export type Schedule<out Output, in Input = unknown> = (
|
|
@@ -87,60 +127,25 @@ export interface ScheduleStep<Output> {
|
|
|
87
127
|
/** Output from the {@link Schedule} step. */
|
|
88
128
|
readonly output: Output;
|
|
89
129
|
|
|
90
|
-
/** Delay before
|
|
130
|
+
/** Delay before the scheduled recurrence executes. */
|
|
91
131
|
readonly delay: Millis;
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
/**
|
|
95
|
-
* Internal per-step metrics computed from timestamps.
|
|
96
|
-
*
|
|
97
|
-
* The schedule computes this internally from deps.time.now().
|
|
98
|
-
*/
|
|
99
|
-
interface ScheduleStepMetrics {
|
|
100
|
-
/** Current attempt number. */
|
|
101
|
-
readonly attempt: PositiveInt;
|
|
102
|
-
/** Milliseconds elapsed since the schedule started. */
|
|
103
|
-
readonly elapsed: Millis;
|
|
104
|
-
/** Milliseconds since the previous step. On first step, this is 0. */
|
|
105
|
-
readonly elapsedSincePrevious: Millis;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Creates an internal per-step metrics tracker.
|
|
110
|
-
*
|
|
111
|
-
* Each call updates internal state and returns computed metrics.
|
|
112
|
-
*/
|
|
113
|
-
const createScheduleStepMetrics = (
|
|
114
|
-
deps: TimeDep,
|
|
115
|
-
): (() => ScheduleStepMetrics) => {
|
|
116
|
-
let attempt = onePositiveInt;
|
|
117
|
-
let start: Millis | null = null;
|
|
118
|
-
let previous: Millis | null = null;
|
|
119
|
-
|
|
120
|
-
return () => {
|
|
121
|
-
const now = deps.time.now();
|
|
122
|
-
const currentAttempt = attempt;
|
|
123
|
-
attempt = PositiveInt.orThrow(increment(attempt));
|
|
124
|
-
start ??= now;
|
|
125
|
-
const elapsed = saturateMillis(now - start);
|
|
126
|
-
const elapsedSincePrevious =
|
|
127
|
-
previous === null ? minMillis : saturateMillis(now - previous);
|
|
128
|
-
previous = now;
|
|
129
|
-
return { attempt: currentAttempt, elapsed, elapsedSincePrevious };
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
|
-
|
|
133
134
|
/**
|
|
134
135
|
* A schedule that never stops and has no delay.
|
|
135
136
|
*
|
|
136
137
|
* Outputs the attempt count (0, 1, 2, ...). Useful as a base for composition or
|
|
137
138
|
* for immediate retry without backoff.
|
|
138
139
|
*
|
|
139
|
-
* ###
|
|
140
|
+
* ### Recurring immediately
|
|
140
141
|
*
|
|
141
142
|
* ```ts
|
|
142
|
-
*
|
|
143
|
+
* import { forever, take, testCreateDeps } from "@evolu/common";
|
|
144
|
+
*
|
|
145
|
+
* // Retry immediately, up to 5 times.
|
|
143
146
|
* const immediate = take(5)(forever);
|
|
147
|
+
* const step = immediate(testCreateDeps());
|
|
148
|
+
* expectOk(step(undefined), [0, 0]);
|
|
144
149
|
* ```
|
|
145
150
|
*
|
|
146
151
|
* @group Constructors
|
|
@@ -153,13 +158,18 @@ export const forever: Schedule<number> = () => {
|
|
|
153
158
|
/**
|
|
154
159
|
* A schedule that runs exactly once with no delay.
|
|
155
160
|
*
|
|
156
|
-
*
|
|
161
|
+
* Equivalent to `take(1)(forever)`. Useful for one-shot operations.
|
|
157
162
|
*
|
|
158
|
-
* ###
|
|
163
|
+
* ### Scheduling one recurrence
|
|
159
164
|
*
|
|
160
165
|
* ```ts
|
|
161
|
-
*
|
|
166
|
+
* import { done, once, testCreateDeps } from "@evolu/common";
|
|
167
|
+
*
|
|
168
|
+
* // Produce one scheduled recurrence, then stop.
|
|
162
169
|
* const oneShot = once;
|
|
170
|
+
* const step = oneShot(testCreateDeps());
|
|
171
|
+
* expectOk(step(undefined), [0, 0]);
|
|
172
|
+
* expectErr(step(undefined), done());
|
|
163
173
|
* ```
|
|
164
174
|
*
|
|
165
175
|
* @group Constructors
|
|
@@ -179,16 +189,27 @@ export const once: Schedule<number> = () => {
|
|
|
179
189
|
* Convenience for `take(n)(forever)`. Outputs the current repetition count (0,
|
|
180
190
|
* 1, 2, ..., n-1).
|
|
181
191
|
*
|
|
182
|
-
*
|
|
192
|
+
* `n` uses {@link Int0To100OrNonNegativeInt}: pass `0` to `100` as a literal, or
|
|
193
|
+
* a validated {@link NonNegativeInt} for larger or dynamic values.
|
|
194
|
+
*
|
|
195
|
+
* ### Limiting recurrence count
|
|
183
196
|
*
|
|
184
197
|
* ```ts
|
|
185
|
-
*
|
|
198
|
+
* import { done, recurs, testCreateDeps } from "@evolu/common";
|
|
199
|
+
*
|
|
200
|
+
* // Retry up to 3 times (4 total attempts including the initial operation).
|
|
186
201
|
* const retry = recurs(3);
|
|
202
|
+
* const step = retry(testCreateDeps());
|
|
203
|
+
* expectOk(step(undefined), [0, 0]);
|
|
204
|
+
* step(undefined);
|
|
205
|
+
* step(undefined);
|
|
206
|
+
* expectErr(step(undefined), done());
|
|
187
207
|
* ```
|
|
188
208
|
*
|
|
189
209
|
* @group Constructors
|
|
190
210
|
*/
|
|
191
|
-
export const recurs = (n:
|
|
211
|
+
export const recurs = (n: Int0To100OrNonNegativeInt): Schedule<number> =>
|
|
212
|
+
take(n)(forever);
|
|
192
213
|
|
|
193
214
|
/**
|
|
194
215
|
* Constant delay schedule.
|
|
@@ -196,17 +217,20 @@ export const recurs = (n: number): Schedule<number> => take(n)(forever);
|
|
|
196
217
|
* Always waits the same duration after each execution completes. Never stops —
|
|
197
218
|
* combine with {@link take} or {@link maxElapsed} to limit.
|
|
198
219
|
*
|
|
199
|
-
* ###
|
|
220
|
+
* ### Constant spacing
|
|
200
221
|
*
|
|
201
222
|
* ```ts
|
|
202
|
-
*
|
|
203
|
-
* const poll = spaced("1s");
|
|
223
|
+
* import { spaced, take, testCreateDeps } from "@evolu/common";
|
|
204
224
|
*
|
|
205
|
-
* //
|
|
225
|
+
* // Poll every second, retry three times, or run a long-lived heartbeat.
|
|
226
|
+
* const poll = spaced("1s");
|
|
206
227
|
* const retry = take(3)(spaced("500ms"));
|
|
207
|
-
*
|
|
208
|
-
* // Heartbeat schedule
|
|
209
228
|
* const heartbeat = spaced("30s");
|
|
229
|
+
* const deps = testCreateDeps();
|
|
230
|
+
*
|
|
231
|
+
* expectOk(poll(deps)(undefined), [1000, 1000]);
|
|
232
|
+
* expectOk(retry(deps)(undefined), [500, 500]);
|
|
233
|
+
* expectOk(heartbeat(deps)(undefined), [30000, 30000]);
|
|
210
234
|
* ```
|
|
211
235
|
*
|
|
212
236
|
* @group Constructors
|
|
@@ -221,63 +245,75 @@ export const spaced =
|
|
|
221
245
|
/**
|
|
222
246
|
* Exponential backoff schedule.
|
|
223
247
|
*
|
|
224
|
-
* Computes delay as `base * factor^(
|
|
248
|
+
* Computes delay as `base * factor^(step - 1)`:
|
|
225
249
|
*
|
|
226
|
-
* -
|
|
227
|
-
* -
|
|
228
|
-
* -
|
|
250
|
+
* - Step 1: `base`
|
|
251
|
+
* - Step 2: `base * factor`
|
|
252
|
+
* - Step 3: `base * factor²`
|
|
229
253
|
* - ...
|
|
230
254
|
*
|
|
231
255
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
232
256
|
*
|
|
233
|
-
* ###
|
|
257
|
+
* ### Exponential growth factors
|
|
234
258
|
*
|
|
235
259
|
* ```ts
|
|
236
|
-
*
|
|
237
|
-
* const exp = exponential("100ms");
|
|
260
|
+
* import { exponential, testCreateDeps } from "@evolu/common";
|
|
238
261
|
*
|
|
239
|
-
* //
|
|
262
|
+
* // Standard doubling and gentler 1.5× growth.
|
|
263
|
+
* const standard = exponential("100ms");
|
|
240
264
|
* const gentle = exponential("100ms", 1.5);
|
|
265
|
+
* const standardStep = standard(testCreateDeps());
|
|
266
|
+
* const gentleStep = gentle(testCreateDeps());
|
|
267
|
+
* expectOk(standardStep(undefined), [100, 100]);
|
|
268
|
+
* expectOk(standardStep(undefined), [200, 200]);
|
|
269
|
+
* expectOk(gentleStep(undefined), [100, 100]);
|
|
270
|
+
* expectOk(gentleStep(undefined), [150, 150]);
|
|
241
271
|
* ```
|
|
242
272
|
*
|
|
243
273
|
* @group Constructors
|
|
244
274
|
*/
|
|
245
275
|
export const exponential = (base: Duration, factor = 2): Schedule<Millis> => {
|
|
246
|
-
assertType(
|
|
247
|
-
NonNegativeFiniteNumber,
|
|
248
|
-
factor,
|
|
249
|
-
"Expected factor to be a non-negative finite number.",
|
|
250
|
-
);
|
|
276
|
+
assertType(NonNegativeFiniteNumber, factor);
|
|
251
277
|
|
|
252
278
|
return () => {
|
|
253
279
|
const baseMs = durationToMillis(base);
|
|
254
280
|
let attempt = 0;
|
|
255
281
|
return () => {
|
|
256
282
|
attempt++;
|
|
257
|
-
const rawDelay =
|
|
258
|
-
|
|
283
|
+
const rawDelay =
|
|
284
|
+
baseMs === 0 ? minMillis : baseMs * Math.pow(factor, attempt - 1);
|
|
285
|
+
const delay = saturateComputedMillis(rawDelay);
|
|
259
286
|
return ok([delay, delay]);
|
|
260
287
|
};
|
|
261
288
|
};
|
|
262
289
|
};
|
|
263
290
|
|
|
291
|
+
const saturateComputedMillis = (value: number): Millis => {
|
|
292
|
+
assertType(NonNaNNumber, value);
|
|
293
|
+
return saturateMillis(value);
|
|
294
|
+
};
|
|
295
|
+
|
|
264
296
|
/**
|
|
265
297
|
* Linear backoff schedule.
|
|
266
298
|
*
|
|
267
|
-
* Delay increases linearly: `base *
|
|
299
|
+
* Delay increases linearly: `base * step`:
|
|
268
300
|
*
|
|
269
|
-
* -
|
|
270
|
-
* -
|
|
271
|
-
* -
|
|
301
|
+
* - Step 1: `base`
|
|
302
|
+
* - Step 2: `base * 2`
|
|
303
|
+
* - Step 3: `base * 3`
|
|
272
304
|
* - ...
|
|
273
305
|
*
|
|
274
306
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
275
307
|
*
|
|
276
|
-
* ###
|
|
308
|
+
* ### Linear growth
|
|
277
309
|
*
|
|
278
310
|
* ```ts
|
|
311
|
+
* import { linear, testCreateDeps } from "@evolu/common";
|
|
312
|
+
*
|
|
279
313
|
* // 100ms, 200ms, 300ms, 400ms, ...
|
|
280
|
-
* const
|
|
314
|
+
* const step = linear("100ms")(testCreateDeps());
|
|
315
|
+
* expectOk(step(undefined), [100, 100]);
|
|
316
|
+
* expectOk(step(undefined), [200, 200]);
|
|
281
317
|
* ```
|
|
282
318
|
*
|
|
283
319
|
* @group Constructors
|
|
@@ -289,7 +325,7 @@ export const linear =
|
|
|
289
325
|
let attempt = 0;
|
|
290
326
|
return () => {
|
|
291
327
|
attempt++;
|
|
292
|
-
const delay =
|
|
328
|
+
const delay = saturateComputedMillis(ms * attempt);
|
|
293
329
|
return ok([delay, delay]);
|
|
294
330
|
};
|
|
295
331
|
};
|
|
@@ -299,20 +335,25 @@ export const linear =
|
|
|
299
335
|
*
|
|
300
336
|
* Delays follow the Fibonacci sequence, growing more slowly than exponential:
|
|
301
337
|
*
|
|
302
|
-
* -
|
|
303
|
-
* -
|
|
304
|
-
* -
|
|
305
|
-
* -
|
|
306
|
-
* -
|
|
338
|
+
* - Step 1: `initial`
|
|
339
|
+
* - Step 2: `initial`
|
|
340
|
+
* - Step 3: `initial * 2`
|
|
341
|
+
* - Step 4: `initial * 3`
|
|
342
|
+
* - Step 5: `initial * 5`
|
|
307
343
|
* - ...
|
|
308
344
|
*
|
|
309
345
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
310
346
|
*
|
|
311
|
-
* ###
|
|
347
|
+
* ### Fibonacci growth
|
|
312
348
|
*
|
|
313
349
|
* ```ts
|
|
314
|
-
*
|
|
315
|
-
*
|
|
350
|
+
* import { fibonacci, testCreateDeps } from "@evolu/common";
|
|
351
|
+
*
|
|
352
|
+
* // 100ms, 100ms, 200ms, 300ms, 500ms, ...
|
|
353
|
+
* const step = fibonacci("100ms")(testCreateDeps());
|
|
354
|
+
* expectOk(step(undefined), [100, 100]);
|
|
355
|
+
* expectOk(step(undefined), [100, 100]);
|
|
356
|
+
* expectOk(step(undefined), [200, 200]);
|
|
316
357
|
* ```
|
|
317
358
|
*
|
|
318
359
|
* @group Constructors
|
|
@@ -323,7 +364,7 @@ export const fibonacci =
|
|
|
323
364
|
const ms = durationToMillis(initial);
|
|
324
365
|
let index = FibonacciIndex.orThrow(1);
|
|
325
366
|
return () => {
|
|
326
|
-
const delay =
|
|
367
|
+
const delay = saturateComputedMillis(ms * fibonacciAt(index));
|
|
327
368
|
index = FibonacciIndex.orNull(increment(index)) ?? index;
|
|
328
369
|
return ok([delay, delay]);
|
|
329
370
|
};
|
|
@@ -334,20 +375,27 @@ export const fibonacci =
|
|
|
334
375
|
*
|
|
335
376
|
* Recurs on a fixed interval, outputting the repetition count (0, 1, 2, ...).
|
|
336
377
|
* Unlike {@link spaced}, which waits a duration _after_ each execution, `fixed`
|
|
337
|
-
* maintains a consistent cadence from
|
|
378
|
+
* maintains a consistent cadence from the first schedule step.
|
|
338
379
|
*
|
|
339
|
-
* If execution
|
|
340
|
-
* immediately
|
|
341
|
-
*
|
|
380
|
+
* If execution falls behind by one or more intervals, missed recurrences happen
|
|
381
|
+
* immediately until the schedule catches up to the original cadence. Use
|
|
382
|
+
* {@link windowed} to skip missed recurrences instead.
|
|
342
383
|
*
|
|
343
|
-
* ###
|
|
384
|
+
* ### Maintaining a fixed cadence
|
|
344
385
|
*
|
|
345
386
|
* ```ts
|
|
346
|
-
*
|
|
347
|
-
* const healthCheck = take(10)(fixed("5s"));
|
|
387
|
+
* import { fixed, take, testCreateDeps } from "@evolu/common";
|
|
348
388
|
*
|
|
349
|
-
* //
|
|
389
|
+
* // A bounded health check and an unbounded cron-like cadence.
|
|
390
|
+
* const healthCheck = take(10)(fixed("5s"));
|
|
350
391
|
* const cronLike = fixed("1m");
|
|
392
|
+
*
|
|
393
|
+
* const healthDeps = testCreateDeps();
|
|
394
|
+
* const healthStep = healthCheck(healthDeps);
|
|
395
|
+
* expectOk(healthStep(undefined), [0, 5000]);
|
|
396
|
+
* healthDeps.time.advance("3s");
|
|
397
|
+
* expectOk(healthStep(undefined), [1, 2000]);
|
|
398
|
+
* expectOk(cronLike(testCreateDeps())(undefined), [0, 60000]);
|
|
351
399
|
* ```
|
|
352
400
|
*
|
|
353
401
|
* @group Constructors
|
|
@@ -367,24 +415,68 @@ export const fixed =
|
|
|
367
415
|
const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
|
|
368
416
|
const boundary = intervalMs - remainder;
|
|
369
417
|
const delay = runningBehind ? 0 : boundary;
|
|
370
|
-
return ok([count++,
|
|
418
|
+
return ok([count++, saturateComputedMillis(delay)]);
|
|
371
419
|
};
|
|
372
420
|
};
|
|
373
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Internal per-step metrics computed from timestamps.
|
|
424
|
+
*
|
|
425
|
+
* The schedule computes this internally from deps.time.now().
|
|
426
|
+
*/
|
|
427
|
+
interface ScheduleStepMetrics {
|
|
428
|
+
/** Milliseconds elapsed since the first step. */
|
|
429
|
+
readonly elapsed: Millis;
|
|
430
|
+
/** Milliseconds since the previous step. On first step, this is 0. */
|
|
431
|
+
readonly elapsedSincePrevious: Millis;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Creates an internal per-step metrics tracker.
|
|
436
|
+
*
|
|
437
|
+
* Each call updates internal state and returns computed metrics.
|
|
438
|
+
*/
|
|
439
|
+
const createScheduleStepMetrics = (
|
|
440
|
+
deps: TimeDep,
|
|
441
|
+
): (() => ScheduleStepMetrics) => {
|
|
442
|
+
let start: Millis | null = null;
|
|
443
|
+
let previous: Millis | null = null;
|
|
444
|
+
|
|
445
|
+
return () => {
|
|
446
|
+
const now = deps.time.now();
|
|
447
|
+
start ??= now;
|
|
448
|
+
const elapsed = saturateComputedMillis(now - start);
|
|
449
|
+
const elapsedSincePrevious =
|
|
450
|
+
previous === null ? minMillis : saturateComputedMillis(now - previous);
|
|
451
|
+
previous = now;
|
|
452
|
+
return { elapsed, elapsedSincePrevious };
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
|
|
374
456
|
/**
|
|
375
457
|
* Divides the timeline into fixed windows and sleeps until the next boundary.
|
|
376
458
|
*
|
|
377
|
-
* Similar to {@link fixed}, but
|
|
378
|
-
*
|
|
459
|
+
* Similar to {@link fixed}, but skips missed recurrences and always sleeps until
|
|
460
|
+
* the next window boundary. Outputs the repetition count.
|
|
379
461
|
*
|
|
380
|
-
* Useful for aligning executions to regular intervals from the
|
|
462
|
+
* Useful for aligning executions to regular intervals from the first step.
|
|
381
463
|
*
|
|
382
|
-
* ###
|
|
464
|
+
* ### Aligning to time windows
|
|
383
465
|
*
|
|
384
466
|
* ```ts
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
467
|
+
* import { testCreateDeps, windowed } from "@evolu/common";
|
|
468
|
+
*
|
|
469
|
+
* const stepAfter = (elapsed: "3s" | "7s") => {
|
|
470
|
+
* const deps = testCreateDeps();
|
|
471
|
+
* const step = windowed("5s")(deps);
|
|
472
|
+
* step(undefined);
|
|
473
|
+
* deps.time.advance(elapsed);
|
|
474
|
+
* return step(undefined);
|
|
475
|
+
* };
|
|
476
|
+
*
|
|
477
|
+
* // At 3s the next boundary is 2s away; at 7s it is 3s away.
|
|
478
|
+
* expectOk(stepAfter("3s"), [1, 2000]);
|
|
479
|
+
* expectOk(stepAfter("7s"), [1, 3000]);
|
|
388
480
|
* ```
|
|
389
481
|
*
|
|
390
482
|
* @group Constructors
|
|
@@ -399,7 +491,7 @@ export const windowed =
|
|
|
399
491
|
const { elapsed } = metrics();
|
|
400
492
|
const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
|
|
401
493
|
const delay = intervalMs === 0 ? 0 : intervalMs - remainder;
|
|
402
|
-
return ok([count++,
|
|
494
|
+
return ok([count++, saturateComputedMillis(delay)]);
|
|
403
495
|
};
|
|
404
496
|
};
|
|
405
497
|
|
|
@@ -408,11 +500,15 @@ export const windowed =
|
|
|
408
500
|
*
|
|
409
501
|
* Convenience for `take(1)(spaced(delay))`. Useful for simple one-shot delays.
|
|
410
502
|
*
|
|
411
|
-
* ###
|
|
503
|
+
* ### Scheduling one delayed recurrence
|
|
412
504
|
*
|
|
413
505
|
* ```ts
|
|
414
|
-
*
|
|
415
|
-
*
|
|
506
|
+
* import { done, fromDelay, testCreateDeps } from "@evolu/common";
|
|
507
|
+
*
|
|
508
|
+
* // Wait one second, then stop.
|
|
509
|
+
* const step = fromDelay("1s")(testCreateDeps());
|
|
510
|
+
* expectOk(step(undefined), [1000, 1000]);
|
|
511
|
+
* expectErr(step(undefined), done());
|
|
416
512
|
* ```
|
|
417
513
|
*
|
|
418
514
|
* @group Constructors
|
|
@@ -424,13 +520,20 @@ export const fromDelay = (delay: Duration): Schedule<Millis> =>
|
|
|
424
520
|
* A schedule that runs through a sequence of delays.
|
|
425
521
|
*
|
|
426
522
|
* Convenience for sequencing single-delay schedules. Useful for predefined
|
|
427
|
-
* retry patterns.
|
|
523
|
+
* retry patterns. With no delays, returns a schedule that stops immediately.
|
|
428
524
|
*
|
|
429
|
-
* ###
|
|
525
|
+
* ### Sequencing custom delays
|
|
430
526
|
*
|
|
431
527
|
* ```ts
|
|
432
|
-
*
|
|
528
|
+
* import { done, fromDelays, testCreateDeps } from "@evolu/common";
|
|
529
|
+
*
|
|
530
|
+
* // A custom retry sequence: 100ms, 500ms, then 2s.
|
|
433
531
|
* const custom = fromDelays("100ms", "500ms", "2s");
|
|
532
|
+
* const step = custom(testCreateDeps());
|
|
533
|
+
* expectOk(step(undefined), [100, 100]);
|
|
534
|
+
* expectOk(step(undefined), [500, 500]);
|
|
535
|
+
* expectOk(step(undefined), [2000, 2000]);
|
|
536
|
+
* expectErr(step(undefined), done());
|
|
434
537
|
* ```
|
|
435
538
|
*
|
|
436
539
|
* @group Constructors
|
|
@@ -440,22 +543,37 @@ export const fromDelays = (
|
|
|
440
543
|
): Schedule<Millis> => sequenceSchedules(...delays.map((d) => fromDelay(d)));
|
|
441
544
|
|
|
442
545
|
/**
|
|
443
|
-
* A schedule that outputs the total elapsed time since
|
|
546
|
+
* A schedule that outputs the total elapsed time since its first step.
|
|
444
547
|
*
|
|
445
548
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit. Useful
|
|
446
549
|
* for tracking how long a retry sequence has been running.
|
|
447
550
|
*
|
|
448
|
-
* ###
|
|
551
|
+
* ### Tracking elapsed time
|
|
449
552
|
*
|
|
450
553
|
* ```ts
|
|
451
|
-
*
|
|
554
|
+
* import {
|
|
555
|
+
* done,
|
|
556
|
+
* elapsed,
|
|
557
|
+
* exponential,
|
|
558
|
+
* intersectSchedules,
|
|
559
|
+
* testCreateDeps,
|
|
560
|
+
* whileScheduleOutput,
|
|
561
|
+
* type Millis,
|
|
562
|
+
* } from "@evolu/common";
|
|
563
|
+
*
|
|
564
|
+
* // Track elapsed time alongside each backoff step.
|
|
452
565
|
* const withTiming = intersectSchedules(exponential("100ms"), elapsed);
|
|
453
|
-
*
|
|
566
|
+
* expectOk(withTiming(testCreateDeps())(undefined), [[100, 0], 100]);
|
|
454
567
|
*
|
|
455
|
-
* //
|
|
568
|
+
* // Or stop a schedule after 30 seconds of elapsed time.
|
|
456
569
|
* const timeLimited = whileScheduleOutput((ms: Millis) => ms < 30000)(
|
|
457
570
|
* elapsed,
|
|
458
571
|
* );
|
|
572
|
+
* const deps = testCreateDeps();
|
|
573
|
+
* const step = timeLimited(deps);
|
|
574
|
+
* step(undefined);
|
|
575
|
+
* deps.time.advance("30s");
|
|
576
|
+
* expectErr(step(undefined), done());
|
|
459
577
|
* ```
|
|
460
578
|
*
|
|
461
579
|
* @group Constructors
|
|
@@ -471,17 +589,31 @@ export const elapsed: Schedule<Millis> = (deps) => {
|
|
|
471
589
|
* Outputs the elapsed time. Useful for time-boxed operations or combining with
|
|
472
590
|
* other schedules to create time-limited variants.
|
|
473
591
|
*
|
|
474
|
-
* ###
|
|
592
|
+
* ### Time-boxing a schedule
|
|
475
593
|
*
|
|
476
594
|
* ```ts
|
|
477
|
-
*
|
|
595
|
+
* import {
|
|
596
|
+
* done,
|
|
597
|
+
* during,
|
|
598
|
+
* exponential,
|
|
599
|
+
* intersectSchedules,
|
|
600
|
+
* testCreateDeps,
|
|
601
|
+
* } from "@evolu/common";
|
|
602
|
+
*
|
|
603
|
+
* // Run for at most 30 seconds.
|
|
478
604
|
* const timeLimited = during("30s");
|
|
605
|
+
* const deps = testCreateDeps();
|
|
606
|
+
* const step = timeLimited(deps);
|
|
607
|
+
* expectOk(step(undefined), [0, 0]);
|
|
608
|
+
* deps.time.advance("30.1s");
|
|
609
|
+
* expectErr(step(undefined), done());
|
|
479
610
|
*
|
|
480
|
-
* // Combine with
|
|
611
|
+
* // Combine elapsed time with backoff for a time-boxed retry.
|
|
481
612
|
* const timedRetry = intersectSchedules(
|
|
482
613
|
* exponential("100ms"),
|
|
483
614
|
* during("10s"),
|
|
484
615
|
* );
|
|
616
|
+
* expectOk(timedRetry(testCreateDeps())(undefined), [[100, 0], 100]);
|
|
485
617
|
* ```
|
|
486
618
|
*
|
|
487
619
|
* @group Constructors
|
|
@@ -496,17 +628,29 @@ export const during = (duration: Duration): Schedule<Millis> =>
|
|
|
496
628
|
*
|
|
497
629
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
498
630
|
*
|
|
499
|
-
* ###
|
|
631
|
+
* ### Adding constant output
|
|
500
632
|
*
|
|
501
633
|
* ```ts
|
|
502
|
-
*
|
|
634
|
+
* import {
|
|
635
|
+
* always,
|
|
636
|
+
* exponential,
|
|
637
|
+
* intersectSchedules,
|
|
638
|
+
* testCreateDeps,
|
|
639
|
+
* } from "@evolu/common";
|
|
640
|
+
*
|
|
641
|
+
* // Always emit the same label.
|
|
503
642
|
* const labeled = always("retry");
|
|
643
|
+
* expectOk(labeled(testCreateDeps())(undefined), ["retry", 0]);
|
|
504
644
|
*
|
|
505
|
-
* //
|
|
645
|
+
* // Add a label while preserving exponential timing.
|
|
506
646
|
* const withLabel = intersectSchedules(
|
|
507
647
|
* exponential("100ms"),
|
|
508
648
|
* always("backoff"),
|
|
509
649
|
* );
|
|
650
|
+
* expectOk(withLabel(testCreateDeps())(undefined), [
|
|
651
|
+
* [100, "backoff"],
|
|
652
|
+
* 100,
|
|
653
|
+
* ]);
|
|
510
654
|
* ```
|
|
511
655
|
*
|
|
512
656
|
* @group Constructors
|
|
@@ -521,18 +665,17 @@ export const always = <A>(value: A): Schedule<A> =>
|
|
|
521
665
|
* provided function. Never stops — combine with {@link take} or
|
|
522
666
|
* {@link maxElapsed} to limit.
|
|
523
667
|
*
|
|
524
|
-
* ###
|
|
668
|
+
* ### Unfolding state
|
|
525
669
|
*
|
|
526
670
|
* ```ts
|
|
527
|
-
*
|
|
528
|
-
* const counter = unfoldSchedule(0, (n) => n + 1);
|
|
671
|
+
* import { testCreateDeps, unfoldSchedule } from "@evolu/common";
|
|
529
672
|
*
|
|
530
|
-
* //
|
|
673
|
+
* // Unfold counters, custom backoff values, or state machines.
|
|
674
|
+
* const counter = unfoldSchedule(0, (n) => n + 1);
|
|
531
675
|
* const customBackoff = unfoldSchedule(100, (delay) =>
|
|
532
676
|
* Math.round(delay * 1.5),
|
|
533
677
|
* );
|
|
534
678
|
*
|
|
535
|
-
* // State machine
|
|
536
679
|
* type Phase = "init" | "warmup" | "active";
|
|
537
680
|
* const phases = unfoldSchedule<Phase>("init", (phase) => {
|
|
538
681
|
* switch (phase) {
|
|
@@ -544,6 +687,16 @@ export const always = <A>(value: A): Schedule<A> =>
|
|
|
544
687
|
* return "active";
|
|
545
688
|
* }
|
|
546
689
|
* });
|
|
690
|
+
*
|
|
691
|
+
* const counterStep = counter(testCreateDeps());
|
|
692
|
+
* const backoffStep = customBackoff(testCreateDeps());
|
|
693
|
+
* const phaseStep = phases(testCreateDeps());
|
|
694
|
+
* expectOk(counterStep(undefined), [0, 0]);
|
|
695
|
+
* expectOk(counterStep(undefined), [1, 0]);
|
|
696
|
+
* backoffStep(undefined);
|
|
697
|
+
* phaseStep(undefined);
|
|
698
|
+
* expectOk(backoffStep(undefined), [150, 0]);
|
|
699
|
+
* expectOk(phaseStep(undefined), ["warmup", 0]);
|
|
547
700
|
* ```
|
|
548
701
|
*
|
|
549
702
|
* @group Constructors
|
|
@@ -560,48 +713,62 @@ export const unfoldSchedule =
|
|
|
560
713
|
};
|
|
561
714
|
|
|
562
715
|
/**
|
|
563
|
-
* Limits a schedule to a maximum number of
|
|
716
|
+
* Limits a schedule to a maximum number of steps.
|
|
564
717
|
*
|
|
565
|
-
* After `n`
|
|
718
|
+
* After `n` steps, returns `Err(Done<void>)` (stop).
|
|
566
719
|
*
|
|
567
|
-
*
|
|
720
|
+
* `n` uses {@link Int0To100OrNonNegativeInt}: pass `0` to `100` as a literal, or
|
|
721
|
+
* a validated {@link NonNegativeInt} for larger or dynamic values.
|
|
722
|
+
*
|
|
723
|
+
* ### Limiting a schedule
|
|
568
724
|
*
|
|
569
725
|
* ```ts
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
* //
|
|
726
|
+
* import { done, exponential, take, testCreateDeps } from "@evolu/common";
|
|
727
|
+
*
|
|
728
|
+
* // Three exponential retries, then Done.
|
|
729
|
+
* const step = take(3)(exponential("100ms"))(testCreateDeps());
|
|
730
|
+
* step(undefined);
|
|
731
|
+
* step(undefined);
|
|
732
|
+
* step(undefined);
|
|
733
|
+
* expectErr(step(undefined), done());
|
|
573
734
|
* ```
|
|
574
735
|
*
|
|
575
736
|
* @group Limiting
|
|
576
737
|
*/
|
|
577
|
-
export const take =
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return (input)
|
|
587
|
-
attempt++;
|
|
588
|
-
if (attempt > n) return err(done());
|
|
589
|
-
return step(input);
|
|
590
|
-
};
|
|
738
|
+
export const take =
|
|
739
|
+
(n: Int0To100OrNonNegativeInt) =>
|
|
740
|
+
<Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
741
|
+
(deps) => {
|
|
742
|
+
const step = schedule(deps);
|
|
743
|
+
let attempt = 0;
|
|
744
|
+
return (input) => {
|
|
745
|
+
attempt++;
|
|
746
|
+
if (attempt > n) return err(done());
|
|
747
|
+
return step(input);
|
|
591
748
|
};
|
|
592
|
-
};
|
|
749
|
+
};
|
|
593
750
|
|
|
594
751
|
/**
|
|
595
|
-
* Limits schedule execution to a maximum elapsed time.
|
|
752
|
+
* Limits schedule execution to a maximum elapsed time since its first step.
|
|
596
753
|
*
|
|
597
|
-
* After `duration` has elapsed since the
|
|
598
|
-
* `Err(Done<void>)`.
|
|
754
|
+
* After `duration` has elapsed since the first step, returns `Err(Done<void>)`.
|
|
599
755
|
*
|
|
600
|
-
* ###
|
|
756
|
+
* ### Limiting elapsed time
|
|
601
757
|
*
|
|
602
758
|
* ```ts
|
|
603
|
-
*
|
|
604
|
-
*
|
|
759
|
+
* import {
|
|
760
|
+
* done,
|
|
761
|
+
* exponential,
|
|
762
|
+
* maxElapsed,
|
|
763
|
+
* testCreateDeps,
|
|
764
|
+
* } from "@evolu/common";
|
|
765
|
+
*
|
|
766
|
+
* // Retry for at most 30 seconds.
|
|
767
|
+
* const deps = testCreateDeps();
|
|
768
|
+
* const step = maxElapsed("30s")(exponential("1s"))(deps);
|
|
769
|
+
* expectOk(step(undefined), [1000, 1000]);
|
|
770
|
+
* deps.time.advance("30s");
|
|
771
|
+
* expectErr(step(undefined), done());
|
|
605
772
|
* ```
|
|
606
773
|
*
|
|
607
774
|
* @group Limiting
|
|
@@ -614,9 +781,15 @@ export const maxElapsed = (duration: Duration) => {
|
|
|
614
781
|
(deps) => {
|
|
615
782
|
const step = schedule(deps);
|
|
616
783
|
const metrics = createScheduleStepMetrics(deps);
|
|
784
|
+
let stopped = false;
|
|
617
785
|
return (input) => {
|
|
786
|
+
if (stopped) return err(done());
|
|
618
787
|
const { elapsed } = metrics();
|
|
619
|
-
|
|
788
|
+
if (elapsed >= maxMs) {
|
|
789
|
+
stopped = true;
|
|
790
|
+
return err(done());
|
|
791
|
+
}
|
|
792
|
+
return step(input);
|
|
620
793
|
};
|
|
621
794
|
};
|
|
622
795
|
};
|
|
@@ -626,12 +799,18 @@ export const maxElapsed = (duration: Duration) => {
|
|
|
626
799
|
*
|
|
627
800
|
* If the schedule returns a delay greater than `max`, returns `max` instead.
|
|
628
801
|
*
|
|
629
|
-
* ###
|
|
802
|
+
* ### Capping delays
|
|
630
803
|
*
|
|
631
804
|
* ```ts
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* // 1s, 2s, 4s, 8s,
|
|
805
|
+
* import { exponential, maxDelay, testCreateDeps } from "@evolu/common";
|
|
806
|
+
*
|
|
807
|
+
* // Exponential delays grow 1s, 2s, 4s, 8s, then stay capped at 10s.
|
|
808
|
+
* const step = maxDelay("10s")(exponential("1s"))(testCreateDeps());
|
|
809
|
+
* step(undefined);
|
|
810
|
+
* step(undefined);
|
|
811
|
+
* step(undefined);
|
|
812
|
+
* step(undefined);
|
|
813
|
+
* expectOk(step(undefined), [16000, 10000]);
|
|
635
814
|
* ```
|
|
636
815
|
*
|
|
637
816
|
* @group Limiting
|
|
@@ -647,65 +826,96 @@ export const maxDelay = (max: Duration) => {
|
|
|
647
826
|
const result = step(input);
|
|
648
827
|
if (!result.ok) return result;
|
|
649
828
|
const [output, delay] = result.value;
|
|
650
|
-
return ok([output,
|
|
829
|
+
return ok([output, min(delay, maxMs)]);
|
|
651
830
|
};
|
|
652
831
|
};
|
|
653
832
|
};
|
|
654
833
|
|
|
655
834
|
/**
|
|
656
|
-
*
|
|
835
|
+
* Randomizes delays by up to a percentage.
|
|
657
836
|
*
|
|
658
837
|
* Jitter helps prevent "thundering herd" when many clients retry simultaneously
|
|
659
|
-
* after a service recovers.
|
|
838
|
+
* after a service recovers. By default, the original delay is the upper bound:
|
|
839
|
+
*
|
|
840
|
+
* - `"0%"` — no jitter (original delay)
|
|
841
|
+
* - `"50%"` — equal jitter, shortens the delay by up to 50%
|
|
842
|
+
* - `"100%"` — full jitter, shortens the delay by up to 100%
|
|
660
843
|
*
|
|
661
|
-
*
|
|
662
|
-
* - `factor = 0.5` — delay varies ±50% (e.g., 1s becomes 500ms-1500ms)
|
|
663
|
-
* - `factor = 1` — full jitter, delay varies 0-200% (e.g., 1s becomes 0-2s)
|
|
844
|
+
* Pass `"around"` to preserve the average delay for periodic work:
|
|
664
845
|
*
|
|
665
|
-
*
|
|
846
|
+
* - `"0%"` — no jitter (original delay)
|
|
847
|
+
* - `"50%"` — varies by up to 50% below or above the original delay
|
|
848
|
+
* - `"100%"` — varies by up to 100% below or above the original delay
|
|
849
|
+
*
|
|
850
|
+
* ### Jittering below or around a delay
|
|
666
851
|
*
|
|
667
852
|
* ```ts
|
|
668
|
-
*
|
|
669
|
-
*
|
|
853
|
+
* import {
|
|
854
|
+
* exponential,
|
|
855
|
+
* jitter,
|
|
856
|
+
* spaced,
|
|
857
|
+
* testCreateDeps,
|
|
858
|
+
* type RandomNumber,
|
|
859
|
+
* } from "@evolu/common";
|
|
860
|
+
*
|
|
861
|
+
* const deps = {
|
|
862
|
+
* ...testCreateDeps(),
|
|
863
|
+
* random: { next: () => 0.5 as RandomNumber },
|
|
864
|
+
* };
|
|
670
865
|
*
|
|
671
|
-
* //
|
|
672
|
-
* const conservative = jitter(
|
|
866
|
+
* // Shorten retry delays by at most 25%.
|
|
867
|
+
* const conservative = jitter("25%")(exponential("1s"));
|
|
868
|
+
* // Poll around a 30s target cadence, from 27s to 33s.
|
|
869
|
+
* const polling = jitter("10%", "around")(spaced("30s"));
|
|
870
|
+
*
|
|
871
|
+
* expectOk(conservative(deps)(undefined), [1000, 875]);
|
|
872
|
+
* expectOk(polling(deps)(undefined), [30000, 30000]);
|
|
673
873
|
* ```
|
|
674
874
|
*
|
|
675
875
|
* @group Delay
|
|
676
876
|
*/
|
|
677
|
-
export const jitter = (
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
877
|
+
export const jitter = (
|
|
878
|
+
percentage: Percentage = "50%",
|
|
879
|
+
mode: "below" | "around" = "below",
|
|
880
|
+
): (<Output, Input>(
|
|
881
|
+
schedule: Schedule<Output, Input>,
|
|
882
|
+
) => Schedule<Output, Input>) => {
|
|
883
|
+
const ratio = percentageToRatio(percentage);
|
|
884
|
+
return createJitter(ratio, mode === "around" ? 1 + ratio : 1);
|
|
885
|
+
};
|
|
683
886
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
887
|
+
const createJitter =
|
|
888
|
+
(factor: Ratio, maxMultiplier: number) =>
|
|
889
|
+
<Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
890
|
+
(deps) => {
|
|
891
|
+
const step = schedule(deps);
|
|
892
|
+
return (input) => {
|
|
893
|
+
const result = step(input);
|
|
894
|
+
if (!result.ok) return result;
|
|
895
|
+
const [output, delay] = result.value;
|
|
896
|
+
const minMultiplier = 1 - factor;
|
|
897
|
+
const jittered =
|
|
898
|
+
delay *
|
|
899
|
+
(minMultiplier + deps.random.next() * (maxMultiplier - minMultiplier));
|
|
900
|
+
return ok([output, saturateComputedMillis(jittered)]);
|
|
696
901
|
};
|
|
697
|
-
};
|
|
902
|
+
};
|
|
698
903
|
|
|
699
904
|
/**
|
|
700
|
-
*
|
|
905
|
+
* Replaces the schedule's first delay.
|
|
701
906
|
*
|
|
702
|
-
*
|
|
907
|
+
* The first successful step uses `initialDelay` instead of the delay produced
|
|
908
|
+
* by the schedule. Subsequent steps use the schedule's delays unchanged.
|
|
703
909
|
*
|
|
704
|
-
* ###
|
|
910
|
+
* ### Replacing the first delay
|
|
705
911
|
*
|
|
706
912
|
* ```ts
|
|
707
|
-
*
|
|
708
|
-
*
|
|
913
|
+
* import { delayed, exponential, testCreateDeps } from "@evolu/common";
|
|
914
|
+
*
|
|
915
|
+
* const step = delayed("1s")(exponential("100ms"))(testCreateDeps());
|
|
916
|
+
* // Only the first delay is replaced; later exponential delays are unchanged.
|
|
917
|
+
* expectOk(step(undefined), [100, 1000]);
|
|
918
|
+
* expectOk(step(undefined), [200, 200]);
|
|
709
919
|
* ```
|
|
710
920
|
*
|
|
711
921
|
* @group Delay
|
|
@@ -733,12 +943,14 @@ export const delayed = (initialDelay: Duration) => {
|
|
|
733
943
|
/**
|
|
734
944
|
* Adds a fixed delay to the schedule's existing delay.
|
|
735
945
|
*
|
|
736
|
-
* ###
|
|
946
|
+
* ### Adding to every delay
|
|
737
947
|
*
|
|
738
948
|
* ```ts
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
* //
|
|
949
|
+
* import { addDelay, exponential, testCreateDeps } from "@evolu/common";
|
|
950
|
+
*
|
|
951
|
+
* // Add 500ms to every exponential delay.
|
|
952
|
+
* const step = addDelay("500ms")(exponential("100ms"))(testCreateDeps());
|
|
953
|
+
* expectOk(step(undefined), [100, 600]);
|
|
742
954
|
* ```
|
|
743
955
|
*
|
|
744
956
|
* @group Delay
|
|
@@ -757,14 +969,21 @@ export const addDelay = (
|
|
|
757
969
|
*
|
|
758
970
|
* More flexible than {@link maxDelay} — can implement any delay transformation.
|
|
759
971
|
*
|
|
760
|
-
* ###
|
|
972
|
+
* ### Transforming delays
|
|
761
973
|
*
|
|
762
974
|
* ```ts
|
|
763
|
-
*
|
|
764
|
-
* const slower = modifyDelay((d) => d * 2)(exponential("100ms"));
|
|
975
|
+
* import { exponential, modifyDelay, testCreateDeps } from "@evolu/common";
|
|
765
976
|
*
|
|
766
|
-
* //
|
|
767
|
-
* const
|
|
977
|
+
* // Arbitrary transformations can double or cap delays.
|
|
978
|
+
* const slower = modifyDelay((delay) => delay * 2)(exponential("100ms"));
|
|
979
|
+
* // Equivalent to maxDelay("10s") for this schedule.
|
|
980
|
+
* const capped = modifyDelay((delay) => Math.min(delay, 10000))(
|
|
981
|
+
* exponential("20s"),
|
|
982
|
+
* );
|
|
983
|
+
* const deps = testCreateDeps();
|
|
984
|
+
*
|
|
985
|
+
* expectOk(slower(deps)(undefined), [100, 200]);
|
|
986
|
+
* expectOk(capped(deps)(undefined), [20000, 10000]);
|
|
768
987
|
* ```
|
|
769
988
|
*
|
|
770
989
|
* @group Delay
|
|
@@ -778,24 +997,42 @@ export const modifyDelay =
|
|
|
778
997
|
const result = step(input);
|
|
779
998
|
if (!result.ok) return result;
|
|
780
999
|
const [output, delay] = result.value;
|
|
781
|
-
return ok([output,
|
|
1000
|
+
return ok([output, saturateComputedMillis(f(delay))]);
|
|
782
1001
|
};
|
|
783
1002
|
};
|
|
784
1003
|
|
|
785
1004
|
/**
|
|
786
|
-
* Adjusts delay by subtracting
|
|
1005
|
+
* Adjusts delay by subtracting time elapsed beyond the previously returned
|
|
1006
|
+
* delay.
|
|
787
1007
|
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
1008
|
+
* In a normal executor loop, this corresponds to the previous execution time.
|
|
1009
|
+
* If the runtime wakes later than requested, the extra lag is also compensated.
|
|
1010
|
+
* If execution and lag took longer than the delay, returns 0.
|
|
1011
|
+
*
|
|
1012
|
+
* When composing with delay-shaping combinators such as {@link maxDelay}, put
|
|
1013
|
+
* `compensate` near the outside of the stack so it observes the final returned
|
|
1014
|
+
* delay.
|
|
790
1015
|
*
|
|
791
1016
|
* For window-aligned scheduling, use {@link fixed} instead.
|
|
792
1017
|
*
|
|
793
|
-
* ###
|
|
1018
|
+
* ### Compensating for execution time
|
|
794
1019
|
*
|
|
795
1020
|
* ```ts
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
1021
|
+
* import { compensate, spaced, testCreateDeps } from "@evolu/common";
|
|
1022
|
+
*
|
|
1023
|
+
* const fastDeps = testCreateDeps();
|
|
1024
|
+
* const fastStep = compensate(spaced("5s"))(fastDeps);
|
|
1025
|
+
* expectOk(fastStep(undefined), [5000, 5000]);
|
|
1026
|
+
* // Five seconds waiting plus one second working leaves four seconds.
|
|
1027
|
+
* fastDeps.time.advance("6s");
|
|
1028
|
+
* expectOk(fastStep(undefined), [5000, 4000]);
|
|
1029
|
+
*
|
|
1030
|
+
* const slowDeps = testCreateDeps();
|
|
1031
|
+
* const slowStep = compensate(spaced("5s"))(slowDeps);
|
|
1032
|
+
* expectOk(slowStep(undefined), [5000, 5000]);
|
|
1033
|
+
* // Five seconds waiting plus six seconds working leaves no delay.
|
|
1034
|
+
* slowDeps.time.advance("11s");
|
|
1035
|
+
* expectOk(slowStep(undefined), [5000, 0]);
|
|
799
1036
|
* ```
|
|
800
1037
|
*
|
|
801
1038
|
* @group Delay
|
|
@@ -805,12 +1042,18 @@ export const compensate =
|
|
|
805
1042
|
(deps) => {
|
|
806
1043
|
const step = schedule(deps);
|
|
807
1044
|
const metrics = createScheduleStepMetrics(deps);
|
|
1045
|
+
let previousReturnedDelay = minMillis;
|
|
808
1046
|
return (input) => {
|
|
809
1047
|
const { elapsedSincePrevious } = metrics();
|
|
810
1048
|
const result = step(input);
|
|
811
1049
|
if (!result.ok) return result;
|
|
812
1050
|
const [output, delay] = result.value;
|
|
813
|
-
|
|
1051
|
+
const executionTime = saturateComputedMillis(
|
|
1052
|
+
elapsedSincePrevious - previousReturnedDelay,
|
|
1053
|
+
);
|
|
1054
|
+
const compensatedDelay = saturateComputedMillis(delay - executionTime);
|
|
1055
|
+
previousReturnedDelay = compensatedDelay;
|
|
1056
|
+
return ok([output, compensatedDelay]);
|
|
814
1057
|
};
|
|
815
1058
|
};
|
|
816
1059
|
|
|
@@ -821,15 +1064,26 @@ export const compensate =
|
|
|
821
1064
|
* Useful for input-aware retry strategies, e.g., only retry certain error
|
|
822
1065
|
* types.
|
|
823
1066
|
*
|
|
824
|
-
* ###
|
|
1067
|
+
* ### Continuing by input
|
|
825
1068
|
*
|
|
826
1069
|
* ```ts
|
|
1070
|
+
* import {
|
|
1071
|
+
* done,
|
|
1072
|
+
* exponential,
|
|
1073
|
+
* testCreateDeps,
|
|
1074
|
+
* whileScheduleInput,
|
|
1075
|
+
* type Typed,
|
|
1076
|
+
* } from "@evolu/common";
|
|
1077
|
+
*
|
|
827
1078
|
* interface MyError extends Typed<"Transient" | "Fatal"> {}
|
|
828
1079
|
*
|
|
829
|
-
* //
|
|
1080
|
+
* // Retry only transient errors.
|
|
830
1081
|
* const retryTransient = whileScheduleInput(
|
|
831
1082
|
* (error: MyError) => error.type === "Transient",
|
|
832
1083
|
* )(exponential("100ms"));
|
|
1084
|
+
* const step = retryTransient(testCreateDeps());
|
|
1085
|
+
* expectOk(step({ type: "Transient" }), [100, 100]);
|
|
1086
|
+
* expectErr(step({ type: "Fatal" }), done());
|
|
833
1087
|
* ```
|
|
834
1088
|
*
|
|
835
1089
|
* @group Filtering
|
|
@@ -839,8 +1093,13 @@ export const whileScheduleInput =
|
|
|
839
1093
|
<Output>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
840
1094
|
(deps) => {
|
|
841
1095
|
const step = schedule(deps);
|
|
1096
|
+
let stopped = false;
|
|
842
1097
|
return (input) => {
|
|
843
|
-
if (
|
|
1098
|
+
if (stopped) return err(done());
|
|
1099
|
+
if (!predicate(input)) {
|
|
1100
|
+
stopped = true;
|
|
1101
|
+
return err(done());
|
|
1102
|
+
}
|
|
844
1103
|
return step(input);
|
|
845
1104
|
};
|
|
846
1105
|
};
|
|
@@ -851,15 +1110,26 @@ export const whileScheduleInput =
|
|
|
851
1110
|
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `true`.
|
|
852
1111
|
* Useful for stopping retry on specific error conditions.
|
|
853
1112
|
*
|
|
854
|
-
* ###
|
|
1113
|
+
* ### Stopping by input
|
|
855
1114
|
*
|
|
856
1115
|
* ```ts
|
|
1116
|
+
* import {
|
|
1117
|
+
* done,
|
|
1118
|
+
* exponential,
|
|
1119
|
+
* testCreateDeps,
|
|
1120
|
+
* untilScheduleInput,
|
|
1121
|
+
* type Typed,
|
|
1122
|
+
* } from "@evolu/common";
|
|
1123
|
+
*
|
|
857
1124
|
* interface MyError extends Typed<"Transient" | "Fatal"> {}
|
|
858
1125
|
*
|
|
859
|
-
* // Stop retrying
|
|
1126
|
+
* // Stop retrying when an error is fatal.
|
|
860
1127
|
* const stopOnFatal = untilScheduleInput(
|
|
861
1128
|
* (error: MyError) => error.type === "Fatal",
|
|
862
1129
|
* )(exponential("100ms"));
|
|
1130
|
+
* const step = stopOnFatal(testCreateDeps());
|
|
1131
|
+
* expectOk(step({ type: "Transient" }), [100, 100]);
|
|
1132
|
+
* expectErr(step({ type: "Fatal" }), done());
|
|
863
1133
|
* ```
|
|
864
1134
|
*
|
|
865
1135
|
* @group Filtering
|
|
@@ -869,8 +1139,13 @@ export const untilScheduleInput =
|
|
|
869
1139
|
<Output>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
870
1140
|
(deps) => {
|
|
871
1141
|
const step = schedule(deps);
|
|
1142
|
+
let stopped = false;
|
|
872
1143
|
return (input) => {
|
|
873
|
-
if (
|
|
1144
|
+
if (stopped) return err(done());
|
|
1145
|
+
if (predicate(input)) {
|
|
1146
|
+
stopped = true;
|
|
1147
|
+
return err(done());
|
|
1148
|
+
}
|
|
874
1149
|
return step(input);
|
|
875
1150
|
};
|
|
876
1151
|
};
|
|
@@ -880,13 +1155,26 @@ export const untilScheduleInput =
|
|
|
880
1155
|
*
|
|
881
1156
|
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `false`.
|
|
882
1157
|
*
|
|
883
|
-
* ###
|
|
1158
|
+
* ### Continuing by output
|
|
884
1159
|
*
|
|
885
1160
|
* ```ts
|
|
886
|
-
*
|
|
1161
|
+
* import {
|
|
1162
|
+
* done,
|
|
1163
|
+
* exponential,
|
|
1164
|
+
* testCreateDeps,
|
|
1165
|
+
* whileScheduleOutput,
|
|
1166
|
+
* type Millis,
|
|
1167
|
+
* } from "@evolu/common";
|
|
1168
|
+
*
|
|
1169
|
+
* // Continue while the exponential delay is below five seconds.
|
|
887
1170
|
* const capped = whileScheduleOutput((delay: Millis) => delay < 5000)(
|
|
888
1171
|
* exponential("1s"),
|
|
889
1172
|
* );
|
|
1173
|
+
* const step = capped(testCreateDeps());
|
|
1174
|
+
* step(undefined);
|
|
1175
|
+
* step(undefined);
|
|
1176
|
+
* step(undefined);
|
|
1177
|
+
* expectErr(step(undefined), done());
|
|
890
1178
|
* ```
|
|
891
1179
|
*
|
|
892
1180
|
* @group Filtering
|
|
@@ -896,10 +1184,15 @@ export const whileScheduleOutput =
|
|
|
896
1184
|
<Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
897
1185
|
(deps) => {
|
|
898
1186
|
const step = schedule(deps);
|
|
1187
|
+
let stopped = false;
|
|
899
1188
|
return (input) => {
|
|
1189
|
+
if (stopped) return err(done());
|
|
900
1190
|
const result = step(input);
|
|
901
1191
|
if (!result.ok) return result;
|
|
902
|
-
if (!predicate(result.value[0]))
|
|
1192
|
+
if (!predicate(result.value[0])) {
|
|
1193
|
+
stopped = true;
|
|
1194
|
+
return err(done());
|
|
1195
|
+
}
|
|
903
1196
|
return result;
|
|
904
1197
|
};
|
|
905
1198
|
};
|
|
@@ -909,13 +1202,27 @@ export const whileScheduleOutput =
|
|
|
909
1202
|
*
|
|
910
1203
|
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `true`.
|
|
911
1204
|
*
|
|
912
|
-
* ###
|
|
1205
|
+
* ### Stopping by output
|
|
913
1206
|
*
|
|
914
1207
|
* ```ts
|
|
915
|
-
*
|
|
1208
|
+
* import {
|
|
1209
|
+
* done,
|
|
1210
|
+
* exponential,
|
|
1211
|
+
* testCreateDeps,
|
|
1212
|
+
* untilScheduleOutput,
|
|
1213
|
+
* type Millis,
|
|
1214
|
+
* } from "@evolu/common";
|
|
1215
|
+
*
|
|
1216
|
+
* // Stop once the exponential delay reaches at least one second.
|
|
916
1217
|
* const limited = untilScheduleOutput((delay: Millis) => delay >= 1000)(
|
|
917
1218
|
* exponential("100ms"),
|
|
918
1219
|
* );
|
|
1220
|
+
* const step = limited(testCreateDeps());
|
|
1221
|
+
* step(undefined);
|
|
1222
|
+
* step(undefined);
|
|
1223
|
+
* step(undefined);
|
|
1224
|
+
* step(undefined);
|
|
1225
|
+
* expectErr(step(undefined), done());
|
|
919
1226
|
* ```
|
|
920
1227
|
*
|
|
921
1228
|
* @group Filtering
|
|
@@ -925,33 +1232,48 @@ export const untilScheduleOutput =
|
|
|
925
1232
|
<Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
|
|
926
1233
|
(deps) => {
|
|
927
1234
|
const step = schedule(deps);
|
|
1235
|
+
let stopped = false;
|
|
928
1236
|
return (input) => {
|
|
1237
|
+
if (stopped) return err(done());
|
|
929
1238
|
const result = step(input);
|
|
930
1239
|
if (!result.ok) return result;
|
|
931
|
-
if (predicate(result.value[0]))
|
|
1240
|
+
if (predicate(result.value[0])) {
|
|
1241
|
+
stopped = true;
|
|
1242
|
+
return err(done());
|
|
1243
|
+
}
|
|
932
1244
|
return result;
|
|
933
1245
|
};
|
|
934
1246
|
};
|
|
935
1247
|
|
|
936
1248
|
/**
|
|
937
|
-
* Resets
|
|
1249
|
+
* Resets a running schedule after a period of inactivity.
|
|
938
1250
|
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
1251
|
+
* Before each step, if the time since the previous step is at least `duration`,
|
|
1252
|
+
* replaces the wrapped schedule with fresh state. Once the wrapped schedule
|
|
1253
|
+
* returns `Done`, termination is final.
|
|
942
1254
|
*
|
|
943
|
-
* ###
|
|
1255
|
+
* ### Resetting after inactivity
|
|
944
1256
|
*
|
|
945
1257
|
* ```ts
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
1258
|
+
* import {
|
|
1259
|
+
* exponential,
|
|
1260
|
+
* resetScheduleAfter,
|
|
1261
|
+
* testCreateDeps,
|
|
1262
|
+
* } from "@evolu/common";
|
|
1263
|
+
*
|
|
1264
|
+
* const backoff = resetScheduleAfter("1m")(exponential("1s"));
|
|
1265
|
+
* const deps = testCreateDeps();
|
|
1266
|
+
* const step = backoff(deps);
|
|
1267
|
+
* expectOk(step(undefined), [1000, 1000]);
|
|
1268
|
+
* deps.time.advance("1m");
|
|
1269
|
+
* expectOk(step(undefined), [1000, 1000]);
|
|
950
1270
|
* ```
|
|
951
1271
|
*
|
|
952
1272
|
* @group State
|
|
953
1273
|
*/
|
|
954
|
-
export const resetScheduleAfter = (
|
|
1274
|
+
export const resetScheduleAfter = (
|
|
1275
|
+
duration: DurationLiteral | PositiveMillis,
|
|
1276
|
+
) => {
|
|
955
1277
|
const resetMs = durationToMillis(duration);
|
|
956
1278
|
return <Output, Input>(
|
|
957
1279
|
schedule: Schedule<Output, Input>,
|
|
@@ -959,12 +1281,16 @@ export const resetScheduleAfter = (duration: Duration) => {
|
|
|
959
1281
|
(deps) => {
|
|
960
1282
|
let step = schedule(deps);
|
|
961
1283
|
const metrics = createScheduleStepMetrics(deps);
|
|
1284
|
+
let stopped = false;
|
|
962
1285
|
return (input) => {
|
|
1286
|
+
if (stopped) return err(done());
|
|
963
1287
|
const { elapsedSincePrevious } = metrics();
|
|
964
1288
|
if (elapsedSincePrevious >= resetMs) {
|
|
965
1289
|
step = schedule(deps);
|
|
966
1290
|
}
|
|
967
|
-
|
|
1291
|
+
const result = step(input);
|
|
1292
|
+
if (!result.ok) stopped = true;
|
|
1293
|
+
return result;
|
|
968
1294
|
};
|
|
969
1295
|
};
|
|
970
1296
|
};
|
|
@@ -974,15 +1300,22 @@ export const resetScheduleAfter = (duration: Duration) => {
|
|
|
974
1300
|
*
|
|
975
1301
|
* The delay (second tuple element) remains unchanged.
|
|
976
1302
|
*
|
|
977
|
-
* ###
|
|
1303
|
+
* ### Mapping schedule output
|
|
978
1304
|
*
|
|
979
1305
|
* ```ts
|
|
980
|
-
* import {
|
|
1306
|
+
* import {
|
|
1307
|
+
* exponential,
|
|
1308
|
+
* mapSchedule,
|
|
1309
|
+
* testCreateDeps,
|
|
1310
|
+
* type Millis,
|
|
1311
|
+
* } from "@evolu/common";
|
|
981
1312
|
*
|
|
982
|
-
* const schedule = mapSchedule((delay) => ({
|
|
1313
|
+
* const schedule = mapSchedule((delay: Millis) => ({
|
|
983
1314
|
* delay,
|
|
984
1315
|
* doubled: delay * 2,
|
|
985
1316
|
* }))(exponential("100ms"));
|
|
1317
|
+
* const step = schedule(testCreateDeps());
|
|
1318
|
+
* expectOk(step(undefined), [{ delay: 100, doubled: 200 }, 100]);
|
|
986
1319
|
* ```
|
|
987
1320
|
*
|
|
988
1321
|
* @group Transform
|
|
@@ -1008,20 +1341,23 @@ export const mapSchedule =
|
|
|
1008
1341
|
* directly (the "identity" schedule). When called with a schedule, wraps it to
|
|
1009
1342
|
* preserve timing behavior but replace output with input.
|
|
1010
1343
|
*
|
|
1011
|
-
* ###
|
|
1344
|
+
* ### Passing through input
|
|
1012
1345
|
*
|
|
1013
1346
|
* ```ts
|
|
1014
|
-
* import { exponential, passthrough } from "@evolu/common";
|
|
1347
|
+
* import { exponential, passthrough, testCreateDeps } from "@evolu/common";
|
|
1015
1348
|
*
|
|
1016
1349
|
* interface MyError {
|
|
1017
1350
|
* readonly message: string;
|
|
1018
1351
|
* }
|
|
1019
1352
|
*
|
|
1020
|
-
* // Constructor
|
|
1353
|
+
* // Constructor form emits input immediately; combinator form keeps timing.
|
|
1021
1354
|
* const identity = passthrough<MyError>();
|
|
1022
|
-
*
|
|
1023
|
-
* // Combinator: preserve timing, replace output
|
|
1024
1355
|
* const withInput = passthrough(exponential("100ms"));
|
|
1356
|
+
* const error = { message: "Unavailable" };
|
|
1357
|
+
* const deps = testCreateDeps();
|
|
1358
|
+
*
|
|
1359
|
+
* expectOk(identity(deps)(error), [error, 0]);
|
|
1360
|
+
* expectOk(withInput(deps)(error), [error, 100]);
|
|
1025
1361
|
* ```
|
|
1026
1362
|
*
|
|
1027
1363
|
* @group Constructors
|
|
@@ -1053,31 +1389,45 @@ export function passthrough<Output, Input>(
|
|
|
1053
1389
|
* Each step outputs the accumulated value. Useful for tracking totals,
|
|
1054
1390
|
* collecting outputs, or building up metadata across attempts.
|
|
1055
1391
|
*
|
|
1056
|
-
* ###
|
|
1392
|
+
* ### Folding schedule output
|
|
1057
1393
|
*
|
|
1058
1394
|
* ```ts
|
|
1059
|
-
*
|
|
1395
|
+
* import {
|
|
1396
|
+
* exponential,
|
|
1397
|
+
* foldSchedule,
|
|
1398
|
+
* minMillis,
|
|
1399
|
+
* spaced,
|
|
1400
|
+
* take,
|
|
1401
|
+
* testCreateDeps,
|
|
1402
|
+
* type Millis,
|
|
1403
|
+
* } from "@evolu/common";
|
|
1404
|
+
*
|
|
1405
|
+
* // Accumulate totals, complete output histories, or structured metadata.
|
|
1060
1406
|
* const withTotal = foldSchedule(
|
|
1061
1407
|
* 0,
|
|
1062
1408
|
* (total: number, delay: Millis) => total + delay,
|
|
1063
1409
|
* )(exponential("100ms"));
|
|
1064
|
-
* // Outputs: 100, 300, 700, 1500, ... (cumulative)
|
|
1065
|
-
*
|
|
1066
|
-
* // Collect all outputs
|
|
1067
1410
|
* const collected = foldSchedule<ReadonlyArray<Millis>, Millis>(
|
|
1068
1411
|
* [],
|
|
1069
|
-
* (
|
|
1412
|
+
* (outputs, delay) => [...outputs, delay],
|
|
1070
1413
|
* )(take(3)(spaced("1s")));
|
|
1071
|
-
* // Outputs: [1000], [1000, 1000], [1000, 1000, 1000]
|
|
1072
|
-
*
|
|
1073
|
-
* // Count attempts with custom output
|
|
1074
1414
|
* const counted = foldSchedule(
|
|
1075
1415
|
* { attempts: 0, lastDelay: minMillis },
|
|
1076
|
-
* (
|
|
1077
|
-
* attempts:
|
|
1416
|
+
* (state, delay: Millis) => ({
|
|
1417
|
+
* attempts: state.attempts + 1,
|
|
1078
1418
|
* lastDelay: delay,
|
|
1079
1419
|
* }),
|
|
1080
1420
|
* )(exponential("100ms"));
|
|
1421
|
+
*
|
|
1422
|
+
* const deps = testCreateDeps();
|
|
1423
|
+
* const totalStep = withTotal(deps);
|
|
1424
|
+
* totalStep(undefined);
|
|
1425
|
+
* expectOk(totalStep(undefined), [300, 200]);
|
|
1426
|
+
* expectOk(collected(deps)(undefined), [[1000], 1000]);
|
|
1427
|
+
* expectOk(counted(deps)(undefined), [
|
|
1428
|
+
* { attempts: 1, lastDelay: 100 },
|
|
1429
|
+
* 100,
|
|
1430
|
+
* ]);
|
|
1081
1431
|
* ```
|
|
1082
1432
|
*
|
|
1083
1433
|
* @group Transform
|
|
@@ -1104,12 +1454,15 @@ export const foldSchedule =
|
|
|
1104
1454
|
* Outputs 0, 1, 2, ... while preserving the underlying schedule's timing and
|
|
1105
1455
|
* termination behavior.
|
|
1106
1456
|
*
|
|
1107
|
-
* ###
|
|
1457
|
+
* ### Counting repetitions
|
|
1108
1458
|
*
|
|
1109
1459
|
* ```ts
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
* //
|
|
1460
|
+
* import { exponential, repetitions, testCreateDeps } from "@evolu/common";
|
|
1461
|
+
*
|
|
1462
|
+
* // Count retries while preserving exponential timing.
|
|
1463
|
+
* const step = repetitions(exponential("100ms"))(testCreateDeps());
|
|
1464
|
+
* expectOk(step(undefined), [0, 100]);
|
|
1465
|
+
* expectOk(step(undefined), [1, 200]);
|
|
1113
1466
|
* ```
|
|
1114
1467
|
*
|
|
1115
1468
|
* @group Transform
|
|
@@ -1124,17 +1477,28 @@ export const repetitions = <Output, Input>(
|
|
|
1124
1477
|
* Wraps a schedule to output its delay (in milliseconds) instead of the
|
|
1125
1478
|
* original output. Useful for monitoring or logging delay patterns.
|
|
1126
1479
|
*
|
|
1127
|
-
* ###
|
|
1480
|
+
* ### Exposing and observing delays
|
|
1128
1481
|
*
|
|
1129
1482
|
* ```ts
|
|
1130
|
-
*
|
|
1483
|
+
* import {
|
|
1484
|
+
* delays,
|
|
1485
|
+
* exponential,
|
|
1486
|
+
* tapScheduleOutput,
|
|
1487
|
+
* testCreateDeps,
|
|
1488
|
+
* type Millis,
|
|
1489
|
+
* } from "@evolu/common";
|
|
1490
|
+
*
|
|
1491
|
+
* // Expose delays for monitoring, or observe them without changing output.
|
|
1131
1492
|
* const monitorDelays = delays(exponential("100ms"));
|
|
1132
|
-
*
|
|
1493
|
+
* const observed: Array<Millis> = [];
|
|
1494
|
+
* const logged = tapScheduleOutput((delay: Millis) => {
|
|
1495
|
+
* observed.push(delay);
|
|
1496
|
+
* })(delays(exponential("100ms")));
|
|
1497
|
+
* const deps = testCreateDeps();
|
|
1133
1498
|
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
* );
|
|
1499
|
+
* expectOk(monitorDelays(deps)(undefined), [100, 100]);
|
|
1500
|
+
* expectOk(logged(deps)(undefined), [100, 100]);
|
|
1501
|
+
* expect(observed).toEqual([100]);
|
|
1138
1502
|
* ```
|
|
1139
1503
|
*
|
|
1140
1504
|
* @group Transform
|
|
@@ -1154,14 +1518,25 @@ export const delays =
|
|
|
1154
1518
|
/**
|
|
1155
1519
|
* Collects all outputs into an array.
|
|
1156
1520
|
*
|
|
1157
|
-
* Each step outputs
|
|
1521
|
+
* Each step outputs a new snapshot containing all outputs so far. Because all
|
|
1522
|
+
* outputs are retained and copied on each step, use this combinator with finite
|
|
1523
|
+
* schedules.
|
|
1158
1524
|
*
|
|
1159
|
-
* ###
|
|
1525
|
+
* ### Collecting outputs
|
|
1160
1526
|
*
|
|
1161
1527
|
* ```ts
|
|
1162
|
-
*
|
|
1528
|
+
* import {
|
|
1529
|
+
* collectAllScheduleOutputs,
|
|
1530
|
+
* spaced,
|
|
1531
|
+
* take,
|
|
1532
|
+
* testCreateDeps,
|
|
1533
|
+
* } from "@evolu/common";
|
|
1534
|
+
*
|
|
1535
|
+
* // Retain every delay produced by the finite schedule.
|
|
1163
1536
|
* const collected = collectAllScheduleOutputs(take(3)(spaced("100ms")));
|
|
1164
|
-
*
|
|
1537
|
+
* const step = collected(testCreateDeps());
|
|
1538
|
+
* step(undefined);
|
|
1539
|
+
* expectOk(step(undefined), [[100, 100], 100]);
|
|
1165
1540
|
* ```
|
|
1166
1541
|
*
|
|
1167
1542
|
* @group Collection
|
|
@@ -1179,14 +1554,24 @@ export const collectAllScheduleOutputs = <Output, Input>(
|
|
|
1179
1554
|
* Each step outputs an array containing all inputs received so far. Mirror of
|
|
1180
1555
|
* {@link collectAllScheduleOutputs} but for inputs.
|
|
1181
1556
|
*
|
|
1182
|
-
* ###
|
|
1557
|
+
* ### Collecting inputs
|
|
1183
1558
|
*
|
|
1184
1559
|
* ```ts
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1560
|
+
* import {
|
|
1561
|
+
* collectScheduleInputs,
|
|
1562
|
+
* exponential,
|
|
1563
|
+
* take,
|
|
1564
|
+
* testCreateDeps,
|
|
1565
|
+
* type Millis,
|
|
1566
|
+
* type Schedule,
|
|
1567
|
+
* } from "@evolu/common";
|
|
1568
|
+
*
|
|
1569
|
+
* const retries: Schedule<Millis, string> = take(3)(exponential("100ms"));
|
|
1570
|
+
* // Keep every error received during retry.
|
|
1571
|
+
* const errorHistory = collectScheduleInputs(retries);
|
|
1572
|
+
* const step = errorHistory(testCreateDeps());
|
|
1573
|
+
* step("network");
|
|
1574
|
+
* expectOk(step("timeout"), [["network", "timeout"], 200]);
|
|
1190
1575
|
* ```
|
|
1191
1576
|
*
|
|
1192
1577
|
* @group Collection
|
|
@@ -1202,14 +1587,27 @@ export const collectScheduleInputs = <Output, Input>(
|
|
|
1202
1587
|
* More flexible than {@link collectAllScheduleOutputs} — stops collecting when
|
|
1203
1588
|
* the predicate returns false.
|
|
1204
1589
|
*
|
|
1205
|
-
* ###
|
|
1590
|
+
* ### Collecting while output matches
|
|
1206
1591
|
*
|
|
1207
1592
|
* ```ts
|
|
1208
|
-
*
|
|
1593
|
+
* import {
|
|
1594
|
+
* collectWhileScheduleOutput,
|
|
1595
|
+
* done,
|
|
1596
|
+
* exponential,
|
|
1597
|
+
* testCreateDeps,
|
|
1598
|
+
* type Millis,
|
|
1599
|
+
* } from "@evolu/common";
|
|
1600
|
+
*
|
|
1601
|
+
* // Collect delays only while they remain below one second.
|
|
1209
1602
|
* const smallDelays = collectWhileScheduleOutput(
|
|
1210
1603
|
* (delay: Millis) => delay < 1000,
|
|
1211
1604
|
* )(exponential("100ms"));
|
|
1212
|
-
*
|
|
1605
|
+
* const step = smallDelays(testCreateDeps());
|
|
1606
|
+
* step(undefined);
|
|
1607
|
+
* step(undefined);
|
|
1608
|
+
* step(undefined);
|
|
1609
|
+
* expectOk(step(undefined), [[100, 200, 400, 800], 800]);
|
|
1610
|
+
* expectErr(step(undefined), done());
|
|
1213
1611
|
* ```
|
|
1214
1612
|
*
|
|
1215
1613
|
* @group Collection
|
|
@@ -1225,16 +1623,29 @@ export const collectWhileScheduleOutput =
|
|
|
1225
1623
|
* Collects outputs until a predicate becomes true.
|
|
1226
1624
|
*
|
|
1227
1625
|
* Mirror of {@link collectWhileScheduleOutput} — stops collecting when the
|
|
1228
|
-
* predicate returns true
|
|
1626
|
+
* predicate returns true.
|
|
1229
1627
|
*
|
|
1230
|
-
* ###
|
|
1628
|
+
* ### Collecting until output matches
|
|
1231
1629
|
*
|
|
1232
1630
|
* ```ts
|
|
1233
|
-
*
|
|
1631
|
+
* import {
|
|
1632
|
+
* collectUntilScheduleOutput,
|
|
1633
|
+
* done,
|
|
1634
|
+
* exponential,
|
|
1635
|
+
* testCreateDeps,
|
|
1636
|
+
* type Millis,
|
|
1637
|
+
* } from "@evolu/common";
|
|
1638
|
+
*
|
|
1639
|
+
* // Collect delays until the next delay reaches at least one second.
|
|
1234
1640
|
* const untilLarge = collectUntilScheduleOutput(
|
|
1235
1641
|
* (delay: Millis) => delay >= 1000,
|
|
1236
1642
|
* )(exponential("100ms"));
|
|
1237
|
-
*
|
|
1643
|
+
* const step = untilLarge(testCreateDeps());
|
|
1644
|
+
* step(undefined);
|
|
1645
|
+
* step(undefined);
|
|
1646
|
+
* step(undefined);
|
|
1647
|
+
* expectOk(step(undefined), [[100, 200, 400, 800], 800]);
|
|
1648
|
+
* expectErr(step(undefined), done());
|
|
1238
1649
|
* ```
|
|
1239
1650
|
*
|
|
1240
1651
|
* @group Collection
|
|
@@ -1252,16 +1663,27 @@ export const collectUntilScheduleOutput =
|
|
|
1252
1663
|
* Useful for adaptive strategies that start aggressive and become more
|
|
1253
1664
|
* conservative over time.
|
|
1254
1665
|
*
|
|
1255
|
-
* ###
|
|
1666
|
+
* ### Sequencing strategies
|
|
1256
1667
|
*
|
|
1257
1668
|
* ```ts
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
1669
|
+
* import {
|
|
1670
|
+
* exponential,
|
|
1671
|
+
* fixed,
|
|
1672
|
+
* sequenceSchedules,
|
|
1673
|
+
* take,
|
|
1674
|
+
* testCreateDeps,
|
|
1675
|
+
* } from "@evolu/common";
|
|
1676
|
+
*
|
|
1677
|
+
* // Fast retries first, then slower retries, then a steady fallback.
|
|
1678
|
+
* const step = sequenceSchedules(
|
|
1260
1679
|
* take(3)(exponential("100ms")),
|
|
1261
1680
|
* take(5)(fixed("500ms")),
|
|
1262
1681
|
* fixed("1s"),
|
|
1263
|
-
* );
|
|
1264
|
-
*
|
|
1682
|
+
* )(testCreateDeps());
|
|
1683
|
+
* step(undefined);
|
|
1684
|
+
* step(undefined);
|
|
1685
|
+
* step(undefined);
|
|
1686
|
+
* expectOk(step(undefined), [0, 500]);
|
|
1265
1687
|
* ```
|
|
1266
1688
|
*
|
|
1267
1689
|
* @group Composition
|
|
@@ -1273,8 +1695,7 @@ export const sequenceSchedules =
|
|
|
1273
1695
|
(deps) => {
|
|
1274
1696
|
let index = 0;
|
|
1275
1697
|
type Step =
|
|
1276
|
-
|
|
1277
|
-
| null;
|
|
1698
|
+
((input: Input) => NextResult<readonly [Output, Millis]>) | null;
|
|
1278
1699
|
let currentStep: Step = schedules.length > 0 ? schedules[0](deps) : null;
|
|
1279
1700
|
return (input) => {
|
|
1280
1701
|
while (currentStep !== null) {
|
|
@@ -1294,14 +1715,28 @@ export const sequenceSchedules =
|
|
|
1294
1715
|
*
|
|
1295
1716
|
* Continues only while both schedules want to continue. Uses the maximum delay.
|
|
1296
1717
|
*
|
|
1297
|
-
* ###
|
|
1718
|
+
* ### Combining constraints with AND
|
|
1298
1719
|
*
|
|
1299
1720
|
* ```ts
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1721
|
+
* import {
|
|
1722
|
+
* done,
|
|
1723
|
+
* exponential,
|
|
1724
|
+
* forever,
|
|
1725
|
+
* intersectSchedules,
|
|
1726
|
+
* maxElapsed,
|
|
1727
|
+
* take,
|
|
1728
|
+
* testCreateDeps,
|
|
1729
|
+
* } from "@evolu/common";
|
|
1730
|
+
*
|
|
1731
|
+
* // Retry at most 5 times and only within 30 seconds.
|
|
1732
|
+
* const deps = testCreateDeps();
|
|
1733
|
+
* const step = intersectSchedules(
|
|
1302
1734
|
* take(5)(exponential("1s")),
|
|
1303
1735
|
* maxElapsed("30s")(forever),
|
|
1304
|
-
* );
|
|
1736
|
+
* )(deps);
|
|
1737
|
+
* expectOk(step(undefined), [[1000, 0], 1000]);
|
|
1738
|
+
* deps.time.advance("30s");
|
|
1739
|
+
* expectErr(step(undefined), done());
|
|
1305
1740
|
* ```
|
|
1306
1741
|
*
|
|
1307
1742
|
* @group Composition
|
|
@@ -1314,13 +1749,18 @@ export const intersectSchedules =
|
|
|
1314
1749
|
(deps) => {
|
|
1315
1750
|
const stepA = a(deps);
|
|
1316
1751
|
const stepB = b(deps);
|
|
1752
|
+
let stopped = false;
|
|
1317
1753
|
return (input) => {
|
|
1754
|
+
if (stopped) return err(done());
|
|
1318
1755
|
const resultA = stepA(input);
|
|
1319
1756
|
const resultB = stepB(input);
|
|
1320
|
-
if (!resultA.ok || !resultB.ok)
|
|
1757
|
+
if (!resultA.ok || !resultB.ok) {
|
|
1758
|
+
stopped = true;
|
|
1759
|
+
return err(done());
|
|
1760
|
+
}
|
|
1321
1761
|
const [outputA, delayA] = resultA.value;
|
|
1322
1762
|
const [outputB, delayB] = resultB.value;
|
|
1323
|
-
return ok([[outputA, outputB],
|
|
1763
|
+
return ok([[outputA, outputB], max(delayA, delayB)]);
|
|
1324
1764
|
};
|
|
1325
1765
|
};
|
|
1326
1766
|
|
|
@@ -1329,14 +1769,26 @@ export const intersectSchedules =
|
|
|
1329
1769
|
*
|
|
1330
1770
|
* Continues while either schedule wants to continue. Uses the minimum delay.
|
|
1331
1771
|
*
|
|
1332
|
-
* ###
|
|
1772
|
+
* ### Combining constraints with OR
|
|
1333
1773
|
*
|
|
1334
1774
|
* ```ts
|
|
1335
|
-
*
|
|
1775
|
+
* import {
|
|
1776
|
+
* done,
|
|
1777
|
+
* spaced,
|
|
1778
|
+
* take,
|
|
1779
|
+
* testCreateDeps,
|
|
1780
|
+
* unionSchedules,
|
|
1781
|
+
* } from "@evolu/common";
|
|
1782
|
+
*
|
|
1783
|
+
* // The second policy keeps the union alive after the first one stops.
|
|
1336
1784
|
* const either = unionSchedules(
|
|
1337
|
-
* take(
|
|
1338
|
-
*
|
|
1785
|
+
* take(1)(spaced("100ms")),
|
|
1786
|
+
* take(2)(spaced("200ms")),
|
|
1339
1787
|
* );
|
|
1788
|
+
* const step = either(testCreateDeps());
|
|
1789
|
+
* expectOk(step(undefined), [100, 100]);
|
|
1790
|
+
* expectOk(step(undefined), [200, 200]);
|
|
1791
|
+
* expectErr(step(undefined), done());
|
|
1340
1792
|
* ```
|
|
1341
1793
|
*
|
|
1342
1794
|
* @group Composition
|
|
@@ -1347,20 +1799,28 @@ export const unionSchedules =
|
|
|
1347
1799
|
b: Schedule<OutputB, Input>,
|
|
1348
1800
|
): Schedule<OutputA | OutputB, Input> =>
|
|
1349
1801
|
(deps) => {
|
|
1350
|
-
|
|
1351
|
-
|
|
1802
|
+
let stepA:
|
|
1803
|
+
((input: Input) => NextResult<readonly [OutputA, Millis]>) | null =
|
|
1804
|
+
a(deps);
|
|
1805
|
+
let stepB:
|
|
1806
|
+
((input: Input) => NextResult<readonly [OutputB, Millis]>) | null =
|
|
1807
|
+
b(deps);
|
|
1352
1808
|
return (input) => {
|
|
1353
|
-
|
|
1354
|
-
const resultB = stepB(input);
|
|
1809
|
+
if (stepA === null && stepB === null) return err(done());
|
|
1355
1810
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1811
|
+
const resultA = stepA?.(input) ?? null;
|
|
1812
|
+
const resultB = stepB?.(input) ?? null;
|
|
1813
|
+
|
|
1814
|
+
if (resultA !== null && !resultA.ok) stepA = null;
|
|
1815
|
+
if (resultB !== null && !resultB.ok) stepB = null;
|
|
1816
|
+
|
|
1817
|
+
if (!resultA?.ok) return resultB?.ok ? resultB : err(done());
|
|
1818
|
+
if (!resultB?.ok) return resultA;
|
|
1359
1819
|
|
|
1360
1820
|
const [outputA, delayA] = resultA.value;
|
|
1361
1821
|
const [outputB, delayB] = resultB.value;
|
|
1362
1822
|
// Use minimum delay, output from the one with smaller delay
|
|
1363
|
-
const minDelay =
|
|
1823
|
+
const minDelay = min(delayA, delayB);
|
|
1364
1824
|
return delayA <= delayB
|
|
1365
1825
|
? ok([outputA, minDelay])
|
|
1366
1826
|
: ok([outputB, minDelay]);
|
|
@@ -1374,15 +1834,36 @@ export const unionSchedules =
|
|
|
1374
1834
|
* base schedule. Useful for implementing error-aware backoff where certain
|
|
1375
1835
|
* errors (e.g., throttling) use different delays.
|
|
1376
1836
|
*
|
|
1377
|
-
*
|
|
1837
|
+
* Each branch has independent state. Place combinators such as {@link take}
|
|
1838
|
+
* outside `whenInput` when their state must be shared across both branches.
|
|
1839
|
+
*
|
|
1840
|
+
* ### Selecting a schedule by input
|
|
1378
1841
|
*
|
|
1379
1842
|
* ```ts
|
|
1843
|
+
* import {
|
|
1844
|
+
* done,
|
|
1845
|
+
* exponential,
|
|
1846
|
+
* take,
|
|
1847
|
+
* testCreateDeps,
|
|
1848
|
+
* whenInput,
|
|
1849
|
+
* type Millis,
|
|
1850
|
+
* type Typed,
|
|
1851
|
+
* } from "@evolu/common";
|
|
1852
|
+
*
|
|
1380
1853
|
* interface MyError extends Typed<"Throttled" | "NetworkError"> {}
|
|
1381
1854
|
*
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1855
|
+
* // The outer take shares one retry limit across both error branches.
|
|
1856
|
+
* const awsWithThrottling = take(3)(
|
|
1857
|
+
* whenInput<MyError, Millis>(
|
|
1858
|
+
* (error) => error.type === "Throttled",
|
|
1859
|
+
* exponential("1s"),
|
|
1860
|
+
* )(exponential("100ms")),
|
|
1861
|
+
* );
|
|
1862
|
+
* const step = awsWithThrottling(testCreateDeps());
|
|
1863
|
+
* expectOk(step({ type: "Throttled" }), [1000, 1000]);
|
|
1864
|
+
* expectOk(step({ type: "NetworkError" }), [100, 100]);
|
|
1865
|
+
* expectOk(step({ type: "Throttled" }), [2000, 2000]);
|
|
1866
|
+
* expectErr(step({ type: "NetworkError" }), done());
|
|
1386
1867
|
* ```
|
|
1387
1868
|
*
|
|
1388
1869
|
* @group Composition
|
|
@@ -1396,9 +1877,12 @@ export const whenInput =
|
|
|
1396
1877
|
(deps) => {
|
|
1397
1878
|
const normalStep = schedule(deps);
|
|
1398
1879
|
const altStep = altSchedule(deps);
|
|
1880
|
+
let stopped = false;
|
|
1399
1881
|
return (input) => {
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1882
|
+
if (stopped) return err(done());
|
|
1883
|
+
const result = predicate(input) ? altStep(input) : normalStep(input);
|
|
1884
|
+
if (!result.ok) stopped = true;
|
|
1885
|
+
return result;
|
|
1402
1886
|
};
|
|
1403
1887
|
};
|
|
1404
1888
|
|
|
@@ -1408,19 +1892,34 @@ export const whenInput =
|
|
|
1408
1892
|
* Useful for logging, monitoring, or debugging without changing schedule
|
|
1409
1893
|
* behavior.
|
|
1410
1894
|
*
|
|
1411
|
-
* ###
|
|
1895
|
+
* ### Observing schedule output
|
|
1412
1896
|
*
|
|
1413
1897
|
* ```ts
|
|
1414
|
-
*
|
|
1898
|
+
* import {
|
|
1899
|
+
* exponential,
|
|
1900
|
+
* retryStrategyAws,
|
|
1901
|
+
* tapScheduleOutput,
|
|
1902
|
+
* testCreateDeps,
|
|
1903
|
+
* type Millis,
|
|
1904
|
+
* } from "@evolu/common";
|
|
1905
|
+
*
|
|
1906
|
+
* // Log each delay for debugging.
|
|
1907
|
+
* const messages: Array<string> = [];
|
|
1415
1908
|
* const logged = tapScheduleOutput((delay: Millis) => {
|
|
1416
|
-
*
|
|
1909
|
+
* messages.push(`Next delay: ${delay}ms`);
|
|
1417
1910
|
* })(exponential("100ms"));
|
|
1418
1911
|
*
|
|
1419
|
-
* // Track
|
|
1420
|
-
* const
|
|
1421
|
-
* const tracked = tapScheduleOutput((
|
|
1422
|
-
*
|
|
1912
|
+
* // Track the preset's pre-jitter delay candidates without changing it.
|
|
1913
|
+
* const recordedCandidates: Array<Millis> = [];
|
|
1914
|
+
* const tracked = tapScheduleOutput((candidate: Millis) => {
|
|
1915
|
+
* recordedCandidates.push(candidate);
|
|
1423
1916
|
* })(retryStrategyAws);
|
|
1917
|
+
* const deps = testCreateDeps();
|
|
1918
|
+
* logged(deps)(undefined);
|
|
1919
|
+
* tracked(deps)(undefined);
|
|
1920
|
+
*
|
|
1921
|
+
* expect(messages).toEqual(["Next delay: 100ms"]);
|
|
1922
|
+
* expect(recordedCandidates).toEqual([50]);
|
|
1424
1923
|
* ```
|
|
1425
1924
|
*
|
|
1426
1925
|
* @group Side effects
|
|
@@ -1444,23 +1943,38 @@ export const tapScheduleOutput =
|
|
|
1444
1943
|
* Useful for logging errors during retry or monitoring what values are being
|
|
1445
1944
|
* processed.
|
|
1446
1945
|
*
|
|
1447
|
-
* ###
|
|
1946
|
+
* ### Observing schedule input
|
|
1448
1947
|
*
|
|
1449
1948
|
* ```ts
|
|
1450
|
-
*
|
|
1949
|
+
* import {
|
|
1950
|
+
* exponential,
|
|
1951
|
+
* tapScheduleInput,
|
|
1952
|
+
* testCreateDeps,
|
|
1953
|
+
* type Millis,
|
|
1954
|
+
* type Schedule,
|
|
1955
|
+
* type Typed,
|
|
1956
|
+
* } from "@evolu/common";
|
|
1451
1957
|
*
|
|
1958
|
+
* interface MyError extends Typed<"NetworkError" | "Timeout"> {}
|
|
1452
1959
|
* const retrySchedule: Schedule<Millis, MyError> = exponential("100ms");
|
|
1453
|
-
*
|
|
1454
|
-
*
|
|
1960
|
+
* // Log errors for debugging.
|
|
1961
|
+
* const messages: Array<string> = [];
|
|
1455
1962
|
* const logged = tapScheduleInput((error: MyError) => {
|
|
1456
|
-
*
|
|
1963
|
+
* messages.push(`Retrying after error: ${error.type}`);
|
|
1457
1964
|
* })(retrySchedule);
|
|
1458
1965
|
*
|
|
1459
|
-
* //
|
|
1966
|
+
* // Or retain just the retry reasons for metrics.
|
|
1460
1967
|
* const reasons: Array<string> = [];
|
|
1461
1968
|
* const tracked = tapScheduleInput((error: MyError) => {
|
|
1462
1969
|
* reasons.push(error.type);
|
|
1463
1970
|
* })(retrySchedule);
|
|
1971
|
+
* const deps = testCreateDeps();
|
|
1972
|
+
* const error: MyError = { type: "NetworkError" };
|
|
1973
|
+
* logged(deps)(error);
|
|
1974
|
+
* tracked(deps)(error);
|
|
1975
|
+
*
|
|
1976
|
+
* expect(messages).toEqual(["Retrying after error: NetworkError"]);
|
|
1977
|
+
* expect(reasons).toEqual(["NetworkError"]);
|
|
1464
1978
|
* ```
|
|
1465
1979
|
*
|
|
1466
1980
|
* @group Side effects
|
|
@@ -1477,17 +1991,19 @@ export const tapScheduleInput =
|
|
|
1477
1991
|
};
|
|
1478
1992
|
|
|
1479
1993
|
/**
|
|
1480
|
-
* AWS
|
|
1994
|
+
* AWS SDK for Java 2.1 ordinary-failure retry timing.
|
|
1481
1995
|
*
|
|
1482
|
-
* Exponential backoff (
|
|
1996
|
+
* Exponential backoff (50ms base), max 2 retries (3 total attempts), 20s cap,
|
|
1483
1997
|
* full jitter.
|
|
1484
1998
|
*
|
|
1999
|
+
* This schedule does not model throttling-specific timing, token accounting, or
|
|
2000
|
+
* circuit breaking.
|
|
2001
|
+
*
|
|
1485
2002
|
* @group Retry Strategies
|
|
1486
|
-
* @see https://github.com/aws/aws-sdk-java-v2/blob/
|
|
1487
|
-
* @see https://github.com/aws/aws-sdk-java-v2/blob/master/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java
|
|
2003
|
+
* @see https://github.com/aws/aws-sdk-java-v2/blob/b69b75f07b6ebd93fd44b032d49b76a3b71fbb90/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java
|
|
1488
2004
|
*/
|
|
1489
|
-
export const retryStrategyAws: Schedule<Millis> = /*#__PURE__*/ jitter(
|
|
2005
|
+
export const retryStrategyAws: Schedule<Millis> = /*#__PURE__*/ jitter("100%")(
|
|
1490
2006
|
/*#__PURE__*/ maxDelay("20s")(
|
|
1491
|
-
/*#__PURE__*/ take(2)(/*#__PURE__*/ exponential("
|
|
2007
|
+
/*#__PURE__*/ take(2)(/*#__PURE__*/ exponential("50ms")),
|
|
1492
2008
|
),
|
|
1493
2009
|
);
|