@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,131 @@
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
+ let schedule = None
30
+ }
31
+
32
+ module FromOrders = AutomationSlice.Mapping.Make(
33
+ OrderSource,
34
+ CancelUnshippedSpec,
35
+ {
36
+ let collect = (event: OrderSource.event, ~sourceId as _, ctx: AutomationSlice.context) =>
37
+ switch (event, ctx.recordedAt) {
38
+ | (OrderPlaced({orderId, within}), Some(placedAt)) => [
39
+ (orderId, ({orderId, dueAt: placedAt +. within}: CancelUnshippedSpec.todoItem)),
40
+ ]
41
+ | (OrderPlaced(_), None) => []
42
+ }
43
+ let resolve = (_event: OrderSource.event) => None
44
+ let reschedule = _ => []
45
+ },
46
+ )
47
+
48
+ module CancelUnshippedAutomation: AutomationSlice.Automation
49
+ with module Spec := CancelUnshippedSpec = {
50
+ let process = (id, item: CancelUnshippedSpec.todoItem, ~now) =>
51
+ now >= item.dueAt ? Some((id, CancelUnshippedSpec.CancelOrder({orderId: item.orderId}))) : None
52
+ let onExhausted = (_id, _item: CancelUnshippedSpec.todoItem) => None
53
+ let moduleUrl: string = %raw(`import.meta.url`)
54
+ module M = AutomationSlice.Mappings.Make(CancelUnshippedSpec)
55
+ module type Mapping = M.Mapping
56
+ let mappings: array<module(Mapping)> = [module(FromOrders)]
57
+ }
58
+
59
+ let orderResource: ReventlessInfra.Adapter.resource = ReventlessInfra.Adapter.make(
60
+ ~name=OrderSource.name->Pulumi.Output.make,
61
+ ~id=OrderSource.name->Pulumi.Output.make,
62
+ ~urn=OrderSource.name->Pulumi.Output.make,
63
+ ~service="memory:InMemory"->Pulumi.Output.make,
64
+ )
65
+
66
+ let allEventTopics: ReventlessCore.EventTopic.allOutputs = Dict.fromArray([
67
+ (OrderSource.name, {ReventlessInfra.EventTopic.resources: [orderResource]}),
68
+ ])
69
+
70
+ module Bus = LocalBus.Make()
71
+ let _ = TestRunner.setup()
72
+
73
+ module AutomationSliceMaker = AutomationSlice_Builder.Make(Bus)
74
+ module CancelUnshipped = AutomationSliceMaker.Make(CancelUnshippedSpec, CancelUnshippedAutomation)
75
+
76
+ // Commands are captured; `failPublish` makes the next publishes throw.
77
+ let publishedCommands: ref<array<Reventless.Message.commandJson>> = ref([])
78
+ let failPublish = ref(false)
79
+ let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
80
+ if failPublish.contents {
81
+ JsError.throwWithMessage("command topic unavailable")
82
+ } else {
83
+ publishedCommands := publishedCommands.contents->Array.concat(cmds)
84
+ }
85
+
86
+ let slice = CancelUnshipped.make(
87
+ ~allEventTopics,
88
+ ~publishJsons=publishJsons->Pulumi.Output.make,
89
+ ~context={
90
+ environment: "local",
91
+ platformName: "local",
92
+ pluginName: "TestPlugin",
93
+ sliceName: CancelUnshippedSpec.name,
94
+ },
95
+ )
96
+
97
+ /** Places an order now, as the fake Date has it, due `within` milliseconds later. */
98
+ let placeOrder = async (orderId, ~within) => {
99
+ let meta = {
100
+ ...makeTestMeta(~service=OrderSource.name),
101
+ time: Date.now()->Date.fromTime->Date.toISOString,
102
+ }
103
+ let eventJson = JSON.Encode.object(
104
+ Dict.fromArray([
105
+ ("id", orderId->JSON.Encode.string),
106
+ ("meta", meta->Reventless.Util_Sury.toJson(Message.metaSchema)),
107
+ (
108
+ "event",
109
+ OrderSource.OrderPlaced({orderId, within})->Reventless.Util_Sury.toJson(
110
+ OrderSource.eventSchema,
111
+ ),
112
+ ),
113
+ ]),
114
+ )
115
+ await Bus.publishEvent(OrderSource.name, "test", meta, eventJson)
116
+ // The event's own phase 2 runs detached; let it finish.
117
+ let _ = await Promise.resolve()
118
+ let _ = await Promise.resolve()
119
+ }
120
+
121
+ /** The stored to-do row of `id`, as the query store holds it. */
122
+ let storedRow = id =>
123
+ Bus.getQueryDbScan(CancelUnshipped.queryDbName)
124
+ ->Option.map(scan => scan())
125
+ ->Option.getOr([])
126
+ ->Array.find(row =>
127
+ row->JSON.Decode.object->Option.flatMap(o => o->Dict.get("id")) == Some(id->JSON.Encode.string)
128
+ )
129
+ ->Option.map(row =>
130
+ row->Reventless.Util_Sury.fromJson(ReventlessCore.AutomationSlice_Callback.todoRowSchema)
131
+ )
@@ -0,0 +1,223 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.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 Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.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 Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
13
+ import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
14
+ import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
15
+ import * as AutomationSlice$Reventless from "@reventlessdev/reventless-spec/src/components/AutomationSlice.res.mjs";
16
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
17
+ import * as TestFixtures$ReventlessLocal from "../../TestFixtures.res.mjs";
18
+ import * as AutomationSlice_Builder$ReventlessLocal from "../../../src/components/AutomationSlice_Builder.res.mjs";
19
+ import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
20
+
21
+ let name = "DeadlineOrderEventTopic";
22
+
23
+ let eventSchema = Sury.$schema(s => ({
24
+ TAG: "OrderPlaced",
25
+ orderId: s.m(Sury.string),
26
+ within: s.m(Sury.number)
27
+ }));
28
+
29
+ let OrderSource = {
30
+ Id: undefined,
31
+ name: name,
32
+ eventSchema: eventSchema
33
+ };
34
+
35
+ let name$1 = "CancelUnshipped";
36
+
37
+ let moduleUrl = import.meta.url;
38
+
39
+ let todoItemSchema = Sury.$schema(s => ({
40
+ orderId: s.m(Sury.string),
41
+ dueAt: s.m(Sury.number)
42
+ }));
43
+
44
+ let commandSchema = Sury.$schema(s => ({
45
+ TAG: "CancelOrder",
46
+ orderId: s.m(DcbTag$Reventless.string)
47
+ }));
48
+
49
+ function authorizationOf(param) {
50
+ return "AllowAuthenticated";
51
+ }
52
+
53
+ function commandTransition(param) {
54
+ return "Undeclared";
55
+ }
56
+
57
+ let traits = [];
58
+
59
+ let CancelUnshippedSpec = {
60
+ name: name$1,
61
+ moduleUrl: moduleUrl,
62
+ todoItemSchema: todoItemSchema,
63
+ commandSchema: commandSchema,
64
+ maxRetries: 2,
65
+ heartbeatInterval: 60,
66
+ targetName: "CancelOrder",
67
+ schedule: undefined,
68
+ authorizationOf: authorizationOf,
69
+ commandTransition: commandTransition,
70
+ traits: traits
71
+ };
72
+
73
+ function collect(event, param, ctx) {
74
+ let match = ctx.recordedAt;
75
+ if (match === undefined) {
76
+ return [];
77
+ }
78
+ let orderId = event.orderId;
79
+ return [[
80
+ orderId,
81
+ {
82
+ orderId: orderId,
83
+ dueAt: match + event.within
84
+ }
85
+ ]];
86
+ }
87
+
88
+ function resolve(_event) {
89
+
90
+ }
91
+
92
+ function reschedule(param) {
93
+ return [];
94
+ }
95
+
96
+ let FromOrders = AutomationSlice$Reventless.Mapping.Make({
97
+ Id: Id$Reventless.$$String,
98
+ name: name,
99
+ eventSchema: eventSchema
100
+ })(CancelUnshippedSpec)({
101
+ collect: collect,
102
+ resolve: resolve,
103
+ reschedule: reschedule
104
+ });
105
+
106
+ function process(id, item, now) {
107
+ if (now >= item.dueAt) {
108
+ return [
109
+ id,
110
+ {
111
+ TAG: "CancelOrder",
112
+ orderId: item.orderId
113
+ }
114
+ ];
115
+ }
116
+ }
117
+
118
+ function onExhausted(_id, _item) {
119
+
120
+ }
121
+
122
+ let moduleUrl$1 = import.meta.url;
123
+
124
+ AutomationSlice$Reventless.Mappings.Make(CancelUnshippedSpec);
125
+
126
+ let mappings = [FromOrders];
127
+
128
+ let CancelUnshippedAutomation = {
129
+ process: process,
130
+ onExhausted: onExhausted,
131
+ moduleUrl: moduleUrl$1,
132
+ mappings: mappings
133
+ };
134
+
135
+ let orderResource = Adapter$ReventlessInfra.make(Pulumi.output(name), Pulumi.output(name), Pulumi.output(name), Pulumi.output("memory:InMemory"), undefined, undefined, undefined, undefined, undefined, undefined);
136
+
137
+ let allEventTopics = Object.fromEntries([[
138
+ name,
139
+ {
140
+ resources: [orderResource]
141
+ }
142
+ ]]);
143
+
144
+ let Bus = LocalBus$ReventlessLocal.Make({});
145
+
146
+ TestRunner$ReventlessLocal.setup();
147
+
148
+ let AutomationSliceMaker = AutomationSlice_Builder$ReventlessLocal.Make(Bus);
149
+
150
+ let CancelUnshipped = AutomationSliceMaker.Make(CancelUnshippedSpec)(CancelUnshippedAutomation);
151
+
152
+ let publishedCommands = {
153
+ contents: []
154
+ };
155
+
156
+ let failPublish = {
157
+ contents: false
158
+ };
159
+
160
+ async function publishJsons(cmds) {
161
+ if (failPublish.contents) {
162
+ return Stdlib_JsError.throwWithMessage("command topic unavailable");
163
+ } else {
164
+ publishedCommands.contents = publishedCommands.contents.concat(cmds);
165
+ return;
166
+ }
167
+ }
168
+
169
+ let slice = CancelUnshipped.make(allEventTopics, Pulumi.output(publishJsons), {
170
+ environment: "local",
171
+ platformName: "local",
172
+ pluginName: "TestPlugin",
173
+ sliceName: name$1
174
+ }, undefined, undefined);
175
+
176
+ async function placeOrder(orderId, within) {
177
+ let newrecord = {...TestFixtures$ReventlessLocal.makeTestMeta(name)};
178
+ newrecord.time = new Date(Date.now()).toISOString();
179
+ let eventJson = Object.fromEntries([
180
+ [
181
+ "id",
182
+ orderId
183
+ ],
184
+ [
185
+ "meta",
186
+ Util_Sury$Reventless.toJson(newrecord, Message$Reventless.metaSchema)
187
+ ],
188
+ [
189
+ "event",
190
+ Util_Sury$Reventless.toJson({
191
+ TAG: "OrderPlaced",
192
+ orderId: orderId,
193
+ within: within
194
+ }, eventSchema)
195
+ ]
196
+ ]);
197
+ await Bus.publishEvent(name, "test", newrecord, eventJson);
198
+ await Promise.resolve();
199
+ await Promise.resolve();
200
+ }
201
+
202
+ function storedRow(id) {
203
+ return Stdlib_Option.map(Stdlib_Option.getOr(Stdlib_Option.map(Bus.getQueryDbScan(CancelUnshipped.queryDbName), scan => scan()), []).find(row => Primitive_object.equal(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(row), o => o["id"]), id)), row => Util_Sury$Reventless.fromJson(row, AutomationSlice_Callback$ReventlessCore.todoRowSchema));
204
+ }
205
+
206
+ export {
207
+ OrderSource,
208
+ CancelUnshippedSpec,
209
+ FromOrders,
210
+ CancelUnshippedAutomation,
211
+ orderResource,
212
+ allEventTopics,
213
+ Bus,
214
+ AutomationSliceMaker,
215
+ CancelUnshipped,
216
+ publishedCommands,
217
+ failPublish,
218
+ publishJsons,
219
+ slice,
220
+ placeOrder,
221
+ storedRow,
222
+ }
223
+ /* eventSchema Not a pure module */
@@ -0,0 +1,107 @@
1
+ // The local platform sweeps pending automation items as AWS does: a deadline,
2
+ // counted from its event's recorded time, passes without another event, a failed command is retried up to maxRetries,
3
+ // and what the sweep moved is written back to the query store.
4
+
5
+ open JestGlobals
6
+ open AutomationSweepFixtures
7
+
8
+ module Callback = ReventlessCore.AutomationSlice_Callback
9
+
10
+ // Only Date is faked, so `process` sees the time a test sets while Effect's own
11
+ // scheduling still runs on real timers.
12
+ type jestObj
13
+ type fakeTimersConfig = {doNotFake: array<string>}
14
+ @module("@jest/globals") external jest: jestObj = "jest"
15
+ @send external useFakeTimers: (jestObj, fakeTimersConfig) => unit = "useFakeTimers"
16
+ @send external useRealTimers: jestObj => unit = "useRealTimers"
17
+ @send external setSystemTime: (jestObj, float) => unit = "setSystemTime"
18
+
19
+ let setNow = millis => jest->setSystemTime(millis)
20
+
21
+ let commandIds = () => publishedCommands.contents->Array.map(c => c.id)
22
+
23
+ describe("the local automation sweep", () => {
24
+ let _ = beforeAll(() =>
25
+ jest->useFakeTimers({
26
+ doNotFake: [
27
+ "hrtime",
28
+ "nextTick",
29
+ "performance",
30
+ "queueMicrotask",
31
+ "setImmediate",
32
+ "clearImmediate",
33
+ "setInterval",
34
+ "clearInterval",
35
+ "setTimeout",
36
+ "clearTimeout",
37
+ ],
38
+ })
39
+ )
40
+ let _ = afterAll(() => jest->useRealTimers)
41
+
42
+ let _ = beforeAllAsync(async () => {
43
+ let _ = await slice->ReventlessCore.Component.operations->TestRunner.resolve
44
+ let _ = await orderResource.name->TestRunner.resolve
45
+ let _ = await Promise.resolve()
46
+ let _ = await Promise.resolve()
47
+ })
48
+
49
+ let _ = beforeEach(() => {
50
+ publishedCommands := []
51
+ failPublish := false
52
+ setNow(0.)
53
+ })
54
+
55
+ testPromise("sends a deadline's command once it is due, with no further event", async () => {
56
+ await placeOrder("o1", ~within=100.)
57
+ expect(commandIds())->toEqual([])
58
+
59
+ await LocalAutomationSweep.sweepOnce()
60
+ expect(commandIds())->toEqual([])
61
+
62
+ setNow(100.)
63
+ await LocalAutomationSweep.sweepOnce()
64
+ expect(commandIds())->toEqual(["o1"])
65
+ })
66
+
67
+ testPromise("writes what it moved back to the query store", async () => {
68
+ await placeOrder("o2", ~within=100.)
69
+ expect(storedRow("o2")->Option.map(row => row.status))->toEqual(Some(Callback.Pending))
70
+
71
+ setNow(100.)
72
+ await LocalAutomationSweep.sweepOnce()
73
+ expect(storedRow("o2")->Option.map(row => row.status))->toEqual(Some(Callback.Processing))
74
+ })
75
+
76
+ testPromise("retries a failed command up to maxRetries, then abandons it", async () => {
77
+ setNow(100.)
78
+ failPublish := true
79
+ await placeOrder("o3", ~within=0.)
80
+ expect(storedRow("o3")->Option.map(row => (row.status, row.retryCount)))->toEqual(
81
+ Some((Callback.Failed, 1)),
82
+ )
83
+
84
+ await LocalAutomationSweep.sweepOnce()
85
+ expect(storedRow("o3")->Option.map(row => (row.status, row.retryCount)))->toEqual(
86
+ Some((Callback.Abandoned, 2)),
87
+ )
88
+
89
+ failPublish := false
90
+ await LocalAutomationSweep.sweepOnce()
91
+ expect(commandIds()->Array.includes("o3"))->toBe(false)
92
+ })
93
+
94
+ testSync("waits five minutes by default, as on AWS", () => {
95
+ expect(LocalAutomationSweep.intervalSeconds())->toBe(300)
96
+ })
97
+
98
+ testSync("waits REVENTLESS_LOCAL_SWEEP_SECONDS when it is a positive number", () => {
99
+ let key = "REVENTLESS_LOCAL_SWEEP_SECONDS"
100
+ NodeProcess.env->Dict.set(key, "15")
101
+ let shortened = LocalAutomationSweep.intervalSeconds()
102
+ NodeProcess.env->Dict.set(key, "soon")
103
+ let malformed = LocalAutomationSweep.intervalSeconds()
104
+ NodeProcess.env->Dict.delete(key)
105
+ expect((shortened, malformed))->toEqual((15, 300))
106
+ })
107
+ })
@@ -0,0 +1,116 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Globals from "@jest/globals";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
7
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
8
+ import * as LocalAutomationSweep$ReventlessLocal from "../../../src/components/LocalAutomationSweep.res.mjs";
9
+ import * as AutomationSweepFixtures$ReventlessLocal from "./AutomationSweepFixtures.res.mjs";
10
+
11
+ function setNow(millis) {
12
+ Globals.jest.setSystemTime(millis);
13
+ }
14
+
15
+ function commandIds() {
16
+ return AutomationSweepFixtures$ReventlessLocal.publishedCommands.contents.map(c => c.id);
17
+ }
18
+
19
+ globalThis.describe("the local automation sweep", () => {
20
+ globalThis.beforeAll(() => {
21
+ Globals.jest.useFakeTimers({
22
+ doNotFake: [
23
+ "hrtime",
24
+ "nextTick",
25
+ "performance",
26
+ "queueMicrotask",
27
+ "setImmediate",
28
+ "clearImmediate",
29
+ "setInterval",
30
+ "clearInterval",
31
+ "setTimeout",
32
+ "clearTimeout"
33
+ ]
34
+ });
35
+ });
36
+ globalThis.afterAll(() => {
37
+ Globals.jest.useRealTimers();
38
+ });
39
+ globalThis.beforeAll(async () => {
40
+ await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(AutomationSweepFixtures$ReventlessLocal.slice));
41
+ await TestRunner$ReventlessLocal.resolve(AutomationSweepFixtures$ReventlessLocal.orderResource.name);
42
+ await Promise.resolve();
43
+ await Promise.resolve();
44
+ });
45
+ globalThis.beforeEach(() => {
46
+ AutomationSweepFixtures$ReventlessLocal.publishedCommands.contents = [];
47
+ AutomationSweepFixtures$ReventlessLocal.failPublish.contents = false;
48
+ Globals.jest.setSystemTime(0);
49
+ });
50
+ globalThis.test("sends a deadline's command once it is due, with no further event", async () => {
51
+ await AutomationSweepFixtures$ReventlessLocal.placeOrder("o1", 100);
52
+ globalThis.expect(commandIds()).toEqual([]);
53
+ await LocalAutomationSweep$ReventlessLocal.sweepOnce();
54
+ globalThis.expect(commandIds()).toEqual([]);
55
+ Globals.jest.setSystemTime(100);
56
+ await LocalAutomationSweep$ReventlessLocal.sweepOnce();
57
+ globalThis.expect(commandIds()).toEqual(["o1"]);
58
+ });
59
+ globalThis.test("writes what it moved back to the query store", async () => {
60
+ await AutomationSweepFixtures$ReventlessLocal.placeOrder("o2", 100);
61
+ globalThis.expect(Stdlib_Option.map(AutomationSweepFixtures$ReventlessLocal.storedRow("o2"), row => row.status)).toEqual("Pending");
62
+ Globals.jest.setSystemTime(100);
63
+ await LocalAutomationSweep$ReventlessLocal.sweepOnce();
64
+ globalThis.expect(Stdlib_Option.map(AutomationSweepFixtures$ReventlessLocal.storedRow("o2"), row => row.status)).toEqual("Processing");
65
+ });
66
+ globalThis.test("retries a failed command up to maxRetries, then abandons it", async () => {
67
+ Globals.jest.setSystemTime(100);
68
+ AutomationSweepFixtures$ReventlessLocal.failPublish.contents = true;
69
+ await AutomationSweepFixtures$ReventlessLocal.placeOrder("o3", 0);
70
+ globalThis.expect(Stdlib_Option.map(AutomationSweepFixtures$ReventlessLocal.storedRow("o3"), row => [
71
+ row.status,
72
+ row.retryCount
73
+ ])).toEqual([
74
+ "Failed",
75
+ 1
76
+ ]);
77
+ await LocalAutomationSweep$ReventlessLocal.sweepOnce();
78
+ globalThis.expect(Stdlib_Option.map(AutomationSweepFixtures$ReventlessLocal.storedRow("o3"), row => [
79
+ row.status,
80
+ row.retryCount
81
+ ])).toEqual([
82
+ "Abandoned",
83
+ 2
84
+ ]);
85
+ AutomationSweepFixtures$ReventlessLocal.failPublish.contents = false;
86
+ await LocalAutomationSweep$ReventlessLocal.sweepOnce();
87
+ globalThis.expect(commandIds().includes("o3")).toBe(false);
88
+ });
89
+ globalThis.test("waits five minutes by default, as on AWS", () => {
90
+ globalThis.expect(LocalAutomationSweep$ReventlessLocal.intervalSeconds()).toBe(300);
91
+ });
92
+ globalThis.test("waits REVENTLESS_LOCAL_SWEEP_SECONDS when it is a positive number", () => {
93
+ let key = "REVENTLESS_LOCAL_SWEEP_SECONDS";
94
+ process.env[key] = "15";
95
+ let shortened = LocalAutomationSweep$ReventlessLocal.intervalSeconds();
96
+ process.env[key] = "soon";
97
+ let malformed = LocalAutomationSweep$ReventlessLocal.intervalSeconds();
98
+ Stdlib_Dict.$$delete(process.env, key);
99
+ globalThis.expect([
100
+ shortened,
101
+ malformed
102
+ ]).toEqual([
103
+ 15,
104
+ 300
105
+ ]);
106
+ });
107
+ });
108
+
109
+ let Callback;
110
+
111
+ export {
112
+ Callback,
113
+ setNow,
114
+ commandIds,
115
+ }
116
+ /* Not a pure module */
@@ -55,6 +55,7 @@ module AutoFulfillSpec = {
55
55
  let maxRetries = 3
56
56
  let heartbeatInterval = 60
57
57
  let targetName = "MarkFulfilled"
58
+ let schedule = None
58
59
  }
