@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +11 -11
  3. package/src/adapter/Scheduler/LocalScheduledPublisher.res +11 -51
  4. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -40
  5. package/src/adapter/Scheduler/LocalTimers.res +62 -0
  6. package/src/adapter/Scheduler/LocalTimers.res.mjs +63 -0
  7. package/src/components/AutomationSlice_Builder.res +31 -2
  8. package/src/components/AutomationSlice_Builder.res.mjs +31 -3
  9. package/src/components/LocalAutomationSchedules.res +59 -0
  10. package/src/components/LocalAutomationSchedules.res.mjs +72 -0
  11. package/src/components/LocalAutomationSweep.res +61 -0
  12. package/src/components/LocalAutomationSweep.res.mjs +83 -0
  13. package/src/test/TestRunner.res +2 -0
  14. package/src/test/TestRunner.res.mjs +12 -0
  15. package/tests/adapter/ScheduledPublisherTest.res +156 -155
  16. package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
  17. package/tests/components/automationslice/AutomationRescheduleFixtures.res +142 -0
  18. package/tests/components/automationslice/AutomationRescheduleFixtures.res.mjs +259 -0
  19. package/tests/components/automationslice/AutomationRescheduleTest.res +96 -0
  20. package/tests/components/automationslice/AutomationRescheduleTest.res.mjs +177 -0
  21. package/tests/components/automationslice/AutomationScheduleFixtures.res +118 -0
  22. package/tests/components/automationslice/AutomationScheduleFixtures.res.mjs +263 -0
  23. package/tests/components/automationslice/AutomationScheduleTest.res +88 -0
  24. package/tests/components/automationslice/AutomationScheduleTest.res.mjs +108 -0
  25. package/tests/components/automationslice/AutomationSliceFixtures.res +6 -2
  26. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +16 -4
  27. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +6 -1
  28. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +8 -2
  29. package/tests/components/automationslice/AutomationSweepFixtures.res +131 -0
  30. package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +223 -0
  31. package/tests/components/automationslice/AutomationSweepTest.res +107 -0
  32. package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
  33. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +4 -1
  34. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +14 -3
  35. package/tests/components/scheduler/SchedulerTest.res +5 -5
  36. package/tests/components/scheduler/SchedulerTest.res.mjs +22 -14
