@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.
- package/CHANGELOG.md +19 -0
- package/package.json +9 -9
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +43 -36
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +32 -25
- package/src/components/AutomationSlice_Builder.res +10 -2
- package/src/components/AutomationSlice_Builder.res.mjs +13 -2
- package/src/components/LocalAutomationSweep.res +61 -0
- package/src/components/LocalAutomationSweep.res.mjs +83 -0
- package/src/test/TestRunner.res +1 -0
- package/src/test/TestRunner.res.mjs +6 -0
- package/tests/adapter/ScheduledPublisherTest.res +156 -155
- package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
- package/tests/components/automationslice/AutomationSliceFixtures.res +2 -2
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +2 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +4 -1
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +1 -1
- package/tests/components/automationslice/AutomationSweepFixtures.res +129 -0
- package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +217 -0
- package/tests/components/automationslice/AutomationSweepTest.res +107 -0
- package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -1
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +1 -1
- package/tests/components/scheduler/SchedulerTest.res +5 -5
- package/tests/components/scheduler/SchedulerTest.res.mjs +22 -14
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
authorizationOf: authorizationOf,
|
|
68
|
+
commandTransition: commandTransition,
|
|
69
|
+
traits: traits
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function collect(event, param, ctx) {
|
|
73
|
+
let match = ctx.recordedAt;
|
|
74
|
+
if (match === undefined) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
let orderId = event.orderId;
|
|
78
|
+
return [[
|
|
79
|
+
orderId,
|
|
80
|
+
{
|
|
81
|
+
orderId: orderId,
|
|
82
|
+
dueAt: match + event.within
|
|
83
|
+
}
|
|
84
|
+
]];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolve(_event) {
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let FromOrders = AutomationSlice$Reventless.Mapping.Make({
|
|
92
|
+
Id: Id$Reventless.$$String,
|
|
93
|
+
name: name,
|
|
94
|
+
eventSchema: eventSchema
|
|
95
|
+
})(CancelUnshippedSpec)({
|
|
96
|
+
collect: collect,
|
|
97
|
+
resolve: resolve
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function process(id, item, now) {
|
|
101
|
+
if (now >= item.dueAt) {
|
|
102
|
+
return [
|
|
103
|
+
id,
|
|
104
|
+
{
|
|
105
|
+
TAG: "CancelOrder",
|
|
106
|
+
orderId: item.orderId
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onExhausted(_id, _item) {
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let moduleUrl$1 = import.meta.url;
|
|
117
|
+
|
|
118
|
+
AutomationSlice$Reventless.Mappings.Make(CancelUnshippedSpec);
|
|
119
|
+
|
|
120
|
+
let mappings = [FromOrders];
|
|
121
|
+
|
|
122
|
+
let CancelUnshippedAutomation = {
|
|
123
|
+
process: process,
|
|
124
|
+
onExhausted: onExhausted,
|
|
125
|
+
moduleUrl: moduleUrl$1,
|
|
126
|
+
mappings: mappings
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let orderResource = Adapter$ReventlessInfra.make(Pulumi.output(name), Pulumi.output(name), Pulumi.output(name), Pulumi.output("memory:InMemory"), undefined, undefined, undefined, undefined, undefined, undefined);
|
|
130
|
+
|
|
131
|
+
let allEventTopics = Object.fromEntries([[
|
|
132
|
+
name,
|
|
133
|
+
{
|
|
134
|
+
resources: [orderResource]
|
|
135
|
+
}
|
|
136
|
+
]]);
|
|
137
|
+
|
|
138
|
+
let Bus = LocalBus$ReventlessLocal.Make({});
|
|
139
|
+
|
|
140
|
+
TestRunner$ReventlessLocal.setup();
|
|
141
|
+
|
|
142
|
+
let AutomationSliceMaker = AutomationSlice_Builder$ReventlessLocal.Make(Bus);
|
|
143
|
+
|
|
144
|
+
let CancelUnshipped = AutomationSliceMaker.Make(CancelUnshippedSpec)(CancelUnshippedAutomation);
|
|
145
|
+
|
|
146
|
+
let publishedCommands = {
|
|
147
|
+
contents: []
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
let failPublish = {
|
|
151
|
+
contents: false
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
async function publishJsons(cmds) {
|
|
155
|
+
if (failPublish.contents) {
|
|
156
|
+
return Stdlib_JsError.throwWithMessage("command topic unavailable");
|
|
157
|
+
} else {
|
|
158
|
+
publishedCommands.contents = publishedCommands.contents.concat(cmds);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let slice = CancelUnshipped.make(allEventTopics, Pulumi.output(publishJsons), {
|
|
164
|
+
environment: "local",
|
|
165
|
+
platformName: "local",
|
|
166
|
+
pluginName: "TestPlugin",
|
|
167
|
+
sliceName: name$1
|
|
168
|
+
}, undefined, undefined);
|
|
169
|
+
|
|
170
|
+
async function placeOrder(orderId, within) {
|
|
171
|
+
let newrecord = {...TestFixtures$ReventlessLocal.makeTestMeta(name)};
|
|
172
|
+
newrecord.time = new Date(Date.now()).toISOString();
|
|
173
|
+
let eventJson = Object.fromEntries([
|
|
174
|
+
[
|
|
175
|
+
"id",
|
|
176
|
+
orderId
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
"meta",
|
|
180
|
+
Util_Sury$Reventless.toJson(newrecord, Message$Reventless.metaSchema)
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
"event",
|
|
184
|
+
Util_Sury$Reventless.toJson({
|
|
185
|
+
TAG: "OrderPlaced",
|
|
186
|
+
orderId: orderId,
|
|
187
|
+
within: within
|
|
188
|
+
}, eventSchema)
|
|
189
|
+
]
|
|
190
|
+
]);
|
|
191
|
+
await Bus.publishEvent(name, "test", newrecord, eventJson);
|
|
192
|
+
await Promise.resolve();
|
|
193
|
+
await Promise.resolve();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function storedRow(id) {
|
|
197
|
+
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));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
OrderSource,
|
|
202
|
+
CancelUnshippedSpec,
|
|
203
|
+
FromOrders,
|
|
204
|
+
CancelUnshippedAutomation,
|
|
205
|
+
orderResource,
|
|
206
|
+
allEventTopics,
|
|
207
|
+
Bus,
|
|
208
|
+
AutomationSliceMaker,
|
|
209
|
+
CancelUnshipped,
|
|
210
|
+
publishedCommands,
|
|
211
|
+
failPublish,
|
|
212
|
+
publishJsons,
|
|
213
|
+
slice,
|
|
214
|
+
placeOrder,
|
|
215
|
+
storedRow,
|
|
216
|
+
}
|
|
217
|
+
/* 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 */
|
|
@@ -123,7 +123,7 @@ module FromInventoryDcb = AutomationSlice.Mapping.Make(
|
|
|
123
123
|
// ─────────────────────────────────────────────────────────────
|
|
124
124
|
|
|
125
125
|
module AutoFulfillAutomation: AutomationSlice.Automation with module Spec := AutoFulfillSpec = {
|
|
126
|
-
let process = (id, item: AutoFulfillSpec.todoItem) => Some((
|
|
126
|
+
let process = (id, item: AutoFulfillSpec.todoItem, ~now as _) => Some((
|
|
127
127
|
id,
|
|
128
128
|
AutoFulfillSpec.MarkFulfilled({orderId: item.orderId, productId: item.productId}),
|
|
129
129
|
))
|
|
@@ -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)
|
|
@@ -16,9 +16,12 @@ globalThis.describe("Scheduler_Builder.Make:", () => {
|
|
|
16
16
|
});
|
|
17
17
|
ops.createSchedule([SchedulerFixtures$ReventlessLocal.makeTopicResource("sched-recurring")], {
|
|
18
18
|
name: "test-recurring",
|
|
19
|
-
|
|
20
|
-
TAG: "
|
|
21
|
-
_0:
|
|
19
|
+
time: {
|
|
20
|
+
TAG: "Every",
|
|
21
|
+
_0: {
|
|
22
|
+
TAG: "Minutes",
|
|
23
|
+
_0: 1
|
|
24
|
+
}
|
|
22
25
|
},
|
|
23
26
|
payload: "{}"
|
|
24
27
|
});
|
|
@@ -41,17 +44,19 @@ globalThis.describe("Scheduler_Builder.Make:", () => {
|
|
|
41
44
|
});
|
|
42
45
|
ops.createSchedule([SchedulerFixtures$ReventlessLocal.makeTopicResource("sched-single")], {
|
|
43
46
|
name: "test-single",
|
|
44
|
-
|
|
45
|
-
TAG: "
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
time: {
|
|
48
|
+
TAG: "Once",
|
|
49
|
+
moment: {
|
|
50
|
+
TAG: "After",
|
|
51
|
+
_0: {
|
|
52
|
+
TAG: "Minutes",
|
|
53
|
+
_0: 1
|
|
54
|
+
}
|
|
55
|
+
}
|
|
51
56
|
},
|
|
52
57
|
payload: "{}"
|
|
53
58
|
});
|
|
54
|
-
Globals.jest.advanceTimersByTime(
|
|
59
|
+
Globals.jest.advanceTimersByTime(60000);
|
|
55
60
|
await Promise.resolve();
|
|
56
61
|
await Promise.resolve();
|
|
57
62
|
globalThis.expect(count.contents).toBe(1);
|
|
@@ -70,9 +75,12 @@ globalThis.describe("Scheduler_Builder.Make:", () => {
|
|
|
70
75
|
});
|
|
71
76
|
ops.createSchedule([SchedulerFixtures$ReventlessLocal.makeTopicResource("sched-delete")], {
|
|
72
77
|
name: "test-delete",
|
|
73
|
-
|
|
74
|
-
TAG: "
|
|
75
|
-
_0:
|
|
78
|
+
time: {
|
|
79
|
+
TAG: "Every",
|
|
80
|
+
_0: {
|
|
81
|
+
TAG: "Minutes",
|
|
82
|
+
_0: 2
|
|
83
|
+
}
|
|
76
84
|
},
|
|
77
85
|
payload: "{}"
|
|
78
86
|
});
|