@reventlessdev/reventless-local 3.0.0-alpha.286 → 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 +17 -0
- package/package.json +10 -10
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +5 -52
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -47
- package/src/adapter/Scheduler/LocalTimers.res +62 -0
- package/src/adapter/Scheduler/LocalTimers.res.mjs +63 -0
- package/src/components/AutomationSlice_Builder.res +23 -2
- package/src/components/AutomationSlice_Builder.res.mjs +19 -2
- package/src/components/LocalAutomationSchedules.res +59 -0
- package/src/components/LocalAutomationSchedules.res.mjs +72 -0
- package/src/test/TestRunner.res +1 -0
- package/src/test/TestRunner.res.mjs +6 -0
- 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 +4 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +14 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +2 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +7 -1
- package/tests/components/automationslice/AutomationSweepFixtures.res +2 -0
- package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +7 -1
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +3 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.287 (2026-10-16)
|
|
7
|
+
|
|
8
|
+
* feat!(automation): schedules set by events ([53bc436](https://github.com/ReventlessDev/reventless-core/commit/53bc4363dffce81ef36475728bf0dae704716021))
|
|
9
|
+
* feat!(automation): declared schedules tick an automation ([420acd2](https://github.com/ReventlessDev/reventless-core/commit/420acd2233aadf700141a31d9d3e0fdc8074bef7))
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **examples:** a marketing digest at the time each customer chooses ([3521b10](https://github.com/ReventlessDev/reventless-core/commit/3521b10c4f3ff64f3e4561e94e051892cdfe5d5f))
|
|
13
|
+
|
|
14
|
+
### BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* a hand-written AutomationSlice.Mapping.Make the PPX does
|
|
17
|
+
not see must define `let reschedule = _ => []`.
|
|
18
|
+
* a hand-written AutomationSlice spec the PPX does not see must
|
|
19
|
+
declare `let schedule = None`.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
# 3.0.0-alpha.286 (2026-10-15)
|
|
7
24
|
|
|
8
25
|
* feat!: an automation's process takes ~now, and collect the event's recorded time ([8c8f3f5](https://github.com/ReventlessDev/reventless-core/commit/8c8f3f570474b1658b605b726077debc7d6a1dc7))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.287",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -35,23 +35,23 @@
|
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "1.0.0-alpha.0",
|
|
38
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
39
|
-
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
|
|
40
38
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
39
|
+
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
|
|
41
40
|
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
42
|
-
"@reventlessdev/
|
|
43
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.143",
|
|
44
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.243",
|
|
41
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
45
42
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
|
|
46
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.144",
|
|
44
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.296",
|
|
45
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.244",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.193",
|
|
47
47
|
"@reventlessdev/reventless-seed": "1.0.0-alpha.34",
|
|
48
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
49
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.165",
|
|
49
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.160"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
|
53
53
|
"sury-ppx": "11.0.0",
|
|
54
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
54
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.101"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"rescript": "^12.3.1"
|
|
@@ -9,44 +9,7 @@
|
|
|
9
9
|
// and pass the resulting component to Plugin_Builder or Task_Builder.
|
|
10
10
|
|
|
11
11
|
module Make = (Bus: LocalBus.T) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@val
|
|
15
|
-
external setTimeoutJs: (unit => unit, float) => timerHandle = "setTimeout"
|
|
16
|
-
@val
|
|
17
|
-
external clearTimerJs: timerHandle => unit = "clearTimeout"
|
|
18
|
-
|
|
19
|
-
let activeTimers: ref<dict<timerHandle>> = ref(Dict.make())
|
|
20
|
-
|
|
21
|
-
// Node fires a delay above 2^31 - 1 ms at once, so a far firing is reached a day at a time.
|
|
22
|
-
let maxWaitMs = 86_400_000.
|
|
23
|
-
|
|
24
|
-
let stop = name =>
|
|
25
|
-
switch activeTimers.contents->Dict.get(name) {
|
|
26
|
-
| Some(handle) =>
|
|
27
|
-
clearTimerJs(handle)
|
|
28
|
-
activeTimers.contents->Dict.delete(name)
|
|
29
|
-
| None => ()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let rec waitUntil = (name, at, fire) => {
|
|
33
|
-
let delay = at -. Date.now()
|
|
34
|
-
let handle =
|
|
35
|
-
delay > maxWaitMs
|
|
36
|
-
? setTimeoutJs(() => waitUntil(name, at, fire), maxWaitMs)
|
|
37
|
-
: setTimeoutJs(fire, Math.max(delay, 0.))
|
|
38
|
-
activeTimers.contents->Dict.set(name, handle)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let rec arm = (name, time, ~after, ~publish) =>
|
|
42
|
-
switch ReventlessCore.ScheduleTime.nextFiring(time, ~after) {
|
|
43
|
-
| None => activeTimers.contents->Dict.delete(name)
|
|
44
|
-
| Some(at) =>
|
|
45
|
-
waitUntil(name, at, () => {
|
|
46
|
-
publish()
|
|
47
|
-
arm(name, time, ~after=at, ~publish)
|
|
48
|
-
})
|
|
49
|
-
}
|
|
12
|
+
let timers = LocalTimers.make()
|
|
50
13
|
|
|
51
14
|
let scheduleMeta: ReventlessCore.Message.meta = {
|
|
52
15
|
service: "Scheduler",
|
|
@@ -62,7 +25,6 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
62
25
|
channelResources,
|
|
63
26
|
schedule,
|
|
64
27
|
) => {
|
|
65
|
-
ReventlessCore.ScheduleTime.validate(schedule.time)
|
|
66
28
|
let topicName = switch channelResources {
|
|
67
29
|
| [] => ""
|
|
68
30
|
| resources => (resources->Array.getUnsafe(0)).name
|
|
@@ -71,19 +33,13 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
71
33
|
| json => json
|
|
72
34
|
| exception _ => schedule.payload->JSON.Encode.string
|
|
73
35
|
}
|
|
74
|
-
|
|
36
|
+
timers->LocalTimers.start(schedule.name, schedule.time, ~setAt=Date.now(), ~fire=_ =>
|
|
75
37
|
Bus.publishEvent(topicName, "Scheduler", scheduleMeta, payloadJson)->ignore
|
|
76
|
-
let setAt = Date.now()
|
|
77
|
-
stop(schedule.name)
|
|
78
|
-
arm(
|
|
79
|
-
schedule.name,
|
|
80
|
-
schedule.time->ReventlessCore.ScheduleTime.resolveOnce(~from=setAt),
|
|
81
|
-
~after=setAt,
|
|
82
|
-
~publish,
|
|
83
38
|
)
|
|
84
39
|
}
|
|
85
40
|
|
|
86
|
-
let deleteSchedule: ReventlessInfra.Scheduler.deleteSchedule = async (_, name) =>
|
|
41
|
+
let deleteSchedule: ReventlessInfra.Scheduler.deleteSchedule = async (_, name) =>
|
|
42
|
+
timers->LocalTimers.stop(name)
|
|
87
43
|
|
|
88
44
|
{
|
|
89
45
|
resource: ReventlessInfra.Adapter.make(
|
|
@@ -102,8 +58,5 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
102
58
|
}
|
|
103
59
|
|
|
104
60
|
// Clear all active timers — call in afterAll/afterEach for test isolation.
|
|
105
|
-
let reset = () =>
|
|
106
|
-
activeTimers.contents->Dict.valuesToArray->Array.forEach(clearTimerJs)
|
|
107
|
-
activeTimers.contents = Dict.make()
|
|
108
|
-
}
|
|
61
|
+
let reset = () => timers->LocalTimers.reset
|
|
109
62
|
}
|
|
@@ -1,38 +1,11 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
3
|
import * as Pulumi from "@pulumi/pulumi";
|
|
5
|
-
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
6
4
|
import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
|
|
7
|
-
import * as
|
|
5
|
+
import * as LocalTimers$ReventlessLocal from "./LocalTimers.res.mjs";
|
|
8
6
|
|
|
9
7
|
function Make(Bus) {
|
|
10
|
-
let
|
|
11
|
-
contents: {}
|
|
12
|
-
};
|
|
13
|
-
let stop = name => {
|
|
14
|
-
let handle = activeTimers.contents[name];
|
|
15
|
-
if (handle !== undefined) {
|
|
16
|
-
clearTimeout(Primitive_option.valFromOption(handle));
|
|
17
|
-
return Stdlib_Dict.$$delete(activeTimers.contents, name);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
let waitUntil = (name, at, fire) => {
|
|
21
|
-
let delay = at - Date.now();
|
|
22
|
-
let handle = delay > 86400000 ? setTimeout(() => waitUntil(name, at, fire), 86400000) : setTimeout(fire, Math.max(delay, 0));
|
|
23
|
-
activeTimers.contents[name] = handle;
|
|
24
|
-
};
|
|
25
|
-
let arm = (name, time, after, publish) => {
|
|
26
|
-
let at = ScheduleTime$ReventlessCore.nextFiring(time, after);
|
|
27
|
-
if (at !== undefined) {
|
|
28
|
-
return waitUntil(name, at, () => {
|
|
29
|
-
publish();
|
|
30
|
-
arm(name, time, at, publish);
|
|
31
|
-
});
|
|
32
|
-
} else {
|
|
33
|
-
return Stdlib_Dict.$$delete(activeTimers.contents, name);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
8
|
+
let timers = LocalTimers$ReventlessLocal.make();
|
|
36
9
|
let scheduleMeta = {
|
|
37
10
|
service: "Scheduler",
|
|
38
11
|
time: "",
|
|
@@ -43,7 +16,6 @@ function Make(Bus) {
|
|
|
43
16
|
};
|
|
44
17
|
let make = (param, param$1) => {
|
|
45
18
|
let createSchedule = async (channelResources, schedule) => {
|
|
46
|
-
ScheduleTime$ReventlessCore.validate(schedule.time);
|
|
47
19
|
let topicName = channelResources.length !== 0 ? channelResources[0].name : "";
|
|
48
20
|
let payloadJson;
|
|
49
21
|
try {
|
|
@@ -51,14 +23,11 @@ function Make(Bus) {
|
|
|
51
23
|
} catch (exn) {
|
|
52
24
|
payloadJson = schedule.payload;
|
|
53
25
|
}
|
|
54
|
-
|
|
26
|
+
return LocalTimers$ReventlessLocal.start(timers, schedule.name, schedule.time, Date.now(), param => {
|
|
55
27
|
Bus.publishEvent(topicName, "Scheduler", scheduleMeta, payloadJson);
|
|
56
|
-
};
|
|
57
|
-
let setAt = Date.now();
|
|
58
|
-
stop(schedule.name);
|
|
59
|
-
return arm(schedule.name, ScheduleTime$ReventlessCore.resolveOnce(schedule.time, setAt), setAt, publish);
|
|
28
|
+
});
|
|
60
29
|
};
|
|
61
|
-
let deleteSchedule = async (param, name) => stop(name);
|
|
30
|
+
let deleteSchedule = async (param, name) => LocalTimers$ReventlessLocal.stop(timers, name);
|
|
62
31
|
return {
|
|
63
32
|
resource: Adapter$ReventlessInfra.make(Pulumi.output(""), Pulumi.output(""), Pulumi.output(""), Pulumi.output("memory:InMemory"), undefined, undefined, undefined, undefined, undefined, undefined),
|
|
64
33
|
operations: Pulumi.output({
|
|
@@ -67,18 +36,9 @@ function Make(Bus) {
|
|
|
67
36
|
})
|
|
68
37
|
};
|
|
69
38
|
};
|
|
70
|
-
let reset = () =>
|
|
71
|
-
Object.values(activeTimers.contents).forEach(prim => {
|
|
72
|
-
clearTimeout(prim);
|
|
73
|
-
});
|
|
74
|
-
activeTimers.contents = {};
|
|
75
|
-
};
|
|
39
|
+
let reset = () => LocalTimers$ReventlessLocal.reset(timers);
|
|
76
40
|
return {
|
|
77
|
-
|
|
78
|
-
maxWaitMs: 86400000,
|
|
79
|
-
stop: stop,
|
|
80
|
-
waitUntil: waitUntil,
|
|
81
|
-
arm: arm,
|
|
41
|
+
timers: timers,
|
|
82
42
|
scheduleMeta: scheduleMeta,
|
|
83
43
|
make: make,
|
|
84
44
|
reset: reset
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Timers that wait for each firing of a `Schedule.time`, as `ScheduleTime.nextFiring`
|
|
2
|
+
// names it, and call `fire` with the scheduled time. Shared by the scheduled publisher
|
|
3
|
+
// and the automations' schedules, so both fire when AWS would.
|
|
4
|
+
|
|
5
|
+
type timerHandle
|
|
6
|
+
|
|
7
|
+
@val
|
|
8
|
+
external setTimeoutJs: (unit => unit, float) => timerHandle = "setTimeout"
|
|
9
|
+
@val
|
|
10
|
+
external clearTimerJs: timerHandle => unit = "clearTimeout"
|
|
11
|
+
|
|
12
|
+
/** The pending wait of each timer, by name. */
|
|
13
|
+
type t = ref<dict<timerHandle>>
|
|
14
|
+
|
|
15
|
+
let make = (): t => ref(Dict.make())
|
|
16
|
+
|
|
17
|
+
// Node fires a delay above 2^31 - 1 ms at once, so a far firing is reached a day at a time.
|
|
18
|
+
let maxWaitMs = 86_400_000.
|
|
19
|
+
|
|
20
|
+
let stop = (timers: t, name) =>
|
|
21
|
+
switch timers.contents->Dict.get(name) {
|
|
22
|
+
| Some(handle) =>
|
|
23
|
+
clearTimerJs(handle)
|
|
24
|
+
timers.contents->Dict.delete(name)
|
|
25
|
+
| None => ()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let rec waitUntil = (timers: t, name, at, fire) => {
|
|
29
|
+
let delay = at -. Date.now()
|
|
30
|
+
let handle =
|
|
31
|
+
delay > maxWaitMs
|
|
32
|
+
? setTimeoutJs(() => waitUntil(timers, name, at, fire), maxWaitMs)
|
|
33
|
+
: setTimeoutJs(fire, Math.max(delay, 0.))
|
|
34
|
+
timers.contents->Dict.set(name, handle)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let rec arm = (timers: t, name, time, ~after, ~fire) =>
|
|
38
|
+
switch ReventlessCore.ScheduleTime.nextFiring(time, ~after) {
|
|
39
|
+
| None => timers.contents->Dict.delete(name)
|
|
40
|
+
| Some(at) =>
|
|
41
|
+
waitUntil(timers, name, at, () => {
|
|
42
|
+
fire(at)
|
|
43
|
+
arm(timers, name, time, ~after=at, ~fire)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
Fires `fire(at)` at every firing of `time` after `setAt`, replacing a timer of the
|
|
49
|
+
same name. A once-only time fires once; one already past never. Throws
|
|
50
|
+
`ScheduleTime.InvalidTime` for a time that cannot fire.
|
|
51
|
+
*/
|
|
52
|
+
let start = (timers: t, name, time, ~setAt, ~fire) => {
|
|
53
|
+
ReventlessCore.ScheduleTime.validate(time)
|
|
54
|
+
stop(timers, name)
|
|
55
|
+
arm(timers, name, time->ReventlessCore.ScheduleTime.resolveOnce(~from=setAt), ~after=setAt, ~fire)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Clears every timer — call in afterAll/afterEach for test isolation. */
|
|
59
|
+
let reset = (timers: t) => {
|
|
60
|
+
timers.contents->Dict.valuesToArray->Array.forEach(clearTimerJs)
|
|
61
|
+
timers.contents = Dict.make()
|
|
62
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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 Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
5
|
+
import * as ScheduleTime$ReventlessCore from "@reventlessdev/reventless-core/src/util/ScheduleTime.res.mjs";
|
|
6
|
+
|
|
7
|
+
function make() {
|
|
8
|
+
return {
|
|
9
|
+
contents: {}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stop(timers, name) {
|
|
14
|
+
let handle = timers.contents[name];
|
|
15
|
+
if (handle !== undefined) {
|
|
16
|
+
clearTimeout(Primitive_option.valFromOption(handle));
|
|
17
|
+
return Stdlib_Dict.$$delete(timers.contents, name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function waitUntil(timers, name, at, fire) {
|
|
22
|
+
let delay = at - Date.now();
|
|
23
|
+
let handle = delay > 86400000 ? setTimeout(() => waitUntil(timers, name, at, fire), 86400000) : setTimeout(fire, Math.max(delay, 0));
|
|
24
|
+
timers.contents[name] = handle;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function arm(timers, name, time, after, fire) {
|
|
28
|
+
let at = ScheduleTime$ReventlessCore.nextFiring(time, after);
|
|
29
|
+
if (at !== undefined) {
|
|
30
|
+
return waitUntil(timers, name, at, () => {
|
|
31
|
+
fire(at);
|
|
32
|
+
arm(timers, name, time, at, fire);
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
return Stdlib_Dict.$$delete(timers.contents, name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function start(timers, name, time, setAt, fire) {
|
|
40
|
+
ScheduleTime$ReventlessCore.validate(time);
|
|
41
|
+
stop(timers, name);
|
|
42
|
+
arm(timers, name, ScheduleTime$ReventlessCore.resolveOnce(time, setAt), setAt, fire);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function reset(timers) {
|
|
46
|
+
Object.values(timers.contents).forEach(prim => {
|
|
47
|
+
clearTimeout(prim);
|
|
48
|
+
});
|
|
49
|
+
timers.contents = {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let maxWaitMs = 86400000;
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
make,
|
|
56
|
+
maxWaitMs,
|
|
57
|
+
stop,
|
|
58
|
+
waitUntil,
|
|
59
|
+
arm,
|
|
60
|
+
start,
|
|
61
|
+
reset,
|
|
62
|
+
}
|
|
63
|
+
/* ScheduleTime-ReventlessCore Not a pure module */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// In-memory AutomationSlice builder.
|
|
2
2
|
// Wires in-memory adapters and delegates to the core ReventlessCore.AutomationSlice_Builder;
|
|
3
|
-
// each slice it makes joins the local automation sweep.
|
|
3
|
+
// each slice it makes joins the local automation sweep, and its schedules start.
|
|
4
4
|
|
|
5
5
|
module Make = (Bus: LocalBus.T) => {
|
|
6
6
|
module RuntimeEnvironment = LocalRuntimeEnvironment
|
|
@@ -25,6 +25,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
25
25
|
EventCollectorChannel,
|
|
26
26
|
EventCollectorRuntimeBuilder,
|
|
27
27
|
Api,
|
|
28
|
+
LocalAutomationSchedules,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
module Make = (
|
|
@@ -36,13 +37,33 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
36
37
|
module Automation = Automation
|
|
37
38
|
type component = Inner.component
|
|
38
39
|
let queryDbName = Inner.queryDbName
|
|
40
|
+
let schedulesDbName = Inner.schedulesDbName
|
|
39
41
|
let sourceNames = Inner.sourceNames
|
|
40
42
|
let make = (~allEventTopics, ~publishJsons, ~context, ~runtime=?, ~opts=?) => {
|
|
41
43
|
let component = Inner.make(~allEventTopics, ~publishJsons, ~context, ~runtime?, ~opts?)
|
|
44
|
+
let operations = () => component->ReventlessCore.Component.operations->TestRunner.resolve
|
|
42
45
|
LocalAutomationSweep.register(~name=Spec.name, async () => {
|
|
43
|
-
let ops = await
|
|
46
|
+
let ops = await operations()
|
|
44
47
|
await ops.processPending()
|
|
45
48
|
})
|
|
49
|
+
LocalAutomationSchedules.register(~slice=Spec.name, async (~key, ~at) => {
|
|
50
|
+
let ops = await operations()
|
|
51
|
+
await ops.tick(~key, ~at)
|
|
52
|
+
})
|
|
53
|
+
// As a deploy does on AWS: the declared time into the rows, the timers to the rows.
|
|
54
|
+
let _ =
|
|
55
|
+
operations()
|
|
56
|
+
->Promise.then(ops => ops.reconcileSchedules())
|
|
57
|
+
->Promise.catch(exn => {
|
|
58
|
+
ReventlessCore.EffectLogger.logError(
|
|
59
|
+
~comp=`AutomationSlice(${Spec.name})`,
|
|
60
|
+
`the schedules could not be reconciled: ${exn
|
|
61
|
+
->JsExn.fromException
|
|
62
|
+
->Option.flatMap(JsExn.message)
|
|
63
|
+
->Option.getOr("unknown")}`,
|
|
64
|
+
)->Effect.runSync
|
|
65
|
+
Promise.resolve()
|
|
66
|
+
})
|
|
46
67
|
component
|
|
47
68
|
}
|
|
48
69
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
3
7
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
4
8
|
import * as TestRunner$ReventlessLocal from "../test/TestRunner.res.mjs";
|
|
9
|
+
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
5
10
|
import * as LocalQueryDbStorage$ReventlessLocal from "../adapter/QueryDb/LocalQueryDbStorage.res.mjs";
|
|
6
11
|
import * as LocalAutomationSweep$ReventlessLocal from "./LocalAutomationSweep.res.mjs";
|
|
7
12
|
import * as AutomationSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs";
|
|
8
13
|
import * as LocalRuntimeEnvironment$ReventlessLocal from "../adapter/Runtime/LocalRuntimeEnvironment.res.mjs";
|
|
14
|
+
import * as LocalAutomationSchedules$ReventlessLocal from "./LocalAutomationSchedules.res.mjs";
|
|
9
15
|
import * as QueryDbResolvers_GraphQL$ReventlessLocal from "../adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs";
|
|
10
16
|
import * as LocalEventCollectorChannel$ReventlessLocal from "../adapter/EventCollector/LocalEventCollectorChannel.res.mjs";
|
|
11
17
|
import * as LocalEventCollectorRuntime_Builder$ReventlessLocal from "../adapter/Runtime/LocalEventCollectorRuntime_Builder.res.mjs";
|
|
@@ -34,7 +40,9 @@ function Make(Bus) {
|
|
|
34
40
|
make: QueryDbStorage.make
|
|
35
41
|
})({
|
|
36
42
|
make: QueryDbResolvers.make
|
|
37
|
-
})(EventCollectorChannel)(EventCollectorRuntimeBuilder)(Api)
|
|
43
|
+
})(EventCollectorChannel)(EventCollectorRuntimeBuilder)(Api)({
|
|
44
|
+
platformFor: LocalAutomationSchedules$ReventlessLocal.platformFor
|
|
45
|
+
});
|
|
38
46
|
let Make$1 = Spec => (Automation => {
|
|
39
47
|
let Inner = CoreMaker.Make(Spec)(Automation);
|
|
40
48
|
let make = (allEventTopics, publishJsons, context, runtime, opts) => {
|
|
@@ -43,12 +51,21 @@ function Make(Bus) {
|
|
|
43
51
|
let ops = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(component));
|
|
44
52
|
return await ops.processPending();
|
|
45
53
|
});
|
|
54
|
+
LocalAutomationSchedules$ReventlessLocal.register(Spec.name, async (key, at) => {
|
|
55
|
+
let ops = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(component));
|
|
56
|
+
return await ops.tick(key, at);
|
|
57
|
+
});
|
|
58
|
+
Stdlib_Promise.$$catch(TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(component)).then(ops => ops.reconcileSchedules()), exn => {
|
|
59
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `the schedules could not be reconciled: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown")));
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
});
|
|
46
62
|
return component;
|
|
47
63
|
};
|
|
48
64
|
return {
|
|
49
65
|
Spec: Spec,
|
|
50
66
|
Automation: Automation,
|
|
51
67
|
queryDbName: Inner.queryDbName,
|
|
68
|
+
schedulesDbName: Inner.schedulesDbName,
|
|
52
69
|
sourceNames: Inner.sourceNames,
|
|
53
70
|
make: make
|
|
54
71
|
};
|
|
@@ -68,4 +85,4 @@ function Make(Bus) {
|
|
|
68
85
|
export {
|
|
69
86
|
Make,
|
|
70
87
|
}
|
|
71
|
-
/*
|
|
88
|
+
/* Stdlib_JsExn Not a pure module */
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The local platform's Scheduler for automations: one timer per slice and key, which
|
|
2
|
+
// delivers `Tick({key, at})` to the slice when its time comes, as EventBridge
|
|
3
|
+
// Scheduler does on AWS. A slice registers how it is ticked when it is made; its
|
|
4
|
+
// schedules are put by its rows, through `platformFor`.
|
|
5
|
+
|
|
6
|
+
let timers = LocalTimers.make()
|
|
7
|
+
|
|
8
|
+
let ticks: dict<(~key: string, ~at: float) => promise<unit>> = Dict.make()
|
|
9
|
+
|
|
10
|
+
let timerName = (~slice, ~key) => `${slice}/${key}`
|
|
11
|
+
|
|
12
|
+
let tickFailed = (~slice, ~key, message) =>
|
|
13
|
+
ReventlessCore.EffectLogger.logError(
|
|
14
|
+
~comp="LocalAutomationSchedules",
|
|
15
|
+
`tick ${key} of ${slice} failed: ${message}`,
|
|
16
|
+
)->Effect.runSync
|
|
17
|
+
|
|
18
|
+
/** How the slice is ticked when one of its schedules comes due. */
|
|
19
|
+
let register = (~slice, tick) => ticks->Dict.set(slice, tick)
|
|
20
|
+
|
|
21
|
+
let fire = (~slice, ~key, at) =>
|
|
22
|
+
switch ticks->Dict.get(slice) {
|
|
23
|
+
| Some(tick) =>
|
|
24
|
+
let _ = tick(~key, ~at)->Promise.catch(exn => {
|
|
25
|
+
tickFailed(
|
|
26
|
+
~slice,
|
|
27
|
+
~key,
|
|
28
|
+
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"),
|
|
29
|
+
)
|
|
30
|
+
Promise.resolve()
|
|
31
|
+
})
|
|
32
|
+
| None => tickFailed(~slice, ~key, "the slice is not registered")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The slice's schedules on the local platform, named by their keys. */
|
|
36
|
+
let platformFor = (~slice): ReventlessInfra.AutomationSlice.platformSchedules => {
|
|
37
|
+
put: async (~key, time) =>
|
|
38
|
+
timers->LocalTimers.start(timerName(~slice, ~key), time, ~setAt=Date.now(), ~fire=at =>
|
|
39
|
+
fire(~slice, ~key, at)
|
|
40
|
+
),
|
|
41
|
+
removeName: async key => timers->LocalTimers.stop(timerName(~slice, ~key)),
|
|
42
|
+
names: async () => {
|
|
43
|
+
let prefix = timerName(~slice, ~key="")
|
|
44
|
+
timers.contents
|
|
45
|
+
->Dict.keysToArray
|
|
46
|
+
->Array.filterMap(name =>
|
|
47
|
+
name->String.startsWith(prefix)
|
|
48
|
+
? Some(name->String.slice(~start=prefix->String.length))
|
|
49
|
+
: None
|
|
50
|
+
)
|
|
51
|
+
},
|
|
52
|
+
nameOf: key => key,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Stops every schedule and forgets every slice — call in afterAll/afterEach for test isolation. */
|
|
56
|
+
let reset = () => {
|
|
57
|
+
timers->LocalTimers.reset
|
|
58
|
+
ticks->Dict.keysToArray->Array.forEach(slice => ticks->Dict.delete(slice))
|
|
59
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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 Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
9
|
+
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
10
|
+
import * as LocalTimers$ReventlessLocal from "../adapter/Scheduler/LocalTimers.res.mjs";
|
|
11
|
+
|
|
12
|
+
let timers = LocalTimers$ReventlessLocal.make();
|
|
13
|
+
|
|
14
|
+
let ticks = {};
|
|
15
|
+
|
|
16
|
+
function timerName(slice, key) {
|
|
17
|
+
return slice + `/` + key;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tickFailed(slice, key, message) {
|
|
21
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSchedules", undefined, `tick ` + key + ` of ` + slice + ` failed: ` + message));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function register(slice, tick) {
|
|
25
|
+
ticks[slice] = tick;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fire(slice, key, at) {
|
|
29
|
+
let tick = ticks[slice];
|
|
30
|
+
if (tick !== undefined) {
|
|
31
|
+
Stdlib_Promise.$$catch(tick(key, at), exn => {
|
|
32
|
+
tickFailed(slice, key, Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown"));
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
} else {
|
|
37
|
+
return tickFailed(slice, key, "the slice is not registered");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function platformFor(slice) {
|
|
42
|
+
return {
|
|
43
|
+
put: async (key, time) => LocalTimers$ReventlessLocal.start(timers, timerName(slice, key), time, Date.now(), at => fire(slice, key, at)),
|
|
44
|
+
removeName: async key => LocalTimers$ReventlessLocal.stop(timers, timerName(slice, key)),
|
|
45
|
+
names: async () => {
|
|
46
|
+
let prefix = timerName(slice, "");
|
|
47
|
+
return Stdlib_Array.filterMap(Object.keys(timers.contents), name => {
|
|
48
|
+
if (name.startsWith(prefix)) {
|
|
49
|
+
return name.slice(prefix.length);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
nameOf: key => key
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function reset() {
|
|
58
|
+
LocalTimers$ReventlessLocal.reset(timers);
|
|
59
|
+
Object.keys(ticks).forEach(slice => Stdlib_Dict.$$delete(ticks, slice));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
timers,
|
|
64
|
+
ticks,
|
|
65
|
+
timerName,
|
|
66
|
+
tickFailed,
|
|
67
|
+
register,
|
|
68
|
+
fire,
|
|
69
|
+
platformFor,
|
|
70
|
+
reset,
|
|
71
|
+
}
|
|
72
|
+
/* timers Not a pure module */
|
package/src/test/TestRunner.res
CHANGED
|
@@ -45,6 +45,7 @@ let resetGraphQLServer = () => DomainGraphQL_Server.reset()
|
|
|
45
45
|
// Call in afterAll when using Plugin_Builder with LocalHeartbeatRunner.
|
|
46
46
|
let resetHeartbeatRunner = () => LocalHeartbeatRunner.reset()
|
|
47
47
|
let resetAutomationSweep = () => LocalAutomationSweep.reset()
|
|
48
|
+
let resetAutomationSchedules = () => LocalAutomationSchedules.reset()
|
|
48
49
|
|
|
49
50
|
// ─────────────────────────────────────────────────────────────
|
|
50
51
|
// Bus test utilities
|
|
@@ -4,6 +4,7 @@ import * as Pulumi from "@pulumi/pulumi";
|
|
|
4
4
|
import * as DomainGraphQL_Server$ReventlessLocal from "../adapter/DomainGraphQL_Server.res.mjs";
|
|
5
5
|
import * as LocalAutomationSweep$ReventlessLocal from "../components/LocalAutomationSweep.res.mjs";
|
|
6
6
|
import * as LocalHeartbeatRunner$ReventlessLocal from "../adapter/Heartbeat/LocalHeartbeatRunner.res.mjs";
|
|
7
|
+
import * as LocalAutomationSchedules$ReventlessLocal from "../components/LocalAutomationSchedules.res.mjs";
|
|
7
8
|
|
|
8
9
|
function setup() {
|
|
9
10
|
Pulumi.runtime.setMocks({
|
|
@@ -35,6 +36,10 @@ function resetAutomationSweep() {
|
|
|
35
36
|
LocalAutomationSweep$ReventlessLocal.reset();
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function resetAutomationSchedules() {
|
|
40
|
+
LocalAutomationSchedules$ReventlessLocal.reset();
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
function collectNEvents(subscribeToEvents, topicName, n) {
|
|
39
44
|
if (n <= 0) {
|
|
40
45
|
return Promise.resolve([]);
|
|
@@ -72,6 +77,7 @@ export {
|
|
|
72
77
|
resetGraphQLServer,
|
|
73
78
|
resetHeartbeatRunner,
|
|
74
79
|
resetAutomationSweep,
|
|
80
|
+
resetAutomationSchedules,
|
|
75
81
|
collectNEvents,
|
|
76
82
|
}
|
|
77
83
|
/* @pulumi/pulumi Not a pure module */
|