@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.
Files changed (24) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +9 -9
  3. package/src/adapter/Scheduler/LocalScheduledPublisher.res +43 -36
  4. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +32 -25
  5. package/src/components/AutomationSlice_Builder.res +10 -2
  6. package/src/components/AutomationSlice_Builder.res.mjs +13 -2
  7. package/src/components/LocalAutomationSweep.res +61 -0
  8. package/src/components/LocalAutomationSweep.res.mjs +83 -0
  9. package/src/test/TestRunner.res +1 -0
  10. package/src/test/TestRunner.res.mjs +6 -0
  11. package/tests/adapter/ScheduledPublisherTest.res +156 -155
  12. package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
  13. package/tests/components/automationslice/AutomationSliceFixtures.res +2 -2
  14. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +2 -2
  15. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +4 -1
  16. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +1 -1
  17. package/tests/components/automationslice/AutomationSweepFixtures.res +129 -0
  18. package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +217 -0
  19. package/tests/components/automationslice/AutomationSweepTest.res +107 -0
  20. package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
  21. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -1
  22. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +1 -1
  23. package/tests/components/scheduler/SchedulerTest.res +5 -5
  24. package/tests/components/scheduler/SchedulerTest.res.mjs +22 -14
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
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
+
6
25
  # 3.0.0-alpha.285 (2026-10-14)
7
26
 
8
27
  **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.285",
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": "0.1.0-alpha.35",
37
+ "@reventlessdev/rescript-effect": "1.0.0-alpha.0",
38
38
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
39
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
39
40
  "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
40
41
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
41
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
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-core": "3.0.0-alpha.294",
44
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.142",
45
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.242",
46
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.192",
46
47
  "@reventlessdev/reventless-seed": "1.0.0-alpha.34",
47
- "@reventlessdev/reventless-spec": "3.0.0-alpha.163",
48
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.158",
49
- "@reventlessdev/reventless-infra": "3.0.0-alpha.191"
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: sets up a setInterval that fires Bus.publishEvent when the schedule triggers.
4
- // deleteSchedule: clears the interval.
5
- //
6
- // The schedule.payload is published as the event body. For Single(…) schedules a
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 setIntervalJs: (unit => unit, int) => timerHandle = "setInterval"
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
- let rateToMs = (rate: Reventless.Schedule.rate) =>
25
- switch rate {
26
- | Minutes(n) => n * 60 * 1000
27
- | Hours(n) => n * 60 * 60 * 1000
28
- | Days(n) => n * 24 * 60 * 60 * 1000
29
- | Daily(_, _) => 24 * 60 * 60 * 1000
30
- | Weekdays(_, _) => 24 * 60 * 60 * 1000
31
- | WeekdaysAndSaturday(_, _) => 24 * 60 * 60 * 1000
32
- | Single(_, _, _, _, _) => 0
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 isSingleShot = rate =>
36
- switch rate {
37
- | Reventless.Schedule.Single(_, _, _, _, _) => true
38
- | _ => false
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 fire = () => Bus.publishEvent(topicName, "Scheduler", scheduleMeta, payloadJson)->ignore
64
- let handle = if schedule.rate->isSingleShot {
65
- setTimeoutJs(fire, 0)
66
- } else {
67
- setIntervalJs(fire, rateToMs(schedule.rate))
68
- }
69
- activeTimers.contents->Dict.set(schedule.name, handle)
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 rateToMs = rate => {
13
- switch (rate.TAG) {
14
- case "Single" :
15
- return 0;
16
- case "Minutes" :
17
- return (rate._0 * 60 | 0) * 1000 | 0;
18
- case "Hours" :
19
- return ((rate._0 * 60 | 0) * 60 | 0) * 1000 | 0;
20
- case "Days" :
21
- return (((rate._0 * 24 | 0) * 60 | 0) * 60 | 0) * 1000 | 0;
22
- default:
23
- return 86400000;
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 fire = () => {
54
+ let publish = () => {
45
55
  Bus.publishEvent(topicName, "Scheduler", scheduleMeta, payloadJson);
46
56
  };
47
- let handle = isSingleShot(schedule.rate) ? setTimeout(fire, 0) : setInterval(fire, rateToMs(schedule.rate));
48
- activeTimers.contents[schedule.name] = handle;
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
- rateToMs: rateToMs,
74
- isSingleShot: isSingleShot,
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 = Inner.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: Inner.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
- /* LocalQueryDbStorage-ReventlessLocal Not a pure module */
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 */
@@ -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 */