@reventlessdev/reventless-local 3.0.0-alpha.285 → 3.0.0-alpha.287
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 +36 -0
- package/package.json +11 -11
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +11 -51
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -40
- package/src/adapter/Scheduler/LocalTimers.res +62 -0
- package/src/adapter/Scheduler/LocalTimers.res.mjs +63 -0
- package/src/components/AutomationSlice_Builder.res +31 -2
- package/src/components/AutomationSlice_Builder.res.mjs +31 -3
- package/src/components/LocalAutomationSchedules.res +59 -0
- package/src/components/LocalAutomationSchedules.res.mjs +72 -0
- package/src/components/LocalAutomationSweep.res +61 -0
- package/src/components/LocalAutomationSweep.res.mjs +83 -0
- package/src/test/TestRunner.res +2 -0
- package/src/test/TestRunner.res.mjs +12 -0
- package/tests/adapter/ScheduledPublisherTest.res +156 -155
- package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
- package/tests/components/automationslice/AutomationRescheduleFixtures.res +142 -0
- package/tests/components/automationslice/AutomationRescheduleFixtures.res.mjs +259 -0
- package/tests/components/automationslice/AutomationRescheduleTest.res +96 -0
- package/tests/components/automationslice/AutomationRescheduleTest.res.mjs +177 -0
- package/tests/components/automationslice/AutomationScheduleFixtures.res +118 -0
- package/tests/components/automationslice/AutomationScheduleFixtures.res.mjs +263 -0
- package/tests/components/automationslice/AutomationScheduleTest.res +88 -0
- package/tests/components/automationslice/AutomationScheduleTest.res.mjs +108 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res +6 -2
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +16 -4
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +6 -1
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +8 -2
- package/tests/components/automationslice/AutomationSweepFixtures.res +131 -0
- package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +223 -0
- package/tests/components/automationslice/AutomationSweepTest.res +107 -0
- package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +4 -1
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +14 -3
- 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 */
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// A digest per recipient on the local platform: the slice declares no time; a
|
|
2
|
+
// recipient's choice sets their schedule, unsubscribing clears it, and each tick
|
|
3
|
+
// sends that recipient's digest. The slice is made by `makeSlice`, so a test can fix
|
|
4
|
+
// the time first.
|
|
5
|
+
|
|
6
|
+
open TestFixtures
|
|
7
|
+
open Reventless
|
|
8
|
+
|
|
9
|
+
module PreferencesSource = {
|
|
10
|
+
module Id = Id.String
|
|
11
|
+
let name = "DigestPreferencesEventTopic"
|
|
12
|
+
@schema
|
|
13
|
+
type event =
|
|
14
|
+
| DigestTimeChosen({recipientId: string, time: Schedule.time})
|
|
15
|
+
| Unsubscribed({recipientId: string})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module DigestSpec = {
|
|
19
|
+
let name = "LocalDigest"
|
|
20
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
21
|
+
|
|
22
|
+
@schema
|
|
23
|
+
type todoItem = {recipientId: string}
|
|
24
|
+
|
|
25
|
+
@schema
|
|
26
|
+
type command = SendDigest({recipientId: @s.matches(DcbTag.string) string})
|
|
27
|
+
|
|
28
|
+
let maxRetries = 2
|
|
29
|
+
let heartbeatInterval = 60
|
|
30
|
+
let targetName = "SendDigest"
|
|
31
|
+
let schedule = None
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module FromClock = AutomationSlice.Mapping.Make(
|
|
35
|
+
Clock,
|
|
36
|
+
DigestSpec,
|
|
37
|
+
{
|
|
38
|
+
let collect = (Clock.Tick({key, at}), ~sourceId as _, _ctx) => [
|
|
39
|
+
(Clock.itemId(key, at), ({recipientId: key}: DigestSpec.todoItem)),
|
|
40
|
+
]
|
|
41
|
+
let resolve = _ => None
|
|
42
|
+
let reschedule = _ => []
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
module FromPreferences = AutomationSlice.Mapping.Make(
|
|
47
|
+
PreferencesSource,
|
|
48
|
+
DigestSpec,
|
|
49
|
+
{
|
|
50
|
+
let collect = (_, ~sourceId as _, _ctx) => []
|
|
51
|
+
let resolve = _ => None
|
|
52
|
+
let reschedule = event =>
|
|
53
|
+
switch event {
|
|
54
|
+
| PreferencesSource.DigestTimeChosen({recipientId, time}) => [
|
|
55
|
+
AutomationSlice.Set({key: recipientId, time}),
|
|
56
|
+
]
|
|
57
|
+
| Unsubscribed({recipientId}) => [Clear({key: recipientId})]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
module DigestAutomation: AutomationSlice.Automation with module Spec := DigestSpec = {
|
|
63
|
+
let process = (_id, item: DigestSpec.todoItem, ~now as _) => Some((
|
|
64
|
+
item.recipientId,
|
|
65
|
+
DigestSpec.SendDigest({recipientId: item.recipientId}),
|
|
66
|
+
))
|
|
67
|
+
let onExhausted = (_id, _item: DigestSpec.todoItem) => None
|
|
68
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
69
|
+
module M = AutomationSlice.Mappings.Make(DigestSpec)
|
|
70
|
+
module type Mapping = M.Mapping
|
|
71
|
+
let mappings: array<module(Mapping)> = [module(FromClock), module(FromPreferences)]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let preferencesResource: ReventlessInfra.Adapter.resource = ReventlessInfra.Adapter.make(
|
|
75
|
+
~name=PreferencesSource.name->Pulumi.Output.make,
|
|
76
|
+
~id=PreferencesSource.name->Pulumi.Output.make,
|
|
77
|
+
~urn=PreferencesSource.name->Pulumi.Output.make,
|
|
78
|
+
~service="memory:InMemory"->Pulumi.Output.make,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
module Bus = LocalBus.Make()
|
|
82
|
+
let _ = TestRunner.setup()
|
|
83
|
+
|
|
84
|
+
module AutomationSliceMaker = AutomationSlice_Builder.Make(Bus)
|
|
85
|
+
module Digest = AutomationSliceMaker.Make(DigestSpec, DigestAutomation)
|
|
86
|
+
|
|
87
|
+
let publishedCommands: ref<array<Reventless.Message.commandJson>> = ref([])
|
|
88
|
+
let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
|
|
89
|
+
publishedCommands := publishedCommands.contents->Array.concat(cmds)
|
|
90
|
+
|
|
91
|
+
let makeSlice = () =>
|
|
92
|
+
Digest.make(
|
|
93
|
+
~allEventTopics=Dict.fromArray([
|
|
94
|
+
(PreferencesSource.name, {ReventlessInfra.EventTopic.resources: [preferencesResource]}),
|
|
95
|
+
]),
|
|
96
|
+
~publishJsons=publishJsons->Pulumi.Output.make,
|
|
97
|
+
~context={
|
|
98
|
+
environment: "local",
|
|
99
|
+
platformName: "local",
|
|
100
|
+
pluginName: "TestPlugin",
|
|
101
|
+
sliceName: DigestSpec.name,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
/** An event as the log would hold it, recorded now as the fake Date has it. */
|
|
106
|
+
let eventJson = (event: PreferencesSource.event, ~recipientId) => {
|
|
107
|
+
let meta = {
|
|
108
|
+
...makeTestMeta(~service=PreferencesSource.name),
|
|
109
|
+
time: Date.now()->Date.fromTime->Date.toISOString,
|
|
110
|
+
}
|
|
111
|
+
(
|
|
112
|
+
meta,
|
|
113
|
+
JSON.Encode.object(
|
|
114
|
+
Dict.fromArray([
|
|
115
|
+
("id", recipientId->JSON.Encode.string),
|
|
116
|
+
("meta", meta->Reventless.Util_Sury.toJson(Message.metaSchema)),
|
|
117
|
+
("event", event->Reventless.Util_Sury.toJson(PreferencesSource.eventSchema)),
|
|
118
|
+
]),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let publish = async (event, ~recipientId) => {
|
|
124
|
+
let (meta, json) = eventJson(event, ~recipientId)
|
|
125
|
+
await Bus.publishEvent(PreferencesSource.name, "test", meta, json)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The keys and times the schedules store holds. */
|
|
129
|
+
let storedSchedules = () =>
|
|
130
|
+
Bus.getQueryDbScan(Digest.schedulesDbName->Option.getOr(""))
|
|
131
|
+
->Option.map(scan => scan())
|
|
132
|
+
->Option.getOr([])
|
|
133
|
+
->Array.map(row =>
|
|
134
|
+
row->Reventless.Util_Sury.fromJson(ReventlessCore.AutomationSlice_Schedules.rowSchema)
|
|
135
|
+
)
|
|
136
|
+
->Array.filterMap(row =>
|
|
137
|
+
row->ReventlessCore.AutomationSlice_Schedules.timeOf->Option.map(t => (row.key, t))
|
|
138
|
+
)
|
|
139
|
+
->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
|
|
140
|
+
|
|
141
|
+
/** The keys whose timers the local platform holds for the slice. */
|
|
142
|
+
let timers = () => LocalAutomationSchedules.platformFor(~slice=DigestSpec.name).names()
|