@reventlessdev/reventless-local 3.0.0-alpha.286 → 3.0.0-alpha.288
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 +25 -0
- package/package.json +9 -9
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +5 -52
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -47
- package/src/adapter/Scheduler/LocalTimers.res +62 -0
- package/src/adapter/Scheduler/LocalTimers.res.mjs +63 -0
- package/src/components/AutomationSlice_Builder.res +23 -2
- package/src/components/AutomationSlice_Builder.res.mjs +19 -2
- package/src/components/LocalAutomationSchedules.res +59 -0
- package/src/components/LocalAutomationSchedules.res.mjs +72 -0
- package/src/test/TestRunner.res +1 -0
- package/src/test/TestRunner.res.mjs +6 -0
- 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 +4 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +14 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +2 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +7 -1
- package/tests/components/automationslice/AutomationSweepFixtures.res +2 -0
- package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +7 -1
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +3 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +13 -2
|
@@ -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()
|
|
@@ -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 */
|