@reventlessdev/reventless-gwt 1.0.0-alpha.243 → 1.0.0-alpha.244
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 +14 -0
- package/package.json +6 -6
- package/src/Automation_GWT.res +100 -15
- package/src/Automation_GWT.res.mjs +94 -7
- package/src/Flow_GWT.res.mjs +1 -1
- package/tests/AutomationDeadlineGwtTest.res +2 -0
- package/tests/AutomationDeadlineGwtTest.res.mjs +25 -1
- package/tests/AutomationFromSliceGwtTest.res +3 -0
- package/tests/AutomationFromSliceGwtTest.res.mjs +31 -2
- package/tests/AutomationRescheduleGwtTest.res +264 -0
- package/tests/AutomationRescheduleGwtTest.res.mjs +723 -0
- package/tests/AutomationScheduleGwtTest.res +140 -0
- package/tests/AutomationScheduleGwtTest.res.mjs +356 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Schedules set by events. The marketing digest declares no time: each recipient's
|
|
2
|
+
// time comes from the event that records their choice, a tick of their key makes
|
|
3
|
+
// their digest's item, and unsubscribing clears their schedule. The reminder sets a
|
|
4
|
+
// once-only time counted from when the order was placed, cleared when it ships.
|
|
5
|
+
|
|
6
|
+
module MarketingDigest = {
|
|
7
|
+
let name = "MarketingDigest"
|
|
8
|
+
let moduleUrl = ""
|
|
9
|
+
|
|
10
|
+
@schema
|
|
11
|
+
type todoItem = {recipientId: string}
|
|
12
|
+
|
|
13
|
+
@schema
|
|
14
|
+
type command = RequestNotification({recipientId: string, reference: string})
|
|
15
|
+
|
|
16
|
+
let maxRetries = 3
|
|
17
|
+
let heartbeatInterval = 60
|
|
18
|
+
let targetName = "NotificationPreferences"
|
|
19
|
+
let schedule = None
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module MarketingDigest_Automation = {
|
|
23
|
+
module M = Reventless.AutomationSlice.Mappings.Make(MarketingDigest)
|
|
24
|
+
module type Mapping = M.Mapping
|
|
25
|
+
let moduleUrl = ""
|
|
26
|
+
|
|
27
|
+
module OrderingDcbSource = {
|
|
28
|
+
module Id = Reventless.Id.String
|
|
29
|
+
let name = "OrderingDcbEventLog"
|
|
30
|
+
@schema
|
|
31
|
+
type category = Marketing | Orders
|
|
32
|
+
@schema
|
|
33
|
+
type event =
|
|
34
|
+
| DigestTimeChosen({recipientId: string, time: Reventless.Schedule.time})
|
|
35
|
+
| NotificationUnsubscribed({recipientId: string, category: category})
|
|
36
|
+
| NotificationRequested({recipientId: string, reference: string})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module FromClock = Reventless.AutomationSlice.Mapping.Make(
|
|
40
|
+
Reventless.Clock,
|
|
41
|
+
MarketingDigest,
|
|
42
|
+
{
|
|
43
|
+
open Reventless.Clock
|
|
44
|
+
// The key is the recipient whose time came.
|
|
45
|
+
let collect = (Tick({key, at}), ~sourceId as _, _ctx) => [
|
|
46
|
+
(itemId(key, at), ({recipientId: key}: MarketingDigest.todoItem)),
|
|
47
|
+
]
|
|
48
|
+
let resolve = _ => None
|
|
49
|
+
let reschedule = _ => []
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
module FromOrderingDcb = Reventless.AutomationSlice.Mapping.Make(
|
|
54
|
+
OrderingDcbSource,
|
|
55
|
+
MarketingDigest,
|
|
56
|
+
{
|
|
57
|
+
open OrderingDcbSource
|
|
58
|
+
let collect = (_, ~sourceId as _, _ctx) => []
|
|
59
|
+
let resolve = event =>
|
|
60
|
+
switch event {
|
|
61
|
+
| NotificationRequested({reference}) => Some(reference)
|
|
62
|
+
| _ => None
|
|
63
|
+
}
|
|
64
|
+
let reschedule = event =>
|
|
65
|
+
switch event {
|
|
66
|
+
| DigestTimeChosen({recipientId, time}) => [
|
|
67
|
+
Reventless.AutomationSlice.Set({key: recipientId, time}),
|
|
68
|
+
]
|
|
69
|
+
| NotificationUnsubscribed({recipientId, category: Marketing}) => [
|
|
70
|
+
Clear({key: recipientId}),
|
|
71
|
+
]
|
|
72
|
+
| _ => []
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
let mappings: array<module(Mapping)> = [module(FromClock), module(FromOrderingDcb)]
|
|
78
|
+
|
|
79
|
+
// The item id travels as the request's reference and comes back on NotificationRequested.
|
|
80
|
+
let process = (id, item: MarketingDigest.todoItem, ~now as _) => Some((
|
|
81
|
+
item.recipientId,
|
|
82
|
+
MarketingDigest.RequestNotification({recipientId: item.recipientId, reference: id}),
|
|
83
|
+
))
|
|
84
|
+
let onExhausted = (_id, _item: MarketingDigest.todoItem) => None
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module Digest = {
|
|
88
|
+
open MarketingDigest_Automation
|
|
89
|
+
include Automation_GWT.FromSlice(MarketingDigest, MarketingDigest_Automation)
|
|
90
|
+
module OrderingLog = Mapping(FromOrderingDcb)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let time = iso => Date.fromString(iso)->Date.getTime
|
|
94
|
+
let at18 = Reventless.Schedule.Calendar({calendar: Daily({hour: 18, minute: 0})})
|
|
95
|
+
let at730Berlin = Reventless.Schedule.Calendar({
|
|
96
|
+
calendar: Daily({hour: 7, minute: 30}),
|
|
97
|
+
zone: "Europe/Berlin",
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
JestBind.describe("a schedule per customer: the marketing digest", () => {
|
|
101
|
+
open Digest
|
|
102
|
+
open MarketingDigest_Automation.OrderingDcbSource
|
|
103
|
+
|
|
104
|
+
test("each recipient's choice sets their own schedule", () =>
|
|
105
|
+
givenEvents([
|
|
106
|
+
OrderingLog.event(DigestTimeChosen({recipientId: "r1", time: at18})),
|
|
107
|
+
OrderingLog.event(DigestTimeChosen({recipientId: "r2", time: at730Berlin})),
|
|
108
|
+
])
|
|
109
|
+
->whenSweep
|
|
110
|
+
->thenSchedules([("r1", at18), ("r2", at730Berlin)])
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
test("a recipient's tick makes their digest, with the item id as reference", () =>
|
|
114
|
+
givenEvents([OrderingLog.event(DigestTimeChosen({recipientId: "r1", time: at18}))])
|
|
115
|
+
->whenTick(~key="r1", ~at=time("2026-10-16T18:00:00Z"))
|
|
116
|
+
->thenCommands([
|
|
117
|
+
(
|
|
118
|
+
"r1",
|
|
119
|
+
MarketingDigest.RequestNotification({recipientId: "r1", reference: "r1@2026-10-16T18:00Z"}),
|
|
120
|
+
),
|
|
121
|
+
])
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
test("the request's own event completes the digest", () =>
|
|
125
|
+
givenEvents([OrderingLog.event(DigestTimeChosen({recipientId: "r1", time: at18}))])
|
|
126
|
+
->whenTick(~key="r1", ~at=time("2026-10-16T18:00:00Z"))
|
|
127
|
+
->andThenEvents([
|
|
128
|
+
OrderingLog.event(
|
|
129
|
+
NotificationRequested({recipientId: "r1", reference: "r1@2026-10-16T18:00Z"}),
|
|
130
|
+
),
|
|
131
|
+
])
|
|
132
|
+
->thenScenarioTodos([])
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
test("unsubscribing from marketing clears the recipient's schedule, and only theirs", () =>
|
|
136
|
+
givenEvents([
|
|
137
|
+
OrderingLog.event(DigestTimeChosen({recipientId: "r1", time: at18})),
|
|
138
|
+
OrderingLog.event(DigestTimeChosen({recipientId: "r2", time: at730Berlin})),
|
|
139
|
+
OrderingLog.event(NotificationUnsubscribed({recipientId: "r1", category: Marketing})),
|
|
140
|
+
OrderingLog.event(NotificationUnsubscribed({recipientId: "r2", category: Orders})),
|
|
141
|
+
])
|
|
142
|
+
->whenSweep
|
|
143
|
+
->thenSchedules([("r2", at730Berlin)])
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
test("a later choice replaces the earlier one", () =>
|
|
147
|
+
givenEvents([
|
|
148
|
+
OrderingLog.event(DigestTimeChosen({recipientId: "r1", time: at18})),
|
|
149
|
+
OrderingLog.event(DigestTimeChosen({recipientId: "r1", time: at730Berlin})),
|
|
150
|
+
])
|
|
151
|
+
->whenSweep
|
|
152
|
+
->thenSchedules([("r1", at730Berlin)])
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
module Reminder = {
|
|
157
|
+
let name = "OrderReminder"
|
|
158
|
+
let moduleUrl = ""
|
|
159
|
+
|
|
160
|
+
@schema
|
|
161
|
+
type todoItem = {orderId: string}
|
|
162
|
+
|
|
163
|
+
@schema
|
|
164
|
+
type command = RemindOrder({orderId: string})
|
|
165
|
+
|
|
166
|
+
let maxRetries = 3
|
|
167
|
+
let heartbeatInterval = 60
|
|
168
|
+
let targetName = "Order"
|
|
169
|
+
let schedule = None
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module Reminder_Automation = {
|
|
173
|
+
module M = Reventless.AutomationSlice.Mappings.Make(Reminder)
|
|
174
|
+
module type Mapping = M.Mapping
|
|
175
|
+
let moduleUrl = ""
|
|
176
|
+
|
|
177
|
+
module OrderingDcbSource = {
|
|
178
|
+
module Id = Reventless.Id.String
|
|
179
|
+
let name = "OrderingDcbEventLog"
|
|
180
|
+
@schema
|
|
181
|
+
type event = OrderPlaced({orderId: string}) | OrderShipped({orderId: string})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module FromClock = Reventless.AutomationSlice.Mapping.Make(
|
|
185
|
+
Reventless.Clock,
|
|
186
|
+
Reminder,
|
|
187
|
+
{
|
|
188
|
+
let collect = (Reventless.Clock.Tick({key, at}), ~sourceId as _, _ctx) => [
|
|
189
|
+
(Reventless.Clock.itemId(key, at), ({orderId: key}: Reminder.todoItem)),
|
|
190
|
+
]
|
|
191
|
+
let resolve = _ => None
|
|
192
|
+
let reschedule = _ => []
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
module FromOrdering = Reventless.AutomationSlice.Mapping.Make(
|
|
197
|
+
OrderingDcbSource,
|
|
198
|
+
Reminder,
|
|
199
|
+
{
|
|
200
|
+
let collect = (_, ~sourceId as _, _ctx) => []
|
|
201
|
+
let resolve = _ => None
|
|
202
|
+
let reschedule = event =>
|
|
203
|
+
switch event {
|
|
204
|
+
| OrderingDcbSource.OrderPlaced({orderId}) => [
|
|
205
|
+
Reventless.AutomationSlice.Set({
|
|
206
|
+
key: orderId,
|
|
207
|
+
time: Once({moment: After(Minutes(10))}),
|
|
208
|
+
}),
|
|
209
|
+
]
|
|
210
|
+
| OrderShipped({orderId}) => [Clear({key: orderId})]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
let mappings: array<module(Mapping)> = [module(FromClock), module(FromOrdering)]
|
|
216
|
+
|
|
217
|
+
let process = (_id, item: Reminder.todoItem, ~now as _) => Some((
|
|
218
|
+
item.orderId,
|
|
219
|
+
Reminder.RemindOrder({orderId: item.orderId}),
|
|
220
|
+
))
|
|
221
|
+
let onExhausted = (_id, _item: Reminder.todoItem) => None
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module ReminderGwt = {
|
|
225
|
+
open Reminder_Automation
|
|
226
|
+
include Automation_GWT.FromSlice(Reminder, Reminder_Automation)
|
|
227
|
+
module OrderingLog = Mapping(FromOrdering)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let placedAt = time("2026-10-16T09:00:00Z")
|
|
231
|
+
let tenPast = Reventless.Schedule.Once({
|
|
232
|
+
moment: At({year: 2026, month: 10, day: 16, hour: 9, minute: 10}),
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
JestBind.describe("a once-only time: the reminder", () => {
|
|
236
|
+
open ReminderGwt
|
|
237
|
+
open Reminder_Automation.OrderingDcbSource
|
|
238
|
+
|
|
239
|
+
test("is set ten minutes after the order was recorded", () =>
|
|
240
|
+
givenEvents([OrderingLog.event(OrderPlaced({orderId: "o1"}), ~recordedAt=placedAt)])
|
|
241
|
+
->whenSweep
|
|
242
|
+
->thenSchedules([("o1", tenPast)])
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
test("is cleared when the order ships", () =>
|
|
246
|
+
givenEvents([
|
|
247
|
+
OrderingLog.event(OrderPlaced({orderId: "o1"}), ~recordedAt=placedAt),
|
|
248
|
+
OrderingLog.event(OrderShipped({orderId: "o1"}), ~recordedAt=placedAt +. 60_000.),
|
|
249
|
+
])
|
|
250
|
+
->whenSweep
|
|
251
|
+
->thenSchedules([])
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
test("is cleared once it has fired, and its tick sends the reminder", () => {
|
|
255
|
+
let scenario =
|
|
256
|
+
givenEvents([
|
|
257
|
+
OrderingLog.event(OrderPlaced({orderId: "o1"}), ~recordedAt=placedAt),
|
|
258
|
+
])->whenTick(~key="o1", ~at=time("2026-10-16T09:10:00Z"))
|
|
259
|
+
switch scenario->thenSchedules([]) {
|
|
260
|
+
| Ok() => scenario->thenCommands([("o1", Reminder.RemindOrder({orderId: "o1"}))])
|
|
261
|
+
| failed => failed
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
})
|