@reventlessdev/reventless-local 3.0.0-alpha.285 → 3.0.0-alpha.286

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 (24) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +9 -9
  3. package/src/adapter/Scheduler/LocalScheduledPublisher.res +43 -36
  4. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +32 -25
  5. package/src/components/AutomationSlice_Builder.res +10 -2
  6. package/src/components/AutomationSlice_Builder.res.mjs +13 -2
  7. package/src/components/LocalAutomationSweep.res +61 -0
  8. package/src/components/LocalAutomationSweep.res.mjs +83 -0
  9. package/src/test/TestRunner.res +1 -0
  10. package/src/test/TestRunner.res.mjs +6 -0
  11. package/tests/adapter/ScheduledPublisherTest.res +156 -155
  12. package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
  13. package/tests/components/automationslice/AutomationSliceFixtures.res +2 -2
  14. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +2 -2
  15. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +4 -1
  16. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +1 -1
  17. package/tests/components/automationslice/AutomationSweepFixtures.res +129 -0
  18. package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +217 -0
  19. package/tests/components/automationslice/AutomationSweepTest.res +107 -0
  20. package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
  21. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -1
  22. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +1 -1
  23. package/tests/components/scheduler/SchedulerTest.res +5 -5
  24. 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.describe("createSchedule", () => {
35
- globalThis.test("single-shot schedule fires event after runAllTimers", async () => {
36
- let TestBus = LocalBus$ReventlessLocal.Make({});
37
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
38
- let count = {
39
- contents: 0
40
- };
41
- TestBus.subscribeToEvents("sched-topic", async (param, param$1, param$2) => {
42
- count.contents = count.contents + 1 | 0;
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 pub = SP.make("scheduler", {});
45
- let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
46
- await ops.createSchedule([makeTopicResource("sched-topic")], {
47
- name: "one-shot",
48
- rate: {
49
- TAG: "Single",
50
- _0: 2024,
51
- _1: 1,
52
- _2: 1,
53
- _3: 0,
54
- _4: 0
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
- payload: "{}"
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
- Globals.jest.runAllTimers();
59
- await Promise.resolve();
60
- await Promise.resolve();
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("recurring schedule fires on each interval advance", async () => {
65
- let TestBus = LocalBus$ReventlessLocal.Make({});
66
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
67
- let count = {
68
- contents: 0
69
- };
70
- TestBus.subscribeToEvents("repeat-topic", async (param, param$1, param$2) => {
71
- count.contents = count.contents + 1 | 0;
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
- let pub = SP.make("scheduler2", {});
74
- let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
75
- await ops.createSchedule([makeTopicResource("repeat-topic")], {
76
- name: "every-minute",
77
- rate: {
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
- Globals.jest.advanceTimersByTime(60000);
84
- await Promise.resolve();
85
- await Promise.resolve();
86
- Globals.jest.advanceTimersByTime(60000);
87
- await Promise.resolve();
88
- await Promise.resolve();
89
- globalThis.expect(count.contents).toBe(2);
90
- return SP.reset();
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("clears a named schedule; advancing timers does not fire it", async () => {
95
- let TestBus = LocalBus$ReventlessLocal.Make({});
96
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
97
- let count = {
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 ops.deleteSchedule([], "to-delete");
114
- Globals.jest.advanceTimersByTime(60000);
115
- globalThis.expect(count.contents).toBe(0);
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 TestBus = LocalBus$ReventlessLocal.Make({});
122
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
123
- let count = {
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
- Globals.jest.advanceTimersByTime(300000);
141
- globalThis.expect(count.contents).toBe(0);
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 */
@@ -106,7 +106,7 @@ module SkipProcessMapping = Reventless.AutomationSlice.Mapping.Make(
106
106
 
107
107
  module ShipOrderAutomation: Reventless.AutomationSlice.Automation
108
108
  with module Spec := ShipOrderSpec = {
109
- let process = (id, _item: ShipOrderSpec.todoItem) => Some((
109
+ let process = (id, _item: ShipOrderSpec.todoItem, ~now as _) => Some((
110
110
  id,
111
111
  ShipOrderSpec.CreateShipment({orderId: id}),
112
112
  ))
@@ -119,7 +119,7 @@ module ShipOrderAutomation: Reventless.AutomationSlice.Automation
119
119
 
120
120
  module SkipProcessAutomation: Reventless.AutomationSlice.Automation
121
121
  with module Spec := SkipProcessSpec = {
122
- let process = (_id, _item: SkipProcessSpec.todoItem) => None
122
+ let process = (_id, _item: SkipProcessSpec.todoItem, ~now as _) => None
123
123
  let onExhausted = (_id, _item: SkipProcessSpec.todoItem) => None
124
124
  let moduleUrl: string = %raw(`import.meta.url`)
125
125
  module M = Reventless.AutomationSlice.Mappings.Make(SkipProcessSpec)
@@ -174,7 +174,7 @@ let SkipProcessMapping = AutomationSlice$Reventless.Mapping.Make({
174
174
  resolve: resolve$1
175
175
  });
176
176
 
177
- function process(id, _item) {
177
+ function process(id, _item, param) {
178
178
  return [
179
179
  id,
180
180
  {
@@ -201,7 +201,7 @@ let ShipOrderAutomation = {
201
201
  mappings: mappings
202
202
  };
203
203
 
204
- function process$1(_id, _item) {
204
+ function process$1(_id, _item, param) {
205
205
 
206
206
  }
207
207
 
@@ -156,7 +156,10 @@ module FromDcb = AutomationSlice.Mapping.Make(
156
156
  )
157
157
 
158
158
  module AutoShipAutomation: AutomationSlice.Automation with module Spec := AutoShipSpec = {
159
- let process = (id, _item: AutoShipSpec.todoItem) => Some((id, AutoShipSpec.Ship({orderId: id})))
159
+ let process = (id, _item: AutoShipSpec.todoItem, ~now as _) => Some((
160
+ id,
161
+ AutoShipSpec.Ship({orderId: id}),
162
+ ))
160
163
  let onExhausted = (_id, _item: AutoShipSpec.todoItem) => None
161
164
  let moduleUrl: string = %raw(`import.meta.url`)
162
165
  module M = AutomationSlice.Mappings.Make(AutoShipSpec)
@@ -278,7 +278,7 @@ let FromDcb = AutomationSlice$Reventless.Mapping.Make({
278
278
  resolve: resolve
279
279
  });
280
280
 
281
- function process(id, _item) {
281
+ function process(id, _item, param) {
282
282
  return [
283
283
  id,
284
284
  {
@@ -0,0 +1,129 @@
1
+ // A deadline: `collect` counts when an order is due from the time its event was
2
+ // recorded, and `process` sends nothing until `~now` reaches it. Only a sweep
3
+ // moves the item once its time has come, since no further event arrives. The
4
+ // tests fix the time with Jest's fake Date.
5
+
6
+ open TestFixtures
7
+ open Reventless
8
+
9
+ module OrderSource = {
10
+ module Id = Id.String
11
+ let name = "DeadlineOrderEventTopic"
12
+ @schema
13
+ type event = OrderPlaced({orderId: string, within: float})
14
+ }
15
+
16
+ module CancelUnshippedSpec = {
17
+ let name = "CancelUnshipped"
18
+ let moduleUrl: string = %raw(`import.meta.url`)
19
+
20
+ @schema
21
+ type todoItem = {orderId: string, dueAt: float}
22
+
23
+ @schema
24
+ type command = CancelOrder({orderId: @s.matches(DcbTag.string) string})
25
+
26
+ let maxRetries = 2
27
+ let heartbeatInterval = 60
28
+ let targetName = "CancelOrder"
29
+ }
30
+
31
+ module FromOrders = AutomationSlice.Mapping.Make(
32
+ OrderSource,
33
+ CancelUnshippedSpec,
34
+ {
35
+ let collect = (event: OrderSource.event, ~sourceId as _, ctx: AutomationSlice.context) =>
36
+ switch (event, ctx.recordedAt) {
37
+ | (OrderPlaced({orderId, within}), Some(placedAt)) => [
38
+ (orderId, ({orderId, dueAt: placedAt +. within}: CancelUnshippedSpec.todoItem)),
39
+ ]
40
+ | (OrderPlaced(_), None) => []
41
+ }
42
+ let resolve = (_event: OrderSource.event) => None
43
+ },
44
+ )
45
+
46
+ module CancelUnshippedAutomation: AutomationSlice.Automation
47
+ with module Spec := CancelUnshippedSpec = {
48
+ let process = (id, item: CancelUnshippedSpec.todoItem, ~now) =>
49
+ now >= item.dueAt ? Some((id, CancelUnshippedSpec.CancelOrder({orderId: item.orderId}))) : None
50
+ let onExhausted = (_id, _item: CancelUnshippedSpec.todoItem) => None
51
+ let moduleUrl: string = %raw(`import.meta.url`)
52
+ module M = AutomationSlice.Mappings.Make(CancelUnshippedSpec)
53
+ module type Mapping = M.Mapping
54
+ let mappings: array<module(Mapping)> = [module(FromOrders)]
55
+ }
56
+
57
+ let orderResource: ReventlessInfra.Adapter.resource = ReventlessInfra.Adapter.make(
58
+ ~name=OrderSource.name->Pulumi.Output.make,
59
+ ~id=OrderSource.name->Pulumi.Output.make,
60
+ ~urn=OrderSource.name->Pulumi.Output.make,
61
+ ~service="memory:InMemory"->Pulumi.Output.make,
62
+ )
63
+
64
+ let allEventTopics: ReventlessCore.EventTopic.allOutputs = Dict.fromArray([
65
+ (OrderSource.name, {ReventlessInfra.EventTopic.resources: [orderResource]}),
66
+ ])
67
+
68
+ module Bus = LocalBus.Make()
69
+ let _ = TestRunner.setup()
70
+
71
+ module AutomationSliceMaker = AutomationSlice_Builder.Make(Bus)
72
+ module CancelUnshipped = AutomationSliceMaker.Make(CancelUnshippedSpec, CancelUnshippedAutomation)
73
+
74
+ // Commands are captured; `failPublish` makes the next publishes throw.
75
+ let publishedCommands: ref<array<Reventless.Message.commandJson>> = ref([])
76
+ let failPublish = ref(false)
77
+ let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
78
+ if failPublish.contents {
79
+ JsError.throwWithMessage("command topic unavailable")
80
+ } else {
81
+ publishedCommands := publishedCommands.contents->Array.concat(cmds)
82
+ }
83
+
84
+ let slice = CancelUnshipped.make(
85
+ ~allEventTopics,
86
+ ~publishJsons=publishJsons->Pulumi.Output.make,
87
+ ~context={
88
+ environment: "local",
89
+ platformName: "local",
90
+ pluginName: "TestPlugin",
91
+ sliceName: CancelUnshippedSpec.name,
92
+ },
93
+ )
94
+
95
+ /** Places an order now, as the fake Date has it, due `within` milliseconds later. */
96
+ let placeOrder = async (orderId, ~within) => {
97
+ let meta = {
98
+ ...makeTestMeta(~service=OrderSource.name),
99
+ time: Date.now()->Date.fromTime->Date.toISOString,
100
+ }
101
+ let eventJson = JSON.Encode.object(
102
+ Dict.fromArray([
103
+ ("id", orderId->JSON.Encode.string),
104
+ ("meta", meta->Reventless.Util_Sury.toJson(Message.metaSchema)),
105
+ (
106
+ "event",
107
+ OrderSource.OrderPlaced({orderId, within})->Reventless.Util_Sury.toJson(
108
+ OrderSource.eventSchema,
109
+ ),
110
+ ),
111
+ ]),
112
+ )
113
+ await Bus.publishEvent(OrderSource.name, "test", meta, eventJson)
114
+ // The event's own phase 2 runs detached; let it finish.
115
+ let _ = await Promise.resolve()
116
+ let _ = await Promise.resolve()
117
+ }
118
+
119
+ /** The stored to-do row of `id`, as the query store holds it. */
120
+ let storedRow = id =>
121
+ Bus.getQueryDbScan(CancelUnshipped.queryDbName)
122
+ ->Option.map(scan => scan())
123
+ ->Option.getOr([])
124
+ ->Array.find(row =>
125
+ row->JSON.Decode.object->Option.flatMap(o => o->Dict.get("id")) == Some(id->JSON.Encode.string)
126
+ )
127
+ ->Option.map(row =>
128
+ row->Reventless.Util_Sury.fromJson(ReventlessCore.AutomationSlice_Callback.todoRowSchema)
129
+ )