@evolu/common 8.7.0 → 8.8.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 +21 -21
- package/dist/src/Array.js +5 -5
- package/dist/src/Assert.d.ts +478 -141
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +644 -99
- package/dist/src/{Buffer.d.ts → Binary.d.ts} +85 -12
- package/dist/src/Binary.d.ts.map +1 -0
- package/dist/src/{Buffer.js → Binary.js} +206 -24
- package/dist/src/Console.d.ts +2 -10
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +6 -12
- package/dist/src/Crypto.d.ts +5 -5
- package/dist/src/Crypto.js +1 -1
- package/dist/src/Eq.d.ts +35 -40
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +51 -87
- package/dist/src/Function.d.ts +9 -14
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +7 -9
- package/dist/src/Http.d.ts +6 -7
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +1 -0
- package/dist/src/LockManager.d.ts +1 -1
- package/dist/src/LockManager.js +1 -1
- package/dist/src/Lookup.d.ts +3 -2
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +4 -3
- package/dist/src/Number.d.ts +1 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +1 -1
- package/dist/src/Object.d.ts +4 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -4
- package/dist/src/Option.d.ts +1 -2
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +1 -2
- package/dist/src/Platform.js +2 -0
- package/dist/src/Polyfills.js +4 -1
- package/dist/src/Random.d.ts +1 -1
- package/dist/src/Result.d.ts +49 -54
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +17 -22
- package/dist/src/Set.d.ts +6 -6
- package/dist/src/Set.js +2 -2
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/String.js +3 -2
- package/dist/src/Task.d.ts +32 -32
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +20 -22
- package/dist/src/Test.d.ts +31 -3
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +46 -3
- package/dist/src/Time.d.ts +3 -11
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +10 -18
- package/dist/src/Type.d.ts +87 -85
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +70 -63
- package/dist/src/Types.d.ts +12 -12
- package/dist/src/Types.js +1 -1
- package/dist/src/WebSocket.d.ts +4 -7
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/Worker.d.ts +1 -1
- package/dist/src/Worker.js +7 -7
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +1 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +1 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +1 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +1 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +1 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +1 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +1 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +1 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +1 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +1 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +1 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +1 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +1 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +1 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +1 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +1 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +1 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +1 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +1 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +1 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +1 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +1 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +1 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +1 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +1 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +1 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +1 -1
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +1 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +1 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +1 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +1 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +1 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +1 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +1 -1
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +1 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +1 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +1 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +1 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +1 -1
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +1 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +1 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +1 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +1 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +1 -1
- package/dist/src/local-first/Evolu.d.ts +12 -12
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +2 -2
- package/dist/src/local-first/Protocol.d.ts +14 -76
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +29 -206
- package/dist/src/local-first/Query.d.ts +11 -11
- package/dist/src/local-first/Query.js +6 -6
- package/dist/src/local-first/Relay.d.ts +2 -2
- package/dist/src/local-first/Schema.d.ts +3 -5
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +2 -4
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +5 -5
- package/dist/src/local-first/Storage.js +4 -4
- package/dist/src/local-first/Timestamp.d.ts +9 -0
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +13 -10
- package/package.json +7 -4
- package/src/Array.test.ts +809 -0
- package/src/Array.ts +21 -21
- package/src/Assert.test.ts +1181 -0
- package/src/Assert.ts +971 -227
- package/src/BigInt.test.ts +28 -0
- package/src/Binary.test.ts +1128 -0
- package/src/{Buffer.ts → Binary.ts} +273 -46
- package/src/Brand.test.ts +90 -0
- package/src/Cache.test.ts +183 -0
- package/src/Callbacks.test.ts +66 -0
- package/src/Console.test.ts +809 -0
- package/src/Console.ts +9 -17
- package/src/Crypto.test.ts +138 -0
- package/src/Crypto.ts +5 -5
- package/src/Eq.test.ts +333 -0
- package/src/Eq.ts +110 -176
- package/src/Error.test.ts +118 -0
- package/src/Function.test.ts +204 -0
- package/src/Function.ts +10 -15
- package/src/Http.test.ts +816 -0
- package/src/Http.ts +6 -7
- package/src/LeakDetector.test.ts +210 -0
- package/src/LeakDetector.ts +1 -0
- package/src/LockManager.ts +2 -2
- package/src/Lookup.test.ts +418 -0
- package/src/Lookup.ts +4 -3
- package/src/Microtask.test.ts +140 -0
- package/src/Number.test.ts +218 -0
- package/src/Number.ts +2 -2
- package/src/Object.test.ts +293 -0
- package/src/Object.ts +5 -4
- package/src/Option.test.ts +104 -0
- package/src/Option.ts +1 -2
- package/src/Order.test.ts +56 -0
- package/src/Platform.ts +2 -0
- package/src/Polyfills.ts +3 -4
- package/src/Random.test.ts +89 -0
- package/src/Random.ts +1 -1
- package/src/Ref.test.ts +139 -0
- package/src/RefCount.test.ts +184 -0
- package/src/Relation.test.ts +505 -0
- package/src/Resource.test.ts +4623 -0
- package/src/Result.test.ts +1829 -0
- package/src/Result.ts +49 -54
- package/src/Schedule.test.ts +1554 -0
- package/src/Set.test.ts +279 -0
- package/src/Set.ts +6 -6
- package/src/Sqlite.ts +1 -1
- package/src/Store.test.ts +288 -0
- package/src/String.test.ts +54 -0
- package/src/String.ts +3 -3
- package/src/Task.test.ts +10820 -0
- package/src/Task.ts +38 -43
- package/src/Test.test.ts +88 -0
- package/src/Test.ts +49 -3
- package/src/Time.test.ts +926 -0
- package/src/Time.ts +13 -21
- package/src/Type.base64.test.ts +88 -0
- package/src/Type.test.ts +20465 -0
- package/src/Type.ts +114 -111
- package/src/Types.test.ts +324 -0
- package/src/Types.ts +13 -13
- package/src/WebSocket.ts +4 -7
- package/src/Worker.test.ts +755 -0
- package/src/Worker.ts +13 -18
- package/src/index.ts +1 -1
- package/src/intl/_en.ts +1 -1
- package/src/intl/ar.ts +1 -1
- package/src/intl/bn.ts +1 -1
- package/src/intl/ca.ts +1 -1
- package/src/intl/cs.ts +1 -1
- package/src/intl/da.ts +1 -1
- package/src/intl/de.ts +1 -1
- package/src/intl/el.ts +1 -1
- package/src/intl/es.ts +1 -1
- package/src/intl/fa.ts +1 -1
- package/src/intl/fi.ts +1 -1
- package/src/intl/fil.ts +1 -1
- package/src/intl/fr.ts +1 -1
- package/src/intl/he.ts +1 -1
- package/src/intl/hi.ts +1 -1
- package/src/intl/hr.ts +1 -1
- package/src/intl/hu.ts +1 -1
- package/src/intl/id.ts +1 -1
- package/src/intl/intl.test.ts +3106 -0
- package/src/intl/it.ts +1 -1
- package/src/intl/ja.ts +1 -1
- package/src/intl/ko.ts +1 -1
- package/src/intl/ml.ts +1 -1
- package/src/intl/mr.ts +1 -1
- package/src/intl/ms.ts +1 -1
- package/src/intl/nb.ts +1 -1
- package/src/intl/nl.ts +1 -1
- package/src/intl/pa.ts +1 -1
- package/src/intl/pl.ts +1 -1
- package/src/intl/pt-BR.ts +1 -1
- package/src/intl/pt.ts +1 -1
- package/src/intl/ro.ts +1 -1
- package/src/intl/sk.ts +1 -1
- package/src/intl/sl.ts +1 -1
- package/src/intl/sv.ts +1 -1
- package/src/intl/sw.ts +1 -1
- package/src/intl/ta.ts +1 -1
- package/src/intl/te.ts +1 -1
- package/src/intl/th.ts +1 -1
- package/src/intl/tr.ts +1 -1
- package/src/intl/uk.ts +1 -1
- package/src/intl/ur.ts +1 -1
- package/src/intl/vi.ts +1 -1
- package/src/intl/zh-CN.ts +1 -1
- package/src/intl/zh-TW.ts +1 -1
- package/src/local-first/Evolu.test.ts +1921 -0
- package/src/local-first/Evolu.ts +18 -14
- package/src/local-first/Owner.test.ts +83 -0
- package/src/local-first/Protocol.test.ts +1037 -0
- package/src/local-first/Protocol.ts +58 -261
- package/src/local-first/Query.test.ts +349 -0
- package/src/local-first/Query.ts +11 -11
- package/src/local-first/Relay.ts +2 -2
- package/src/local-first/Schema.ts +3 -5
- package/src/local-first/Shared.test.ts +1828 -0
- package/src/local-first/Shared.ts +9 -8
- package/src/local-first/Storage.ts +4 -4
- package/src/local-first/Timestamp.test.ts +374 -0
- package/src/local-first/Timestamp.ts +22 -10
- package/dist/src/Buffer.d.ts.map +0 -1
|
@@ -0,0 +1,1554 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import {
|
|
3
|
+
assertEqual,
|
|
4
|
+
assertOk,
|
|
5
|
+
assertThrowsInstanceOf,
|
|
6
|
+
assertTrue,
|
|
7
|
+
} from "./Assert.ts";
|
|
8
|
+
|
|
9
|
+
import type { RandomNumber } from "./Random.ts";
|
|
10
|
+
import type { NextResult } from "./Result.ts";
|
|
11
|
+
import { done, err, ok } from "./Result.ts";
|
|
12
|
+
import type { Schedule } from "./Schedule.ts";
|
|
13
|
+
import {
|
|
14
|
+
addDelay,
|
|
15
|
+
always,
|
|
16
|
+
collectAllScheduleOutputs,
|
|
17
|
+
collectScheduleInputs,
|
|
18
|
+
collectUntilScheduleOutput,
|
|
19
|
+
collectWhileScheduleOutput,
|
|
20
|
+
compensate,
|
|
21
|
+
delayed,
|
|
22
|
+
delays,
|
|
23
|
+
during,
|
|
24
|
+
elapsed,
|
|
25
|
+
exponential,
|
|
26
|
+
fibonacci,
|
|
27
|
+
fixed,
|
|
28
|
+
foldSchedule,
|
|
29
|
+
forever,
|
|
30
|
+
fromDelay,
|
|
31
|
+
fromDelays,
|
|
32
|
+
intersectSchedules,
|
|
33
|
+
jitter,
|
|
34
|
+
linear,
|
|
35
|
+
mapSchedule,
|
|
36
|
+
maxDelay,
|
|
37
|
+
maxElapsed,
|
|
38
|
+
modifyDelay,
|
|
39
|
+
once,
|
|
40
|
+
passthrough,
|
|
41
|
+
recurs,
|
|
42
|
+
repetitions,
|
|
43
|
+
resetScheduleAfter,
|
|
44
|
+
retryStrategyAws,
|
|
45
|
+
sequenceSchedules,
|
|
46
|
+
spaced,
|
|
47
|
+
take,
|
|
48
|
+
tapScheduleInput,
|
|
49
|
+
tapScheduleOutput,
|
|
50
|
+
unfoldSchedule,
|
|
51
|
+
unionSchedules,
|
|
52
|
+
untilScheduleInput,
|
|
53
|
+
untilScheduleOutput,
|
|
54
|
+
whenInput,
|
|
55
|
+
whileScheduleInput,
|
|
56
|
+
whileScheduleOutput,
|
|
57
|
+
windowed,
|
|
58
|
+
} from "./Schedule.ts";
|
|
59
|
+
import { testCreateDeps } from "./Task.ts";
|
|
60
|
+
import {
|
|
61
|
+
maxMillis,
|
|
62
|
+
Millis,
|
|
63
|
+
minMillis,
|
|
64
|
+
PositiveMillis,
|
|
65
|
+
testCreateTime,
|
|
66
|
+
} from "./Time.ts";
|
|
67
|
+
import { type DateIso, NonNegativeInt, Ratio } from "./Type.ts";
|
|
68
|
+
|
|
69
|
+
// Helper to create scheduleDeps with controllable time
|
|
70
|
+
const createScheduleDeps = (startAt = 0) => {
|
|
71
|
+
const deps = testCreateDeps();
|
|
72
|
+
const time = testCreateTime({ startAt: Millis.orThrow(startAt) });
|
|
73
|
+
return { ...deps, time };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const createScheduleDepsWithNow = (...times: ReadonlyArray<number>) => {
|
|
77
|
+
const deps = testCreateDeps();
|
|
78
|
+
let index = 0;
|
|
79
|
+
const time = testCreateTime({ startAt: Millis.orThrow(times[0] ?? 0) });
|
|
80
|
+
function now(): Millis;
|
|
81
|
+
function now(type: "DateIso"): DateIso;
|
|
82
|
+
function now(type?: "DateIso"): Millis | DateIso {
|
|
83
|
+
if (type === "DateIso") return time.now(type);
|
|
84
|
+
return Millis.orThrow(times[Math.min(index++, times.length - 1)]);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
...deps,
|
|
88
|
+
time: {
|
|
89
|
+
...time,
|
|
90
|
+
now,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const createScheduleDepsWithRandom = (value: RandomNumber) => ({
|
|
96
|
+
...createScheduleDeps(),
|
|
97
|
+
random: { next: () => value },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const expectOk = (
|
|
101
|
+
result: NextResult<readonly [unknown, Millis]>,
|
|
102
|
+
[output, delay]: readonly [unknown, number],
|
|
103
|
+
): void => {
|
|
104
|
+
assertOk(result);
|
|
105
|
+
if (!result.ok) return;
|
|
106
|
+
assertEqual(result.value[0], output);
|
|
107
|
+
assertEqual(result.value[1], Millis.orThrow(delay));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const expectDone = (result: NextResult<readonly [unknown, Millis]>): void => {
|
|
111
|
+
assertEqual(result, err(done()));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
describe("Schedule", () => {
|
|
115
|
+
it("Output is covariant (out)", () => {
|
|
116
|
+
const deps = createScheduleDeps();
|
|
117
|
+
// Substitutability by output: a schedule producing more info can be used where
|
|
118
|
+
// less info is required.
|
|
119
|
+
interface RetryInfo {
|
|
120
|
+
readonly attempt: number;
|
|
121
|
+
readonly delay: Millis;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Create a schedule that outputs detailed info
|
|
125
|
+
const detailedSchedule: Schedule<RetryInfo> = () => {
|
|
126
|
+
let attempt = 0;
|
|
127
|
+
return () => {
|
|
128
|
+
attempt++;
|
|
129
|
+
const delay = Millis.orThrow(100 * attempt);
|
|
130
|
+
return ok([{ attempt, delay }, delay]);
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Create a schedule that outputs less detailed info
|
|
135
|
+
const lessDetailedSchedule: Schedule<{ readonly attempt: number }> = () => {
|
|
136
|
+
let attempt = 0;
|
|
137
|
+
return () => {
|
|
138
|
+
attempt++;
|
|
139
|
+
const delay = Millis.orThrow(100 * attempt);
|
|
140
|
+
return ok([{ attempt }, delay]);
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// ✓ Allowed: detailed output → less detailed output
|
|
145
|
+
const useSchedule = (s: Schedule<{ readonly attempt: number }>) => {
|
|
146
|
+
const step = s(deps);
|
|
147
|
+
return step(undefined);
|
|
148
|
+
};
|
|
149
|
+
const result = useSchedule(detailedSchedule);
|
|
150
|
+
assertOk(result);
|
|
151
|
+
assertEqual(result.ok ? result.value[0].attempt : null, 1);
|
|
152
|
+
|
|
153
|
+
// ✗ Not assignable in the other direction (output is missing fields).
|
|
154
|
+
// This is a structural type error; that's the expected mechanism.
|
|
155
|
+
// @ts-expect-error - Output lacks "delay"
|
|
156
|
+
const _invalid: Schedule<RetryInfo> = lessDetailedSchedule;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("Input is contravariant (in)", () => {
|
|
160
|
+
const deps = createScheduleDeps();
|
|
161
|
+
// Substitutability by input: a schedule that accepts broader inputs can be
|
|
162
|
+
// used where narrower inputs are provided.
|
|
163
|
+
interface HttpError {
|
|
164
|
+
readonly status: number;
|
|
165
|
+
readonly message: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface RateLimitError extends HttpError {
|
|
169
|
+
readonly retryAfter: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// spaced("1s") accepts unknown input - it doesn't inspect input at all
|
|
173
|
+
const genericSchedule: Schedule<Millis> = spaced("1s");
|
|
174
|
+
|
|
175
|
+
// ✓ Allowed: broader input → narrower input
|
|
176
|
+
const retryWithSchedule = (
|
|
177
|
+
error: RateLimitError,
|
|
178
|
+
schedule: Schedule<Millis, RateLimitError>,
|
|
179
|
+
) => {
|
|
180
|
+
const step = schedule(deps);
|
|
181
|
+
return step(error);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const error: RateLimitError = {
|
|
185
|
+
status: 429,
|
|
186
|
+
message: "Too many",
|
|
187
|
+
retryAfter: 60,
|
|
188
|
+
};
|
|
189
|
+
const result = retryWithSchedule(error, genericSchedule);
|
|
190
|
+
assertOk(result);
|
|
191
|
+
assertEqual(result.ok ? result.value[1] : null, 1000);
|
|
192
|
+
|
|
193
|
+
// ✗ Not assignable in the other direction (schedule expects RateLimitError).
|
|
194
|
+
const specificSchedule: Schedule<Millis, RateLimitError> = whenInput<
|
|
195
|
+
RateLimitError,
|
|
196
|
+
Millis
|
|
197
|
+
>(
|
|
198
|
+
(e) => e.retryAfter > 0,
|
|
199
|
+
spaced("5s"),
|
|
200
|
+
)(spaced("1s"));
|
|
201
|
+
// @ts-expect-error - Input is too narrow
|
|
202
|
+
const _invalid: Schedule<Millis, HttpError> = specificSchedule;
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("forever", () => {
|
|
207
|
+
it("returns attempt count and 0 delay", () => {
|
|
208
|
+
const deps = createScheduleDeps();
|
|
209
|
+
const step = forever(deps);
|
|
210
|
+
expectOk(step(undefined), [0, 0]);
|
|
211
|
+
expectOk(step(undefined), [1, 0]);
|
|
212
|
+
expectOk(step(undefined), [2, 0]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("once", () => {
|
|
217
|
+
it("runs exactly once", () => {
|
|
218
|
+
const deps = createScheduleDeps();
|
|
219
|
+
const step = once(deps);
|
|
220
|
+
expectOk(step(undefined), [0, 0]);
|
|
221
|
+
expectDone(step(undefined));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("recurs", () => {
|
|
226
|
+
it("limits repetitions", () => {
|
|
227
|
+
const deps = createScheduleDeps();
|
|
228
|
+
const step = recurs(3)(deps);
|
|
229
|
+
expectOk(step(undefined), [0, 0]);
|
|
230
|
+
expectOk(step(undefined), [1, 0]);
|
|
231
|
+
expectOk(step(undefined), [2, 0]);
|
|
232
|
+
expectDone(step(undefined));
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("spaced", () => {
|
|
237
|
+
it("returns constant delay", () => {
|
|
238
|
+
const deps = createScheduleDeps();
|
|
239
|
+
const step = spaced("100ms")(deps);
|
|
240
|
+
expectOk(step(undefined), [100, 100]);
|
|
241
|
+
expectOk(step(undefined), [100, 100]);
|
|
242
|
+
expectOk(step(undefined), [100, 100]);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("exponential", () => {
|
|
247
|
+
it("grows by factor", () => {
|
|
248
|
+
const deps = createScheduleDeps();
|
|
249
|
+
const step = exponential("100ms")(deps);
|
|
250
|
+
expectOk(step(undefined), [100, 100]);
|
|
251
|
+
expectOk(step(undefined), [200, 200]);
|
|
252
|
+
expectOk(step(undefined), [400, 400]);
|
|
253
|
+
expectOk(step(undefined), [800, 800]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("with custom factor", () => {
|
|
257
|
+
const deps = createScheduleDeps();
|
|
258
|
+
const step = exponential("100ms", 3)(deps);
|
|
259
|
+
expectOk(step(undefined), [100, 100]);
|
|
260
|
+
expectOk(step(undefined), [300, 300]);
|
|
261
|
+
expectOk(step(undefined), [900, 900]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("with fractional factor rounds to millis", () => {
|
|
265
|
+
const deps = createScheduleDeps();
|
|
266
|
+
const step = exponential("100ms", 1.5)(deps);
|
|
267
|
+
expectOk(step(undefined), [100, 100]);
|
|
268
|
+
expectOk(step(undefined), [150, 150]);
|
|
269
|
+
expectOk(step(undefined), [225, 225]);
|
|
270
|
+
expectOk(step(undefined), [338, 338]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("states are independent (stateful)", () => {
|
|
274
|
+
const deps = createScheduleDeps();
|
|
275
|
+
const schedule = exponential("100ms");
|
|
276
|
+
|
|
277
|
+
const step1 = schedule(deps);
|
|
278
|
+
const step2 = schedule(deps);
|
|
279
|
+
|
|
280
|
+
// Each step has its own data
|
|
281
|
+
expectOk(step1(undefined), [100, 100]);
|
|
282
|
+
expectOk(step1(undefined), [200, 200]);
|
|
283
|
+
|
|
284
|
+
// step2 starts fresh
|
|
285
|
+
expectOk(step2(undefined), [100, 100]);
|
|
286
|
+
expectOk(step2(undefined), [200, 200]);
|
|
287
|
+
|
|
288
|
+
// step1 continues from its data
|
|
289
|
+
expectOk(step1(undefined), [400, 400]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("saturates at maxMillis instead of throwing on overflow", () => {
|
|
293
|
+
const deps = createScheduleDeps();
|
|
294
|
+
const step = exponential("100ms")(deps);
|
|
295
|
+
// Advance to attempt 43 where 100 * 2^42 > maxMillis
|
|
296
|
+
for (let i = 0; i < 42; i++) step(undefined);
|
|
297
|
+
assertEqual(step(undefined), ok([maxMillis, maxMillis]));
|
|
298
|
+
assertEqual(step(undefined), ok([maxMillis, maxMillis]));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("keeps a zero base at zero after the power overflows", () => {
|
|
302
|
+
const step = exponential(minMillis)(createScheduleDeps());
|
|
303
|
+
for (let i = 0; i < 1025; i++) step(undefined);
|
|
304
|
+
assertEqual(step(undefined), ok([minMillis, minMillis]));
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("throws for invalid factor", () => {
|
|
308
|
+
const nanError = assertThrowsInstanceOf(
|
|
309
|
+
() => exponential("100ms", Number.NaN),
|
|
310
|
+
Error,
|
|
311
|
+
);
|
|
312
|
+
assertTrue(nanError.message.includes("Expected NonNegative."));
|
|
313
|
+
const infinityError = assertThrowsInstanceOf(
|
|
314
|
+
() => exponential("100ms", Number.POSITIVE_INFINITY),
|
|
315
|
+
Error,
|
|
316
|
+
);
|
|
317
|
+
assertTrue(infinityError.message.includes("Expected NonNegative."));
|
|
318
|
+
const negativeError = assertThrowsInstanceOf(
|
|
319
|
+
() => exponential("100ms", -1),
|
|
320
|
+
Error,
|
|
321
|
+
);
|
|
322
|
+
assertTrue(negativeError.message.includes("Expected NonNegative."));
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("linear", () => {
|
|
327
|
+
it("grows linearly", () => {
|
|
328
|
+
const deps = createScheduleDeps();
|
|
329
|
+
const step = linear("100ms")(deps);
|
|
330
|
+
expectOk(step(undefined), [100, 100]);
|
|
331
|
+
expectOk(step(undefined), [200, 200]);
|
|
332
|
+
expectOk(step(undefined), [300, 300]);
|
|
333
|
+
expectOk(step(undefined), [400, 400]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("saturates at maxMillis instead of throwing on overflow", () => {
|
|
337
|
+
const deps = createScheduleDeps();
|
|
338
|
+
const step = linear(maxMillis)(deps);
|
|
339
|
+
assertEqual(step(undefined), ok([maxMillis, maxMillis]));
|
|
340
|
+
assertEqual(step(undefined), ok([maxMillis, maxMillis]));
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("fibonacci", () => {
|
|
345
|
+
it("grows by fibonacci sequence", () => {
|
|
346
|
+
const deps = createScheduleDeps();
|
|
347
|
+
const step = fibonacci("100ms")(deps);
|
|
348
|
+
// F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, F(6)=8
|
|
349
|
+
// 100 * 1
|
|
350
|
+
expectOk(step(undefined), [100, 100]);
|
|
351
|
+
// 100 * 1
|
|
352
|
+
expectOk(step(undefined), [100, 100]);
|
|
353
|
+
// 100 * 2
|
|
354
|
+
expectOk(step(undefined), [200, 200]);
|
|
355
|
+
// 100 * 3
|
|
356
|
+
expectOk(step(undefined), [300, 300]);
|
|
357
|
+
// 100 * 5
|
|
358
|
+
expectOk(step(undefined), [500, 500]);
|
|
359
|
+
// 100 * 8
|
|
360
|
+
expectOk(step(undefined), [800, 800]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("saturates at maxMillis instead of throwing after max Fibonacci index", () => {
|
|
364
|
+
const deps = createScheduleDeps();
|
|
365
|
+
const step = fibonacci("1ms")(deps);
|
|
366
|
+
for (let i = 0; i < 78; i++) step(undefined);
|
|
367
|
+
assertEqual(step(undefined), ok([maxMillis, maxMillis]));
|
|
368
|
+
assertEqual(step(undefined), ok([maxMillis, maxMillis]));
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("fixed", () => {
|
|
373
|
+
it("aligns to window boundaries", () => {
|
|
374
|
+
const deps = createScheduleDeps();
|
|
375
|
+
const step = fixed("10s")(deps);
|
|
376
|
+
|
|
377
|
+
// First execution at T=0, outputs count 0, wait 10s to align to first window
|
|
378
|
+
expectOk(step(undefined), [0, 10000]);
|
|
379
|
+
|
|
380
|
+
// Second at T=13 (3s into second window), outputs count 1
|
|
381
|
+
// Next window boundary is at T=20, so wait 7s
|
|
382
|
+
deps.time.advance("13s");
|
|
383
|
+
expectOk(step(undefined), [1, 7000]);
|
|
384
|
+
|
|
385
|
+
// Third at T=27 (7s into third window), outputs count 2
|
|
386
|
+
// Next window boundary is at T=30, so wait 3s
|
|
387
|
+
// now at 27s
|
|
388
|
+
deps.time.advance("14s");
|
|
389
|
+
expectOk(step(undefined), [2, 3000]);
|
|
390
|
+
|
|
391
|
+
// Fourth at T=35 (5s into fourth window), outputs count 3
|
|
392
|
+
// Next window boundary is at T=40, so wait 5s
|
|
393
|
+
// now at 35s
|
|
394
|
+
deps.time.advance("8s");
|
|
395
|
+
expectOk(step(undefined), [3, 5000]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("handles running behind (execution > interval)", () => {
|
|
399
|
+
const deps = createScheduleDeps();
|
|
400
|
+
const step = fixed("10s")(deps);
|
|
401
|
+
|
|
402
|
+
// First execution starts
|
|
403
|
+
expectOk(step(undefined), [0, 10000]);
|
|
404
|
+
|
|
405
|
+
// Execution took 25s, now at T=25 (should have run at T=10, T=20)
|
|
406
|
+
// Running behind: delay is 0, but next boundary is T=30
|
|
407
|
+
deps.time.advance("25s");
|
|
408
|
+
expectOk(step(undefined), [1, 0]);
|
|
409
|
+
|
|
410
|
+
// Now at T=28, not running behind anymore, align to T=30
|
|
411
|
+
deps.time.advance("3s");
|
|
412
|
+
expectOk(step(undefined), [2, 2000]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("catches up missed recurrences before realigning", () => {
|
|
416
|
+
const deps = createScheduleDeps();
|
|
417
|
+
const step = fixed("10s")(deps);
|
|
418
|
+
|
|
419
|
+
expectOk(step(undefined), [0, 10000]);
|
|
420
|
+
deps.time.advance("55s");
|
|
421
|
+
expectOk(step(undefined), [1, 0]);
|
|
422
|
+
expectOk(step(undefined), [2, 0]);
|
|
423
|
+
expectOk(step(undefined), [3, 0]);
|
|
424
|
+
expectOk(step(undefined), [4, 0]);
|
|
425
|
+
expectOk(step(undefined), [5, 5000]);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("with zero interval", () => {
|
|
429
|
+
const deps = createScheduleDeps();
|
|
430
|
+
const step = fixed(minMillis)(deps);
|
|
431
|
+
expectOk(step(undefined), [0, 0]);
|
|
432
|
+
deps.time.advance("100ms");
|
|
433
|
+
expectOk(step(undefined), [1, 0]);
|
|
434
|
+
deps.time.advance("100ms");
|
|
435
|
+
expectOk(step(undefined), [2, 0]);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("windowed", () => {
|
|
440
|
+
it("skips missed recurrences before realigning", () => {
|
|
441
|
+
const deps = createScheduleDeps();
|
|
442
|
+
const step = windowed("10s")(deps);
|
|
443
|
+
|
|
444
|
+
expectOk(step(undefined), [0, 10000]);
|
|
445
|
+
deps.time.advance("55s");
|
|
446
|
+
expectOk(step(undefined), [1, 5000]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("waits until the next interval at an exact boundary", () => {
|
|
450
|
+
const deps = createScheduleDeps();
|
|
451
|
+
const step = windowed("100ms")(deps);
|
|
452
|
+
|
|
453
|
+
expectOk(step(undefined), [0, 100]);
|
|
454
|
+
deps.time.advance("100ms");
|
|
455
|
+
expectOk(step(undefined), [1, 100]);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("with zero interval", () => {
|
|
459
|
+
const deps = createScheduleDeps();
|
|
460
|
+
const step = windowed(minMillis)(deps);
|
|
461
|
+
expectOk(step(undefined), [0, 0]);
|
|
462
|
+
deps.time.advance("100ms");
|
|
463
|
+
expectOk(step(undefined), [1, 0]);
|
|
464
|
+
deps.time.advance("100ms");
|
|
465
|
+
expectOk(step(undefined), [2, 0]);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("aligns to window boundaries", () => {
|
|
469
|
+
const schedule = windowed("100ms");
|
|
470
|
+
const deps = createScheduleDeps();
|
|
471
|
+
|
|
472
|
+
// At elapsed=0, wait full 100ms to next boundary
|
|
473
|
+
const step = schedule(deps);
|
|
474
|
+
expectOk(step(undefined), [0, 100]);
|
|
475
|
+
// At elapsed=30, wait 70ms to next boundary
|
|
476
|
+
deps.time.advance("30ms");
|
|
477
|
+
expectOk(step(undefined), [1, 70]);
|
|
478
|
+
// At elapsed=150 (30 + 120 more), wait 50ms to next boundary (200ms)
|
|
479
|
+
deps.time.advance("120ms");
|
|
480
|
+
expectOk(step(undefined), [2, 50]);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe("fromDelay", () => {
|
|
485
|
+
it("creates single-delay schedule", () => {
|
|
486
|
+
const deps = createScheduleDeps();
|
|
487
|
+
const schedule = fromDelay("500ms");
|
|
488
|
+
const step = schedule(deps);
|
|
489
|
+
|
|
490
|
+
expectOk(step(undefined), [500, 500]);
|
|
491
|
+
expectDone(step(undefined));
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe("fromDelays", () => {
|
|
496
|
+
it("creates sequence of delays", () => {
|
|
497
|
+
const deps = createScheduleDeps();
|
|
498
|
+
const schedule = fromDelays("100ms", "500ms", "2s");
|
|
499
|
+
const step = schedule(deps);
|
|
500
|
+
|
|
501
|
+
expectOk(step(undefined), [100, 100]);
|
|
502
|
+
expectOk(step(undefined), [500, 500]);
|
|
503
|
+
expectOk(step(undefined), [2000, 2000]);
|
|
504
|
+
expectDone(step(undefined));
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("stops immediately with no delays", () => {
|
|
508
|
+
const step = fromDelays()(createScheduleDeps());
|
|
509
|
+
|
|
510
|
+
expectDone(step(undefined));
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe("elapsed", () => {
|
|
515
|
+
it("outputs total elapsed time", () => {
|
|
516
|
+
const deps = createScheduleDeps();
|
|
517
|
+
const step = elapsed(deps);
|
|
518
|
+
|
|
519
|
+
expectOk(step(undefined), [0, 0]);
|
|
520
|
+
deps.time.advance("100ms");
|
|
521
|
+
expectOk(step(undefined), [100, 0]);
|
|
522
|
+
deps.time.advance("400ms");
|
|
523
|
+
expectOk(step(undefined), [500, 0]);
|
|
524
|
+
deps.time.advance("500ms");
|
|
525
|
+
expectOk(step(undefined), [1000, 0]);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("returns zero elapsed time when time moves backwards", () => {
|
|
529
|
+
const deps = createScheduleDepsWithNow(100, 50);
|
|
530
|
+
const step = elapsed(deps);
|
|
531
|
+
|
|
532
|
+
expectOk(step(undefined), [0, 0]);
|
|
533
|
+
expectOk(step(undefined), [0, 0]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("combined with other schedule", () => {
|
|
537
|
+
const deps = createScheduleDeps();
|
|
538
|
+
const step = intersectSchedules(take(3)(spaced("100ms")), elapsed)(deps);
|
|
539
|
+
|
|
540
|
+
const result1 = step(undefined);
|
|
541
|
+
assertOk(result1);
|
|
542
|
+
// spaced output
|
|
543
|
+
assertEqual(result1.ok ? result1.value[0][0] : null, 100);
|
|
544
|
+
// elapsed output
|
|
545
|
+
assertEqual(result1.ok ? result1.value[0][1] : null, 0);
|
|
546
|
+
// max delay
|
|
547
|
+
assertEqual(result1.ok ? result1.value[1] : null, 100);
|
|
548
|
+
|
|
549
|
+
deps.time.advance("150ms");
|
|
550
|
+
const result2 = step(undefined);
|
|
551
|
+
assertOk(result2);
|
|
552
|
+
assertEqual(result2.ok ? result2.value[0][0] : null, 100);
|
|
553
|
+
assertEqual(result2.ok ? result2.value[0][1] : null, 150);
|
|
554
|
+
|
|
555
|
+
deps.time.advance("150ms");
|
|
556
|
+
const result3 = step(undefined);
|
|
557
|
+
assertOk(result3);
|
|
558
|
+
assertEqual(result3.ok ? result3.value[0][0] : null, 100);
|
|
559
|
+
assertEqual(result3.ok ? result3.value[0][1] : null, 300);
|
|
560
|
+
|
|
561
|
+
deps.time.advance("100ms");
|
|
562
|
+
// take(3) exhausted
|
|
563
|
+
expectDone(step(undefined));
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe("during", () => {
|
|
568
|
+
it("runs for specified duration then stops", () => {
|
|
569
|
+
const schedule = during("100ms");
|
|
570
|
+
const deps = createScheduleDeps();
|
|
571
|
+
const step = schedule(deps);
|
|
572
|
+
|
|
573
|
+
expectOk(step(undefined), [0, 0]);
|
|
574
|
+
deps.time.advance("50ms");
|
|
575
|
+
expectOk(step(undefined), [50, 0]);
|
|
576
|
+
deps.time.advance("50ms");
|
|
577
|
+
// exactly at limit
|
|
578
|
+
expectOk(step(undefined), [100, 0]);
|
|
579
|
+
deps.time.advance("1ms");
|
|
580
|
+
// over limit
|
|
581
|
+
expectDone(step(undefined));
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
describe("always", () => {
|
|
586
|
+
it("always outputs constant value", () => {
|
|
587
|
+
const deps = createScheduleDeps();
|
|
588
|
+
const step = take(3)(always("retry"))(deps);
|
|
589
|
+
expectOk(step(undefined), ["retry", 0]);
|
|
590
|
+
expectOk(step(undefined), ["retry", 0]);
|
|
591
|
+
expectOk(step(undefined), ["retry", 0]);
|
|
592
|
+
expectDone(step(undefined));
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
describe("unfoldSchedule", () => {
|
|
597
|
+
it("creates schedule from state function", () => {
|
|
598
|
+
const deps = createScheduleDeps();
|
|
599
|
+
// Simple counter
|
|
600
|
+
const step = take(4)(unfoldSchedule(0, (n) => n + 1))(deps);
|
|
601
|
+
|
|
602
|
+
expectOk(step(undefined), [0, 0]);
|
|
603
|
+
expectOk(step(undefined), [1, 0]);
|
|
604
|
+
expectOk(step(undefined), [2, 0]);
|
|
605
|
+
expectOk(step(undefined), [3, 0]);
|
|
606
|
+
expectDone(step(undefined));
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("with custom state transformation", () => {
|
|
610
|
+
const deps = createScheduleDeps();
|
|
611
|
+
// Multiply by 2 each time
|
|
612
|
+
const step = take(4)(unfoldSchedule(1, (n) => n * 2))(deps);
|
|
613
|
+
|
|
614
|
+
expectOk(step(undefined), [1, 0]);
|
|
615
|
+
expectOk(step(undefined), [2, 0]);
|
|
616
|
+
expectOk(step(undefined), [4, 0]);
|
|
617
|
+
expectOk(step(undefined), [8, 0]);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it("with object state", () => {
|
|
621
|
+
const deps = createScheduleDeps();
|
|
622
|
+
interface Phase {
|
|
623
|
+
readonly name: string;
|
|
624
|
+
readonly count: number;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const nextPhase = (phase: Phase): Phase => ({
|
|
628
|
+
name: phase.count < 2 ? "warmup" : "active",
|
|
629
|
+
count: phase.count + 1,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const step = take(4)(unfoldSchedule({ name: "init", count: 0 }, nextPhase))(
|
|
633
|
+
deps,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
expectOk(step(undefined), [{ name: "init", count: 0 }, 0]);
|
|
637
|
+
expectOk(step(undefined), [{ name: "warmup", count: 1 }, 0]);
|
|
638
|
+
expectOk(step(undefined), [{ name: "warmup", count: 2 }, 0]);
|
|
639
|
+
expectOk(step(undefined), [{ name: "active", count: 3 }, 0]);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
describe("take", () => {
|
|
644
|
+
it("limits attempts", () => {
|
|
645
|
+
const deps = createScheduleDeps();
|
|
646
|
+
const step = take(3)(exponential("100ms"))(deps);
|
|
647
|
+
expectOk(step(undefined), [100, 100]);
|
|
648
|
+
expectOk(step(undefined), [200, 200]);
|
|
649
|
+
expectOk(step(undefined), [400, 400]);
|
|
650
|
+
expectDone(step(undefined));
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("accepts literals from 0 to 100 or a validated NonNegativeInt", () => {
|
|
654
|
+
take(0);
|
|
655
|
+
take(100);
|
|
656
|
+
take(NonNegativeInt.orThrow(101));
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("requires validation for other numbers", () => {
|
|
660
|
+
// @ts-expect-error - Dynamic numbers require validation.
|
|
661
|
+
take(Math.random());
|
|
662
|
+
// @ts-expect-error - Literals above 100 require validation.
|
|
663
|
+
take(101);
|
|
664
|
+
// @ts-expect-error - Negative integers are invalid.
|
|
665
|
+
take(-1);
|
|
666
|
+
// @ts-expect-error - Fractions are invalid.
|
|
667
|
+
take(1.5);
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
describe("maxElapsed", () => {
|
|
672
|
+
it("stops after duration", () => {
|
|
673
|
+
const deps = createScheduleDeps();
|
|
674
|
+
const step = maxElapsed("250ms")(exponential("100ms"))(deps);
|
|
675
|
+
expectOk(step(undefined), [100, 100]);
|
|
676
|
+
deps.time.advance("100ms");
|
|
677
|
+
expectOk(step(undefined), [200, 200]);
|
|
678
|
+
// now at 250ms
|
|
679
|
+
deps.time.advance("150ms");
|
|
680
|
+
// elapsed >= 250
|
|
681
|
+
expectDone(step(undefined));
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("keeps terminal done when time moves backwards", () => {
|
|
685
|
+
const step = maxElapsed("250ms")(exponential("100ms"))(
|
|
686
|
+
createScheduleDepsWithNow(0, 250, 0),
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
expectOk(step(undefined), [100, 100]);
|
|
690
|
+
expectDone(step(undefined));
|
|
691
|
+
expectDone(step(undefined));
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
describe("maxDelay", () => {
|
|
696
|
+
it("caps delay", () => {
|
|
697
|
+
const deps = createScheduleDeps();
|
|
698
|
+
const step = maxDelay("300ms")(exponential("100ms"))(deps);
|
|
699
|
+
expectOk(step(undefined), [100, 100]);
|
|
700
|
+
expectOk(step(undefined), [200, 200]);
|
|
701
|
+
// capped
|
|
702
|
+
expectOk(step(undefined), [400, 300]);
|
|
703
|
+
// capped
|
|
704
|
+
expectOk(step(undefined), [800, 300]);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("maxDelay preserves done", () => {
|
|
708
|
+
const deps = createScheduleDeps();
|
|
709
|
+
const step = maxDelay("300ms")(once)(deps);
|
|
710
|
+
expectOk(step(undefined), [0, 0]);
|
|
711
|
+
expectDone(step(undefined));
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe("jitter", () => {
|
|
716
|
+
it("0% preserves delay", () => {
|
|
717
|
+
const deps = createScheduleDeps();
|
|
718
|
+
const step = jitter("0%")(spaced("100ms"))(deps);
|
|
719
|
+
|
|
720
|
+
expectOk(step(undefined), [100, 100]);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it("accepts Ratio", () => {
|
|
724
|
+
jitter(Ratio.orThrow(1));
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
describe("below", () => {
|
|
728
|
+
it("defaults to 50% below jitter", () => {
|
|
729
|
+
const result = jitter()(spaced("100ms"))(
|
|
730
|
+
createScheduleDepsWithRandom(0.5 as RandomNumber),
|
|
731
|
+
)(undefined);
|
|
732
|
+
const explicitResult = jitter("50%", "below")(spaced("100ms"))(
|
|
733
|
+
createScheduleDepsWithRandom(0.5 as RandomNumber),
|
|
734
|
+
)(undefined);
|
|
735
|
+
|
|
736
|
+
assertEqual(result, explicitResult);
|
|
737
|
+
expectOk(result, [100, 75]);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("50% applies equal jitter", () => {
|
|
741
|
+
expectOk(
|
|
742
|
+
jitter("50%")(spaced("100ms"))(
|
|
743
|
+
createScheduleDepsWithRandom(0 as RandomNumber),
|
|
744
|
+
)(undefined),
|
|
745
|
+
[100, 50],
|
|
746
|
+
);
|
|
747
|
+
expectOk(
|
|
748
|
+
jitter("50%", "below")(spaced("100ms"))(
|
|
749
|
+
createScheduleDepsWithRandom(0.5 as RandomNumber),
|
|
750
|
+
)(undefined),
|
|
751
|
+
[100, 75],
|
|
752
|
+
);
|
|
753
|
+
expectOk(
|
|
754
|
+
jitter("50%")(spaced("100ms"))(
|
|
755
|
+
createScheduleDepsWithRandom(0.999999999 as RandomNumber),
|
|
756
|
+
)(undefined),
|
|
757
|
+
[100, 100],
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("100% applies full jitter", () => {
|
|
762
|
+
expectOk(
|
|
763
|
+
jitter("100%")(spaced("100ms"))(
|
|
764
|
+
createScheduleDepsWithRandom(0 as RandomNumber),
|
|
765
|
+
)(undefined),
|
|
766
|
+
[100, 0],
|
|
767
|
+
);
|
|
768
|
+
expectOk(
|
|
769
|
+
jitter("100%")(spaced("100ms"))(
|
|
770
|
+
createScheduleDepsWithRandom(0.5 as RandomNumber),
|
|
771
|
+
)(undefined),
|
|
772
|
+
[100, 50],
|
|
773
|
+
);
|
|
774
|
+
expectOk(
|
|
775
|
+
jitter("100%")(spaced("100ms"))(
|
|
776
|
+
createScheduleDepsWithRandom(0.999999999 as RandomNumber),
|
|
777
|
+
)(undefined),
|
|
778
|
+
[100, 100],
|
|
779
|
+
);
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
describe("around", () => {
|
|
784
|
+
it("randomizes around the original delay", () => {
|
|
785
|
+
expectOk(
|
|
786
|
+
jitter("50%", "around")(spaced("100ms"))(
|
|
787
|
+
createScheduleDepsWithRandom(0 as RandomNumber),
|
|
788
|
+
)(undefined),
|
|
789
|
+
[100, 50],
|
|
790
|
+
);
|
|
791
|
+
expectOk(
|
|
792
|
+
jitter("50%", "around")(spaced("100ms"))(
|
|
793
|
+
createScheduleDepsWithRandom(0.5 as RandomNumber),
|
|
794
|
+
)(undefined),
|
|
795
|
+
[100, 100],
|
|
796
|
+
);
|
|
797
|
+
expectOk(
|
|
798
|
+
jitter("50%", "around")(spaced("100ms"))(
|
|
799
|
+
createScheduleDepsWithRandom(0.999999999 as RandomNumber),
|
|
800
|
+
)(undefined),
|
|
801
|
+
[100, 150],
|
|
802
|
+
);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it("saturates at maxMillis instead of throwing on overflow", () => {
|
|
806
|
+
const step = jitter("100%", "around")(spaced(maxMillis))(
|
|
807
|
+
createScheduleDepsWithRandom(0.999999999 as RandomNumber),
|
|
808
|
+
);
|
|
809
|
+
assertEqual(step(undefined), ok([maxMillis, maxMillis]));
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("jitter preserves done", () => {
|
|
814
|
+
const deps = createScheduleDeps();
|
|
815
|
+
const step = jitter("50%")(once)(deps);
|
|
816
|
+
expectOk(step(undefined), [0, 0]);
|
|
817
|
+
expectDone(step(undefined));
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
describe("delayed", () => {
|
|
822
|
+
it("replaces the first delay", () => {
|
|
823
|
+
const deps = createScheduleDeps();
|
|
824
|
+
const step = delayed("500ms")(exponential("100ms"))(deps);
|
|
825
|
+
// Initial delay
|
|
826
|
+
expectOk(step(undefined), [100, 500]);
|
|
827
|
+
// Normal delays
|
|
828
|
+
expectOk(step(undefined), [200, 200]);
|
|
829
|
+
expectOk(step(undefined), [400, 400]);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it("does not override termination", () => {
|
|
833
|
+
const deps = createScheduleDeps();
|
|
834
|
+
const step = delayed("500ms")(take(0)(forever))(deps);
|
|
835
|
+
expectDone(step(undefined));
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
describe("addDelay", () => {
|
|
840
|
+
it("adds fixed delay to schedule", () => {
|
|
841
|
+
const deps = createScheduleDeps();
|
|
842
|
+
const schedule = addDelay("500ms")(exponential("100ms"));
|
|
843
|
+
const step = schedule(deps);
|
|
844
|
+
|
|
845
|
+
const result1 = step(undefined);
|
|
846
|
+
assertOk(result1);
|
|
847
|
+
// 100 + 500
|
|
848
|
+
assertEqual(result1.ok ? result1.value[1] : null, 600);
|
|
849
|
+
|
|
850
|
+
const result2 = step(undefined);
|
|
851
|
+
assertOk(result2);
|
|
852
|
+
// 200 + 500
|
|
853
|
+
assertEqual(result2.ok ? result2.value[1] : null, 700);
|
|
854
|
+
|
|
855
|
+
const result3 = step(undefined);
|
|
856
|
+
assertOk(result3);
|
|
857
|
+
// 400 + 500
|
|
858
|
+
assertEqual(result3.ok ? result3.value[1] : null, 900);
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
describe("modifyDelay", () => {
|
|
863
|
+
it("transforms delays", () => {
|
|
864
|
+
const deps = createScheduleDeps();
|
|
865
|
+
// Double all delays
|
|
866
|
+
const step = modifyDelay((d) => d * 2)(exponential("100ms"))(deps);
|
|
867
|
+
expectOk(step(undefined), [100, 200]);
|
|
868
|
+
expectOk(step(undefined), [200, 400]);
|
|
869
|
+
expectOk(step(undefined), [400, 800]);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it("passes through termination", () => {
|
|
873
|
+
const deps = createScheduleDeps();
|
|
874
|
+
const step = modifyDelay(() => 123)(take(1)(forever))(deps);
|
|
875
|
+
expectOk(step(undefined), [0, 123]);
|
|
876
|
+
expectDone(step(undefined));
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it("saturates at maxMillis instead of throwing on overflow", () => {
|
|
880
|
+
const deps = createScheduleDeps();
|
|
881
|
+
const step = modifyDelay(() => maxMillis + 1)(once)(deps);
|
|
882
|
+
assertEqual(step(undefined), ok([0, maxMillis]));
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("throws when the transform returns NaN", () => {
|
|
886
|
+
const step = modifyDelay(() => Number.NaN)(once)(createScheduleDeps());
|
|
887
|
+
const error = assertThrowsInstanceOf(() => step(undefined), Error);
|
|
888
|
+
assertTrue(error.message.includes("Expected NonNaN."));
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
describe("compensate", () => {
|
|
893
|
+
it("subtracts execution time, not previous sleep", () => {
|
|
894
|
+
const deps = createScheduleDeps();
|
|
895
|
+
const step = compensate(spaced("1s"))(deps);
|
|
896
|
+
|
|
897
|
+
expectOk(step(undefined), [1000, 1000]);
|
|
898
|
+
|
|
899
|
+
// Simulate: slept 1000ms, then execution took 200ms.
|
|
900
|
+
deps.time.advance("1.2s");
|
|
901
|
+
expectOk(step(undefined), [1000, 800]);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("keeps full delay until previous delay elapses", () => {
|
|
905
|
+
const deps = createScheduleDeps();
|
|
906
|
+
const step = compensate(spaced("1s"))(deps);
|
|
907
|
+
// First attempt at T=0, no previous → full delay
|
|
908
|
+
expectOk(step(undefined), [1000, 1000]);
|
|
909
|
+
// Second at T=200, before the previous 1000ms delay could have elapsed
|
|
910
|
+
deps.time.advance("200ms");
|
|
911
|
+
expectOk(step(undefined), [1000, 1000]);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it("passes through termination", () => {
|
|
915
|
+
const deps = createScheduleDeps();
|
|
916
|
+
const step = compensate(take(1)(spaced("1s")))(deps);
|
|
917
|
+
expectOk(step(undefined), [1000, 1000]);
|
|
918
|
+
expectDone(step(undefined));
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it("keeps full delay when time moves backwards", () => {
|
|
922
|
+
const deps = createScheduleDepsWithNow(100, 50);
|
|
923
|
+
const step = compensate(spaced("1s"))(deps);
|
|
924
|
+
|
|
925
|
+
expectOk(step(undefined), [1000, 1000]);
|
|
926
|
+
expectOk(step(undefined), [1000, 1000]);
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
describe("whileScheduleInput", () => {
|
|
931
|
+
it("continues while predicate is true", () => {
|
|
932
|
+
const deps = createScheduleDeps();
|
|
933
|
+
interface Error {
|
|
934
|
+
readonly type: "Transient" | "Fatal";
|
|
935
|
+
}
|
|
936
|
+
const schedule = whileScheduleInput<Error>((e) => e.type === "Transient")(
|
|
937
|
+
spaced("100ms"),
|
|
938
|
+
);
|
|
939
|
+
const step = schedule(deps);
|
|
940
|
+
|
|
941
|
+
expectOk(step({ type: "Transient" }), [100, 100]);
|
|
942
|
+
expectOk(step({ type: "Transient" }), [100, 100]);
|
|
943
|
+
expectDone(step({ type: "Fatal" }));
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it("remains done after predicate fails", () => {
|
|
947
|
+
const deps = createScheduleDeps();
|
|
948
|
+
const step = whileScheduleInput((n: number) => n > 0)(forever)(deps);
|
|
949
|
+
|
|
950
|
+
expectOk(step(1), [0, 0]);
|
|
951
|
+
expectDone(step(0));
|
|
952
|
+
expectDone(step(1));
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
describe("untilScheduleInput", () => {
|
|
957
|
+
it("stops when predicate becomes true", () => {
|
|
958
|
+
const deps = createScheduleDeps();
|
|
959
|
+
|
|
960
|
+
interface Error {
|
|
961
|
+
readonly type: "Transient" | "Fatal";
|
|
962
|
+
}
|
|
963
|
+
const schedule = untilScheduleInput<Error>((e) => e.type === "Fatal")(
|
|
964
|
+
spaced("100ms"),
|
|
965
|
+
);
|
|
966
|
+
const step = schedule(deps);
|
|
967
|
+
|
|
968
|
+
expectOk(step({ type: "Transient" }), [100, 100]);
|
|
969
|
+
expectOk(step({ type: "Transient" }), [100, 100]);
|
|
970
|
+
expectDone(step({ type: "Fatal" }));
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
describe("whileScheduleOutput", () => {
|
|
975
|
+
it("continues while predicate is true", () => {
|
|
976
|
+
const deps = createScheduleDeps();
|
|
977
|
+
// Stop when delay exceeds 300ms
|
|
978
|
+
const step = whileScheduleOutput((delay: Millis) => delay <= 300)(
|
|
979
|
+
exponential("100ms"),
|
|
980
|
+
)(deps);
|
|
981
|
+
expectOk(step(undefined), [100, 100]);
|
|
982
|
+
expectOk(step(undefined), [200, 200]);
|
|
983
|
+
// 400 > 300
|
|
984
|
+
expectDone(step(undefined));
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it("passes through termination", () => {
|
|
988
|
+
const deps = createScheduleDeps();
|
|
989
|
+
const step = whileScheduleOutput(() => true)(take(1)(forever))(deps);
|
|
990
|
+
expectOk(step(undefined), [0, 0]);
|
|
991
|
+
expectDone(step(undefined));
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
describe("untilScheduleOutput", () => {
|
|
996
|
+
it("stops when predicate becomes true", () => {
|
|
997
|
+
const deps = createScheduleDeps();
|
|
998
|
+
// Stop when delay reaches 400ms
|
|
999
|
+
const step = untilScheduleOutput((delay: Millis) => delay >= 400)(
|
|
1000
|
+
exponential("100ms"),
|
|
1001
|
+
)(deps);
|
|
1002
|
+
expectOk(step(undefined), [100, 100]);
|
|
1003
|
+
expectOk(step(undefined), [200, 200]);
|
|
1004
|
+
// 400 >= 400
|
|
1005
|
+
expectDone(step(undefined));
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it("passes through termination", () => {
|
|
1009
|
+
const deps = createScheduleDeps();
|
|
1010
|
+
const step = untilScheduleOutput(() => false)(take(1)(forever))(deps);
|
|
1011
|
+
expectOk(step(undefined), [0, 0]);
|
|
1012
|
+
expectDone(step(undefined));
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
describe("predicate filters", () => {
|
|
1017
|
+
it("other predicate stops are sticky", () => {
|
|
1018
|
+
const inputUntilStep = untilScheduleInput((n: number) => n <= 0)(forever)(
|
|
1019
|
+
createScheduleDeps(),
|
|
1020
|
+
);
|
|
1021
|
+
expectOk(inputUntilStep(1), [0, 0]);
|
|
1022
|
+
expectDone(inputUntilStep(0));
|
|
1023
|
+
expectDone(inputUntilStep(1));
|
|
1024
|
+
|
|
1025
|
+
const outputWhileStep = whileScheduleOutput((n: number) => n > 0)(
|
|
1026
|
+
unfoldSchedule(1, (n) => (n === 1 ? 0 : 1)),
|
|
1027
|
+
)(createScheduleDeps());
|
|
1028
|
+
expectOk(outputWhileStep(undefined), [1, 0]);
|
|
1029
|
+
expectDone(outputWhileStep(undefined));
|
|
1030
|
+
expectDone(outputWhileStep(undefined));
|
|
1031
|
+
|
|
1032
|
+
const outputUntilStep = untilScheduleOutput((n: number) => n <= 0)(
|
|
1033
|
+
unfoldSchedule(1, (n) => (n === 1 ? 0 : 1)),
|
|
1034
|
+
)(createScheduleDeps());
|
|
1035
|
+
expectOk(outputUntilStep(undefined), [1, 0]);
|
|
1036
|
+
expectDone(outputUntilStep(undefined));
|
|
1037
|
+
expectDone(outputUntilStep(undefined));
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
describe("resetScheduleAfter", () => {
|
|
1042
|
+
it("requires a positive millis duration", () => {
|
|
1043
|
+
resetScheduleAfter("1ms");
|
|
1044
|
+
resetScheduleAfter(PositiveMillis.orThrow(1));
|
|
1045
|
+
// @ts-expect-error - Zero Millis is not a positive duration.
|
|
1046
|
+
resetScheduleAfter(minMillis);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it("resets a running schedule after inactivity", () => {
|
|
1050
|
+
const deps = createScheduleDeps();
|
|
1051
|
+
const step = resetScheduleAfter("1s")(exponential("100ms"))(deps);
|
|
1052
|
+
|
|
1053
|
+
expectOk(step(undefined), [100, 100]);
|
|
1054
|
+
deps.time.advance("100ms");
|
|
1055
|
+
expectOk(step(undefined), [200, 200]);
|
|
1056
|
+
deps.time.advance("1s");
|
|
1057
|
+
expectOk(step(undefined), [100, 100]);
|
|
1058
|
+
deps.time.advance("100ms");
|
|
1059
|
+
expectOk(step(undefined), [200, 200]);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it("keeps terminal done after inactivity", () => {
|
|
1063
|
+
const deps = createScheduleDeps();
|
|
1064
|
+
const step = resetScheduleAfter("1s")(take(1)(spaced("100ms")))(deps);
|
|
1065
|
+
|
|
1066
|
+
expectOk(step(undefined), [100, 100]);
|
|
1067
|
+
expectDone(step(undefined));
|
|
1068
|
+
deps.time.advance("2s");
|
|
1069
|
+
expectDone(step(undefined));
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
describe("mapSchedule", () => {
|
|
1074
|
+
it("transforms output", () => {
|
|
1075
|
+
const deps = createScheduleDeps();
|
|
1076
|
+
const schedule = mapSchedule((delay: Millis) => ({
|
|
1077
|
+
delay,
|
|
1078
|
+
doubled: delay * 2,
|
|
1079
|
+
}))(exponential("100ms"));
|
|
1080
|
+
const step = schedule(deps);
|
|
1081
|
+
|
|
1082
|
+
const result1 = step(undefined);
|
|
1083
|
+
assertOk(result1);
|
|
1084
|
+
assertEqual(result1.ok ? result1.value[0] : null, {
|
|
1085
|
+
delay: 100,
|
|
1086
|
+
doubled: 200,
|
|
1087
|
+
});
|
|
1088
|
+
// delay unchanged
|
|
1089
|
+
assertEqual(result1.ok ? result1.value[1] : null, 100);
|
|
1090
|
+
|
|
1091
|
+
const result2 = step(undefined);
|
|
1092
|
+
assertOk(result2);
|
|
1093
|
+
assertEqual(result2.ok ? result2.value[0] : null, {
|
|
1094
|
+
delay: 200,
|
|
1095
|
+
doubled: 400,
|
|
1096
|
+
});
|
|
1097
|
+
assertEqual(result2.ok ? result2.value[1] : null, 200);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it("passes through termination", () => {
|
|
1101
|
+
const deps = createScheduleDeps();
|
|
1102
|
+
const step = mapSchedule((n: number) => n + 1)(take(1)(forever))(deps);
|
|
1103
|
+
expectOk(step(undefined), [1, 0]);
|
|
1104
|
+
expectDone(step(undefined));
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
describe("passthrough", () => {
|
|
1109
|
+
it("as constructor outputs input directly", () => {
|
|
1110
|
+
const deps = createScheduleDeps();
|
|
1111
|
+
|
|
1112
|
+
interface MyError {
|
|
1113
|
+
readonly code: number;
|
|
1114
|
+
readonly message: string;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const step = passthrough<MyError>()(deps);
|
|
1118
|
+
const error1: MyError = { code: 500, message: "Internal error" };
|
|
1119
|
+
const error2: MyError = { code: 429, message: "Rate limited" };
|
|
1120
|
+
|
|
1121
|
+
const result1 = step(error1);
|
|
1122
|
+
expectOk(result1, [error1, 0]);
|
|
1123
|
+
|
|
1124
|
+
const result2 = step(error2);
|
|
1125
|
+
expectOk(result2, [error2, 0]);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it("as combinator preserves timing, replaces output", () => {
|
|
1129
|
+
const deps = createScheduleDeps();
|
|
1130
|
+
|
|
1131
|
+
interface MyError {
|
|
1132
|
+
readonly code: number;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const step = passthrough(exponential("100ms"))(deps);
|
|
1136
|
+
const error: MyError = { code: 500 };
|
|
1137
|
+
|
|
1138
|
+
const result1 = step(error);
|
|
1139
|
+
// output is input, delay from exponential
|
|
1140
|
+
expectOk(result1, [error, 100]);
|
|
1141
|
+
|
|
1142
|
+
const result2 = step({ code: 429 });
|
|
1143
|
+
// exponential growth
|
|
1144
|
+
expectOk(result2, [{ code: 429 }, 200]);
|
|
1145
|
+
|
|
1146
|
+
const result3 = step({ code: 503 });
|
|
1147
|
+
expectOk(result3, [{ code: 503 }, 400]);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
it("combinator respects schedule termination", () => {
|
|
1151
|
+
const deps = createScheduleDeps();
|
|
1152
|
+
const step = passthrough(take(2)(spaced("100ms")))(deps);
|
|
1153
|
+
|
|
1154
|
+
expectOk(step("first"), ["first", 100]);
|
|
1155
|
+
expectOk(step("second"), ["second", 100]);
|
|
1156
|
+
// take(2) exhausted
|
|
1157
|
+
expectDone(step("third"));
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
describe("foldSchedule", () => {
|
|
1162
|
+
it("accumulates state across iterations", () => {
|
|
1163
|
+
const deps = createScheduleDeps();
|
|
1164
|
+
|
|
1165
|
+
// Track cumulative delay
|
|
1166
|
+
const schedule = foldSchedule(
|
|
1167
|
+
0,
|
|
1168
|
+
(total: number, delay: Millis) => total + delay,
|
|
1169
|
+
)(take(4)(exponential("100ms")));
|
|
1170
|
+
const step = schedule(deps);
|
|
1171
|
+
|
|
1172
|
+
// 0 + 100
|
|
1173
|
+
expectOk(step(undefined), [100, 100]);
|
|
1174
|
+
// 100 + 200
|
|
1175
|
+
expectOk(step(undefined), [300, 200]);
|
|
1176
|
+
// 300 + 400
|
|
1177
|
+
expectOk(step(undefined), [700, 400]);
|
|
1178
|
+
// 700 + 800
|
|
1179
|
+
expectOk(step(undefined), [1500, 800]);
|
|
1180
|
+
expectDone(step(undefined));
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
describe("repetitions", () => {
|
|
1185
|
+
it("outputs count instead of original output", () => {
|
|
1186
|
+
const deps = createScheduleDeps();
|
|
1187
|
+
|
|
1188
|
+
const schedule = repetitions(exponential("100ms"));
|
|
1189
|
+
const step = schedule(deps);
|
|
1190
|
+
|
|
1191
|
+
expectOk(step(undefined), [0, 100]);
|
|
1192
|
+
expectOk(step(undefined), [1, 200]);
|
|
1193
|
+
expectOk(step(undefined), [2, 400]);
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
describe("delays", () => {
|
|
1198
|
+
it("outputs the delay instead of original output", () => {
|
|
1199
|
+
const deps = createScheduleDeps();
|
|
1200
|
+
|
|
1201
|
+
const schedule = delays(exponential("100ms"));
|
|
1202
|
+
const step = schedule(deps);
|
|
1203
|
+
|
|
1204
|
+
expectOk(step(undefined), [100, 100]);
|
|
1205
|
+
expectOk(step(undefined), [200, 200]);
|
|
1206
|
+
expectOk(step(undefined), [400, 400]);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it("passes through termination", () => {
|
|
1210
|
+
const deps = createScheduleDeps();
|
|
1211
|
+
const step = delays(take(1)(spaced("100ms")))(deps);
|
|
1212
|
+
expectOk(step(undefined), [100, 100]);
|
|
1213
|
+
expectDone(step(undefined));
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
describe("collectAllScheduleOutputs", () => {
|
|
1218
|
+
it("accumulates outputs into array", () => {
|
|
1219
|
+
const deps = createScheduleDeps();
|
|
1220
|
+
const schedule = collectAllScheduleOutputs(take(3)(spaced("100ms")));
|
|
1221
|
+
const step = schedule(deps);
|
|
1222
|
+
|
|
1223
|
+
expectOk(step(undefined), [[100], 100]);
|
|
1224
|
+
expectOk(step(undefined), [[100, 100], 100]);
|
|
1225
|
+
expectOk(step(undefined), [[100, 100, 100], 100]);
|
|
1226
|
+
expectDone(step(undefined));
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
it("returns a new snapshot without changing previous outputs", () => {
|
|
1230
|
+
const step = collectAllScheduleOutputs(take(2)(spaced("100ms")))(
|
|
1231
|
+
createScheduleDeps(),
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
const first = step(undefined);
|
|
1235
|
+
expectOk(first, [[100], 100]);
|
|
1236
|
+
expectOk(step(undefined), [[100, 100], 100]);
|
|
1237
|
+
expectOk(first, [[100], 100]);
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
describe("collectScheduleInputs", () => {
|
|
1242
|
+
it("accumulates inputs into array", () => {
|
|
1243
|
+
const deps = createScheduleDeps();
|
|
1244
|
+
const schedule = collectScheduleInputs(take(3)(spaced("100ms")));
|
|
1245
|
+
const step = schedule(deps);
|
|
1246
|
+
|
|
1247
|
+
expectOk(step("a"), [["a"], 100]);
|
|
1248
|
+
expectOk(step("b"), [["a", "b"], 100]);
|
|
1249
|
+
expectOk(step("c"), [["a", "b", "c"], 100]);
|
|
1250
|
+
expectDone(step("d"));
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
describe("collectWhileScheduleOutput", () => {
|
|
1255
|
+
it("collects outputs while predicate is true", () => {
|
|
1256
|
+
const deps = createScheduleDeps();
|
|
1257
|
+
const schedule = collectWhileScheduleOutput((delay: Millis) => delay < 400)(
|
|
1258
|
+
exponential("100ms"),
|
|
1259
|
+
);
|
|
1260
|
+
const step = schedule(deps);
|
|
1261
|
+
|
|
1262
|
+
expectOk(step(undefined), [[100], 100]);
|
|
1263
|
+
expectOk(step(undefined), [[100, 200], 200]);
|
|
1264
|
+
// 400 >= 400, predicate fails, stops
|
|
1265
|
+
expectDone(step(undefined));
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
describe("collectUntilScheduleOutput", () => {
|
|
1270
|
+
it("collects outputs until predicate becomes true", () => {
|
|
1271
|
+
const deps = createScheduleDeps();
|
|
1272
|
+
const schedule = collectUntilScheduleOutput(
|
|
1273
|
+
(delay: Millis) => delay >= 400,
|
|
1274
|
+
)(exponential("100ms"));
|
|
1275
|
+
const step = schedule(deps);
|
|
1276
|
+
|
|
1277
|
+
expectOk(step(undefined), [[100], 100]);
|
|
1278
|
+
expectOk(step(undefined), [[100, 200], 200]);
|
|
1279
|
+
// 400 >= 400, predicate true, stops
|
|
1280
|
+
expectDone(step(undefined));
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
describe("sequenceSchedules", () => {
|
|
1285
|
+
it("runs schedules in order", () => {
|
|
1286
|
+
const deps = createScheduleDeps();
|
|
1287
|
+
|
|
1288
|
+
const step = sequenceSchedules(
|
|
1289
|
+
take(2)(exponential("100ms")),
|
|
1290
|
+
take(3)(spaced("500ms")),
|
|
1291
|
+
)(deps);
|
|
1292
|
+
|
|
1293
|
+
// First schedule runs
|
|
1294
|
+
expectOk(step(undefined), [100, 100]);
|
|
1295
|
+
expectOk(step(undefined), [200, 200]);
|
|
1296
|
+
// First exhausted, second takes over
|
|
1297
|
+
expectOk(step(undefined), [500, 500]);
|
|
1298
|
+
expectOk(step(undefined), [500, 500]);
|
|
1299
|
+
expectOk(step(undefined), [500, 500]);
|
|
1300
|
+
// Second exhausted
|
|
1301
|
+
expectDone(step(undefined));
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it("handles three schedules", () => {
|
|
1305
|
+
const deps = createScheduleDeps();
|
|
1306
|
+
|
|
1307
|
+
const step = sequenceSchedules(
|
|
1308
|
+
take(1)(spaced("100ms")),
|
|
1309
|
+
take(1)(spaced("200ms")),
|
|
1310
|
+
take(1)(spaced("300ms")),
|
|
1311
|
+
)(deps);
|
|
1312
|
+
|
|
1313
|
+
expectOk(step(undefined), [100, 100]);
|
|
1314
|
+
expectOk(step(undefined), [200, 200]);
|
|
1315
|
+
expectOk(step(undefined), [300, 300]);
|
|
1316
|
+
expectDone(step(undefined));
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
it("stops immediately for empty list", () => {
|
|
1320
|
+
const deps = createScheduleDeps();
|
|
1321
|
+
const step = sequenceSchedules()(deps);
|
|
1322
|
+
expectDone(step(undefined));
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
describe("intersectSchedules", () => {
|
|
1327
|
+
it("continues while both want to continue", () => {
|
|
1328
|
+
const deps = createScheduleDeps();
|
|
1329
|
+
|
|
1330
|
+
const a = take(2)(spaced("100ms"));
|
|
1331
|
+
const b = take(4)(spaced("200ms"));
|
|
1332
|
+
const step = intersectSchedules(a, b)(deps);
|
|
1333
|
+
|
|
1334
|
+
// max(100, 200)
|
|
1335
|
+
expectOk(step(undefined), [[100, 200], 200]);
|
|
1336
|
+
// max(100, 200)
|
|
1337
|
+
expectOk(step(undefined), [[100, 200], 200]);
|
|
1338
|
+
// a stopped, so intersection stops
|
|
1339
|
+
expectDone(step(undefined));
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
it("does not step children after the intersection stops", () => {
|
|
1343
|
+
let aCalls = 0;
|
|
1344
|
+
let bCalls = 0;
|
|
1345
|
+
const step = intersectSchedules(
|
|
1346
|
+
tapScheduleInput(() => {
|
|
1347
|
+
aCalls++;
|
|
1348
|
+
})(take(1)(spaced("100ms"))),
|
|
1349
|
+
tapScheduleInput(() => {
|
|
1350
|
+
bCalls++;
|
|
1351
|
+
})(spaced("200ms")),
|
|
1352
|
+
)(createScheduleDeps());
|
|
1353
|
+
|
|
1354
|
+
expectOk(step(undefined), [[100, 200], 200]);
|
|
1355
|
+
expectDone(step(undefined));
|
|
1356
|
+
expectDone(step(undefined));
|
|
1357
|
+
assertEqual([aCalls, bCalls], [2, 2]);
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
describe("unionSchedules", () => {
|
|
1362
|
+
it("continues while either wants to continue", () => {
|
|
1363
|
+
const deps = createScheduleDeps();
|
|
1364
|
+
|
|
1365
|
+
const a = take(2)(spaced("100ms"));
|
|
1366
|
+
const b = take(4)(spaced("200ms"));
|
|
1367
|
+
const step = unionSchedules(a, b)(deps);
|
|
1368
|
+
|
|
1369
|
+
// min(100, 200)
|
|
1370
|
+
expectOk(step(undefined), [100, 100]);
|
|
1371
|
+
// min(100, 200)
|
|
1372
|
+
expectOk(step(undefined), [100, 100]);
|
|
1373
|
+
// a stopped, b continues
|
|
1374
|
+
expectOk(step(undefined), [200, 200]);
|
|
1375
|
+
expectOk(step(undefined), [200, 200]);
|
|
1376
|
+
// both stopped
|
|
1377
|
+
expectDone(step(undefined));
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
it("continues when second schedule stops first", () => {
|
|
1381
|
+
const deps = createScheduleDeps();
|
|
1382
|
+
|
|
1383
|
+
const a = take(2)(spaced("200ms"));
|
|
1384
|
+
const b = take(1)(spaced("100ms"));
|
|
1385
|
+
const step = unionSchedules(a, b)(deps);
|
|
1386
|
+
|
|
1387
|
+
expectOk(step(undefined), [100, 100]);
|
|
1388
|
+
expectOk(step(undefined), [200, 200]);
|
|
1389
|
+
expectDone(step(undefined));
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
it("returns output from the shorter delay", () => {
|
|
1393
|
+
const deps = createScheduleDeps();
|
|
1394
|
+
|
|
1395
|
+
const a = mapSchedule(() => "a")(spaced("200ms"));
|
|
1396
|
+
const b = mapSchedule(() => "b")(spaced("100ms"));
|
|
1397
|
+
const step = unionSchedules(a, b)(deps);
|
|
1398
|
+
|
|
1399
|
+
expectOk(step(undefined), ["b", 100]);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
it("does not step completed children", () => {
|
|
1403
|
+
let aCalls = 0;
|
|
1404
|
+
let bCalls = 0;
|
|
1405
|
+
const step = unionSchedules(
|
|
1406
|
+
tapScheduleInput(() => {
|
|
1407
|
+
aCalls++;
|
|
1408
|
+
})(take(1)(spaced("100ms"))),
|
|
1409
|
+
tapScheduleInput(() => {
|
|
1410
|
+
bCalls++;
|
|
1411
|
+
})(take(3)(spaced("200ms"))),
|
|
1412
|
+
)(createScheduleDeps());
|
|
1413
|
+
|
|
1414
|
+
expectOk(step(undefined), [100, 100]);
|
|
1415
|
+
expectOk(step(undefined), [200, 200]);
|
|
1416
|
+
expectOk(step(undefined), [200, 200]);
|
|
1417
|
+
assertEqual([aCalls, bCalls], [2, 3]);
|
|
1418
|
+
expectDone(step(undefined));
|
|
1419
|
+
expectDone(step(undefined));
|
|
1420
|
+
assertEqual([aCalls, bCalls], [2, 4]);
|
|
1421
|
+
});
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
describe("whenInput", () => {
|
|
1425
|
+
it("maintains independent branch state", () => {
|
|
1426
|
+
const step = whenInput(
|
|
1427
|
+
(useAlt: boolean) => useAlt,
|
|
1428
|
+
exponential("1s"),
|
|
1429
|
+
)(exponential("100ms"))(createScheduleDeps());
|
|
1430
|
+
|
|
1431
|
+
expectOk(step(false), [100, 100]);
|
|
1432
|
+
expectOk(step(true), [1000, 1000]);
|
|
1433
|
+
expectOk(step(false), [200, 200]);
|
|
1434
|
+
expectOk(step(true), [2000, 2000]);
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
it("shares an outer attempt limit across branches", () => {
|
|
1438
|
+
const step = take(3)(
|
|
1439
|
+
whenInput(
|
|
1440
|
+
(useAlt: boolean) => useAlt,
|
|
1441
|
+
exponential("1s"),
|
|
1442
|
+
)(exponential("100ms")),
|
|
1443
|
+
)(createScheduleDeps());
|
|
1444
|
+
|
|
1445
|
+
expectOk(step(false), [100, 100]);
|
|
1446
|
+
expectOk(step(true), [1000, 1000]);
|
|
1447
|
+
expectOk(step(false), [200, 200]);
|
|
1448
|
+
expectDone(step(true));
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
it("passes through termination from alt schedule", () => {
|
|
1452
|
+
const deps = createScheduleDeps();
|
|
1453
|
+
|
|
1454
|
+
const step = whenInput(() => true, take(1)(spaced("1s")))(spaced("100ms"))(
|
|
1455
|
+
deps,
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
expectOk(step(undefined), [1000, 1000]);
|
|
1459
|
+
expectDone(step(undefined));
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
it("keeps terminal done when the selected branch changes", () => {
|
|
1463
|
+
const step = whenInput(
|
|
1464
|
+
(useAlt: boolean) => useAlt,
|
|
1465
|
+
take(1)(spaced("1s")),
|
|
1466
|
+
)(exponential("100ms"))(createScheduleDeps());
|
|
1467
|
+
|
|
1468
|
+
expectOk(step(false), [100, 100]);
|
|
1469
|
+
expectOk(step(false), [200, 200]);
|
|
1470
|
+
expectOk(step(true), [1000, 1000]);
|
|
1471
|
+
expectDone(step(true));
|
|
1472
|
+
expectDone(step(false));
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
describe("tapScheduleOutput", () => {
|
|
1477
|
+
it("executes side effect without altering schedule", () => {
|
|
1478
|
+
const deps = createScheduleDeps();
|
|
1479
|
+
|
|
1480
|
+
const outputs: Array<Millis> = [];
|
|
1481
|
+
const step = tapScheduleOutput((delay: Millis) => {
|
|
1482
|
+
outputs.push(delay);
|
|
1483
|
+
})(take(3)(exponential("100ms")))(deps);
|
|
1484
|
+
|
|
1485
|
+
expectOk(step(undefined), [100, 100]);
|
|
1486
|
+
expectOk(step(undefined), [200, 200]);
|
|
1487
|
+
expectOk(step(undefined), [400, 400]);
|
|
1488
|
+
expectDone(step(undefined));
|
|
1489
|
+
|
|
1490
|
+
assertEqual(outputs, [100, 200, 400]);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
it("does not call effect when schedule stops", () => {
|
|
1494
|
+
const deps = createScheduleDeps();
|
|
1495
|
+
|
|
1496
|
+
const outputs: Array<number> = [];
|
|
1497
|
+
const step = tapScheduleOutput((n: number) => {
|
|
1498
|
+
outputs.push(n);
|
|
1499
|
+
})(take(2)(forever))(deps);
|
|
1500
|
+
|
|
1501
|
+
step(undefined);
|
|
1502
|
+
step(undefined);
|
|
1503
|
+
// Err(Done<void>)
|
|
1504
|
+
step(undefined);
|
|
1505
|
+
|
|
1506
|
+
assertEqual(outputs, [0, 1]);
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
describe("tapScheduleInput", () => {
|
|
1511
|
+
it("executes side effect on input without altering schedule", () => {
|
|
1512
|
+
const deps = createScheduleDeps();
|
|
1513
|
+
|
|
1514
|
+
const inputs: Array<string> = [];
|
|
1515
|
+
const step = tapScheduleInput((input: string) => {
|
|
1516
|
+
inputs.push(input);
|
|
1517
|
+
})(take(3)(spaced("100ms")))(deps);
|
|
1518
|
+
|
|
1519
|
+
expectOk(step("error1"), [100, 100]);
|
|
1520
|
+
expectOk(step("error2"), [100, 100]);
|
|
1521
|
+
expectOk(step("error3"), [100, 100]);
|
|
1522
|
+
expectDone(step("error4"));
|
|
1523
|
+
|
|
1524
|
+
assertEqual(inputs, ["error1", "error2", "error3", "error4"]);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
it("is called even when schedule stops", () => {
|
|
1528
|
+
const deps = createScheduleDeps();
|
|
1529
|
+
|
|
1530
|
+
const inputs: Array<string> = [];
|
|
1531
|
+
const step = tapScheduleInput((input: string) => {
|
|
1532
|
+
inputs.push(input);
|
|
1533
|
+
})(take(1)(forever))(deps);
|
|
1534
|
+
|
|
1535
|
+
step("first");
|
|
1536
|
+
// Err(Done<void>) but tap still runs
|
|
1537
|
+
step("second");
|
|
1538
|
+
|
|
1539
|
+
assertEqual(inputs, ["first", "second"]);
|
|
1540
|
+
});
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
describe("retryStrategyAws", () => {
|
|
1544
|
+
it("uses AWS 2.1 ordinary-failure timing", () => {
|
|
1545
|
+
// RandomNumber is below 1; millisecond rounding reaches the 50ms upper bound.
|
|
1546
|
+
const step = retryStrategyAws(
|
|
1547
|
+
createScheduleDepsWithRandom(0.999999999 as RandomNumber),
|
|
1548
|
+
);
|
|
1549
|
+
|
|
1550
|
+
expectOk(step(undefined), [50, 50]);
|
|
1551
|
+
expectOk(step(undefined), [100, 100]);
|
|
1552
|
+
expectDone(step(undefined));
|
|
1553
|
+
});
|
|
1554
|
+
});
|