59
60
 
60
61
  // Forward-declared per-source mappings live below; the merged `Automation`
@@ -88,6 +89,7 @@ module FromOrderAggregate = AutomationSlice.Mapping.Make(
88
89
  }
89
90
  }
90
91
  let resolve = (_event: OrderAggregateSource.event) => None
92
+ let reschedule = _ => []
91
93
  },
92
94
  )
93
95
 
@@ -115,6 +117,7 @@ module FromInventoryDcb = AutomationSlice.Mapping.Make(
115
117
  | StockReleased({orderId, productId}) => Some(orderId ++ ":" ++ productId)
116
118
  | StockReserved(_) => None
117
119
  }
120
+ let reschedule = _ => []
118
121
  },
119
122
  )
120
123
 
@@ -123,7 +126,7 @@ module FromInventoryDcb = AutomationSlice.Mapping.Make(
123
126
  // ─────────────────────────────────────────────────────────────
124
127
 
125
128
  module AutoFulfillAutomation: AutomationSlice.Automation with module Spec := AutoFulfillSpec = {
126
- let process = (id, item: AutoFulfillSpec.todoItem) => Some((
129
+ let process = (id, item: AutoFulfillSpec.todoItem, ~now as _) => Some((
127
130
  id,
128
131
  AutoFulfillSpec.MarkFulfilled({orderId: item.orderId, productId: item.productId}),
129
132
  ))
@@ -82,6 +82,7 @@ let AutoFulfillSpec = {
82
82
  maxRetries: 3,
83
83
  heartbeatInterval: 60,
84
84
  targetName: "MarkFulfilled",
85
+ schedule: undefined,
85
86
  authorizationOf: authorizationOf,
86
87
  commandTransition: commandTransition,
87
88
  traits: traits
@@ -109,13 +110,18 @@ function resolve(_event) {
109
110
 
110
111
  }
111
112
 
113
+ function reschedule(param) {
114
+ return [];
115
+ }
116
+
112
117
  let FromOrderAggregate = AutomationSlice$Reventless.Mapping.Make({
113
118
  Id: Id$Reventless.$$String,
114
119
  name: name,
115
120
  eventSchema: eventSchema
116
121
  })(AutoFulfillSpec)({
117
122
  collect: collect,
118
- resolve: resolve
123
+ resolve: resolve,
124
+ reschedule: reschedule
119
125
  });
120
126
 
121
127
  function collect$1(event, param, _ctx) {
@@ -142,16 +148,21 @@ function resolve$1(event) {
142
148
  }
143
149
  }
144
150
 
151
+ function reschedule$1(param) {
152
+ return [];
153
+ }
154
+
145
155
  let FromInventoryDcb = AutomationSlice$Reventless.Mapping.Make({
146
156
  Id: Id$Reventless.$$String,
147
157
  name: name$1,
148
158
  eventSchema: eventSchema$1
149
159
  })(AutoFulfillSpec)({
150
160
  collect: collect$1,
151
- resolve: resolve$1
161
+ resolve: resolve$1,
162
+ reschedule: reschedule$1
152
163
  });
153
164
 
154
- function process(id, item) {
165
+ function process(id, item, param) {
155
166
  return [
156
167
  id,
157
168
  {
@@ -21,10 +21,10 @@ describe("Scheduler_Builder.Make:", () => {
21
21
  count := count.contents + 1
22
22
  },
23
23
  )
24
- // createSchedule async body has no await — setInterval registered synchronously
24
+ // createSchedule async body has no await — the timer is set synchronously
25
25
  let _ = ops.createSchedule(
26
26
  [makeTopicResource("sched-recurring")],
27
- {name: "test-recurring", rate: Reventless.Schedule.Minutes(1), payload: "{}"},
27
+ {name: "test-recurring", time: Every(Minutes(1)), payload: "{}"},
28
28
  )
29
29
  // Two microtask ticks: publishEvent uses Effect.all(concurrency=unbounded) which forks
30
30
  // child fibers via startFork — one tick runs the child fiber (Queue.offer),
@@ -55,11 +55,11 @@ describe("Scheduler_Builder.Make:", () => {
55
55
  [makeTopicResource("sched-single")],
56
56
  {
57
57
  name: "test-single",
58
- rate: Reventless.Schedule.Single(2024, 1, 1, 0, 0),
58
+ time: Once({moment: After(Minutes(1))}),
59
59
  payload: "{}",
60
60
  },
61
61
  )
62
- jest->advanceTimersByTime(0)
62
+ jest->advanceTimersByTime(60 * 1000)
63
63
  let _ = await Promise.resolve()
64
64
  let _ = await Promise.resolve()
65
65
  expect(count.contents)->toBe(1)
@@ -83,7 +83,7 @@ describe("Scheduler_Builder.Make:", () => {
83
83
  )
84
84
  let _ = ops.createSchedule(
85
85
  [makeTopicResource("sched-delete")],
86
- {name: "test-delete", rate: Reventless.Schedule.Minutes(2), payload: "{}"},
86
+ {name: "test-delete", time: Every(Minutes(2)), payload: "{}"},
87
87
  )
88
88
  let _ = ops.deleteSchedule([], "test-delete")
89
89
  jest->advanceTimersByTime(2 * 60 * 1000)