@reventlessdev/reventless-local 3.0.0-alpha.284 → 3.0.0-alpha.286
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/CHANGELOG.md +27 -0
- package/package.json +9 -9
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +43 -36
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +32 -25
- package/src/components/AutomationSlice_Builder.res +10 -2
- package/src/components/AutomationSlice_Builder.res.mjs +13 -2
- package/src/components/LocalAutomationSweep.res +61 -0
- package/src/components/LocalAutomationSweep.res.mjs +83 -0
- package/src/test/TestRunner.res +1 -0
- package/src/test/TestRunner.res.mjs +6 -0
- package/tests/adapter/ScheduledPublisherTest.res +156 -155
- package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
- package/tests/components/automationslice/AutomationSliceFixtures.res +2 -2
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +2 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +4 -1
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +1 -1
- package/tests/components/automationslice/AutomationSweepFixtures.res +129 -0
- package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +217 -0
- package/tests/components/automationslice/AutomationSweepTest.res +107 -0
- package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -1
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +1 -1
- package/tests/components/scheduler/SchedulerTest.res +5 -5
- package/tests/components/scheduler/SchedulerTest.res.mjs +22 -14
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Globals from "@jest/globals";
|
|
4
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
4
5
|
import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
|
|
5
6
|
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
7
|
+
import * as ScheduleTime$ReventlessCore from "@reventlessdev/reventless-core/src/util/ScheduleTime.res.mjs";
|
|
6
8
|
import * as LocalScheduledPublisher$ReventlessLocal from "../../src/adapter/Scheduler/LocalScheduledPublisher.res.mjs";
|
|
7
9
|
|
|
8
10
|
globalThis.beforeAll(() => {
|
|
@@ -30,174 +32,274 @@ function makeTopicResource(topicName) {
|
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
let hour = 60 * 60000;
|
|
36
|
+
|
|
37
|
+
let day = 24 * hour;
|
|
38
|
+
|
|
39
|
+
function at(iso) {
|
|
40
|
+
return new Date(iso).getTime();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function flush() {
|
|
44
|
+
await Promise.resolve();
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function advance(ms) {
|
|
49
|
+
Globals.jest.advanceTimersByTime(ms);
|
|
50
|
+
return await flush();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
54
|
+
|
|
55
|
+
let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
|
|
56
|
+
|
|
57
|
+
let topicCount = {
|
|
58
|
+
contents: 0
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function fired(topic) {
|
|
62
|
+
let count = {
|
|
63
|
+
contents: 0
|
|
64
|
+
};
|
|
65
|
+
TestBus.subscribeToEvents(topic, async (param, param$1, param$2) => {
|
|
66
|
+
count.contents = count.contents + 1 | 0;
|
|
67
|
+
});
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function schedule(now, time) {
|
|
72
|
+
Globals.jest.setSystemTime(new Date(now).getTime());
|
|
73
|
+
topicCount.contents = topicCount.contents + 1 | 0;
|
|
74
|
+
let topic = `sched-topic-` + topicCount.contents.toString();
|
|
75
|
+
let count = fired(topic);
|
|
76
|
+
let ops = await TestRunner$ReventlessLocal.resolve(SP.make("scheduler", {}).operations);
|
|
77
|
+
await ops.createSchedule([makeTopicResource(topic)], {
|
|
78
|
+
name: topic,
|
|
79
|
+
time: time,
|
|
80
|
+
payload: "{}"
|
|
81
|
+
});
|
|
82
|
+
return [
|
|
83
|
+
ops,
|
|
84
|
+
topic,
|
|
85
|
+
count
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
33
89
|
globalThis.describe("LocalScheduledPublisher", () => {
|
|
34
|
-
globalThis.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
90
|
+
globalThis.afterEach(() => SP.reset());
|
|
91
|
+
globalThis.describe("Every", () => {
|
|
92
|
+
globalThis.test("fires one interval after it is set, and again each interval", async () => {
|
|
93
|
+
let match = await schedule("2026-10-16T10:00:00Z", {
|
|
94
|
+
TAG: "Every",
|
|
95
|
+
_0: {
|
|
96
|
+
TAG: "Minutes",
|
|
97
|
+
_0: 3
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
let count = match[2];
|
|
101
|
+
await advance(3 * 60000 - 1);
|
|
102
|
+
globalThis.expect(count.contents).toBe(0);
|
|
103
|
+
await advance(1);
|
|
104
|
+
globalThis.expect(count.contents).toBe(1);
|
|
105
|
+
await advance(3 * 60000);
|
|
106
|
+
globalThis.expect(count.contents).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
globalThis.test("Hours(n) fires after n hours", async () => {
|
|
109
|
+
let match = await schedule("2026-10-16T10:00:00Z", {
|
|
110
|
+
TAG: "Every",
|
|
111
|
+
_0: {
|
|
112
|
+
TAG: "Hours",
|
|
113
|
+
_0: 2
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
await advance(2 * hour);
|
|
117
|
+
globalThis.expect(match[2].contents).toBe(1);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
globalThis.describe("Calendar", () => {
|
|
121
|
+
globalThis.test("fires at the wall-clock time each day, not 24 hours after it is set", async () => {
|
|
122
|
+
let match = await schedule("2026-10-16T05:58:00Z", {
|
|
123
|
+
TAG: "Calendar",
|
|
124
|
+
calendar: {
|
|
125
|
+
TAG: "Daily",
|
|
126
|
+
hour: 6,
|
|
127
|
+
minute: 0
|
|
128
|
+
}
|
|
43
129
|
});
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
130
|
+
let count = match[2];
|
|
131
|
+
await advance(2 * 60000);
|
|
132
|
+
globalThis.expect(count.contents).toBe(1);
|
|
133
|
+
await advance(day);
|
|
134
|
+
globalThis.expect(count.contents).toBe(2);
|
|
135
|
+
});
|
|
136
|
+
globalThis.test("skips a day whose time falls in the hour daylight saving skips", async () => {
|
|
137
|
+
let match = await schedule("2027-03-27T12:00:00Z", {
|
|
138
|
+
TAG: "Calendar",
|
|
139
|
+
calendar: {
|
|
140
|
+
TAG: "Daily",
|
|
141
|
+
hour: 2,
|
|
142
|
+
minute: 30
|
|
55
143
|
},
|
|
56
|
-
|
|
144
|
+
zone: "Europe/Berlin"
|
|
145
|
+
});
|
|
146
|
+
let count = match[2];
|
|
147
|
+
await advance(new Date("2027-03-29T12:00:00Z").getTime() - new Date("2027-03-27T12:00:00Z").getTime());
|
|
148
|
+
globalThis.expect(count.contents).toBe(1);
|
|
149
|
+
await advance(new Date("2027-03-30T12:00:00Z").getTime() - new Date("2027-03-29T12:00:00Z").getTime());
|
|
150
|
+
globalThis.expect(count.contents).toBe(2);
|
|
151
|
+
});
|
|
152
|
+
globalThis.test("reaches a firing further away than a timer can wait", async () => {
|
|
153
|
+
let match = await schedule("2026-10-16T10:00:00Z", {
|
|
154
|
+
TAG: "Calendar",
|
|
155
|
+
calendar: {
|
|
156
|
+
TAG: "Yearly",
|
|
157
|
+
month: 1,
|
|
158
|
+
day: 1,
|
|
159
|
+
hour: 6,
|
|
160
|
+
minute: 0
|
|
161
|
+
}
|
|
57
162
|
});
|
|
58
|
-
|
|
59
|
-
await
|
|
60
|
-
|
|
163
|
+
let count = match[2];
|
|
164
|
+
await advance(new Date("2027-01-01T05:59:00Z").getTime() - new Date("2026-10-16T10:00:00Z").getTime());
|
|
165
|
+
globalThis.expect(count.contents).toBe(0);
|
|
166
|
+
await advance(60000);
|
|
167
|
+
globalThis.expect(count.contents).toBe(1);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
globalThis.describe("Once", () => {
|
|
171
|
+
globalThis.test("fires once, a period after it is set", async () => {
|
|
172
|
+
let match = await schedule("2026-10-16T10:00:00Z", {
|
|
173
|
+
TAG: "Once",
|
|
174
|
+
moment: {
|
|
175
|
+
TAG: "After",
|
|
176
|
+
_0: {
|
|
177
|
+
TAG: "Minutes",
|
|
178
|
+
_0: 10
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
let count = match[2];
|
|
183
|
+
await advance(10 * 60000);
|
|
184
|
+
globalThis.expect(count.contents).toBe(1);
|
|
185
|
+
await advance(day);
|
|
61
186
|
globalThis.expect(count.contents).toBe(1);
|
|
62
|
-
return SP.reset();
|
|
63
187
|
});
|
|
64
|
-
globalThis.test("
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
188
|
+
globalThis.test("never fires at a moment already past", async () => {
|
|
189
|
+
let match = await schedule("2026-10-16T10:00:00Z", {
|
|
190
|
+
TAG: "Once",
|
|
191
|
+
moment: {
|
|
192
|
+
TAG: "At",
|
|
193
|
+
_0: {
|
|
194
|
+
year: 2024,
|
|
195
|
+
month: 1,
|
|
196
|
+
day: 1,
|
|
197
|
+
hour: 0,
|
|
198
|
+
minute: 0
|
|
199
|
+
}
|
|
200
|
+
}
|
|
72
201
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
202
|
+
await advance(day);
|
|
203
|
+
globalThis.expect(match[2].contents).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
globalThis.describe("createSchedule", () => {
|
|
207
|
+
globalThis.test("replaces a schedule of the same name", async () => {
|
|
208
|
+
let match = await schedule("2026-10-16T10:00:00Z", {
|
|
209
|
+
TAG: "Every",
|
|
210
|
+
_0: {
|
|
78
211
|
TAG: "Minutes",
|
|
79
212
|
_0: 1
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
let topic = match[1];
|
|
216
|
+
await match[0].createSchedule([makeTopicResource(topic)], {
|
|
217
|
+
name: topic,
|
|
218
|
+
time: {
|
|
219
|
+
TAG: "Every",
|
|
220
|
+
_0: {
|
|
221
|
+
TAG: "Minutes",
|
|
222
|
+
_0: 5
|
|
223
|
+
}
|
|
80
224
|
},
|
|
81
225
|
payload: "{}"
|
|
82
226
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
await
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
227
|
+
await advance(5 * 60000);
|
|
228
|
+
globalThis.expect(match[2].contents).toBe(1);
|
|
229
|
+
});
|
|
230
|
+
globalThis.test("refuses a time that cannot fire", async () => {
|
|
231
|
+
let ops = await TestRunner$ReventlessLocal.resolve(SP.make("scheduler", {}).operations);
|
|
232
|
+
let refused;
|
|
233
|
+
try {
|
|
234
|
+
await ops.createSchedule([makeTopicResource("refused")], {
|
|
235
|
+
name: "refused",
|
|
236
|
+
time: {
|
|
237
|
+
TAG: "Calendar",
|
|
238
|
+
calendar: {
|
|
239
|
+
TAG: "Daily",
|
|
240
|
+
hour: 25,
|
|
241
|
+
minute: 0
|
|
242
|
+
},
|
|
243
|
+
zone: "Mars/Base"
|
|
244
|
+
},
|
|
245
|
+
payload: "{}"
|
|
246
|
+
});
|
|
247
|
+
refused = false;
|
|
248
|
+
} catch (raw_exn) {
|
|
249
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
250
|
+
if (exn.RE_EXN_ID === ScheduleTime$ReventlessCore.InvalidTime) {
|
|
251
|
+
refused = true;
|
|
252
|
+
} else {
|
|
253
|
+
throw exn;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
globalThis.expect(refused).toBe(true);
|
|
91
257
|
});
|
|
92
258
|
});
|
|
93
259
|
globalThis.describe("deleteSchedule", () => {
|
|
94
|
-
globalThis.test("
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
contents: 0
|
|
99
|
-
};
|
|
100
|
-
TestBus.subscribeToEvents("del-topic", async (param, param$1, param$2) => {
|
|
101
|
-
count.contents = count.contents + 1 | 0;
|
|
102
|
-
});
|
|
103
|
-
let pub = SP.make("scheduler3", {});
|
|
104
|
-
let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
|
|
105
|
-
await ops.createSchedule([makeTopicResource("del-topic")], {
|
|
106
|
-
name: "to-delete",
|
|
107
|
-
rate: {
|
|
260
|
+
globalThis.test("stops a schedule; advancing timers does not fire it", async () => {
|
|
261
|
+
let match = await schedule("2026-10-16T10:00:00Z", {
|
|
262
|
+
TAG: "Every",
|
|
263
|
+
_0: {
|
|
108
264
|
TAG: "Minutes",
|
|
109
265
|
_0: 1
|
|
110
|
-
}
|
|
111
|
-
payload: "{}"
|
|
266
|
+
}
|
|
112
267
|
});
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
globalThis.expect(
|
|
116
|
-
return SP.reset();
|
|
268
|
+
await match[0].deleteSchedule([], match[1]);
|
|
269
|
+
await advance(60000);
|
|
270
|
+
globalThis.expect(match[2].contents).toBe(0);
|
|
117
271
|
});
|
|
118
272
|
});
|
|
119
273
|
globalThis.describe("reset", () => {
|
|
120
274
|
globalThis.test("clears all active timers; subsequent advance does not fire", async () => {
|
|
121
|
-
let
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
contents: 0
|
|
125
|
-
};
|
|
126
|
-
TestBus.subscribeToEvents("reset-topic", async (param, param$1, param$2) => {
|
|
127
|
-
count.contents = count.contents + 1 | 0;
|
|
128
|
-
});
|
|
129
|
-
let pub = SP.make("scheduler4", {});
|
|
130
|
-
let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
|
|
131
|
-
await ops.createSchedule([makeTopicResource("reset-topic")], {
|
|
132
|
-
name: "reset-sched",
|
|
133
|
-
rate: {
|
|
275
|
+
let match = await schedule("2026-10-16T10:00:00Z", {
|
|
276
|
+
TAG: "Every",
|
|
277
|
+
_0: {
|
|
134
278
|
TAG: "Minutes",
|
|
135
279
|
_0: 5
|
|
136
|
-
}
|
|
137
|
-
payload: "{}"
|
|
280
|
+
}
|
|
138
281
|
});
|
|
139
282
|
SP.reset();
|
|
140
|
-
|
|
141
|
-
globalThis.expect(
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
globalThis.describe("rateToMs", () => {
|
|
145
|
-
globalThis.test("Minutes(n) fires after n minutes", async () => {
|
|
146
|
-
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
147
|
-
let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
|
|
148
|
-
let count = {
|
|
149
|
-
contents: 0
|
|
150
|
-
};
|
|
151
|
-
TestBus.subscribeToEvents("rate-topic", async (param, param$1, param$2) => {
|
|
152
|
-
count.contents = count.contents + 1 | 0;
|
|
153
|
-
});
|
|
154
|
-
let pub = SP.make("rate-scheduler", {});
|
|
155
|
-
let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
|
|
156
|
-
await ops.createSchedule([makeTopicResource("rate-topic")], {
|
|
157
|
-
name: "rate-sched",
|
|
158
|
-
rate: {
|
|
159
|
-
TAG: "Minutes",
|
|
160
|
-
_0: 3
|
|
161
|
-
},
|
|
162
|
-
payload: "{}"
|
|
163
|
-
});
|
|
164
|
-
Globals.jest.advanceTimersByTime(179999);
|
|
165
|
-
globalThis.expect(count.contents).toBe(0);
|
|
166
|
-
Globals.jest.advanceTimersByTime(1);
|
|
167
|
-
await Promise.resolve();
|
|
168
|
-
await Promise.resolve();
|
|
169
|
-
globalThis.expect(count.contents).toBe(1);
|
|
170
|
-
return SP.reset();
|
|
171
|
-
});
|
|
172
|
-
globalThis.test("Hours(n) fires after n hours", async () => {
|
|
173
|
-
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
174
|
-
let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
|
|
175
|
-
let count = {
|
|
176
|
-
contents: 0
|
|
177
|
-
};
|
|
178
|
-
TestBus.subscribeToEvents("hours-topic", async (param, param$1, param$2) => {
|
|
179
|
-
count.contents = count.contents + 1 | 0;
|
|
180
|
-
});
|
|
181
|
-
let pub = SP.make("hours-scheduler", {});
|
|
182
|
-
let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
|
|
183
|
-
await ops.createSchedule([makeTopicResource("hours-topic")], {
|
|
184
|
-
name: "hours-sched",
|
|
185
|
-
rate: {
|
|
186
|
-
TAG: "Hours",
|
|
187
|
-
_0: 2
|
|
188
|
-
},
|
|
189
|
-
payload: "{}"
|
|
190
|
-
});
|
|
191
|
-
Globals.jest.advanceTimersByTime(7200000);
|
|
192
|
-
await Promise.resolve();
|
|
193
|
-
await Promise.resolve();
|
|
194
|
-
globalThis.expect(count.contents).toBe(1);
|
|
195
|
-
return SP.reset();
|
|
283
|
+
await advance(5 * 60000);
|
|
284
|
+
globalThis.expect(match[2].contents).toBe(0);
|
|
196
285
|
});
|
|
197
286
|
});
|
|
198
287
|
});
|
|
199
288
|
|
|
289
|
+
let minute = 60000;
|
|
290
|
+
|
|
200
291
|
export {
|
|
201
292
|
makeTopicResource,
|
|
293
|
+
minute,
|
|
294
|
+
hour,
|
|
295
|
+
day,
|
|
296
|
+
at,
|
|
297
|
+
flush,
|
|
298
|
+
advance,
|
|
299
|
+
TestBus,
|
|
300
|
+
SP,
|
|
301
|
+
topicCount,
|
|
302
|
+
fired,
|
|
303
|
+
schedule,
|
|
202
304
|
}
|
|
203
305
|
/* Not a pure module */
|
|
@@ -106,7 +106,7 @@ module SkipProcessMapping = Reventless.AutomationSlice.Mapping.Make(
|
|
|
106
106
|
|
|
107
107
|
module ShipOrderAutomation: Reventless.AutomationSlice.Automation
|
|
108
108
|
with module Spec := ShipOrderSpec = {
|
|
109
|
-
let process = (id, _item: ShipOrderSpec.todoItem) => Some((
|
|
109
|
+
let process = (id, _item: ShipOrderSpec.todoItem, ~now as _) => Some((
|
|
110
110
|
id,
|
|
111
111
|
ShipOrderSpec.CreateShipment({orderId: id}),
|
|
112
112
|
))
|
|
@@ -119,7 +119,7 @@ module ShipOrderAutomation: Reventless.AutomationSlice.Automation
|
|
|
119
119
|
|
|
120
120
|
module SkipProcessAutomation: Reventless.AutomationSlice.Automation
|
|
121
121
|
with module Spec := SkipProcessSpec = {
|
|
122
|
-
let process = (_id, _item: SkipProcessSpec.todoItem) => None
|
|
122
|
+
let process = (_id, _item: SkipProcessSpec.todoItem, ~now as _) => None
|
|
123
123
|
let onExhausted = (_id, _item: SkipProcessSpec.todoItem) => None
|
|
124
124
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
125
125
|
module M = Reventless.AutomationSlice.Mappings.Make(SkipProcessSpec)
|
|
@@ -174,7 +174,7 @@ let SkipProcessMapping = AutomationSlice$Reventless.Mapping.Make({
|
|
|
174
174
|
resolve: resolve$1
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
function process(id, _item) {
|
|
177
|
+
function process(id, _item, param) {
|
|
178
178
|
return [
|
|
179
179
|
id,
|
|
180
180
|
{
|
|
@@ -201,7 +201,7 @@ let ShipOrderAutomation = {
|
|
|
201
201
|
mappings: mappings
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
-
function process$1(_id, _item) {
|
|
204
|
+
function process$1(_id, _item, param) {
|
|
205
205
|
|
|
206
206
|
}
|
|
207
207
|
|
|
@@ -156,7 +156,10 @@ module FromDcb = AutomationSlice.Mapping.Make(
|
|
|
156
156
|
)
|
|
157
157
|
|
|
158
158
|
module AutoShipAutomation: AutomationSlice.Automation with module Spec := AutoShipSpec = {
|
|
159
|
-
let process = (id, _item: AutoShipSpec.todoItem) => Some((
|
|
159
|
+
let process = (id, _item: AutoShipSpec.todoItem, ~now as _) => Some((
|
|
160
|
+
id,
|
|
161
|
+
AutoShipSpec.Ship({orderId: id}),
|
|
162
|
+
))
|
|
160
163
|
let onExhausted = (_id, _item: AutoShipSpec.todoItem) => None
|
|
161
164
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
162
165
|
module M = AutomationSlice.Mappings.Make(AutoShipSpec)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// A deadline: `collect` counts when an order is due from the time its event was
|
|
2
|
+
// recorded, and `process` sends nothing until `~now` reaches it. Only a sweep
|
|
3
|
+
// moves the item once its time has come, since no further event arrives. The
|
|
4
|
+
// tests fix the time with Jest's fake Date.
|
|
5
|
+
|
|
6
|
+
open TestFixtures
|
|
7
|
+
open Reventless
|
|
8
|
+
|
|
9
|
+
module OrderSource = {
|
|
10
|
+
module Id = Id.String
|
|
11
|
+
let name = "DeadlineOrderEventTopic"
|
|
12
|
+
@schema
|
|
13
|
+
type event = OrderPlaced({orderId: string, within: float})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module CancelUnshippedSpec = {
|
|
17
|
+
let name = "CancelUnshipped"
|
|
18
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
19
|
+
|
|
20
|
+
@schema
|
|
21
|
+
type todoItem = {orderId: string, dueAt: float}
|
|
22
|
+
|
|
23
|
+
@schema
|
|
24
|
+
type command = CancelOrder({orderId: @s.matches(DcbTag.string) string})
|
|
25
|
+
|
|
26
|
+
let maxRetries = 2
|
|
27
|
+
let heartbeatInterval = 60
|
|
28
|
+
let targetName = "CancelOrder"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module FromOrders = AutomationSlice.Mapping.Make(
|
|
32
|
+
OrderSource,
|
|
33
|
+
CancelUnshippedSpec,
|
|
34
|
+
{
|
|
35
|
+
let collect = (event: OrderSource.event, ~sourceId as _, ctx: AutomationSlice.context) =>
|
|
36
|
+
switch (event, ctx.recordedAt) {
|
|
37
|
+
| (OrderPlaced({orderId, within}), Some(placedAt)) => [
|
|
38
|
+
(orderId, ({orderId, dueAt: placedAt +. within}: CancelUnshippedSpec.todoItem)),
|
|
39
|
+
]
|
|
40
|
+
| (OrderPlaced(_), None) => []
|
|
41
|
+
}
|
|
42
|
+
let resolve = (_event: OrderSource.event) => None
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
module CancelUnshippedAutomation: AutomationSlice.Automation
|
|
47
|
+
with module Spec := CancelUnshippedSpec = {
|
|
48
|
+
let process = (id, item: CancelUnshippedSpec.todoItem, ~now) =>
|
|
49
|
+
now >= item.dueAt ? Some((id, CancelUnshippedSpec.CancelOrder({orderId: item.orderId}))) : None
|
|
50
|
+
let onExhausted = (_id, _item: CancelUnshippedSpec.todoItem) => None
|
|
51
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
52
|
+
module M = AutomationSlice.Mappings.Make(CancelUnshippedSpec)
|
|
53
|
+
module type Mapping = M.Mapping
|
|
54
|
+
let mappings: array<module(Mapping)> = [module(FromOrders)]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let orderResource: ReventlessInfra.Adapter.resource = ReventlessInfra.Adapter.make(
|
|
58
|
+
~name=OrderSource.name->Pulumi.Output.make,
|
|
59
|
+
~id=OrderSource.name->Pulumi.Output.make,
|
|
60
|
+
~urn=OrderSource.name->Pulumi.Output.make,
|
|
61
|
+
~service="memory:InMemory"->Pulumi.Output.make,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
let allEventTopics: ReventlessCore.EventTopic.allOutputs = Dict.fromArray([
|
|
65
|
+
(OrderSource.name, {ReventlessInfra.EventTopic.resources: [orderResource]}),
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
module Bus = LocalBus.Make()
|
|
69
|
+
let _ = TestRunner.setup()
|
|
70
|
+
|
|
71
|
+
module AutomationSliceMaker = AutomationSlice_Builder.Make(Bus)
|
|
72
|
+
module CancelUnshipped = AutomationSliceMaker.Make(CancelUnshippedSpec, CancelUnshippedAutomation)
|
|
73
|
+
|
|
74
|
+
// Commands are captured; `failPublish` makes the next publishes throw.
|
|
75
|
+
let publishedCommands: ref<array<Reventless.Message.commandJson>> = ref([])
|
|
76
|
+
let failPublish = ref(false)
|
|
77
|
+
let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
|
|
78
|
+
if failPublish.contents {
|
|
79
|
+
JsError.throwWithMessage("command topic unavailable")
|
|
80
|
+
} else {
|
|
81
|
+
publishedCommands := publishedCommands.contents->Array.concat(cmds)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let slice = CancelUnshipped.make(
|
|
85
|
+
~allEventTopics,
|
|
86
|
+
~publishJsons=publishJsons->Pulumi.Output.make,
|
|
87
|
+
~context={
|
|
88
|
+
environment: "local",
|
|
89
|
+
platformName: "local",
|
|
90
|
+
pluginName: "TestPlugin",
|
|
91
|
+
sliceName: CancelUnshippedSpec.name,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
/** Places an order now, as the fake Date has it, due `within` milliseconds later. */
|
|
96
|
+
let placeOrder = async (orderId, ~within) => {
|
|
97
|
+
let meta = {
|
|
98
|
+
...makeTestMeta(~service=OrderSource.name),
|
|
99
|
+
time: Date.now()->Date.fromTime->Date.toISOString,
|
|
100
|
+
}
|
|
101
|
+
let eventJson = JSON.Encode.object(
|
|
102
|
+
Dict.fromArray([
|
|
103
|
+
("id", orderId->JSON.Encode.string),
|
|
104
|
+
("meta", meta->Reventless.Util_Sury.toJson(Message.metaSchema)),
|
|
105
|
+
(
|
|
106
|
+
"event",
|
|
107
|
+
OrderSource.OrderPlaced({orderId, within})->Reventless.Util_Sury.toJson(
|
|
108
|
+
OrderSource.eventSchema,
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
]),
|
|
112
|
+
)
|
|
113
|
+
await Bus.publishEvent(OrderSource.name, "test", meta, eventJson)
|
|
114
|
+
// The event's own phase 2 runs detached; let it finish.
|
|
115
|
+
let _ = await Promise.resolve()
|
|
116
|
+
let _ = await Promise.resolve()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The stored to-do row of `id`, as the query store holds it. */
|
|
120
|
+
let storedRow = id =>
|
|
121
|
+
Bus.getQueryDbScan(CancelUnshipped.queryDbName)
|
|
122
|
+
->Option.map(scan => scan())
|
|
123
|
+
->Option.getOr([])
|
|
124
|
+
->Array.find(row =>
|
|
125
|
+
row->JSON.Decode.object->Option.flatMap(o => o->Dict.get("id")) == Some(id->JSON.Encode.string)
|
|
126
|
+
)
|
|
127
|
+
->Option.map(row =>
|
|
128
|
+
row->Reventless.Util_Sury.fromJson(ReventlessCore.AutomationSlice_Callback.todoRowSchema)
|
|
129
|
+
)
|