@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/dist/src/Schedule.js
CHANGED
|
@@ -3,42 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { fibonacciAt, FibonacciIndex, increment } from "./Number.js";
|
|
6
|
+
import { fibonacciAt, FibonacciIndex, increment, max, min, percentageToRatio, } from "./Number.js";
|
|
8
7
|
import { done, err, ok } from "./Result.js";
|
|
9
|
-
import { durationToMillis, Millis, minMillis, saturateMillis, } from "./Time.js";
|
|
10
|
-
import {
|
|
11
|
-
/**
|
|
12
|
-
* Creates an internal per-step metrics tracker.
|
|
13
|
-
*
|
|
14
|
-
* Each call updates internal state and returns computed metrics.
|
|
15
|
-
*/
|
|
16
|
-
const createScheduleStepMetrics = (deps) => {
|
|
17
|
-
let attempt = onePositiveInt;
|
|
18
|
-
let start = null;
|
|
19
|
-
let previous = null;
|
|
20
|
-
return () => {
|
|
21
|
-
const now = deps.time.now();
|
|
22
|
-
const currentAttempt = attempt;
|
|
23
|
-
attempt = PositiveInt.orThrow(increment(attempt));
|
|
24
|
-
start ??= now;
|
|
25
|
-
const elapsed = saturateMillis(now - start);
|
|
26
|
-
const elapsedSincePrevious = previous === null ? minMillis : saturateMillis(now - previous);
|
|
27
|
-
previous = now;
|
|
28
|
-
return { attempt: currentAttempt, elapsed, elapsedSincePrevious };
|
|
29
|
-
};
|
|
30
|
-
};
|
|
8
|
+
import { durationToMillis, Millis, minMillis, PositiveMillis, saturateMillis, } from "./Time.js";
|
|
9
|
+
import { assertType, NonNaNNumber, NonNegativeFiniteNumber, PositiveInt, } from "./Type.js";
|
|
31
10
|
/**
|
|
32
11
|
* A schedule that never stops and has no delay.
|
|
33
12
|
*
|
|
34
13
|
* Outputs the attempt count (0, 1, 2, ...). Useful as a base for composition or
|
|
35
14
|
* for immediate retry without backoff.
|
|
36
15
|
*
|
|
37
|
-
* ###
|
|
16
|
+
* ### Recurring immediately
|
|
38
17
|
*
|
|
39
18
|
* ```ts
|
|
40
|
-
*
|
|
19
|
+
* import { forever, take, testCreateDeps } from "@evolu/common";
|
|
20
|
+
*
|
|
21
|
+
* // Retry immediately, up to 5 times.
|
|
41
22
|
* const immediate = take(5)(forever);
|
|
23
|
+
* const step = immediate(testCreateDeps());
|
|
24
|
+
* expectOk(step(undefined), [0, 0]);
|
|
42
25
|
* ```
|
|
43
26
|
*
|
|
44
27
|
* @group Constructors
|
|
@@ -50,13 +33,18 @@ export const forever = () => {
|
|
|
50
33
|
/**
|
|
51
34
|
* A schedule that runs exactly once with no delay.
|
|
52
35
|
*
|
|
53
|
-
*
|
|
36
|
+
* Equivalent to `take(1)(forever)`. Useful for one-shot operations.
|
|
54
37
|
*
|
|
55
|
-
* ###
|
|
38
|
+
* ### Scheduling one recurrence
|
|
56
39
|
*
|
|
57
40
|
* ```ts
|
|
58
|
-
*
|
|
41
|
+
* import { done, once, testCreateDeps } from "@evolu/common";
|
|
42
|
+
*
|
|
43
|
+
* // Produce one scheduled recurrence, then stop.
|
|
59
44
|
* const oneShot = once;
|
|
45
|
+
* const step = oneShot(testCreateDeps());
|
|
46
|
+
* expectOk(step(undefined), [0, 0]);
|
|
47
|
+
* expectErr(step(undefined), done());
|
|
60
48
|
* ```
|
|
61
49
|
*
|
|
62
50
|
* @group Constructors
|
|
@@ -76,11 +64,21 @@ export const once = () => {
|
|
|
76
64
|
* Convenience for `take(n)(forever)`. Outputs the current repetition count (0,
|
|
77
65
|
* 1, 2, ..., n-1).
|
|
78
66
|
*
|
|
79
|
-
*
|
|
67
|
+
* `n` uses {@link Int0To100OrNonNegativeInt}: pass `0` to `100` as a literal, or
|
|
68
|
+
* a validated {@link NonNegativeInt} for larger or dynamic values.
|
|
69
|
+
*
|
|
70
|
+
* ### Limiting recurrence count
|
|
80
71
|
*
|
|
81
72
|
* ```ts
|
|
82
|
-
*
|
|
73
|
+
* import { done, recurs, testCreateDeps } from "@evolu/common";
|
|
74
|
+
*
|
|
75
|
+
* // Retry up to 3 times (4 total attempts including the initial operation).
|
|
83
76
|
* const retry = recurs(3);
|
|
77
|
+
* const step = retry(testCreateDeps());
|
|
78
|
+
* expectOk(step(undefined), [0, 0]);
|
|
79
|
+
* step(undefined);
|
|
80
|
+
* step(undefined);
|
|
81
|
+
* expectErr(step(undefined), done());
|
|
84
82
|
* ```
|
|
85
83
|
*
|
|
86
84
|
* @group Constructors
|
|
@@ -92,17 +90,20 @@ export const recurs = (n) => take(n)(forever);
|
|
|
92
90
|
* Always waits the same duration after each execution completes. Never stops —
|
|
93
91
|
* combine with {@link take} or {@link maxElapsed} to limit.
|
|
94
92
|
*
|
|
95
|
-
* ###
|
|
93
|
+
* ### Constant spacing
|
|
96
94
|
*
|
|
97
95
|
* ```ts
|
|
98
|
-
*
|
|
99
|
-
* const poll = spaced("1s");
|
|
96
|
+
* import { spaced, take, testCreateDeps } from "@evolu/common";
|
|
100
97
|
*
|
|
101
|
-
* //
|
|
98
|
+
* // Poll every second, retry three times, or run a long-lived heartbeat.
|
|
99
|
+
* const poll = spaced("1s");
|
|
102
100
|
* const retry = take(3)(spaced("500ms"));
|
|
103
|
-
*
|
|
104
|
-
* // Heartbeat schedule
|
|
105
101
|
* const heartbeat = spaced("30s");
|
|
102
|
+
* const deps = testCreateDeps();
|
|
103
|
+
*
|
|
104
|
+
* expectOk(poll(deps)(undefined), [1000, 1000]);
|
|
105
|
+
* expectOk(retry(deps)(undefined), [500, 500]);
|
|
106
|
+
* expectOk(heartbeat(deps)(undefined), [30000, 30000]);
|
|
106
107
|
* ```
|
|
107
108
|
*
|
|
108
109
|
* @group Constructors
|
|
@@ -114,57 +115,71 @@ export const spaced = (duration) => () => {
|
|
|
114
115
|
/**
|
|
115
116
|
* Exponential backoff schedule.
|
|
116
117
|
*
|
|
117
|
-
* Computes delay as `base * factor^(
|
|
118
|
+
* Computes delay as `base * factor^(step - 1)`:
|
|
118
119
|
*
|
|
119
|
-
* -
|
|
120
|
-
* -
|
|
121
|
-
* -
|
|
120
|
+
* - Step 1: `base`
|
|
121
|
+
* - Step 2: `base * factor`
|
|
122
|
+
* - Step 3: `base * factor²`
|
|
122
123
|
* - ...
|
|
123
124
|
*
|
|
124
125
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
125
126
|
*
|
|
126
|
-
* ###
|
|
127
|
+
* ### Exponential growth factors
|
|
127
128
|
*
|
|
128
129
|
* ```ts
|
|
129
|
-
*
|
|
130
|
-
* const exp = exponential("100ms");
|
|
130
|
+
* import { exponential, testCreateDeps } from "@evolu/common";
|
|
131
131
|
*
|
|
132
|
-
* //
|
|
132
|
+
* // Standard doubling and gentler 1.5× growth.
|
|
133
|
+
* const standard = exponential("100ms");
|
|
133
134
|
* const gentle = exponential("100ms", 1.5);
|
|
135
|
+
* const standardStep = standard(testCreateDeps());
|
|
136
|
+
* const gentleStep = gentle(testCreateDeps());
|
|
137
|
+
* expectOk(standardStep(undefined), [100, 100]);
|
|
138
|
+
* expectOk(standardStep(undefined), [200, 200]);
|
|
139
|
+
* expectOk(gentleStep(undefined), [100, 100]);
|
|
140
|
+
* expectOk(gentleStep(undefined), [150, 150]);
|
|
134
141
|
* ```
|
|
135
142
|
*
|
|
136
143
|
* @group Constructors
|
|
137
144
|
*/
|
|
138
145
|
export const exponential = (base, factor = 2) => {
|
|
139
|
-
assertType(NonNegativeFiniteNumber, factor
|
|
146
|
+
assertType(NonNegativeFiniteNumber, factor);
|
|
140
147
|
return () => {
|
|
141
148
|
const baseMs = durationToMillis(base);
|
|
142
149
|
let attempt = 0;
|
|
143
150
|
return () => {
|
|
144
151
|
attempt++;
|
|
145
|
-
const rawDelay = baseMs * Math.pow(factor, attempt - 1);
|
|
146
|
-
const delay =
|
|
152
|
+
const rawDelay = baseMs === 0 ? minMillis : baseMs * Math.pow(factor, attempt - 1);
|
|
153
|
+
const delay = saturateComputedMillis(rawDelay);
|
|
147
154
|
return ok([delay, delay]);
|
|
148
155
|
};
|
|
149
156
|
};
|
|
150
157
|
};
|
|
158
|
+
const saturateComputedMillis = (value) => {
|
|
159
|
+
assertType(NonNaNNumber, value);
|
|
160
|
+
return saturateMillis(value);
|
|
161
|
+
};
|
|
151
162
|
/**
|
|
152
163
|
* Linear backoff schedule.
|
|
153
164
|
*
|
|
154
|
-
* Delay increases linearly: `base *
|
|
165
|
+
* Delay increases linearly: `base * step`:
|
|
155
166
|
*
|
|
156
|
-
* -
|
|
157
|
-
* -
|
|
158
|
-
* -
|
|
167
|
+
* - Step 1: `base`
|
|
168
|
+
* - Step 2: `base * 2`
|
|
169
|
+
* - Step 3: `base * 3`
|
|
159
170
|
* - ...
|
|
160
171
|
*
|
|
161
172
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
162
173
|
*
|
|
163
|
-
* ###
|
|
174
|
+
* ### Linear growth
|
|
164
175
|
*
|
|
165
176
|
* ```ts
|
|
177
|
+
* import { linear, testCreateDeps } from "@evolu/common";
|
|
178
|
+
*
|
|
166
179
|
* // 100ms, 200ms, 300ms, 400ms, ...
|
|
167
|
-
* const
|
|
180
|
+
* const step = linear("100ms")(testCreateDeps());
|
|
181
|
+
* expectOk(step(undefined), [100, 100]);
|
|
182
|
+
* expectOk(step(undefined), [200, 200]);
|
|
168
183
|
* ```
|
|
169
184
|
*
|
|
170
185
|
* @group Constructors
|
|
@@ -174,7 +189,7 @@ export const linear = (base) => () => {
|
|
|
174
189
|
let attempt = 0;
|
|
175
190
|
return () => {
|
|
176
191
|
attempt++;
|
|
177
|
-
const delay =
|
|
192
|
+
const delay = saturateComputedMillis(ms * attempt);
|
|
178
193
|
return ok([delay, delay]);
|
|
179
194
|
};
|
|
180
195
|
};
|
|
@@ -183,20 +198,25 @@ export const linear = (base) => () => {
|
|
|
183
198
|
*
|
|
184
199
|
* Delays follow the Fibonacci sequence, growing more slowly than exponential:
|
|
185
200
|
*
|
|
186
|
-
* -
|
|
187
|
-
* -
|
|
188
|
-
* -
|
|
189
|
-
* -
|
|
190
|
-
* -
|
|
201
|
+
* - Step 1: `initial`
|
|
202
|
+
* - Step 2: `initial`
|
|
203
|
+
* - Step 3: `initial * 2`
|
|
204
|
+
* - Step 4: `initial * 3`
|
|
205
|
+
* - Step 5: `initial * 5`
|
|
191
206
|
* - ...
|
|
192
207
|
*
|
|
193
208
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
194
209
|
*
|
|
195
|
-
* ###
|
|
210
|
+
* ### Fibonacci growth
|
|
196
211
|
*
|
|
197
212
|
* ```ts
|
|
198
|
-
*
|
|
199
|
-
*
|
|
213
|
+
* import { fibonacci, testCreateDeps } from "@evolu/common";
|
|
214
|
+
*
|
|
215
|
+
* // 100ms, 100ms, 200ms, 300ms, 500ms, ...
|
|
216
|
+
* const step = fibonacci("100ms")(testCreateDeps());
|
|
217
|
+
* expectOk(step(undefined), [100, 100]);
|
|
218
|
+
* expectOk(step(undefined), [100, 100]);
|
|
219
|
+
* expectOk(step(undefined), [200, 200]);
|
|
200
220
|
* ```
|
|
201
221
|
*
|
|
202
222
|
* @group Constructors
|
|
@@ -205,7 +225,7 @@ export const fibonacci = (initial) => () => {
|
|
|
205
225
|
const ms = durationToMillis(initial);
|
|
206
226
|
let index = FibonacciIndex.orThrow(1);
|
|
207
227
|
return () => {
|
|
208
|
-
const delay =
|
|
228
|
+
const delay = saturateComputedMillis(ms * fibonacciAt(index));
|
|
209
229
|
index = FibonacciIndex.orNull(increment(index)) ?? index;
|
|
210
230
|
return ok([delay, delay]);
|
|
211
231
|
};
|
|
@@ -215,20 +235,27 @@ export const fibonacci = (initial) => () => {
|
|
|
215
235
|
*
|
|
216
236
|
* Recurs on a fixed interval, outputting the repetition count (0, 1, 2, ...).
|
|
217
237
|
* Unlike {@link spaced}, which waits a duration _after_ each execution, `fixed`
|
|
218
|
-
* maintains a consistent cadence from
|
|
238
|
+
* maintains a consistent cadence from the first schedule step.
|
|
219
239
|
*
|
|
220
|
-
* If execution
|
|
221
|
-
* immediately
|
|
222
|
-
*
|
|
240
|
+
* If execution falls behind by one or more intervals, missed recurrences happen
|
|
241
|
+
* immediately until the schedule catches up to the original cadence. Use
|
|
242
|
+
* {@link windowed} to skip missed recurrences instead.
|
|
223
243
|
*
|
|
224
|
-
* ###
|
|
244
|
+
* ### Maintaining a fixed cadence
|
|
225
245
|
*
|
|
226
246
|
* ```ts
|
|
227
|
-
*
|
|
228
|
-
* const healthCheck = take(10)(fixed("5s"));
|
|
247
|
+
* import { fixed, take, testCreateDeps } from "@evolu/common";
|
|
229
248
|
*
|
|
230
|
-
* //
|
|
249
|
+
* // A bounded health check and an unbounded cron-like cadence.
|
|
250
|
+
* const healthCheck = take(10)(fixed("5s"));
|
|
231
251
|
* const cronLike = fixed("1m");
|
|
252
|
+
*
|
|
253
|
+
* const healthDeps = testCreateDeps();
|
|
254
|
+
* const healthStep = healthCheck(healthDeps);
|
|
255
|
+
* expectOk(healthStep(undefined), [0, 5000]);
|
|
256
|
+
* healthDeps.time.advance("3s");
|
|
257
|
+
* expectOk(healthStep(undefined), [1, 2000]);
|
|
258
|
+
* expectOk(cronLike(testCreateDeps())(undefined), [0, 60000]);
|
|
232
259
|
* ```
|
|
233
260
|
*
|
|
234
261
|
* @group Constructors
|
|
@@ -246,23 +273,50 @@ export const fixed = (interval) => (deps) => {
|
|
|
246
273
|
const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
|
|
247
274
|
const boundary = intervalMs - remainder;
|
|
248
275
|
const delay = runningBehind ? 0 : boundary;
|
|
249
|
-
return ok([count++,
|
|
276
|
+
return ok([count++, saturateComputedMillis(delay)]);
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* Creates an internal per-step metrics tracker.
|
|
281
|
+
*
|
|
282
|
+
* Each call updates internal state and returns computed metrics.
|
|
283
|
+
*/
|
|
284
|
+
const createScheduleStepMetrics = (deps) => {
|
|
285
|
+
let start = null;
|
|
286
|
+
let previous = null;
|
|
287
|
+
return () => {
|
|
288
|
+
const now = deps.time.now();
|
|
289
|
+
start ??= now;
|
|
290
|
+
const elapsed = saturateComputedMillis(now - start);
|
|
291
|
+
const elapsedSincePrevious = previous === null ? minMillis : saturateComputedMillis(now - previous);
|
|
292
|
+
previous = now;
|
|
293
|
+
return { elapsed, elapsedSincePrevious };
|
|
250
294
|
};
|
|
251
295
|
};
|
|
252
296
|
/**
|
|
253
297
|
* Divides the timeline into fixed windows and sleeps until the next boundary.
|
|
254
298
|
*
|
|
255
|
-
* Similar to {@link fixed}, but
|
|
256
|
-
*
|
|
299
|
+
* Similar to {@link fixed}, but skips missed recurrences and always sleeps until
|
|
300
|
+
* the next window boundary. Outputs the repetition count.
|
|
257
301
|
*
|
|
258
|
-
* Useful for aligning executions to regular intervals from the
|
|
302
|
+
* Useful for aligning executions to regular intervals from the first step.
|
|
259
303
|
*
|
|
260
|
-
* ###
|
|
304
|
+
* ### Aligning to time windows
|
|
261
305
|
*
|
|
262
306
|
* ```ts
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
307
|
+
* import { testCreateDeps, windowed } from "@evolu/common";
|
|
308
|
+
*
|
|
309
|
+
* const stepAfter = (elapsed: "3s" | "7s") => {
|
|
310
|
+
* const deps = testCreateDeps();
|
|
311
|
+
* const step = windowed("5s")(deps);
|
|
312
|
+
* step(undefined);
|
|
313
|
+
* deps.time.advance(elapsed);
|
|
314
|
+
* return step(undefined);
|
|
315
|
+
* };
|
|
316
|
+
*
|
|
317
|
+
* // At 3s the next boundary is 2s away; at 7s it is 3s away.
|
|
318
|
+
* expectOk(stepAfter("3s"), [1, 2000]);
|
|
319
|
+
* expectOk(stepAfter("7s"), [1, 3000]);
|
|
266
320
|
* ```
|
|
267
321
|
*
|
|
268
322
|
* @group Constructors
|
|
@@ -275,7 +329,7 @@ export const windowed = (interval) => (deps) => {
|
|
|
275
329
|
const { elapsed } = metrics();
|
|
276
330
|
const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
|
|
277
331
|
const delay = intervalMs === 0 ? 0 : intervalMs - remainder;
|
|
278
|
-
return ok([count++,
|
|
332
|
+
return ok([count++, saturateComputedMillis(delay)]);
|
|
279
333
|
};
|
|
280
334
|
};
|
|
281
335
|
/**
|
|
@@ -283,11 +337,15 @@ export const windowed = (interval) => (deps) => {
|
|
|
283
337
|
*
|
|
284
338
|
* Convenience for `take(1)(spaced(delay))`. Useful for simple one-shot delays.
|
|
285
339
|
*
|
|
286
|
-
* ###
|
|
340
|
+
* ### Scheduling one delayed recurrence
|
|
287
341
|
*
|
|
288
342
|
* ```ts
|
|
289
|
-
*
|
|
290
|
-
*
|
|
343
|
+
* import { done, fromDelay, testCreateDeps } from "@evolu/common";
|
|
344
|
+
*
|
|
345
|
+
* // Wait one second, then stop.
|
|
346
|
+
* const step = fromDelay("1s")(testCreateDeps());
|
|
347
|
+
* expectOk(step(undefined), [1000, 1000]);
|
|
348
|
+
* expectErr(step(undefined), done());
|
|
291
349
|
* ```
|
|
292
350
|
*
|
|
293
351
|
* @group Constructors
|
|
@@ -297,35 +355,57 @@ export const fromDelay = (delay) => take(1)(spaced(delay));
|
|
|
297
355
|
* A schedule that runs through a sequence of delays.
|
|
298
356
|
*
|
|
299
357
|
* Convenience for sequencing single-delay schedules. Useful for predefined
|
|
300
|
-
* retry patterns.
|
|
358
|
+
* retry patterns. With no delays, returns a schedule that stops immediately.
|
|
301
359
|
*
|
|
302
|
-
* ###
|
|
360
|
+
* ### Sequencing custom delays
|
|
303
361
|
*
|
|
304
362
|
* ```ts
|
|
305
|
-
*
|
|
363
|
+
* import { done, fromDelays, testCreateDeps } from "@evolu/common";
|
|
364
|
+
*
|
|
365
|
+
* // A custom retry sequence: 100ms, 500ms, then 2s.
|
|
306
366
|
* const custom = fromDelays("100ms", "500ms", "2s");
|
|
367
|
+
* const step = custom(testCreateDeps());
|
|
368
|
+
* expectOk(step(undefined), [100, 100]);
|
|
369
|
+
* expectOk(step(undefined), [500, 500]);
|
|
370
|
+
* expectOk(step(undefined), [2000, 2000]);
|
|
371
|
+
* expectErr(step(undefined), done());
|
|
307
372
|
* ```
|
|
308
373
|
*
|
|
309
374
|
* @group Constructors
|
|
310
375
|
*/
|
|
311
376
|
export const fromDelays = (...delays) => sequenceSchedules(...delays.map((d) => fromDelay(d)));
|
|
312
377
|
/**
|
|
313
|
-
* A schedule that outputs the total elapsed time since
|
|
378
|
+
* A schedule that outputs the total elapsed time since its first step.
|
|
314
379
|
*
|
|
315
380
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit. Useful
|
|
316
381
|
* for tracking how long a retry sequence has been running.
|
|
317
382
|
*
|
|
318
|
-
* ###
|
|
383
|
+
* ### Tracking elapsed time
|
|
319
384
|
*
|
|
320
385
|
* ```ts
|
|
321
|
-
*
|
|
386
|
+
* import {
|
|
387
|
+
* done,
|
|
388
|
+
* elapsed,
|
|
389
|
+
* exponential,
|
|
390
|
+
* intersectSchedules,
|
|
391
|
+
* testCreateDeps,
|
|
392
|
+
* whileScheduleOutput,
|
|
393
|
+
* type Millis,
|
|
394
|
+
* } from "@evolu/common";
|
|
395
|
+
*
|
|
396
|
+
* // Track elapsed time alongside each backoff step.
|
|
322
397
|
* const withTiming = intersectSchedules(exponential("100ms"), elapsed);
|
|
323
|
-
*
|
|
398
|
+
* expectOk(withTiming(testCreateDeps())(undefined), [[100, 0], 100]);
|
|
324
399
|
*
|
|
325
|
-
* //
|
|
400
|
+
* // Or stop a schedule after 30 seconds of elapsed time.
|
|
326
401
|
* const timeLimited = whileScheduleOutput((ms: Millis) => ms < 30000)(
|
|
327
402
|
* elapsed,
|
|
328
403
|
* );
|
|
404
|
+
* const deps = testCreateDeps();
|
|
405
|
+
* const step = timeLimited(deps);
|
|
406
|
+
* step(undefined);
|
|
407
|
+
* deps.time.advance("30s");
|
|
408
|
+
* expectErr(step(undefined), done());
|
|
329
409
|
* ```
|
|
330
410
|
*
|
|
331
411
|
* @group Constructors
|
|
@@ -340,17 +420,31 @@ export const elapsed = (deps) => {
|
|
|
340
420
|
* Outputs the elapsed time. Useful for time-boxed operations or combining with
|
|
341
421
|
* other schedules to create time-limited variants.
|
|
342
422
|
*
|
|
343
|
-
* ###
|
|
423
|
+
* ### Time-boxing a schedule
|
|
344
424
|
*
|
|
345
425
|
* ```ts
|
|
346
|
-
*
|
|
426
|
+
* import {
|
|
427
|
+
* done,
|
|
428
|
+
* during,
|
|
429
|
+
* exponential,
|
|
430
|
+
* intersectSchedules,
|
|
431
|
+
* testCreateDeps,
|
|
432
|
+
* } from "@evolu/common";
|
|
433
|
+
*
|
|
434
|
+
* // Run for at most 30 seconds.
|
|
347
435
|
* const timeLimited = during("30s");
|
|
436
|
+
* const deps = testCreateDeps();
|
|
437
|
+
* const step = timeLimited(deps);
|
|
438
|
+
* expectOk(step(undefined), [0, 0]);
|
|
439
|
+
* deps.time.advance("30.1s");
|
|
440
|
+
* expectErr(step(undefined), done());
|
|
348
441
|
*
|
|
349
|
-
* // Combine with
|
|
442
|
+
* // Combine elapsed time with backoff for a time-boxed retry.
|
|
350
443
|
* const timedRetry = intersectSchedules(
|
|
351
444
|
* exponential("100ms"),
|
|
352
445
|
* during("10s"),
|
|
353
446
|
* );
|
|
447
|
+
* expectOk(timedRetry(testCreateDeps())(undefined), [[100, 0], 100]);
|
|
354
448
|
* ```
|
|
355
449
|
*
|
|
356
450
|
* @group Constructors
|
|
@@ -361,17 +455,29 @@ export const during = (duration) => whileScheduleOutput((ms) => ms <= durationTo
|
|
|
361
455
|
*
|
|
362
456
|
* Never stops — combine with {@link take} or {@link maxElapsed} to limit.
|
|
363
457
|
*
|
|
364
|
-
* ###
|
|
458
|
+
* ### Adding constant output
|
|
365
459
|
*
|
|
366
460
|
* ```ts
|
|
367
|
-
*
|
|
461
|
+
* import {
|
|
462
|
+
* always,
|
|
463
|
+
* exponential,
|
|
464
|
+
* intersectSchedules,
|
|
465
|
+
* testCreateDeps,
|
|
466
|
+
* } from "@evolu/common";
|
|
467
|
+
*
|
|
468
|
+
* // Always emit the same label.
|
|
368
469
|
* const labeled = always("retry");
|
|
470
|
+
* expectOk(labeled(testCreateDeps())(undefined), ["retry", 0]);
|
|
369
471
|
*
|
|
370
|
-
* //
|
|
472
|
+
* // Add a label while preserving exponential timing.
|
|
371
473
|
* const withLabel = intersectSchedules(
|
|
372
474
|
* exponential("100ms"),
|
|
373
475
|
* always("backoff"),
|
|
374
476
|
* );
|
|
477
|
+
* expectOk(withLabel(testCreateDeps())(undefined), [
|
|
478
|
+
* [100, "backoff"],
|
|
479
|
+
* 100,
|
|
480
|
+
* ]);
|
|
375
481
|
* ```
|
|
376
482
|
*
|
|
377
483
|
* @group Constructors
|
|
@@ -384,18 +490,17 @@ export const always = (value) => mapSchedule(() => value)(forever);
|
|
|
384
490
|
* provided function. Never stops — combine with {@link take} or
|
|
385
491
|
* {@link maxElapsed} to limit.
|
|
386
492
|
*
|
|
387
|
-
* ###
|
|
493
|
+
* ### Unfolding state
|
|
388
494
|
*
|
|
389
495
|
* ```ts
|
|
390
|
-
*
|
|
391
|
-
* const counter = unfoldSchedule(0, (n) => n + 1);
|
|
496
|
+
* import { testCreateDeps, unfoldSchedule } from "@evolu/common";
|
|
392
497
|
*
|
|
393
|
-
* //
|
|
498
|
+
* // Unfold counters, custom backoff values, or state machines.
|
|
499
|
+
* const counter = unfoldSchedule(0, (n) => n + 1);
|
|
394
500
|
* const customBackoff = unfoldSchedule(100, (delay) =>
|
|
395
501
|
* Math.round(delay * 1.5),
|
|
396
502
|
* );
|
|
397
503
|
*
|
|
398
|
-
* // State machine
|
|
399
504
|
* type Phase = "init" | "warmup" | "active";
|
|
400
505
|
* const phases = unfoldSchedule<Phase>("init", (phase) => {
|
|
401
506
|
* switch (phase) {
|
|
@@ -407,6 +512,16 @@ export const always = (value) => mapSchedule(() => value)(forever);
|
|
|
407
512
|
* return "active";
|
|
408
513
|
* }
|
|
409
514
|
* });
|
|
515
|
+
*
|
|
516
|
+
* const counterStep = counter(testCreateDeps());
|
|
517
|
+
* const backoffStep = customBackoff(testCreateDeps());
|
|
518
|
+
* const phaseStep = phases(testCreateDeps());
|
|
519
|
+
* expectOk(counterStep(undefined), [0, 0]);
|
|
520
|
+
* expectOk(counterStep(undefined), [1, 0]);
|
|
521
|
+
* backoffStep(undefined);
|
|
522
|
+
* phaseStep(undefined);
|
|
523
|
+
* expectOk(backoffStep(undefined), [150, 0]);
|
|
524
|
+
* expectOk(phaseStep(undefined), ["warmup", 0]);
|
|
410
525
|
* ```
|
|
411
526
|
*
|
|
412
527
|
* @group Constructors
|
|
@@ -420,44 +535,59 @@ export const unfoldSchedule = (initial, next) => () => {
|
|
|
420
535
|
};
|
|
421
536
|
};
|
|
422
537
|
/**
|
|
423
|
-
* Limits a schedule to a maximum number of
|
|
538
|
+
* Limits a schedule to a maximum number of steps.
|
|
424
539
|
*
|
|
425
|
-
* After `n`
|
|
540
|
+
* After `n` steps, returns `Err(Done<void>)` (stop).
|
|
426
541
|
*
|
|
427
|
-
*
|
|
542
|
+
* `n` uses {@link Int0To100OrNonNegativeInt}: pass `0` to `100` as a literal, or
|
|
543
|
+
* a validated {@link NonNegativeInt} for larger or dynamic values.
|
|
544
|
+
*
|
|
545
|
+
* ### Limiting a schedule
|
|
428
546
|
*
|
|
429
547
|
* ```ts
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* //
|
|
548
|
+
* import { done, exponential, take, testCreateDeps } from "@evolu/common";
|
|
549
|
+
*
|
|
550
|
+
* // Three exponential retries, then Done.
|
|
551
|
+
* const step = take(3)(exponential("100ms"))(testCreateDeps());
|
|
552
|
+
* step(undefined);
|
|
553
|
+
* step(undefined);
|
|
554
|
+
* step(undefined);
|
|
555
|
+
* expectErr(step(undefined), done());
|
|
433
556
|
* ```
|
|
434
557
|
*
|
|
435
558
|
* @group Limiting
|
|
436
559
|
*/
|
|
437
|
-
export const take = (n) => {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return err(done());
|
|
446
|
-
return step(input);
|
|
447
|
-
};
|
|
560
|
+
export const take = (n) => (schedule) => (deps) => {
|
|
561
|
+
const step = schedule(deps);
|
|
562
|
+
let attempt = 0;
|
|
563
|
+
return (input) => {
|
|
564
|
+
attempt++;
|
|
565
|
+
if (attempt > n)
|
|
566
|
+
return err(done());
|
|
567
|
+
return step(input);
|
|
448
568
|
};
|
|
449
569
|
};
|
|
450
570
|
/**
|
|
451
|
-
* Limits schedule execution to a maximum elapsed time.
|
|
571
|
+
* Limits schedule execution to a maximum elapsed time since its first step.
|
|
452
572
|
*
|
|
453
|
-
* After `duration` has elapsed since the
|
|
454
|
-
* `Err(Done<void>)`.
|
|
573
|
+
* After `duration` has elapsed since the first step, returns `Err(Done<void>)`.
|
|
455
574
|
*
|
|
456
|
-
* ###
|
|
575
|
+
* ### Limiting elapsed time
|
|
457
576
|
*
|
|
458
577
|
* ```ts
|
|
459
|
-
*
|
|
460
|
-
*
|
|
578
|
+
* import {
|
|
579
|
+
* done,
|
|
580
|
+
* exponential,
|
|
581
|
+
* maxElapsed,
|
|
582
|
+
* testCreateDeps,
|
|
583
|
+
* } from "@evolu/common";
|
|
584
|
+
*
|
|
585
|
+
* // Retry for at most 30 seconds.
|
|
586
|
+
* const deps = testCreateDeps();
|
|
587
|
+
* const step = maxElapsed("30s")(exponential("1s"))(deps);
|
|
588
|
+
* expectOk(step(undefined), [1000, 1000]);
|
|
589
|
+
* deps.time.advance("30s");
|
|
590
|
+
* expectErr(step(undefined), done());
|
|
461
591
|
* ```
|
|
462
592
|
*
|
|
463
593
|
* @group Limiting
|
|
@@ -467,9 +597,16 @@ export const maxElapsed = (duration) => {
|
|
|
467
597
|
return (schedule) => (deps) => {
|
|
468
598
|
const step = schedule(deps);
|
|
469
599
|
const metrics = createScheduleStepMetrics(deps);
|
|
600
|
+
let stopped = false;
|
|
470
601
|
return (input) => {
|
|
602
|
+
if (stopped)
|
|
603
|
+
return err(done());
|
|
471
604
|
const { elapsed } = metrics();
|
|
472
|
-
|
|
605
|
+
if (elapsed >= maxMs) {
|
|
606
|
+
stopped = true;
|
|
607
|
+
return err(done());
|
|
608
|
+
}
|
|
609
|
+
return step(input);
|
|
473
610
|
};
|
|
474
611
|
};
|
|
475
612
|
};
|
|
@@ -478,12 +615,18 @@ export const maxElapsed = (duration) => {
|
|
|
478
615
|
*
|
|
479
616
|
* If the schedule returns a delay greater than `max`, returns `max` instead.
|
|
480
617
|
*
|
|
481
|
-
* ###
|
|
618
|
+
* ### Capping delays
|
|
482
619
|
*
|
|
483
620
|
* ```ts
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
* // 1s, 2s, 4s, 8s,
|
|
621
|
+
* import { exponential, maxDelay, testCreateDeps } from "@evolu/common";
|
|
622
|
+
*
|
|
623
|
+
* // Exponential delays grow 1s, 2s, 4s, 8s, then stay capped at 10s.
|
|
624
|
+
* const step = maxDelay("10s")(exponential("1s"))(testCreateDeps());
|
|
625
|
+
* step(undefined);
|
|
626
|
+
* step(undefined);
|
|
627
|
+
* step(undefined);
|
|
628
|
+
* step(undefined);
|
|
629
|
+
* expectOk(step(undefined), [16000, 10000]);
|
|
487
630
|
* ```
|
|
488
631
|
*
|
|
489
632
|
* @group Limiting
|
|
@@ -497,56 +640,85 @@ export const maxDelay = (max) => {
|
|
|
497
640
|
if (!result.ok)
|
|
498
641
|
return result;
|
|
499
642
|
const [output, delay] = result.value;
|
|
500
|
-
return ok([output,
|
|
643
|
+
return ok([output, min(delay, maxMs)]);
|
|
501
644
|
};
|
|
502
645
|
};
|
|
503
646
|
};
|
|
504
647
|
/**
|
|
505
|
-
*
|
|
648
|
+
* Randomizes delays by up to a percentage.
|
|
506
649
|
*
|
|
507
650
|
* Jitter helps prevent "thundering herd" when many clients retry simultaneously
|
|
508
|
-
* after a service recovers.
|
|
651
|
+
* after a service recovers. By default, the original delay is the upper bound:
|
|
509
652
|
*
|
|
510
|
-
* - `
|
|
511
|
-
* - `
|
|
512
|
-
* - `
|
|
653
|
+
* - `"0%"` — no jitter (original delay)
|
|
654
|
+
* - `"50%"` — equal jitter, shortens the delay by up to 50%
|
|
655
|
+
* - `"100%"` — full jitter, shortens the delay by up to 100%
|
|
513
656
|
*
|
|
514
|
-
*
|
|
657
|
+
* Pass `"around"` to preserve the average delay for periodic work:
|
|
515
658
|
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
659
|
+
* - `"0%"` — no jitter (original delay)
|
|
660
|
+
* - `"50%"` — varies by up to 50% below or above the original delay
|
|
661
|
+
* - `"100%"` — varies by up to 100% below or above the original delay
|
|
662
|
+
*
|
|
663
|
+
* ### Jittering below or around a delay
|
|
519
664
|
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
665
|
+
* ```ts
|
|
666
|
+
* import {
|
|
667
|
+
* exponential,
|
|
668
|
+
* jitter,
|
|
669
|
+
* spaced,
|
|
670
|
+
* testCreateDeps,
|
|
671
|
+
* type RandomNumber,
|
|
672
|
+
* } from "@evolu/common";
|
|
673
|
+
*
|
|
674
|
+
* const deps = {
|
|
675
|
+
* ...testCreateDeps(),
|
|
676
|
+
* random: { next: () => 0.5 as RandomNumber },
|
|
677
|
+
* };
|
|
678
|
+
*
|
|
679
|
+
* // Shorten retry delays by at most 25%.
|
|
680
|
+
* const conservative = jitter("25%")(exponential("1s"));
|
|
681
|
+
* // Poll around a 30s target cadence, from 27s to 33s.
|
|
682
|
+
* const polling = jitter("10%", "around")(spaced("30s"));
|
|
683
|
+
*
|
|
684
|
+
* expectOk(conservative(deps)(undefined), [1000, 875]);
|
|
685
|
+
* expectOk(polling(deps)(undefined), [30000, 30000]);
|
|
522
686
|
* ```
|
|
523
687
|
*
|
|
524
688
|
* @group Delay
|
|
525
689
|
*/
|
|
526
|
-
export const jitter = (
|
|
527
|
-
|
|
528
|
-
return (
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
690
|
+
export const jitter = (percentage = "50%", mode = "below") => {
|
|
691
|
+
const ratio = percentageToRatio(percentage);
|
|
692
|
+
return createJitter(ratio, mode === "around" ? 1 + ratio : 1);
|
|
693
|
+
};
|
|
694
|
+
const createJitter = (factor, maxMultiplier) => (schedule) => (deps) => {
|
|
695
|
+
const step = schedule(deps);
|
|
696
|
+
return (input) => {
|
|
697
|
+
const result = step(input);
|
|
698
|
+
if (!result.ok)
|
|
699
|
+
return result;
|
|
700
|
+
const [output, delay] = result.value;
|
|
701
|
+
const minMultiplier = 1 - factor;
|
|
702
|
+
const jittered = delay *
|
|
703
|
+
(minMultiplier + deps.random.next() * (maxMultiplier - minMultiplier));
|
|
704
|
+
return ok([output, saturateComputedMillis(jittered)]);
|
|
538
705
|
};
|
|
539
706
|
};
|
|
540
707
|
/**
|
|
541
|
-
*
|
|
708
|
+
* Replaces the schedule's first delay.
|
|
542
709
|
*
|
|
543
|
-
*
|
|
710
|
+
* The first successful step uses `initialDelay` instead of the delay produced
|
|
711
|
+
* by the schedule. Subsequent steps use the schedule's delays unchanged.
|
|
544
712
|
*
|
|
545
|
-
* ###
|
|
713
|
+
* ### Replacing the first delay
|
|
546
714
|
*
|
|
547
715
|
* ```ts
|
|
548
|
-
*
|
|
549
|
-
*
|
|
716
|
+
* import { delayed, exponential, testCreateDeps } from "@evolu/common";
|
|
717
|
+
*
|
|
718
|
+
* const step = delayed("1s")(exponential("100ms"))(testCreateDeps());
|
|
719
|
+
* // Only the first delay is replaced; later exponential delays are unchanged.
|
|
720
|
+
* expectOk(step(undefined), [100, 1000]);
|
|
721
|
+
* expectOk(step(undefined), [200, 200]);
|
|
550
722
|
* ```
|
|
551
723
|
*
|
|
552
724
|
* @group Delay
|
|
@@ -571,12 +743,14 @@ export const delayed = (initialDelay) => {
|
|
|
571
743
|
/**
|
|
572
744
|
* Adds a fixed delay to the schedule's existing delay.
|
|
573
745
|
*
|
|
574
|
-
* ###
|
|
746
|
+
* ### Adding to every delay
|
|
575
747
|
*
|
|
576
748
|
* ```ts
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
* //
|
|
749
|
+
* import { addDelay, exponential, testCreateDeps } from "@evolu/common";
|
|
750
|
+
*
|
|
751
|
+
* // Add 500ms to every exponential delay.
|
|
752
|
+
* const step = addDelay("500ms")(exponential("100ms"))(testCreateDeps());
|
|
753
|
+
* expectOk(step(undefined), [100, 600]);
|
|
580
754
|
* ```
|
|
581
755
|
*
|
|
582
756
|
* @group Delay
|
|
@@ -590,14 +764,21 @@ export const addDelay = (extra) => {
|
|
|
590
764
|
*
|
|
591
765
|
* More flexible than {@link maxDelay} — can implement any delay transformation.
|
|
592
766
|
*
|
|
593
|
-
* ###
|
|
767
|
+
* ### Transforming delays
|
|
594
768
|
*
|
|
595
769
|
* ```ts
|
|
596
|
-
*
|
|
597
|
-
*
|
|
770
|
+
* import { exponential, modifyDelay, testCreateDeps } from "@evolu/common";
|
|
771
|
+
*
|
|
772
|
+
* // Arbitrary transformations can double or cap delays.
|
|
773
|
+
* const slower = modifyDelay((delay) => delay * 2)(exponential("100ms"));
|
|
774
|
+
* // Equivalent to maxDelay("10s") for this schedule.
|
|
775
|
+
* const capped = modifyDelay((delay) => Math.min(delay, 10000))(
|
|
776
|
+
* exponential("20s"),
|
|
777
|
+
* );
|
|
778
|
+
* const deps = testCreateDeps();
|
|
598
779
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
780
|
+
* expectOk(slower(deps)(undefined), [100, 200]);
|
|
781
|
+
* expectOk(capped(deps)(undefined), [20000, 10000]);
|
|
601
782
|
* ```
|
|
602
783
|
*
|
|
603
784
|
* @group Delay
|
|
@@ -609,23 +790,41 @@ export const modifyDelay = (f) => (schedule) => (deps) => {
|
|
|
609
790
|
if (!result.ok)
|
|
610
791
|
return result;
|
|
611
792
|
const [output, delay] = result.value;
|
|
612
|
-
return ok([output,
|
|
793
|
+
return ok([output, saturateComputedMillis(f(delay))]);
|
|
613
794
|
};
|
|
614
795
|
};
|
|
615
796
|
/**
|
|
616
|
-
* Adjusts delay by subtracting
|
|
797
|
+
* Adjusts delay by subtracting time elapsed beyond the previously returned
|
|
798
|
+
* delay.
|
|
617
799
|
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
800
|
+
* In a normal executor loop, this corresponds to the previous execution time.
|
|
801
|
+
* If the runtime wakes later than requested, the extra lag is also compensated.
|
|
802
|
+
* If execution and lag took longer than the delay, returns 0.
|
|
803
|
+
*
|
|
804
|
+
* When composing with delay-shaping combinators such as {@link maxDelay}, put
|
|
805
|
+
* `compensate` near the outside of the stack so it observes the final returned
|
|
806
|
+
* delay.
|
|
620
807
|
*
|
|
621
808
|
* For window-aligned scheduling, use {@link fixed} instead.
|
|
622
809
|
*
|
|
623
|
-
* ###
|
|
810
|
+
* ### Compensating for execution time
|
|
624
811
|
*
|
|
625
812
|
* ```ts
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
813
|
+
* import { compensate, spaced, testCreateDeps } from "@evolu/common";
|
|
814
|
+
*
|
|
815
|
+
* const fastDeps = testCreateDeps();
|
|
816
|
+
* const fastStep = compensate(spaced("5s"))(fastDeps);
|
|
817
|
+
* expectOk(fastStep(undefined), [5000, 5000]);
|
|
818
|
+
* // Five seconds waiting plus one second working leaves four seconds.
|
|
819
|
+
* fastDeps.time.advance("6s");
|
|
820
|
+
* expectOk(fastStep(undefined), [5000, 4000]);
|
|
821
|
+
*
|
|
822
|
+
* const slowDeps = testCreateDeps();
|
|
823
|
+
* const slowStep = compensate(spaced("5s"))(slowDeps);
|
|
824
|
+
* expectOk(slowStep(undefined), [5000, 5000]);
|
|
825
|
+
* // Five seconds waiting plus six seconds working leaves no delay.
|
|
826
|
+
* slowDeps.time.advance("11s");
|
|
827
|
+
* expectOk(slowStep(undefined), [5000, 0]);
|
|
629
828
|
* ```
|
|
630
829
|
*
|
|
631
830
|
* @group Delay
|
|
@@ -633,13 +832,17 @@ export const modifyDelay = (f) => (schedule) => (deps) => {
|
|
|
633
832
|
export const compensate = (schedule) => (deps) => {
|
|
634
833
|
const step = schedule(deps);
|
|
635
834
|
const metrics = createScheduleStepMetrics(deps);
|
|
835
|
+
let previousReturnedDelay = minMillis;
|
|
636
836
|
return (input) => {
|
|
637
837
|
const { elapsedSincePrevious } = metrics();
|
|
638
838
|
const result = step(input);
|
|
639
839
|
if (!result.ok)
|
|
640
840
|
return result;
|
|
641
841
|
const [output, delay] = result.value;
|
|
642
|
-
|
|
842
|
+
const executionTime = saturateComputedMillis(elapsedSincePrevious - previousReturnedDelay);
|
|
843
|
+
const compensatedDelay = saturateComputedMillis(delay - executionTime);
|
|
844
|
+
previousReturnedDelay = compensatedDelay;
|
|
845
|
+
return ok([output, compensatedDelay]);
|
|
643
846
|
};
|
|
644
847
|
};
|
|
645
848
|
/**
|
|
@@ -649,24 +852,40 @@ export const compensate = (schedule) => (deps) => {
|
|
|
649
852
|
* Useful for input-aware retry strategies, e.g., only retry certain error
|
|
650
853
|
* types.
|
|
651
854
|
*
|
|
652
|
-
* ###
|
|
855
|
+
* ### Continuing by input
|
|
653
856
|
*
|
|
654
857
|
* ```ts
|
|
858
|
+
* import {
|
|
859
|
+
* done,
|
|
860
|
+
* exponential,
|
|
861
|
+
* testCreateDeps,
|
|
862
|
+
* whileScheduleInput,
|
|
863
|
+
* type Typed,
|
|
864
|
+
* } from "@evolu/common";
|
|
865
|
+
*
|
|
655
866
|
* interface MyError extends Typed<"Transient" | "Fatal"> {}
|
|
656
867
|
*
|
|
657
|
-
* //
|
|
868
|
+
* // Retry only transient errors.
|
|
658
869
|
* const retryTransient = whileScheduleInput(
|
|
659
870
|
* (error: MyError) => error.type === "Transient",
|
|
660
871
|
* )(exponential("100ms"));
|
|
872
|
+
* const step = retryTransient(testCreateDeps());
|
|
873
|
+
* expectOk(step({ type: "Transient" }), [100, 100]);
|
|
874
|
+
* expectErr(step({ type: "Fatal" }), done());
|
|
661
875
|
* ```
|
|
662
876
|
*
|
|
663
877
|
* @group Filtering
|
|
664
878
|
*/
|
|
665
879
|
export const whileScheduleInput = (predicate) => (schedule) => (deps) => {
|
|
666
880
|
const step = schedule(deps);
|
|
881
|
+
let stopped = false;
|
|
667
882
|
return (input) => {
|
|
668
|
-
if (
|
|
883
|
+
if (stopped)
|
|
669
884
|
return err(done());
|
|
885
|
+
if (!predicate(input)) {
|
|
886
|
+
stopped = true;
|
|
887
|
+
return err(done());
|
|
888
|
+
}
|
|
670
889
|
return step(input);
|
|
671
890
|
};
|
|
672
891
|
};
|
|
@@ -676,24 +895,40 @@ export const whileScheduleInput = (predicate) => (schedule) => (deps) => {
|
|
|
676
895
|
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `true`.
|
|
677
896
|
* Useful for stopping retry on specific error conditions.
|
|
678
897
|
*
|
|
679
|
-
* ###
|
|
898
|
+
* ### Stopping by input
|
|
680
899
|
*
|
|
681
900
|
* ```ts
|
|
901
|
+
* import {
|
|
902
|
+
* done,
|
|
903
|
+
* exponential,
|
|
904
|
+
* testCreateDeps,
|
|
905
|
+
* untilScheduleInput,
|
|
906
|
+
* type Typed,
|
|
907
|
+
* } from "@evolu/common";
|
|
908
|
+
*
|
|
682
909
|
* interface MyError extends Typed<"Transient" | "Fatal"> {}
|
|
683
910
|
*
|
|
684
|
-
* // Stop retrying
|
|
911
|
+
* // Stop retrying when an error is fatal.
|
|
685
912
|
* const stopOnFatal = untilScheduleInput(
|
|
686
913
|
* (error: MyError) => error.type === "Fatal",
|
|
687
914
|
* )(exponential("100ms"));
|
|
915
|
+
* const step = stopOnFatal(testCreateDeps());
|
|
916
|
+
* expectOk(step({ type: "Transient" }), [100, 100]);
|
|
917
|
+
* expectErr(step({ type: "Fatal" }), done());
|
|
688
918
|
* ```
|
|
689
919
|
*
|
|
690
920
|
* @group Filtering
|
|
691
921
|
*/
|
|
692
922
|
export const untilScheduleInput = (predicate) => (schedule) => (deps) => {
|
|
693
923
|
const step = schedule(deps);
|
|
924
|
+
let stopped = false;
|
|
694
925
|
return (input) => {
|
|
695
|
-
if (
|
|
926
|
+
if (stopped)
|
|
927
|
+
return err(done());
|
|
928
|
+
if (predicate(input)) {
|
|
929
|
+
stopped = true;
|
|
696
930
|
return err(done());
|
|
931
|
+
}
|
|
697
932
|
return step(input);
|
|
698
933
|
};
|
|
699
934
|
};
|
|
@@ -702,25 +937,43 @@ export const untilScheduleInput = (predicate) => (schedule) => (deps) => {
|
|
|
702
937
|
*
|
|
703
938
|
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `false`.
|
|
704
939
|
*
|
|
705
|
-
* ###
|
|
940
|
+
* ### Continuing by output
|
|
706
941
|
*
|
|
707
942
|
* ```ts
|
|
708
|
-
*
|
|
943
|
+
* import {
|
|
944
|
+
* done,
|
|
945
|
+
* exponential,
|
|
946
|
+
* testCreateDeps,
|
|
947
|
+
* whileScheduleOutput,
|
|
948
|
+
* type Millis,
|
|
949
|
+
* } from "@evolu/common";
|
|
950
|
+
*
|
|
951
|
+
* // Continue while the exponential delay is below five seconds.
|
|
709
952
|
* const capped = whileScheduleOutput((delay: Millis) => delay < 5000)(
|
|
710
953
|
* exponential("1s"),
|
|
711
954
|
* );
|
|
955
|
+
* const step = capped(testCreateDeps());
|
|
956
|
+
* step(undefined);
|
|
957
|
+
* step(undefined);
|
|
958
|
+
* step(undefined);
|
|
959
|
+
* expectErr(step(undefined), done());
|
|
712
960
|
* ```
|
|
713
961
|
*
|
|
714
962
|
* @group Filtering
|
|
715
963
|
*/
|
|
716
964
|
export const whileScheduleOutput = (predicate) => (schedule) => (deps) => {
|
|
717
965
|
const step = schedule(deps);
|
|
966
|
+
let stopped = false;
|
|
718
967
|
return (input) => {
|
|
968
|
+
if (stopped)
|
|
969
|
+
return err(done());
|
|
719
970
|
const result = step(input);
|
|
720
971
|
if (!result.ok)
|
|
721
972
|
return result;
|
|
722
|
-
if (!predicate(result.value[0]))
|
|
973
|
+
if (!predicate(result.value[0])) {
|
|
974
|
+
stopped = true;
|
|
723
975
|
return err(done());
|
|
976
|
+
}
|
|
724
977
|
return result;
|
|
725
978
|
};
|
|
726
979
|
};
|
|
@@ -729,42 +982,69 @@ export const whileScheduleOutput = (predicate) => (schedule) => (deps) => {
|
|
|
729
982
|
*
|
|
730
983
|
* Stops (returns `Err(Done<void>)`) when {@link Predicate} returns `true`.
|
|
731
984
|
*
|
|
732
|
-
* ###
|
|
985
|
+
* ### Stopping by output
|
|
733
986
|
*
|
|
734
987
|
* ```ts
|
|
735
|
-
*
|
|
988
|
+
* import {
|
|
989
|
+
* done,
|
|
990
|
+
* exponential,
|
|
991
|
+
* testCreateDeps,
|
|
992
|
+
* untilScheduleOutput,
|
|
993
|
+
* type Millis,
|
|
994
|
+
* } from "@evolu/common";
|
|
995
|
+
*
|
|
996
|
+
* // Stop once the exponential delay reaches at least one second.
|
|
736
997
|
* const limited = untilScheduleOutput((delay: Millis) => delay >= 1000)(
|
|
737
998
|
* exponential("100ms"),
|
|
738
999
|
* );
|
|
1000
|
+
* const step = limited(testCreateDeps());
|
|
1001
|
+
* step(undefined);
|
|
1002
|
+
* step(undefined);
|
|
1003
|
+
* step(undefined);
|
|
1004
|
+
* step(undefined);
|
|
1005
|
+
* expectErr(step(undefined), done());
|
|
739
1006
|
* ```
|
|
740
1007
|
*
|
|
741
1008
|
* @group Filtering
|
|
742
1009
|
*/
|
|
743
1010
|
export const untilScheduleOutput = (predicate) => (schedule) => (deps) => {
|
|
744
1011
|
const step = schedule(deps);
|
|
1012
|
+
let stopped = false;
|
|
745
1013
|
return (input) => {
|
|
1014
|
+
if (stopped)
|
|
1015
|
+
return err(done());
|
|
746
1016
|
const result = step(input);
|
|
747
1017
|
if (!result.ok)
|
|
748
1018
|
return result;
|
|
749
|
-
if (predicate(result.value[0]))
|
|
1019
|
+
if (predicate(result.value[0])) {
|
|
1020
|
+
stopped = true;
|
|
750
1021
|
return err(done());
|
|
1022
|
+
}
|
|
751
1023
|
return result;
|
|
752
1024
|
};
|
|
753
1025
|
};
|
|
754
1026
|
/**
|
|
755
|
-
* Resets
|
|
1027
|
+
* Resets a running schedule after a period of inactivity.
|
|
756
1028
|
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
1029
|
+
* Before each step, if the time since the previous step is at least `duration`,
|
|
1030
|
+
* replaces the wrapped schedule with fresh state. Once the wrapped schedule
|
|
1031
|
+
* returns `Done`, termination is final.
|
|
760
1032
|
*
|
|
761
|
-
* ###
|
|
1033
|
+
* ### Resetting after inactivity
|
|
762
1034
|
*
|
|
763
1035
|
* ```ts
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
1036
|
+
* import {
|
|
1037
|
+
* exponential,
|
|
1038
|
+
* resetScheduleAfter,
|
|
1039
|
+
* testCreateDeps,
|
|
1040
|
+
* } from "@evolu/common";
|
|
1041
|
+
*
|
|
1042
|
+
* const backoff = resetScheduleAfter("1m")(exponential("1s"));
|
|
1043
|
+
* const deps = testCreateDeps();
|
|
1044
|
+
* const step = backoff(deps);
|
|
1045
|
+
* expectOk(step(undefined), [1000, 1000]);
|
|
1046
|
+
* deps.time.advance("1m");
|
|
1047
|
+
* expectOk(step(undefined), [1000, 1000]);
|
|
768
1048
|
* ```
|
|
769
1049
|
*
|
|
770
1050
|
* @group State
|
|
@@ -774,12 +1054,18 @@ export const resetScheduleAfter = (duration) => {
|
|
|
774
1054
|
return (schedule) => (deps) => {
|
|
775
1055
|
let step = schedule(deps);
|
|
776
1056
|
const metrics = createScheduleStepMetrics(deps);
|
|
1057
|
+
let stopped = false;
|
|
777
1058
|
return (input) => {
|
|
1059
|
+
if (stopped)
|
|
1060
|
+
return err(done());
|
|
778
1061
|
const { elapsedSincePrevious } = metrics();
|
|
779
1062
|
if (elapsedSincePrevious >= resetMs) {
|
|
780
1063
|
step = schedule(deps);
|
|
781
1064
|
}
|
|
782
|
-
|
|
1065
|
+
const result = step(input);
|
|
1066
|
+
if (!result.ok)
|
|
1067
|
+
stopped = true;
|
|
1068
|
+
return result;
|
|
783
1069
|
};
|
|
784
1070
|
};
|
|
785
1071
|
};
|
|
@@ -788,15 +1074,22 @@ export const resetScheduleAfter = (duration) => {
|
|
|
788
1074
|
*
|
|
789
1075
|
* The delay (second tuple element) remains unchanged.
|
|
790
1076
|
*
|
|
791
|
-
* ###
|
|
1077
|
+
* ### Mapping schedule output
|
|
792
1078
|
*
|
|
793
1079
|
* ```ts
|
|
794
|
-
* import {
|
|
795
|
-
*
|
|
796
|
-
*
|
|
1080
|
+
* import {
|
|
1081
|
+
* exponential,
|
|
1082
|
+
* mapSchedule,
|
|
1083
|
+
* testCreateDeps,
|
|
1084
|
+
* type Millis,
|
|
1085
|
+
* } from "@evolu/common";
|
|
1086
|
+
*
|
|
1087
|
+
* const schedule = mapSchedule((delay: Millis) => ({
|
|
797
1088
|
* delay,
|
|
798
1089
|
* doubled: delay * 2,
|
|
799
1090
|
* }))(exponential("100ms"));
|
|
1091
|
+
* const step = schedule(testCreateDeps());
|
|
1092
|
+
* expectOk(step(undefined), [{ delay: 100, doubled: 200 }, 100]);
|
|
800
1093
|
* ```
|
|
801
1094
|
*
|
|
802
1095
|
* @group Transform
|
|
@@ -831,31 +1124,45 @@ export function passthrough(schedule) {
|
|
|
831
1124
|
* Each step outputs the accumulated value. Useful for tracking totals,
|
|
832
1125
|
* collecting outputs, or building up metadata across attempts.
|
|
833
1126
|
*
|
|
834
|
-
* ###
|
|
1127
|
+
* ### Folding schedule output
|
|
835
1128
|
*
|
|
836
1129
|
* ```ts
|
|
837
|
-
*
|
|
1130
|
+
* import {
|
|
1131
|
+
* exponential,
|
|
1132
|
+
* foldSchedule,
|
|
1133
|
+
* minMillis,
|
|
1134
|
+
* spaced,
|
|
1135
|
+
* take,
|
|
1136
|
+
* testCreateDeps,
|
|
1137
|
+
* type Millis,
|
|
1138
|
+
* } from "@evolu/common";
|
|
1139
|
+
*
|
|
1140
|
+
* // Accumulate totals, complete output histories, or structured metadata.
|
|
838
1141
|
* const withTotal = foldSchedule(
|
|
839
1142
|
* 0,
|
|
840
1143
|
* (total: number, delay: Millis) => total + delay,
|
|
841
1144
|
* )(exponential("100ms"));
|
|
842
|
-
* // Outputs: 100, 300, 700, 1500, ... (cumulative)
|
|
843
|
-
*
|
|
844
|
-
* // Collect all outputs
|
|
845
1145
|
* const collected = foldSchedule<ReadonlyArray<Millis>, Millis>(
|
|
846
1146
|
* [],
|
|
847
|
-
* (
|
|
1147
|
+
* (outputs, delay) => [...outputs, delay],
|
|
848
1148
|
* )(take(3)(spaced("1s")));
|
|
849
|
-
* // Outputs: [1000], [1000, 1000], [1000, 1000, 1000]
|
|
850
|
-
*
|
|
851
|
-
* // Count attempts with custom output
|
|
852
1149
|
* const counted = foldSchedule(
|
|
853
1150
|
* { attempts: 0, lastDelay: minMillis },
|
|
854
|
-
* (
|
|
855
|
-
* attempts:
|
|
1151
|
+
* (state, delay: Millis) => ({
|
|
1152
|
+
* attempts: state.attempts + 1,
|
|
856
1153
|
* lastDelay: delay,
|
|
857
1154
|
* }),
|
|
858
1155
|
* )(exponential("100ms"));
|
|
1156
|
+
*
|
|
1157
|
+
* const deps = testCreateDeps();
|
|
1158
|
+
* const totalStep = withTotal(deps);
|
|
1159
|
+
* totalStep(undefined);
|
|
1160
|
+
* expectOk(totalStep(undefined), [300, 200]);
|
|
1161
|
+
* expectOk(collected(deps)(undefined), [[1000], 1000]);
|
|
1162
|
+
* expectOk(counted(deps)(undefined), [
|
|
1163
|
+
* { attempts: 1, lastDelay: 100 },
|
|
1164
|
+
* 100,
|
|
1165
|
+
* ]);
|
|
859
1166
|
* ```
|
|
860
1167
|
*
|
|
861
1168
|
* @group Transform
|
|
@@ -879,12 +1186,15 @@ export const foldSchedule = (initial, f) => (schedule) => (deps) => {
|
|
|
879
1186
|
* Outputs 0, 1, 2, ... while preserving the underlying schedule's timing and
|
|
880
1187
|
* termination behavior.
|
|
881
1188
|
*
|
|
882
|
-
* ###
|
|
1189
|
+
* ### Counting repetitions
|
|
883
1190
|
*
|
|
884
1191
|
* ```ts
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
* //
|
|
1192
|
+
* import { exponential, repetitions, testCreateDeps } from "@evolu/common";
|
|
1193
|
+
*
|
|
1194
|
+
* // Count retries while preserving exponential timing.
|
|
1195
|
+
* const step = repetitions(exponential("100ms"))(testCreateDeps());
|
|
1196
|
+
* expectOk(step(undefined), [0, 100]);
|
|
1197
|
+
* expectOk(step(undefined), [1, 200]);
|
|
888
1198
|
* ```
|
|
889
1199
|
*
|
|
890
1200
|
* @group Transform
|
|
@@ -896,17 +1206,28 @@ export const repetitions = (schedule) => foldSchedule(-1, (n) => n + 1)(schedule
|
|
|
896
1206
|
* Wraps a schedule to output its delay (in milliseconds) instead of the
|
|
897
1207
|
* original output. Useful for monitoring or logging delay patterns.
|
|
898
1208
|
*
|
|
899
|
-
* ###
|
|
1209
|
+
* ### Exposing and observing delays
|
|
900
1210
|
*
|
|
901
1211
|
* ```ts
|
|
902
|
-
*
|
|
1212
|
+
* import {
|
|
1213
|
+
* delays,
|
|
1214
|
+
* exponential,
|
|
1215
|
+
* tapScheduleOutput,
|
|
1216
|
+
* testCreateDeps,
|
|
1217
|
+
* type Millis,
|
|
1218
|
+
* } from "@evolu/common";
|
|
1219
|
+
*
|
|
1220
|
+
* // Expose delays for monitoring, or observe them without changing output.
|
|
903
1221
|
* const monitorDelays = delays(exponential("100ms"));
|
|
904
|
-
*
|
|
1222
|
+
* const observed: Array<Millis> = [];
|
|
1223
|
+
* const logged = tapScheduleOutput((delay: Millis) => {
|
|
1224
|
+
* observed.push(delay);
|
|
1225
|
+
* })(delays(exponential("100ms")));
|
|
1226
|
+
* const deps = testCreateDeps();
|
|
905
1227
|
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
* );
|
|
1228
|
+
* expectOk(monitorDelays(deps)(undefined), [100, 100]);
|
|
1229
|
+
* expectOk(logged(deps)(undefined), [100, 100]);
|
|
1230
|
+
* expect(observed).toEqual([100]);
|
|
910
1231
|
* ```
|
|
911
1232
|
*
|
|
912
1233
|
* @group Transform
|
|
@@ -924,14 +1245,25 @@ export const delays = (schedule) => (deps) => {
|
|
|
924
1245
|
/**
|
|
925
1246
|
* Collects all outputs into an array.
|
|
926
1247
|
*
|
|
927
|
-
* Each step outputs
|
|
1248
|
+
* Each step outputs a new snapshot containing all outputs so far. Because all
|
|
1249
|
+
* outputs are retained and copied on each step, use this combinator with finite
|
|
1250
|
+
* schedules.
|
|
928
1251
|
*
|
|
929
|
-
* ###
|
|
1252
|
+
* ### Collecting outputs
|
|
930
1253
|
*
|
|
931
1254
|
* ```ts
|
|
932
|
-
*
|
|
1255
|
+
* import {
|
|
1256
|
+
* collectAllScheduleOutputs,
|
|
1257
|
+
* spaced,
|
|
1258
|
+
* take,
|
|
1259
|
+
* testCreateDeps,
|
|
1260
|
+
* } from "@evolu/common";
|
|
1261
|
+
*
|
|
1262
|
+
* // Retain every delay produced by the finite schedule.
|
|
933
1263
|
* const collected = collectAllScheduleOutputs(take(3)(spaced("100ms")));
|
|
934
|
-
*
|
|
1264
|
+
* const step = collected(testCreateDeps());
|
|
1265
|
+
* step(undefined);
|
|
1266
|
+
* expectOk(step(undefined), [[100, 100], 100]);
|
|
935
1267
|
* ```
|
|
936
1268
|
*
|
|
937
1269
|
* @group Collection
|
|
@@ -943,14 +1275,24 @@ export const collectAllScheduleOutputs = (schedule) => foldSchedule([], (acc, ou
|
|
|
943
1275
|
* Each step outputs an array containing all inputs received so far. Mirror of
|
|
944
1276
|
* {@link collectAllScheduleOutputs} but for inputs.
|
|
945
1277
|
*
|
|
946
|
-
* ###
|
|
1278
|
+
* ### Collecting inputs
|
|
947
1279
|
*
|
|
948
1280
|
* ```ts
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
*
|
|
1281
|
+
* import {
|
|
1282
|
+
* collectScheduleInputs,
|
|
1283
|
+
* exponential,
|
|
1284
|
+
* take,
|
|
1285
|
+
* testCreateDeps,
|
|
1286
|
+
* type Millis,
|
|
1287
|
+
* type Schedule,
|
|
1288
|
+
* } from "@evolu/common";
|
|
1289
|
+
*
|
|
1290
|
+
* const retries: Schedule<Millis, string> = take(3)(exponential("100ms"));
|
|
1291
|
+
* // Keep every error received during retry.
|
|
1292
|
+
* const errorHistory = collectScheduleInputs(retries);
|
|
1293
|
+
* const step = errorHistory(testCreateDeps());
|
|
1294
|
+
* step("network");
|
|
1295
|
+
* expectOk(step("timeout"), [["network", "timeout"], 200]);
|
|
954
1296
|
* ```
|
|
955
1297
|
*
|
|
956
1298
|
* @group Collection
|
|
@@ -962,14 +1304,27 @@ export const collectScheduleInputs = (schedule) => collectAllScheduleOutputs(pas
|
|
|
962
1304
|
* More flexible than {@link collectAllScheduleOutputs} — stops collecting when
|
|
963
1305
|
* the predicate returns false.
|
|
964
1306
|
*
|
|
965
|
-
* ###
|
|
1307
|
+
* ### Collecting while output matches
|
|
966
1308
|
*
|
|
967
1309
|
* ```ts
|
|
968
|
-
*
|
|
1310
|
+
* import {
|
|
1311
|
+
* collectWhileScheduleOutput,
|
|
1312
|
+
* done,
|
|
1313
|
+
* exponential,
|
|
1314
|
+
* testCreateDeps,
|
|
1315
|
+
* type Millis,
|
|
1316
|
+
* } from "@evolu/common";
|
|
1317
|
+
*
|
|
1318
|
+
* // Collect delays only while they remain below one second.
|
|
969
1319
|
* const smallDelays = collectWhileScheduleOutput(
|
|
970
1320
|
* (delay: Millis) => delay < 1000,
|
|
971
1321
|
* )(exponential("100ms"));
|
|
972
|
-
*
|
|
1322
|
+
* const step = smallDelays(testCreateDeps());
|
|
1323
|
+
* step(undefined);
|
|
1324
|
+
* step(undefined);
|
|
1325
|
+
* step(undefined);
|
|
1326
|
+
* expectOk(step(undefined), [[100, 200, 400, 800], 800]);
|
|
1327
|
+
* expectErr(step(undefined), done());
|
|
973
1328
|
* ```
|
|
974
1329
|
*
|
|
975
1330
|
* @group Collection
|
|
@@ -979,16 +1334,29 @@ export const collectWhileScheduleOutput = (predicate) => (schedule) => collectAl
|
|
|
979
1334
|
* Collects outputs until a predicate becomes true.
|
|
980
1335
|
*
|
|
981
1336
|
* Mirror of {@link collectWhileScheduleOutput} — stops collecting when the
|
|
982
|
-
* predicate returns true
|
|
1337
|
+
* predicate returns true.
|
|
983
1338
|
*
|
|
984
|
-
* ###
|
|
1339
|
+
* ### Collecting until output matches
|
|
985
1340
|
*
|
|
986
1341
|
* ```ts
|
|
987
|
-
*
|
|
1342
|
+
* import {
|
|
1343
|
+
* collectUntilScheduleOutput,
|
|
1344
|
+
* done,
|
|
1345
|
+
* exponential,
|
|
1346
|
+
* testCreateDeps,
|
|
1347
|
+
* type Millis,
|
|
1348
|
+
* } from "@evolu/common";
|
|
1349
|
+
*
|
|
1350
|
+
* // Collect delays until the next delay reaches at least one second.
|
|
988
1351
|
* const untilLarge = collectUntilScheduleOutput(
|
|
989
1352
|
* (delay: Millis) => delay >= 1000,
|
|
990
1353
|
* )(exponential("100ms"));
|
|
991
|
-
*
|
|
1354
|
+
* const step = untilLarge(testCreateDeps());
|
|
1355
|
+
* step(undefined);
|
|
1356
|
+
* step(undefined);
|
|
1357
|
+
* step(undefined);
|
|
1358
|
+
* expectOk(step(undefined), [[100, 200, 400, 800], 800]);
|
|
1359
|
+
* expectErr(step(undefined), done());
|
|
992
1360
|
* ```
|
|
993
1361
|
*
|
|
994
1362
|
* @group Collection
|
|
@@ -1000,16 +1368,27 @@ export const collectUntilScheduleOutput = (predicate) => (schedule) => collectAl
|
|
|
1000
1368
|
* Useful for adaptive strategies that start aggressive and become more
|
|
1001
1369
|
* conservative over time.
|
|
1002
1370
|
*
|
|
1003
|
-
* ###
|
|
1371
|
+
* ### Sequencing strategies
|
|
1004
1372
|
*
|
|
1005
1373
|
* ```ts
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1374
|
+
* import {
|
|
1375
|
+
* exponential,
|
|
1376
|
+
* fixed,
|
|
1377
|
+
* sequenceSchedules,
|
|
1378
|
+
* take,
|
|
1379
|
+
* testCreateDeps,
|
|
1380
|
+
* } from "@evolu/common";
|
|
1381
|
+
*
|
|
1382
|
+
* // Fast retries first, then slower retries, then a steady fallback.
|
|
1383
|
+
* const step = sequenceSchedules(
|
|
1008
1384
|
* take(3)(exponential("100ms")),
|
|
1009
1385
|
* take(5)(fixed("500ms")),
|
|
1010
1386
|
* fixed("1s"),
|
|
1011
|
-
* );
|
|
1012
|
-
*
|
|
1387
|
+
* )(testCreateDeps());
|
|
1388
|
+
* step(undefined);
|
|
1389
|
+
* step(undefined);
|
|
1390
|
+
* step(undefined);
|
|
1391
|
+
* expectOk(step(undefined), [0, 500]);
|
|
1013
1392
|
* ```
|
|
1014
1393
|
*
|
|
1015
1394
|
* @group Composition
|
|
@@ -1034,14 +1413,28 @@ export const sequenceSchedules = (...schedules) => (deps) => {
|
|
|
1034
1413
|
*
|
|
1035
1414
|
* Continues only while both schedules want to continue. Uses the maximum delay.
|
|
1036
1415
|
*
|
|
1037
|
-
* ###
|
|
1416
|
+
* ### Combining constraints with AND
|
|
1038
1417
|
*
|
|
1039
1418
|
* ```ts
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1419
|
+
* import {
|
|
1420
|
+
* done,
|
|
1421
|
+
* exponential,
|
|
1422
|
+
* forever,
|
|
1423
|
+
* intersectSchedules,
|
|
1424
|
+
* maxElapsed,
|
|
1425
|
+
* take,
|
|
1426
|
+
* testCreateDeps,
|
|
1427
|
+
* } from "@evolu/common";
|
|
1428
|
+
*
|
|
1429
|
+
* // Retry at most 5 times and only within 30 seconds.
|
|
1430
|
+
* const deps = testCreateDeps();
|
|
1431
|
+
* const step = intersectSchedules(
|
|
1042
1432
|
* take(5)(exponential("1s")),
|
|
1043
1433
|
* maxElapsed("30s")(forever),
|
|
1044
|
-
* );
|
|
1434
|
+
* )(deps);
|
|
1435
|
+
* expectOk(step(undefined), [[1000, 0], 1000]);
|
|
1436
|
+
* deps.time.advance("30s");
|
|
1437
|
+
* expectErr(step(undefined), done());
|
|
1045
1438
|
* ```
|
|
1046
1439
|
*
|
|
1047
1440
|
* @group Composition
|
|
@@ -1049,14 +1442,19 @@ export const sequenceSchedules = (...schedules) => (deps) => {
|
|
|
1049
1442
|
export const intersectSchedules = (a, b) => (deps) => {
|
|
1050
1443
|
const stepA = a(deps);
|
|
1051
1444
|
const stepB = b(deps);
|
|
1445
|
+
let stopped = false;
|
|
1052
1446
|
return (input) => {
|
|
1447
|
+
if (stopped)
|
|
1448
|
+
return err(done());
|
|
1053
1449
|
const resultA = stepA(input);
|
|
1054
1450
|
const resultB = stepB(input);
|
|
1055
|
-
if (!resultA.ok || !resultB.ok)
|
|
1451
|
+
if (!resultA.ok || !resultB.ok) {
|
|
1452
|
+
stopped = true;
|
|
1056
1453
|
return err(done());
|
|
1454
|
+
}
|
|
1057
1455
|
const [outputA, delayA] = resultA.value;
|
|
1058
1456
|
const [outputB, delayB] = resultB.value;
|
|
1059
|
-
return ok([[outputA, outputB],
|
|
1457
|
+
return ok([[outputA, outputB], max(delayA, delayB)]);
|
|
1060
1458
|
};
|
|
1061
1459
|
};
|
|
1062
1460
|
/**
|
|
@@ -1064,34 +1462,50 @@ export const intersectSchedules = (a, b) => (deps) => {
|
|
|
1064
1462
|
*
|
|
1065
1463
|
* Continues while either schedule wants to continue. Uses the minimum delay.
|
|
1066
1464
|
*
|
|
1067
|
-
* ###
|
|
1465
|
+
* ### Combining constraints with OR
|
|
1068
1466
|
*
|
|
1069
1467
|
* ```ts
|
|
1070
|
-
*
|
|
1468
|
+
* import {
|
|
1469
|
+
* done,
|
|
1470
|
+
* spaced,
|
|
1471
|
+
* take,
|
|
1472
|
+
* testCreateDeps,
|
|
1473
|
+
* unionSchedules,
|
|
1474
|
+
* } from "@evolu/common";
|
|
1475
|
+
*
|
|
1476
|
+
* // The second policy keeps the union alive after the first one stops.
|
|
1071
1477
|
* const either = unionSchedules(
|
|
1072
|
-
* take(
|
|
1073
|
-
*
|
|
1478
|
+
* take(1)(spaced("100ms")),
|
|
1479
|
+
* take(2)(spaced("200ms")),
|
|
1074
1480
|
* );
|
|
1481
|
+
* const step = either(testCreateDeps());
|
|
1482
|
+
* expectOk(step(undefined), [100, 100]);
|
|
1483
|
+
* expectOk(step(undefined), [200, 200]);
|
|
1484
|
+
* expectErr(step(undefined), done());
|
|
1075
1485
|
* ```
|
|
1076
1486
|
*
|
|
1077
1487
|
* @group Composition
|
|
1078
1488
|
*/
|
|
1079
1489
|
export const unionSchedules = (a, b) => (deps) => {
|
|
1080
|
-
|
|
1081
|
-
|
|
1490
|
+
let stepA = a(deps);
|
|
1491
|
+
let stepB = b(deps);
|
|
1082
1492
|
return (input) => {
|
|
1083
|
-
|
|
1084
|
-
const resultB = stepB(input);
|
|
1085
|
-
if (!resultA.ok && !resultB.ok)
|
|
1493
|
+
if (stepA === null && stepB === null)
|
|
1086
1494
|
return err(done());
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (!
|
|
1495
|
+
const resultA = stepA?.(input) ?? null;
|
|
1496
|
+
const resultB = stepB?.(input) ?? null;
|
|
1497
|
+
if (resultA !== null && !resultA.ok)
|
|
1498
|
+
stepA = null;
|
|
1499
|
+
if (resultB !== null && !resultB.ok)
|
|
1500
|
+
stepB = null;
|
|
1501
|
+
if (!resultA?.ok)
|
|
1502
|
+
return resultB?.ok ? resultB : err(done());
|
|
1503
|
+
if (!resultB?.ok)
|
|
1090
1504
|
return resultA;
|
|
1091
1505
|
const [outputA, delayA] = resultA.value;
|
|
1092
1506
|
const [outputB, delayB] = resultB.value;
|
|
1093
1507
|
// Use minimum delay, output from the one with smaller delay
|
|
1094
|
-
const minDelay =
|
|
1508
|
+
const minDelay = min(delayA, delayB);
|
|
1095
1509
|
return delayA <= delayB
|
|
1096
1510
|
? ok([outputA, minDelay])
|
|
1097
1511
|
: ok([outputB, minDelay]);
|
|
@@ -1104,15 +1518,36 @@ export const unionSchedules = (a, b) => (deps) => {
|
|
|
1104
1518
|
* base schedule. Useful for implementing error-aware backoff where certain
|
|
1105
1519
|
* errors (e.g., throttling) use different delays.
|
|
1106
1520
|
*
|
|
1107
|
-
*
|
|
1521
|
+
* Each branch has independent state. Place combinators such as {@link take}
|
|
1522
|
+
* outside `whenInput` when their state must be shared across both branches.
|
|
1523
|
+
*
|
|
1524
|
+
* ### Selecting a schedule by input
|
|
1108
1525
|
*
|
|
1109
1526
|
* ```ts
|
|
1527
|
+
* import {
|
|
1528
|
+
* done,
|
|
1529
|
+
* exponential,
|
|
1530
|
+
* take,
|
|
1531
|
+
* testCreateDeps,
|
|
1532
|
+
* whenInput,
|
|
1533
|
+
* type Millis,
|
|
1534
|
+
* type Typed,
|
|
1535
|
+
* } from "@evolu/common";
|
|
1536
|
+
*
|
|
1110
1537
|
* interface MyError extends Typed<"Throttled" | "NetworkError"> {}
|
|
1111
1538
|
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1539
|
+
* // The outer take shares one retry limit across both error branches.
|
|
1540
|
+
* const awsWithThrottling = take(3)(
|
|
1541
|
+
* whenInput<MyError, Millis>(
|
|
1542
|
+
* (error) => error.type === "Throttled",
|
|
1543
|
+
* exponential("1s"),
|
|
1544
|
+
* )(exponential("100ms")),
|
|
1545
|
+
* );
|
|
1546
|
+
* const step = awsWithThrottling(testCreateDeps());
|
|
1547
|
+
* expectOk(step({ type: "Throttled" }), [1000, 1000]);
|
|
1548
|
+
* expectOk(step({ type: "NetworkError" }), [100, 100]);
|
|
1549
|
+
* expectOk(step({ type: "Throttled" }), [2000, 2000]);
|
|
1550
|
+
* expectErr(step({ type: "NetworkError" }), done());
|
|
1116
1551
|
* ```
|
|
1117
1552
|
*
|
|
1118
1553
|
* @group Composition
|
|
@@ -1120,10 +1555,14 @@ export const unionSchedules = (a, b) => (deps) => {
|
|
|
1120
1555
|
export const whenInput = (predicate, altSchedule) => (schedule) => (deps) => {
|
|
1121
1556
|
const normalStep = schedule(deps);
|
|
1122
1557
|
const altStep = altSchedule(deps);
|
|
1558
|
+
let stopped = false;
|
|
1123
1559
|
return (input) => {
|
|
1124
|
-
if (
|
|
1125
|
-
return
|
|
1126
|
-
|
|
1560
|
+
if (stopped)
|
|
1561
|
+
return err(done());
|
|
1562
|
+
const result = predicate(input) ? altStep(input) : normalStep(input);
|
|
1563
|
+
if (!result.ok)
|
|
1564
|
+
stopped = true;
|
|
1565
|
+
return result;
|
|
1127
1566
|
};
|
|
1128
1567
|
};
|
|
1129
1568
|
/**
|
|
@@ -1132,19 +1571,34 @@ export const whenInput = (predicate, altSchedule) => (schedule) => (deps) => {
|
|
|
1132
1571
|
* Useful for logging, monitoring, or debugging without changing schedule
|
|
1133
1572
|
* behavior.
|
|
1134
1573
|
*
|
|
1135
|
-
* ###
|
|
1574
|
+
* ### Observing schedule output
|
|
1136
1575
|
*
|
|
1137
1576
|
* ```ts
|
|
1138
|
-
*
|
|
1577
|
+
* import {
|
|
1578
|
+
* exponential,
|
|
1579
|
+
* retryStrategyAws,
|
|
1580
|
+
* tapScheduleOutput,
|
|
1581
|
+
* testCreateDeps,
|
|
1582
|
+
* type Millis,
|
|
1583
|
+
* } from "@evolu/common";
|
|
1584
|
+
*
|
|
1585
|
+
* // Log each delay for debugging.
|
|
1586
|
+
* const messages: Array<string> = [];
|
|
1139
1587
|
* const logged = tapScheduleOutput((delay: Millis) => {
|
|
1140
|
-
*
|
|
1588
|
+
* messages.push(`Next delay: ${delay}ms`);
|
|
1141
1589
|
* })(exponential("100ms"));
|
|
1142
1590
|
*
|
|
1143
|
-
* // Track
|
|
1144
|
-
* const
|
|
1145
|
-
* const tracked = tapScheduleOutput((
|
|
1146
|
-
*
|
|
1591
|
+
* // Track the preset's pre-jitter delay candidates without changing it.
|
|
1592
|
+
* const recordedCandidates: Array<Millis> = [];
|
|
1593
|
+
* const tracked = tapScheduleOutput((candidate: Millis) => {
|
|
1594
|
+
* recordedCandidates.push(candidate);
|
|
1147
1595
|
* })(retryStrategyAws);
|
|
1596
|
+
* const deps = testCreateDeps();
|
|
1597
|
+
* logged(deps)(undefined);
|
|
1598
|
+
* tracked(deps)(undefined);
|
|
1599
|
+
*
|
|
1600
|
+
* expect(messages).toEqual(["Next delay: 100ms"]);
|
|
1601
|
+
* expect(recordedCandidates).toEqual([50]);
|
|
1148
1602
|
* ```
|
|
1149
1603
|
*
|
|
1150
1604
|
* @group Side effects
|
|
@@ -1165,23 +1619,38 @@ export const tapScheduleOutput = (f) => (schedule) => (deps) => {
|
|
|
1165
1619
|
* Useful for logging errors during retry or monitoring what values are being
|
|
1166
1620
|
* processed.
|
|
1167
1621
|
*
|
|
1168
|
-
* ###
|
|
1622
|
+
* ### Observing schedule input
|
|
1169
1623
|
*
|
|
1170
1624
|
* ```ts
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1625
|
+
* import {
|
|
1626
|
+
* exponential,
|
|
1627
|
+
* tapScheduleInput,
|
|
1628
|
+
* testCreateDeps,
|
|
1629
|
+
* type Millis,
|
|
1630
|
+
* type Schedule,
|
|
1631
|
+
* type Typed,
|
|
1632
|
+
* } from "@evolu/common";
|
|
1633
|
+
*
|
|
1634
|
+
* interface MyError extends Typed<"NetworkError" | "Timeout"> {}
|
|
1173
1635
|
* const retrySchedule: Schedule<Millis, MyError> = exponential("100ms");
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1636
|
+
* // Log errors for debugging.
|
|
1637
|
+
* const messages: Array<string> = [];
|
|
1176
1638
|
* const logged = tapScheduleInput((error: MyError) => {
|
|
1177
|
-
*
|
|
1639
|
+
* messages.push(`Retrying after error: ${error.type}`);
|
|
1178
1640
|
* })(retrySchedule);
|
|
1179
1641
|
*
|
|
1180
|
-
* //
|
|
1642
|
+
* // Or retain just the retry reasons for metrics.
|
|
1181
1643
|
* const reasons: Array<string> = [];
|
|
1182
1644
|
* const tracked = tapScheduleInput((error: MyError) => {
|
|
1183
1645
|
* reasons.push(error.type);
|
|
1184
1646
|
* })(retrySchedule);
|
|
1647
|
+
* const deps = testCreateDeps();
|
|
1648
|
+
* const error: MyError = { type: "NetworkError" };
|
|
1649
|
+
* logged(deps)(error);
|
|
1650
|
+
* tracked(deps)(error);
|
|
1651
|
+
*
|
|
1652
|
+
* expect(messages).toEqual(["Retrying after error: NetworkError"]);
|
|
1653
|
+
* expect(reasons).toEqual(["NetworkError"]);
|
|
1185
1654
|
* ```
|
|
1186
1655
|
*
|
|
1187
1656
|
* @group Side effects
|
|
@@ -1194,15 +1663,17 @@ export const tapScheduleInput = (f) => (schedule) => (deps) => {
|
|
|
1194
1663
|
};
|
|
1195
1664
|
};
|
|
1196
1665
|
/**
|
|
1197
|
-
* AWS
|
|
1666
|
+
* AWS SDK for Java 2.1 ordinary-failure retry timing.
|
|
1198
1667
|
*
|
|
1199
|
-
* Exponential backoff (
|
|
1668
|
+
* Exponential backoff (50ms base), max 2 retries (3 total attempts), 20s cap,
|
|
1200
1669
|
* full jitter.
|
|
1201
1670
|
*
|
|
1671
|
+
* This schedule does not model throttling-specific timing, token accounting, or
|
|
1672
|
+
* circuit breaking.
|
|
1673
|
+
*
|
|
1202
1674
|
* @group Retry Strategies
|
|
1203
|
-
* @see https://github.com/aws/aws-sdk-java-v2/blob/
|
|
1204
|
-
* @see https://github.com/aws/aws-sdk-java-v2/blob/master/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java
|
|
1675
|
+
* @see https://github.com/aws/aws-sdk-java-v2/blob/b69b75f07b6ebd93fd44b032d49b76a3b71fbb90/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java
|
|
1205
1676
|
*/
|
|
1206
|
-
export const retryStrategyAws = /*#__PURE__*/ jitter(
|
|
1677
|
+
export const retryStrategyAws = /*#__PURE__*/ jitter("100%")(
|
|
1207
1678
|
/*#__PURE__*/ maxDelay("20s")(
|
|
1208
|
-
/*#__PURE__*/ take(2)(/*#__PURE__*/ exponential("
|
|
1679
|
+
/*#__PURE__*/ take(2)(/*#__PURE__*/ exponential("50ms"))));
|