@@ -0,0 +1,259 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Pulumi from "@pulumi/pulumi";
8
+ import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
9
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
10
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
11
+ import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
12
+ import * as Schedule$Reventless from "@reventlessdev/reventless-spec/src/types/Schedule.res.mjs";
13
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
14
+ import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
15
+ import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
16
+ import * as AutomationSlice$Reventless from "@reventlessdev/reventless-spec/src/components/AutomationSlice.res.mjs";
17
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
18
+ import * as TestFixtures$ReventlessLocal from "../../TestFixtures.res.mjs";
19
+ import * as AutomationSlice_Builder$ReventlessLocal from "../../../src/components/AutomationSlice_Builder.res.mjs";
20
+ import * as AutomationSlice_Schedules$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs";
21
+ import * as LocalAutomationSchedules$ReventlessLocal from "../../../src/components/LocalAutomationSchedules.res.mjs";
22
+
23
+ let name = "DigestPreferencesEventTopic";
24
+
25
+ let eventSchema = Sury.union([
26
+ Sury.$schema(s => ({
27
+ TAG: "DigestTimeChosen",
28
+ recipientId: s.m(Sury.string),
29
+ time: s.m(Schedule$Reventless.timeSchema)
30
+ })),
31
+ Sury.$schema(s => ({
32
+ TAG: "Unsubscribed",
33
+ recipientId: s.m(Sury.string)
34
+ }))
35
+ ]);
36
+
37
+ let PreferencesSource = {
38
+ Id: undefined,
39
+ name: name,
40
+ eventSchema: eventSchema
41
+ };
42
+
43
+ let name$1 = "LocalDigest";
44
+
45
+ let moduleUrl = import.meta.url;
46
+
47
+ let todoItemSchema = Sury.$schema(s => ({
48
+ recipientId: s.m(Sury.string)
49
+ }));
50
+
51
+ let commandSchema = Sury.$schema(s => ({
52
+ TAG: "SendDigest",
53
+ recipientId: s.m(DcbTag$Reventless.string)
54
+ }));
55
+
56
+ function authorizationOf(param) {
57
+ return "AllowAuthenticated";
58
+ }
59
+
60
+ function commandTransition(param) {
61
+ return "Undeclared";
62
+ }
63
+
64
+ let traits = [];
65
+
66
+ let DigestSpec = {
67
+ name: name$1,
68
+ moduleUrl: moduleUrl,
69
+ todoItemSchema: todoItemSchema,
70
+ commandSchema: commandSchema,
71
+ maxRetries: 2,
72
+ heartbeatInterval: 60,
73
+ targetName: "SendDigest",
74
+ schedule: undefined,
75
+ authorizationOf: authorizationOf,
76
+ commandTransition: commandTransition,
77
+ traits: traits
78
+ };
79
+
80
+ function collect(param, param$1, _ctx) {
81
+ let key = param.key;
82
+ return [[
83
+ Clock$Reventless.itemId(key, param.at),
84
+ {
85
+ recipientId: key
86
+ }
87
+ ]];
88
+ }
89
+
90
+ function resolve(param) {
91
+
92
+ }
93
+
94
+ function reschedule(param) {
95
+ return [];
96
+ }
97
+
98
+ let FromClock = AutomationSlice$Reventless.Mapping.Make({
99
+ Id: Id$Reventless.$$String,
100
+ name: Clock$Reventless.name,
101
+ eventSchema: Clock$Reventless.eventSchema
102
+ })(DigestSpec)({
103
+ collect: collect,
104
+ resolve: resolve,
105
+ reschedule: reschedule
106
+ });
107
+
108
+ function collect$1(param, param$1, _ctx) {
109
+ return [];
110
+ }
111
+
112
+ function resolve$1(param) {
113
+
114
+ }
115
+
116
+ function reschedule$1(event) {
117
+ if (event.TAG === "DigestTimeChosen") {
118
+ return [{
119
+ TAG: "Set",
120
+ key: event.recipientId,
121
+ time: event.time
122
+ }];
123
+ } else {
124
+ return [{
125
+ TAG: "Clear",
126
+ key: event.recipientId
127
+ }];
128
+ }
129
+ }
130
+
131
+ let FromPreferences = AutomationSlice$Reventless.Mapping.Make({
132
+ Id: Id$Reventless.$$String,
133
+ name: name,
134
+ eventSchema: eventSchema
135
+ })(DigestSpec)({
136
+ collect: collect$1,
137
+ resolve: resolve$1,
138
+ reschedule: reschedule$1
139
+ });
140
+
141
+ function process(_id, item, param) {
142
+ return [
143
+ item.recipientId,
144
+ {
145
+ TAG: "SendDigest",
146
+ recipientId: item.recipientId
147
+ }
148
+ ];
149
+ }
150
+
151
+ function onExhausted(_id, _item) {
152
+
153
+ }
154
+
155
+ let moduleUrl$1 = import.meta.url;
156
+
157
+ AutomationSlice$Reventless.Mappings.Make(DigestSpec);
158
+
159
+ let mappings = [
160
+ FromClock,
161
+ FromPreferences
162
+ ];
163
+
164
+ let DigestAutomation = {
165
+ process: process,
166
+ onExhausted: onExhausted,
167
+ moduleUrl: moduleUrl$1,
168
+ mappings: mappings
169
+ };
170
+
171
+ let preferencesResource = Adapter$ReventlessInfra.make(Pulumi.output(name), Pulumi.output(name), Pulumi.output(name), Pulumi.output("memory:InMemory"), undefined, undefined, undefined, undefined, undefined, undefined);
172
+
173
+ let Bus = LocalBus$ReventlessLocal.Make({});
174
+
175
+ TestRunner$ReventlessLocal.setup();
176
+
177
+ let AutomationSliceMaker = AutomationSlice_Builder$ReventlessLocal.Make(Bus);
178
+
179
+ let Digest = AutomationSliceMaker.Make(DigestSpec)(DigestAutomation);
180
+
181
+ let publishedCommands = {
182
+ contents: []
183
+ };
184
+
185
+ async function publishJsons(cmds) {
186
+ publishedCommands.contents = publishedCommands.contents.concat(cmds);
187
+ }
188
+
189
+ function makeSlice() {
190
+ return Digest.make(Object.fromEntries([[
191
+ name,
192
+ {
193
+ resources: [preferencesResource]
194
+ }
195
+ ]]), Pulumi.output(publishJsons), {
196
+ environment: "local",
197
+ platformName: "local",
198
+ pluginName: "TestPlugin",
199
+ sliceName: name$1
200
+ }, undefined, undefined);
201
+ }
202
+
203
+ function eventJson(event, recipientId) {
204
+ let newrecord = {...TestFixtures$ReventlessLocal.makeTestMeta(name)};
205
+ newrecord.time = new Date(Date.now()).toISOString();
206
+ return [
207
+ newrecord,
208
+ Object.fromEntries([
209
+ [
210
+ "id",
211
+ recipientId
212
+ ],
213
+ [
214
+ "meta",
215
+ Util_Sury$Reventless.toJson(newrecord, Message$Reventless.metaSchema)
216
+ ],
217
+ [
218
+ "event",
219
+ Util_Sury$Reventless.toJson(event, eventSchema)
220
+ ]
221
+ ])
222
+ ];
223
+ }
224
+
225
+ async function publish(event, recipientId) {
226
+ let match = eventJson(event, recipientId);
227
+ return await Bus.publishEvent(name, "test", match[0], match[1]);
228
+ }
229
+
230
+ function storedSchedules() {
231
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.map(Bus.getQueryDbScan(Stdlib_Option.getOr(Digest.schedulesDbName, "")), scan => scan()), []).map(row => Util_Sury$Reventless.fromJson(row, AutomationSlice_Schedules$ReventlessCore.rowSchema)), row => Stdlib_Option.map(AutomationSlice_Schedules$ReventlessCore.timeOf(row), t => [
232
+ row.key,
233
+ t
234
+ ])).toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
235
+ }
236
+
237
+ function timers() {
238
+ return LocalAutomationSchedules$ReventlessLocal.platformFor(name$1).names();
239
+ }
240
+
241
+ export {
242
+ PreferencesSource,
243
+ DigestSpec,
244
+ FromClock,
245
+ FromPreferences,
246
+ DigestAutomation,
247
+ preferencesResource,
248
+ Bus,
249
+ AutomationSliceMaker,
250
+ Digest,
251
+ publishedCommands,
252
+ publishJsons,
253
+ makeSlice,
254
+ eventJson,
255
+ publish,
256
+ storedSchedules,
257
+ timers,
258
+ }
259
+ /* eventSchema Not a pure module */
@@ -0,0 +1,96 @@
1
+ // Schedules set by events, on the local platform: a recipient's choice is stored and
2
+ // starts their timer, the timer's tick sends their digest, unsubscribing removes
3
+ // both, and a rebuild from the events reproduces the rows and removes a stray timer.
4
+
5
+ open JestGlobals
6
+ open AutomationRescheduleFixtures
7
+
8
+ type jestObj
9
+ @module("@jest/globals") external jest: jestObj = "jest"
10
+ @send external useFakeTimers: jestObj => unit = "useFakeTimers"
11
+ @send external useRealTimers: jestObj => unit = "useRealTimers"
12
+ @send external setSystemTime: (jestObj, float) => unit = "setSystemTime"
13
+ @send
14
+ external advanceTimersByTimeAsync: (jestObj, float) => promise<unit> = "advanceTimersByTimeAsync"
15
+
16
+ let time = iso => Date.fromString(iso)->Date.getTime
17
+ let at18 = Reventless.Schedule.Calendar({calendar: Daily({hour: 18, minute: 0})})
18
+ let at19Berlin = Reventless.Schedule.Calendar({
19
+ calendar: Daily({hour: 19, minute: 0}),
20
+ zone: "Europe/Berlin",
21
+ })
22
+
23
+ let settle = async () =>
24
+ for _ in 1 to 20 {
25
+ await jest->advanceTimersByTimeAsync(0.)
26
+ }
27
+
28
+ let sorted = keys => keys->Array.toSorted(String.compare)
29
+
30
+ describe("schedules set by events, locally", () => {
31
+ let slice = ref(None)
32
+ let ops = async () =>
33
+ await slice.contents->Option.getOrThrow->ReventlessCore.Component.operations->TestRunner.resolve
34
+
35
+ let _ = beforeAllAsync(async () => {
36
+ jest->useFakeTimers
37
+ jest->setSystemTime(time("2026-10-16T12:00:00Z"))
38
+ slice := Some(makeSlice())
39
+ let _ = await ops()
40
+ await settle()
41
+ })
42
+ let _ = afterAll(() => {
43
+ LocalAutomationSchedules.reset()
44
+ jest->useRealTimers
45
+ })
46
+
47
+ testPromise("a choice is stored and starts the recipient's timer", async () => {
48
+ await publish(DigestTimeChosen({recipientId: "r1", time: at18}), ~recipientId="r1")
49
+ await publish(DigestTimeChosen({recipientId: "r2", time: at19Berlin}), ~recipientId="r2")
50
+ await settle()
51
+ expect((storedSchedules(), (await timers())->sorted))->toEqual((
52
+ [("r1", at18), ("r2", at19Berlin)],
53
+ ["r1", "r2"],
54
+ ))
55
+ })
56
+
57
+ testPromise("each recipient's tick comes at their own time and sends their digest", async () => {
58
+ // 18:00 UTC is r1's; 19:00 in Berlin is 17:00 UTC, r2's.
59
+ await jest->advanceTimersByTimeAsync(5. *. 3_600_000. +. 1_000.)
60
+ await settle()
61
+ expect(publishedCommands.contents->Array.map(c => c.id))->toEqual(["r2"])
62
+ await jest->advanceTimersByTimeAsync(3_600_000.)
63
+ await settle()
64
+ // Nothing answers r2's command here, so 18:00's pass, an hour on, sends it again.
65
+ expect(publishedCommands.contents->Array.map(c => c.id))->toEqual(["r2", "r2", "r1"])
66
+ })
67
+
68
+ testPromise("unsubscribing removes the recipient's row and timer, and only theirs", async () => {
69
+ await publish(Unsubscribed({recipientId: "r1"}), ~recipientId="r1")
70
+ await settle()
71
+ expect((storedSchedules(), await timers()))->toEqual(([("r2", at19Berlin)], ["r2"]))
72
+ })
73
+
74
+ testPromise("a later reconcile keeps the times events set", async () => {
75
+ let ops = await ops()
76
+ await ops.reconcileSchedules()
77
+ expect((storedSchedules(), await timers()))->toEqual(([("r2", at19Berlin)], ["r2"]))
78
+ })
79
+
80
+ testPromise(
81
+ "a rebuild reproduces the rows from the events and removes a stray timer",
82
+ async () => {
83
+ let platform = LocalAutomationSchedules.platformFor(~slice=DigestSpec.name)
84
+ await platform.put(~key="stray", at18)
85
+ let events =
86
+ [
87
+ eventJson(DigestTimeChosen({recipientId: "r1", time: at18}), ~recipientId="r1"),
88
+ eventJson(DigestTimeChosen({recipientId: "r3", time: at19Berlin}), ~recipientId="r3"),
89
+ eventJson(Unsubscribed({recipientId: "r1"}), ~recipientId="r1"),
90
+ ]->Array.map(((_, json)) => json)
91
+ let ops = await ops()
92
+ await ops.rebuildSchedules(events)
93
+ expect((storedSchedules(), (await timers())->sorted))->toEqual(([("r3", at19Berlin)], ["r3"]))
94
+ },
95
+ )
96
+ })
@@ -0,0 +1,177 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Globals from "@jest/globals";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
+ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
8
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
9
+ import * as LocalAutomationSchedules$ReventlessLocal from "../../../src/components/LocalAutomationSchedules.res.mjs";
10
+ import * as AutomationRescheduleFixtures$ReventlessLocal from "./AutomationRescheduleFixtures.res.mjs";
11
+
12
+ function time(iso) {
13
+ return new Date(iso).getTime();
14
+ }
15
+
16
+ let at18 = {
17
+ TAG: "Calendar",
18
+ calendar: {
19
+ TAG: "Daily",
20
+ hour: 18,
21
+ minute: 0
22
+ }
23
+ };
24
+
25
+ let at19Berlin = {
26
+ TAG: "Calendar",
27
+ calendar: {
28
+ TAG: "Daily",
29
+ hour: 19,
30
+ minute: 0
31
+ },
32
+ zone: "Europe/Berlin"
33
+ };
34
+
35
+ async function settle() {
36
+ for (let _for = 1; _for <= 20; ++_for) {
37
+ await Globals.jest.advanceTimersByTimeAsync(0);
38
+ }
39
+ }
40
+
41
+ function sorted(keys) {
42
+ return keys.toSorted(Primitive_string.compare);
43
+ }
44
+
45
+ globalThis.describe("schedules set by events, locally", () => {
46
+ let slice = {
47
+ contents: undefined
48
+ };
49
+ let ops = async () => await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(Stdlib_Option.getOrThrow(slice.contents, undefined)));
50
+ globalThis.beforeAll(async () => {
51
+ Globals.jest.useFakeTimers();
52
+ Globals.jest.setSystemTime(new Date("2026-10-16T12:00:00Z").getTime());
53
+ slice.contents = Primitive_option.some(AutomationRescheduleFixtures$ReventlessLocal.makeSlice());
54
+ await ops();
55
+ return await settle();
56
+ });
57
+ globalThis.afterAll(() => {
58
+ LocalAutomationSchedules$ReventlessLocal.reset();
59
+ Globals.jest.useRealTimers();
60
+ });
61
+ globalThis.test("a choice is stored and starts the recipient's timer", async () => {
62
+ await AutomationRescheduleFixtures$ReventlessLocal.publish({
63
+ TAG: "DigestTimeChosen",
64
+ recipientId: "r1",
65
+ time: at18
66
+ }, "r1");
67
+ await AutomationRescheduleFixtures$ReventlessLocal.publish({
68
+ TAG: "DigestTimeChosen",
69
+ recipientId: "r2",
70
+ time: at19Berlin
71
+ }, "r2");
72
+ await settle();
73
+ globalThis.expect([
74
+ AutomationRescheduleFixtures$ReventlessLocal.storedSchedules(),
75
+ sorted(await AutomationRescheduleFixtures$ReventlessLocal.timers())
76
+ ]).toEqual([
77
+ [
78
+ [
79
+ "r1",
80
+ at18
81
+ ],
82
+ [
83
+ "r2",
84
+ at19Berlin
85
+ ]
86
+ ],
87
+ [
88
+ "r1",
89
+ "r2"
90
+ ]
91
+ ]);
92
+ });
93
+ globalThis.test("each recipient's tick comes at their own time and sends their digest", async () => {
94
+ await Globals.jest.advanceTimersByTimeAsync(5 * 3600000 + 1000);
95
+ await settle();
96
+ globalThis.expect(AutomationRescheduleFixtures$ReventlessLocal.publishedCommands.contents.map(c => c.id)).toEqual(["r2"]);
97
+ await Globals.jest.advanceTimersByTimeAsync(3600000);
98
+ await settle();
99
+ globalThis.expect(AutomationRescheduleFixtures$ReventlessLocal.publishedCommands.contents.map(c => c.id)).toEqual([
100
+ "r2",
101
+ "r2",
102
+ "r1"
103
+ ]);
104
+ });
105
+ globalThis.test("unsubscribing removes the recipient's row and timer, and only theirs", async () => {
106
+ await AutomationRescheduleFixtures$ReventlessLocal.publish({
107
+ TAG: "Unsubscribed",
108
+ recipientId: "r1"
109
+ }, "r1");
110
+ await settle();
111
+ globalThis.expect([
112
+ AutomationRescheduleFixtures$ReventlessLocal.storedSchedules(),
113
+ await AutomationRescheduleFixtures$ReventlessLocal.timers()
114
+ ]).toEqual([
115
+ [[
116
+ "r2",
117
+ at19Berlin
118
+ ]],
119
+ ["r2"]
120
+ ]);
121
+ });
122
+ globalThis.test("a later reconcile keeps the times events set", async () => {
123
+ let ops$1 = await ops();
124
+ await ops$1.reconcileSchedules();
125
+ globalThis.expect([
126
+ AutomationRescheduleFixtures$ReventlessLocal.storedSchedules(),
127
+ await AutomationRescheduleFixtures$ReventlessLocal.timers()
128
+ ]).toEqual([
129
+ [[
130
+ "r2",
131
+ at19Berlin
132
+ ]],
133
+ ["r2"]
134
+ ]);
135
+ });
136
+ globalThis.test("a rebuild reproduces the rows from the events and removes a stray timer", async () => {
137
+ let platform = LocalAutomationSchedules$ReventlessLocal.platformFor(AutomationRescheduleFixtures$ReventlessLocal.DigestSpec.name);
138
+ await platform.put("stray", at18);
139
+ let events = [
140
+ AutomationRescheduleFixtures$ReventlessLocal.eventJson({
141
+ TAG: "DigestTimeChosen",
142
+ recipientId: "r1",
143
+ time: at18
144
+ }, "r1"),
145
+ AutomationRescheduleFixtures$ReventlessLocal.eventJson({
146
+ TAG: "DigestTimeChosen",
147
+ recipientId: "r3",
148
+ time: at19Berlin
149
+ }, "r3"),
150
+ AutomationRescheduleFixtures$ReventlessLocal.eventJson({
151
+ TAG: "Unsubscribed",
152
+ recipientId: "r1"
153
+ }, "r1")
154
+ ].map(param => param[1]);
155
+ let ops$1 = await ops();
156
+ await ops$1.rebuildSchedules(events);
157
+ globalThis.expect([
158
+ AutomationRescheduleFixtures$ReventlessLocal.storedSchedules(),
159
+ sorted(await AutomationRescheduleFixtures$ReventlessLocal.timers())
160
+ ]).toEqual([
161
+ [[
162
+ "r3",
163
+ at19Berlin
164
+ ]],
165
+ ["r3"]
166
+ ]);
167
+ });
168
+ });
169
+
170
+ export {
171
+ time,
172
+ at18,
173
+ at19Berlin,
174
+ settle,
175
+ sorted,
176
+ }
177
+ /* Not a pure module */
@@ -0,0 +1,118 @@
1
+ // A declared schedule: the demand report every morning at 06:00 UTC. Its one
2
+ // mapping reads the framework's Clock source; each tick makes the day's item and
3
+ // `process` requests the report. The slice is made by `makeSlice`, not at import, so
4
+ // a test can fix the time its schedule is set at.
5
+
6
+ open Reventless
7
+
8
+ module DemandReportSpec = {
9
+ let name = "LocalDemandReport"
10
+ let moduleUrl: string = %raw(`import.meta.url`)
11
+
12
+ @schema
13
+ type todoItem = {day: float}
14
+
15
+ @schema
16
+ type command = RequestDemandReport({day: float})
17
+
18
+ let maxRetries = 2
19
+ let heartbeatInterval = 60
20
+ let targetName = "ReportSettings"
21
+ let schedule = Some(Schedule.Calendar({calendar: Daily({hour: 6, minute: 0})}))
22
+ }
23
+
24
+ module FromClock = AutomationSlice.Mapping.Make(
25
+ Clock,
26
+ DemandReportSpec,
27
+ {
28
+ open Clock
29
+ let collect = (Tick({key, at}), ~sourceId as _, _ctx) => [
30
+ (itemId(key, at), ({day: at}: DemandReportSpec.todoItem)),
31
+ ]
32
+ let resolve = _ => None
33
+ let reschedule = _ => []
34
+ },
35
+ )
36
+
37
+ module DemandReportAutomation: AutomationSlice.Automation with module Spec := DemandReportSpec = {
38
+ let process = (id, item: DemandReportSpec.todoItem, ~now as _) => Some((
39
+ id,
40
+ DemandReportSpec.RequestDemandReport({day: item.day}),
41
+ ))
42
+ let onExhausted = (_id, _item: DemandReportSpec.todoItem) => None
43
+ let moduleUrl: string = %raw(`import.meta.url`)
44
+ module M = AutomationSlice.Mappings.Make(DemandReportSpec)
45
+ module type Mapping = M.Mapping
46
+ let mappings: array<module(Mapping)> = [module(FromClock)]
47
+ }
48
+
49
+ // The same slice with its schedule left out: nothing would ever tick it.
50
+ module UnscheduledSpec = {
51
+ include DemandReportSpec
52
+ let name = "LocalUnscheduledReport"
53
+ let schedule: option<Schedule.time> = None
54
+ }
55
+
56
+ module FromClockUnscheduled = AutomationSlice.Mapping.Make(
57
+ Clock,
58
+ UnscheduledSpec,
59
+ {
60
+ let collect = (_, ~sourceId as _, _ctx) => []
61
+ let resolve = _ => None
62
+ let reschedule = _ => []
63
+ },
64
+ )
65
+
66
+ module UnscheduledAutomation: AutomationSlice.Automation with module Spec := UnscheduledSpec = {
67
+ let process = (_id, _item: UnscheduledSpec.todoItem, ~now as _) => None
68
+ let onExhausted = (_id, _item: UnscheduledSpec.todoItem) => None
69
+ let moduleUrl: string = %raw(`import.meta.url`)
70
+ module M = AutomationSlice.Mappings.Make(UnscheduledSpec)
71
+ module type Mapping = M.Mapping
72
+ let mappings: array<module(Mapping)> = [module(FromClockUnscheduled)]
73
+ }
74
+
75
+ module Bus = LocalBus.Make()
76
+ let _ = TestRunner.setup()
77
+
78
+ module AutomationSliceMaker = AutomationSlice_Builder.Make(Bus)
79
+ module DemandReport = AutomationSliceMaker.Make(DemandReportSpec, DemandReportAutomation)
80
+ module Unscheduled = AutomationSliceMaker.Make(UnscheduledSpec, UnscheduledAutomation)
81
+
82
+ let publishedCommands: ref<array<Reventless.Message.commandJson>> = ref([])
83
+ let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
84
+ publishedCommands := publishedCommands.contents->Array.concat(cmds)
85
+
86
+ let context = (sliceName): AutomationSlice.context => {
87
+ environment: "local",
88
+ platformName: "local",
89
+ pluginName: "TestPlugin",
90
+ sliceName,
91
+ }
92
+
93
+ /** Makes the demand report, which starts its declared schedule at the current time. */
94
+ let makeSlice = () =>
95
+ DemandReport.make(
96
+ ~allEventTopics=Dict.make(),
97
+ ~publishJsons=publishJsons->Pulumi.Output.make,
98
+ ~context=context(DemandReportSpec.name),
99
+ )
100
+
101
+ let makeUnscheduled = () =>
102
+ Unscheduled.make(
103
+ ~allEventTopics=Dict.make(),
104
+ ~publishJsons=publishJsons->Pulumi.Output.make,
105
+ ~context=context(UnscheduledSpec.name),
106
+ )
107
+
108
+ /** The stored to-do row of `id`, as the query store holds it. */
109
+ let storedRow = id =>
110
+ Bus.getQueryDbScan(DemandReport.queryDbName)
111
+ ->Option.map(scan => scan())
112
+ ->Option.getOr([])
113
+ ->Array.find(row =>
114
+ row->JSON.Decode.object->Option.flatMap(o => o->Dict.get("id")) == Some(id->JSON.Encode.string)
115
+ )
116
+ ->Option.map(row =>
117
+ row->Reventless.Util_Sury.fromJson(ReventlessCore.AutomationSlice_Callback.todoRowSchema)
118
+ )