@reventlessdev/reventless-local 3.0.0-alpha.284 → 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 +27 -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
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
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.286 (2026-10-15)
|
|
7
|
+
|
|
8
|
+
* feat!: an automation's process takes ~now, and collect the event's recorded time ([8c8f3f5](https://github.com/ReventlessDev/reventless-core/commit/8c8f3f570474b1658b605b726077debc7d6a1dc7))
|
|
9
|
+
* feat!: schedules take a Schedule.time, run on EventBridge Scheduler on AWS ([2d74557](https://github.com/ReventlessDev/reventless-core/commit/2d74557a6304917ded8f2b9f4107af9298c91ac0))
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **local:** sweep pending automation items, as AWS does ([96acec0](https://github.com/ReventlessDev/reventless-core/commit/96acec0ba226ee560790bd14b6fd38e44328a47c))
|
|
13
|
+
|
|
14
|
+
### BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* a process the PPX does not see — in an Automation module
|
|
17
|
+
written by hand, or a SliceSpec given to the deprecated Automation_GWT.Make
|
|
18
|
+
or Flow_GWT.AutomationStep — must take ~now.
|
|
19
|
+
* Schedule.rate is replaced by Schedule.time, and a
|
|
20
|
+
schedule's `rate` field is now `time`. A once-only time already past
|
|
21
|
+
never fires, locally as on AWS.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# 3.0.0-alpha.285 (2026-10-14)
|
|
26
|
+
|
|
27
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
6
33
|
# 3.0.0-alpha.284 (2026-10-12)
|
|
7
34
|
|
|
8
35
|
**Note:** Version bump only for package @reventlessdev/reventless-local
|
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.286",
|
|
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": {
|
|
@@ -34,19 +34,19 @@
|
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
|
-
"@reventlessdev/rescript-effect": "
|
|
37
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.0",
|
|
38
38
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
39
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
40
39
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
|
|
40
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
41
41
|
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
42
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.295",
|
|
43
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.143",
|
|
44
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.243",
|
|
42
45
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.241",
|
|
45
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.142",
|
|
46
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.191",
|
|
47
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.158",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.192",
|
|
48
47
|
"@reventlessdev/reventless-seed": "1.0.0-alpha.34",
|
|
49
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.164",
|
|
49
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.159"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// In-memory ScheduledPublisher — satisfies Scheduler_Adapter.ScheduledPublisher.
|
|
2
2
|
//
|
|
3
|
-
// createSchedule
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// one-shot setTimeout is used; all recurring rates use setInterval.
|
|
3
|
+
// createSchedule waits for each firing `ScheduleTime.nextFiring` names and publishes the
|
|
4
|
+
// payload as an event on the first channel resource; a once-only time fires once, one
|
|
5
|
+
// already past never. A second createSchedule under the same name replaces the first,
|
|
6
|
+
// as on AWS. deleteSchedule stops it.
|
|
8
7
|
//
|
|
9
8
|
// Usage: create a Scheduler with Scheduler_Builder.Make(LocalScheduledPublisher.Make(Bus))
|
|
10
9
|
// and pass the resulting component to Plugin_Builder or Task_Builder.
|
|
@@ -13,29 +12,40 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
13
12
|
type timerHandle
|
|
14
13
|
|
|
15
14
|
@val
|
|
16
|
-
external
|
|
17
|
-
@val
|
|
18
|
-
external setTimeoutJs: (unit => unit, int) => timerHandle = "setTimeout"
|
|
15
|
+
external setTimeoutJs: (unit => unit, float) => timerHandle = "setTimeout"
|
|
19
16
|
@val
|
|
20
17
|
external clearTimerJs: timerHandle => unit = "clearTimeout"
|
|
21
18
|
|
|
22
19
|
let activeTimers: ref<dict<timerHandle>> = ref(Dict.make())
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
|
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 => ()
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
})
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
let scheduleMeta: ReventlessCore.Message.meta = {
|
|
@@ -52,6 +62,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
52
62
|
channelResources,
|
|
53
63
|
schedule,
|
|
54
64
|
) => {
|
|
65
|
+
ReventlessCore.ScheduleTime.validate(schedule.time)
|
|
55
66
|
let topicName = switch channelResources {
|
|
56
67
|
| [] => ""
|
|
57
68
|
| resources => (resources->Array.getUnsafe(0)).name
|
|
@@ -60,23 +71,19 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
60
71
|
| json => json
|
|
61
72
|
| exception _ => schedule.payload->JSON.Encode.string
|
|
62
73
|
}
|
|
63
|
-
let
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
let publish = () =>
|
|
75
|
+
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
|
+
)
|
|
70
84
|
}
|
|
71
85
|
|
|
72
|
-
let deleteSchedule: ReventlessInfra.Scheduler.deleteSchedule = async (_, name) =>
|
|
73
|
-
switch activeTimers.contents->Dict.get(name) {
|
|
74
|
-
| Some(handle) =>
|
|
75
|
-
clearTimerJs(handle)
|
|
76
|
-
activeTimers.contents->Dict.delete(name)
|
|
77
|
-
| None => ()
|
|
78
|
-
}
|
|
79
|
-
}
|
|
86
|
+
let deleteSchedule: ReventlessInfra.Scheduler.deleteSchedule = async (_, name) => stop(name)
|
|
80
87
|
|
|
81
88
|
{
|
|
82
89
|
resource: ReventlessInfra.Adapter.make(
|
|
@@ -4,26 +4,35 @@ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
|
4
4
|
import * as Pulumi from "@pulumi/pulumi";
|
|
5
5
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
6
6
|
import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
|
|
7
|
+
import * as ScheduleTime$ReventlessCore from "@reventlessdev/reventless-core/src/util/ScheduleTime.res.mjs";
|
|
7
8
|
|
|
8
9
|
function Make(Bus) {
|
|
9
10
|
let activeTimers = {
|
|
10
11
|
contents: {}
|
|
11
12
|
};
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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);
|
|
24
34
|
}
|
|
25
35
|
};
|
|
26
|
-
let isSingleShot = rate => rate.TAG === "Single";
|
|
27
36
|
let scheduleMeta = {
|
|
28
37
|
service: "Scheduler",
|
|
29
38
|
time: "",
|
|
@@ -34,6 +43,7 @@ function Make(Bus) {
|
|
|
34
43
|
};
|
|
35
44
|
let make = (param, param$1) => {
|
|
36
45
|
let createSchedule = async (channelResources, schedule) => {
|
|
46
|
+
ScheduleTime$ReventlessCore.validate(schedule.time);
|
|
37
47
|
let topicName = channelResources.length !== 0 ? channelResources[0].name : "";
|
|
38
48
|
let payloadJson;
|
|
39
49
|
try {
|
|
@@ -41,19 +51,14 @@ function Make(Bus) {
|
|
|
41
51
|
} catch (exn) {
|
|
42
52
|
payloadJson = schedule.payload;
|
|
43
53
|
}
|
|
44
|
-
let
|
|
54
|
+
let publish = () => {
|
|
45
55
|
Bus.publishEvent(topicName, "Scheduler", scheduleMeta, payloadJson);
|
|
46
56
|
};
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let deleteSchedule = async (param, name) => {
|
|
51
|
-
let handle = activeTimers.contents[name];
|
|
52
|
-
if (handle !== undefined) {
|
|
53
|
-
clearTimeout(Primitive_option.valFromOption(handle));
|
|
54
|
-
return Stdlib_Dict.$$delete(activeTimers.contents, name);
|
|
55
|
-
}
|
|
57
|
+
let setAt = Date.now();
|
|
58
|
+
stop(schedule.name);
|
|
59
|
+
return arm(schedule.name, ScheduleTime$ReventlessCore.resolveOnce(schedule.time, setAt), setAt, publish);
|
|
56
60
|
};
|
|
61
|
+
let deleteSchedule = async (param, name) => stop(name);
|
|
57
62
|
return {
|
|
58
63
|
resource: Adapter$ReventlessInfra.make(Pulumi.output(""), Pulumi.output(""), Pulumi.output(""), Pulumi.output("memory:InMemory"), undefined, undefined, undefined, undefined, undefined, undefined),
|
|
59
64
|
operations: Pulumi.output({
|
|
@@ -70,8 +75,10 @@ function Make(Bus) {
|
|
|
70
75
|
};
|
|
71
76
|
return {
|
|
72
77
|
activeTimers: activeTimers,
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
maxWaitMs: 86400000,
|
|
79
|
+
stop: stop,
|
|
80
|
+
waitUntil: waitUntil,
|
|
81
|
+
arm: arm,
|
|
75
82
|
scheduleMeta: scheduleMeta,
|
|
76
83
|
make: make,
|
|
77
84
|
reset: reset
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// In-memory AutomationSlice builder.
|
|
2
|
-
// Wires in-memory adapters and delegates to the core ReventlessCore.AutomationSlice_Builder
|
|
2
|
+
// Wires in-memory adapters and delegates to the core ReventlessCore.AutomationSlice_Builder;
|
|
3
|
+
// each slice it makes joins the local automation sweep.
|
|
3
4
|
|
|
4
5
|
module Make = (Bus: LocalBus.T) => {
|
|
5
6
|
module RuntimeEnvironment = LocalRuntimeEnvironment
|
|
@@ -36,6 +37,13 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
36
37
|
type component = Inner.component
|
|
37
38
|
let queryDbName = Inner.queryDbName
|
|
38
39
|
let sourceNames = Inner.sourceNames
|
|
39
|
-
let make =
|
|
40
|
+
let make = (~allEventTopics, ~publishJsons, ~context, ~runtime=?, ~opts=?) => {
|
|
41
|
+
let component = Inner.make(~allEventTopics, ~publishJsons, ~context, ~runtime?, ~opts?)
|
|
42
|
+
LocalAutomationSweep.register(~name=Spec.name, async () => {
|
|
43
|
+
let ops = await component->ReventlessCore.Component.operations->TestRunner.resolve
|
|
44
|
+
await ops.processPending()
|
|
45
|
+
})
|
|
46
|
+
component
|
|
47
|
+
}
|
|
40
48
|
}
|
|
41
49
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
4
|
+
import * as TestRunner$ReventlessLocal from "../test/TestRunner.res.mjs";
|
|
3
5
|
import * as LocalQueryDbStorage$ReventlessLocal from "../adapter/QueryDb/LocalQueryDbStorage.res.mjs";
|
|
6
|
+
import * as LocalAutomationSweep$ReventlessLocal from "./LocalAutomationSweep.res.mjs";
|
|
4
7
|
import * as AutomationSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs";
|
|
5
8
|
import * as LocalRuntimeEnvironment$ReventlessLocal from "../adapter/Runtime/LocalRuntimeEnvironment.res.mjs";
|
|
6
9
|
import * as QueryDbResolvers_GraphQL$ReventlessLocal from "../adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs";
|
|
@@ -34,12 +37,20 @@ function Make(Bus) {
|
|
|
34
37
|
})(EventCollectorChannel)(EventCollectorRuntimeBuilder)(Api);
|
|
35
38
|
let Make$1 = Spec => (Automation => {
|
|
36
39
|
let Inner = CoreMaker.Make(Spec)(Automation);
|
|
40
|
+
let make = (allEventTopics, publishJsons, context, runtime, opts) => {
|
|
41
|
+
let component = Inner.make(allEventTopics, publishJsons, context, runtime, opts);
|
|
42
|
+
LocalAutomationSweep$ReventlessLocal.register(Spec.name, async () => {
|
|
43
|
+
let ops = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(component));
|
|
44
|
+
return await ops.processPending();
|
|
45
|
+
});
|
|
46
|
+
return component;
|
|
47
|
+
};
|
|
37
48
|
return {
|
|
38
49
|
Spec: Spec,
|
|
39
50
|
Automation: Automation,
|
|
40
51
|
queryDbName: Inner.queryDbName,
|
|
41
52
|
sourceNames: Inner.sourceNames,
|
|
42
|
-
make:
|
|
53
|
+
make: make
|
|
43
54
|
};
|
|
44
55
|
});
|
|
45
56
|
return {
|
|
@@ -57,4 +68,4 @@ function Make(Bus) {
|
|
|
57
68
|
export {
|
|
58
69
|
Make,
|
|
59
70
|
}
|
|
60
|
-
/*
|
|
71
|
+
/* Component-ReventlessCore Not a pure module */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The local platform's automation sweep. As on AWS, one sweep serves every
|
|
2
|
+
// automation: each interval it runs every slice's `processPending`, one after
|
|
3
|
+
// another, so an item `process` left pending or one whose command failed is tried
|
|
4
|
+
// again without waiting for the next event. The interval is five minutes, as on
|
|
5
|
+
// AWS; REVENTLESS_LOCAL_SWEEP_SECONDS shortens it for development.
|
|
6
|
+
|
|
7
|
+
let defaultIntervalSeconds = 300
|
|
8
|
+
|
|
9
|
+
let intervalSeconds = () =>
|
|
10
|
+
switch NodeProcess.env
|
|
11
|
+
->Dict.get("REVENTLESS_LOCAL_SWEEP_SECONDS")
|
|
12
|
+
->Option.flatMap(s => Int.fromString(s)) {
|
|
13
|
+
| Some(seconds) if seconds > 0 => seconds
|
|
14
|
+
| _ => defaultIntervalSeconds
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let sweeps: dict<unit => promise<unit>> = Dict.make()
|
|
18
|
+
let timer: ref<option<NodeTimers.timeout>> = ref(None)
|
|
19
|
+
let running = ref(false)
|
|
20
|
+
|
|
21
|
+
/** Runs every registered slice's sweep once. A failing slice is logged and the rest still run;
|
|
22
|
+
a sweep still running when the next one is due is not started twice. */
|
|
23
|
+
let sweepOnce = async () =>
|
|
24
|
+
if !running.contents {
|
|
25
|
+
running := true
|
|
26
|
+
let _ = await sweeps
|
|
27
|
+
->Dict.toArray
|
|
28
|
+
->Array.reduce(Promise.resolve(), async (previous, (name, sweep)) => {
|
|
29
|
+
await previous
|
|
30
|
+
try await sweep() catch {
|
|
31
|
+
| exn =>
|
|
32
|
+
let message =
|
|
33
|
+
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
|
|
34
|
+
ReventlessCore.EffectLogger.logError(
|
|
35
|
+
~comp="LocalAutomationSweep",
|
|
36
|
+
`sweep of ${name} failed: ${message}`,
|
|
37
|
+
)->Effect.runSync
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
running := false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Adds a slice's sweep, replacing one of the same name; the first starts the timer. */
|
|
44
|
+
let register = (~name, sweep) => {
|
|
45
|
+
sweeps->Dict.set(name, sweep)
|
|
46
|
+
if timer.contents->Option.isNone {
|
|
47
|
+
let handle = NodeTimers.setInterval(() => {
|
|
48
|
+
let _ = sweepOnce()
|
|
49
|
+
}, intervalSeconds() * 1000)
|
|
50
|
+
// A backstop: never the reason a process stays up.
|
|
51
|
+
handle->NodeTimers.unref
|
|
52
|
+
timer := Some(handle)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Stops the timer and forgets every sweep — call in afterAll/afterEach for test isolation. */
|
|
57
|
+
let reset = () => {
|
|
58
|
+
timer.contents->Option.forEach(NodeTimers.clearInterval)
|
|
59
|
+
timer := None
|
|
60
|
+
sweeps->Dict.keysToArray->Array.forEach(name => sweeps->Dict.delete(name))
|
|
61
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
5
|
+
import * as Nodetimers from "node:timers";
|
|
6
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
7
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
8
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
12
|
+
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
13
|
+
|
|
14
|
+
function intervalSeconds() {
|
|
15
|
+
let seconds = Stdlib_Option.flatMap(process.env["REVENTLESS_LOCAL_SWEEP_SECONDS"], s => Stdlib_Int.fromString(s, undefined));
|
|
16
|
+
if (seconds !== undefined && seconds > 0) {
|
|
17
|
+
return seconds;
|
|
18
|
+
} else {
|
|
19
|
+
return 300;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let sweeps = {};
|
|
24
|
+
|
|
25
|
+
let timer = {
|
|
26
|
+
contents: undefined
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let running = {
|
|
30
|
+
contents: false
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function sweepOnce() {
|
|
34
|
+
if (!running.contents) {
|
|
35
|
+
running.contents = true;
|
|
36
|
+
await Stdlib_Array.reduce(Object.entries(sweeps), Promise.resolve(), async (previous, param) => {
|
|
37
|
+
await previous;
|
|
38
|
+
try {
|
|
39
|
+
return await param[1]();
|
|
40
|
+
} catch (raw_exn) {
|
|
41
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
42
|
+
let message = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
43
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSweep", undefined, `sweep of ` + param[0] + ` failed: ` + message));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
running.contents = false;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function register(name, sweep) {
|
|
52
|
+
sweeps[name] = sweep;
|
|
53
|
+
if (!Stdlib_Option.isNone(timer.contents)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let handle = Nodetimers.setInterval(() => {
|
|
57
|
+
sweepOnce();
|
|
58
|
+
}, intervalSeconds() * 1000 | 0);
|
|
59
|
+
handle.unref();
|
|
60
|
+
timer.contents = Primitive_option.some(handle);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function reset() {
|
|
64
|
+
Stdlib_Option.forEach(timer.contents, prim => {
|
|
65
|
+
Nodetimers.clearInterval(prim);
|
|
66
|
+
});
|
|
67
|
+
timer.contents = undefined;
|
|
68
|
+
Object.keys(sweeps).forEach(name => Stdlib_Dict.$$delete(sweeps, name));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let defaultIntervalSeconds = 300;
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
defaultIntervalSeconds,
|
|
75
|
+
intervalSeconds,
|
|
76
|
+
sweeps,
|
|
77
|
+
timer,
|
|
78
|
+
running,
|
|
79
|
+
sweepOnce,
|
|
80
|
+
register,
|
|
81
|
+
reset,
|
|
82
|
+
}
|
|
83
|
+
/* node:timers Not a pure module */
|
package/src/test/TestRunner.res
CHANGED
|
@@ -44,6 +44,7 @@ let resetGraphQLServer = () => DomainGraphQL_Server.reset()
|
|
|
44
44
|
// Clear all active heartbeat timers started by LocalHeartbeatRunner.
|
|
45
45
|
// Call in afterAll when using Plugin_Builder with LocalHeartbeatRunner.
|
|
46
46
|
let resetHeartbeatRunner = () => LocalHeartbeatRunner.reset()
|
|
47
|
+
let resetAutomationSweep = () => LocalAutomationSweep.reset()
|
|
47
48
|
|
|
48
49
|
// ─────────────────────────────────────────────────────────────
|
|
49
50
|
// Bus test utilities
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Pulumi from "@pulumi/pulumi";
|
|
4
4
|
import * as DomainGraphQL_Server$ReventlessLocal from "../adapter/DomainGraphQL_Server.res.mjs";
|
|
5
|
+
import * as LocalAutomationSweep$ReventlessLocal from "../components/LocalAutomationSweep.res.mjs";
|
|
5
6
|
import * as LocalHeartbeatRunner$ReventlessLocal from "../adapter/Heartbeat/LocalHeartbeatRunner.res.mjs";
|
|
6
7
|
|
|
7
8
|
function setup() {
|
|
@@ -30,6 +31,10 @@ function resetHeartbeatRunner() {
|
|
|
30
31
|
LocalHeartbeatRunner$ReventlessLocal.reset();
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function resetAutomationSweep() {
|
|
35
|
+
LocalAutomationSweep$ReventlessLocal.reset();
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
function collectNEvents(subscribeToEvents, topicName, n) {
|
|
34
39
|
if (n <= 0) {
|
|
35
40
|
return Promise.resolve([]);
|
|
@@ -66,6 +71,7 @@ export {
|
|
|
66
71
|
stopGraphQLServer,
|
|
67
72
|
resetGraphQLServer,
|
|
68
73
|
resetHeartbeatRunner,
|
|
74
|
+
resetAutomationSweep,
|
|
69
75
|
collectNEvents,
|
|
70
76
|
}
|
|
71
77
|
/* @pulumi/pulumi Not a pure module */
|