@reventlessdev/reventless-gwt 1.0.0-alpha.243 → 1.0.0-alpha.245

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.
@@ -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
+ })