@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.
- package/CHANGELOG.md +36 -0
- package/package.json +11 -11
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +11 -51
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -40
- package/src/adapter/Scheduler/LocalTimers.res +62 -0
- package/src/adapter/Scheduler/LocalTimers.res.mjs +63 -0
- package/src/components/AutomationSlice_Builder.res +31 -2
- package/src/components/AutomationSlice_Builder.res.mjs +31 -3
- package/src/components/LocalAutomationSchedules.res +59 -0
- package/src/components/LocalAutomationSchedules.res.mjs +72 -0
- package/src/components/LocalAutomationSweep.res +61 -0
- package/src/components/LocalAutomationSweep.res.mjs +83 -0
- package/src/test/TestRunner.res +2 -0
- package/src/test/TestRunner.res.mjs +12 -0
- package/tests/adapter/ScheduledPublisherTest.res +156 -155
- package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
- 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 +6 -2
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +16 -4
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +6 -1
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +8 -2
- package/tests/components/automationslice/AutomationSweepFixtures.res +131 -0
- package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +223 -0
- package/tests/components/automationslice/AutomationSweepTest.res +107 -0
- package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +4 -1
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +14 -3
- package/tests/components/scheduler/SchedulerTest.res +5 -5
- 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 —
|
|
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",
|
|
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
|
-
|
|
58
|
+
time: Once({moment: After(Minutes(1))}),
|
|
59
59
|
payload: "{}",
|
|
60
60
|
},
|
|
61
61
|
)
|
|
62
|
-
jest->advanceTimersByTime(
|
|
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",
|
|
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)
|