@reventlessdev/reventless-local 3.0.0-alpha.285 → 3.0.0-alpha.287

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +11 -11
  3. package/src/adapter/Scheduler/LocalScheduledPublisher.res +11 -51
  4. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -40
  5. package/src/adapter/Scheduler/LocalTimers.res +62 -0
  6. package/src/adapter/Scheduler/LocalTimers.res.mjs +63 -0
  7. package/src/components/AutomationSlice_Builder.res +31 -2
  8. package/src/components/AutomationSlice_Builder.res.mjs +31 -3
  9. package/src/components/LocalAutomationSchedules.res +59 -0
  10. package/src/components/LocalAutomationSchedules.res.mjs +72 -0
  11. package/src/components/LocalAutomationSweep.res +61 -0
  12. package/src/components/LocalAutomationSweep.res.mjs +83 -0
  13. package/src/test/TestRunner.res +2 -0
  14. package/src/test/TestRunner.res.mjs +12 -0
  15. package/tests/adapter/ScheduledPublisherTest.res +156 -155
  16. package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
  17. package/tests/components/automationslice/AutomationRescheduleFixtures.res +142 -0
  18. package/tests/components/automationslice/AutomationRescheduleFixtures.res.mjs +259 -0
  19. package/tests/components/automationslice/AutomationRescheduleTest.res +96 -0
  20. package/tests/components/automationslice/AutomationRescheduleTest.res.mjs +177 -0
  21. package/tests/components/automationslice/AutomationScheduleFixtures.res +118 -0
  22. package/tests/components/automationslice/AutomationScheduleFixtures.res.mjs +263 -0
  23. package/tests/components/automationslice/AutomationScheduleTest.res +88 -0
  24. package/tests/components/automationslice/AutomationScheduleTest.res.mjs +108 -0
  25. package/tests/components/automationslice/AutomationSliceFixtures.res +6 -2
  26. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +16 -4
  27. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +6 -1
  28. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +8 -2
  29. package/tests/components/automationslice/AutomationSweepFixtures.res +131 -0
  30. package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +223 -0
  31. package/tests/components/automationslice/AutomationSweepTest.res +107 -0
  32. package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
  33. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +4 -1
  34. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +14 -3
  35. package/tests/components/scheduler/SchedulerTest.res +5 -5
  36. package/tests/components/scheduler/SchedulerTest.res.mjs +22 -14
package/CHANGELOG.md CHANGED
@@ -3,6 +3,42 @@
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
+
23
+ # 3.0.0-alpha.286 (2026-10-15)
24
+
25
+ * feat!: an automation's process takes ~now, and collect the event's recorded time ([8c8f3f5](https://github.com/ReventlessDev/reventless-core/commit/8c8f3f570474b1658b605b726077debc7d6a1dc7))
26
+ * feat!: schedules take a Schedule.time, run on EventBridge Scheduler on AWS ([2d74557](https://github.com/ReventlessDev/reventless-core/commit/2d74557a6304917ded8f2b9f4107af9298c91ac0))
27
+ ### Features
28
+
29
+ * **local:** sweep pending automation items, as AWS does ([96acec0](https://github.com/ReventlessDev/reventless-core/commit/96acec0ba226ee560790bd14b6fd38e44328a47c))
30
+
31
+ ### BREAKING CHANGES
32
+
33
+ * a process the PPX does not see — in an Automation module
34
+ written by hand, or a SliceSpec given to the deprecated Automation_GWT.Make
35
+ or Flow_GWT.AutomationStep — must take ~now.
36
+ * Schedule.rate is replaced by Schedule.time, and a
37
+ schedule's `rate` field is now `time`. A once-only time already past
38
+ never fires, locally as on AWS.
39
+
40
+
41
+
6
42
  # 3.0.0-alpha.285 (2026-10-14)
7
43
 
8
44
  **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.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": {
@@ -34,24 +34,24 @@
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",
38
- "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
37
+ "@reventlessdev/rescript-effect": "1.0.0-alpha.0",
39
38
  "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
40
- "@reventlessdev/rescript-node": "2.0.0-alpha.22",
41
39
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
40
+ "@reventlessdev/rescript-node": "2.0.0-alpha.22",
41
+ "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
42
42
  "@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",
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",
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.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.100"
54
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.101"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "rescript": "^12.3.1"
@@ -1,42 +1,15 @@
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.
11
10
 
12
11
  module Make = (Bus: LocalBus.T) => {
13
- type timerHandle
14
-
15
- @val
16
- external setIntervalJs: (unit => unit, int) => timerHandle = "setInterval"
17
- @val
18
- external setTimeoutJs: (unit => unit, int) => timerHandle = "setTimeout"
19
- @val
20
- external clearTimerJs: timerHandle => unit = "clearTimeout"
21
-
22
- let activeTimers: ref<dict<timerHandle>> = ref(Dict.make())
23
-
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
33
- }
34
-
35
- let isSingleShot = rate =>
36
- switch rate {
37
- | Reventless.Schedule.Single(_, _, _, _, _) => true
38
- | _ => false
39
- }
12
+ let timers = LocalTimers.make()
40
13
 
41
14
  let scheduleMeta: ReventlessCore.Message.meta = {
42
15
  service: "Scheduler",
@@ -60,23 +33,13 @@ module Make = (Bus: LocalBus.T) => {
60
33
  | json => json
61
34
  | exception _ => schedule.payload->JSON.Encode.string
62
35
  }
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)
36
+ timers->LocalTimers.start(schedule.name, schedule.time, ~setAt=Date.now(), ~fire=_ =>
37
+ Bus.publishEvent(topicName, "Scheduler", scheduleMeta, payloadJson)->ignore
38
+ )
70
39
  }
71
40
 
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
- }
41
+ let deleteSchedule: ReventlessInfra.Scheduler.deleteSchedule = async (_, name) =>
42
+ timers->LocalTimers.stop(name)
80
43
 
81
44
  {
82
45
  resource: ReventlessInfra.Adapter.make(
@@ -95,8 +58,5 @@ module Make = (Bus: LocalBus.T) => {
95
58
  }
96
59
 
97
60
  // Clear all active timers — call in afterAll/afterEach for test isolation.
98
- let reset = () => {
99
- activeTimers.contents->Dict.valuesToArray->Array.forEach(clearTimerJs)
100
- activeTimers.contents = Dict.make()
101
- }
61
+ let reset = () => timers->LocalTimers.reset
102
62
  }
@@ -1,29 +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";
5
+ import * as LocalTimers$ReventlessLocal from "./LocalTimers.res.mjs";
7
6
 
8
7
  function Make(Bus) {
9
- let activeTimers = {
10
- contents: {}
11
- };
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;
24
- }
25
- };
26
- let isSingleShot = rate => rate.TAG === "Single";
8
+ let timers = LocalTimers$ReventlessLocal.make();
27
9
  let scheduleMeta = {
28
10
  service: "Scheduler",
29
11
  time: "",
@@ -41,19 +23,11 @@ function Make(Bus) {
41
23
  } catch (exn) {
42
24
  payloadJson = schedule.payload;
43
25
  }
44
- let fire = () => {
26
+ return LocalTimers$ReventlessLocal.start(timers, schedule.name, schedule.time, Date.now(), param => {
45
27
  Bus.publishEvent(topicName, "Scheduler", scheduleMeta, payloadJson);
46
- };
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
- }
28
+ });
56
29
  };
30
+ let deleteSchedule = async (param, name) => LocalTimers$ReventlessLocal.stop(timers, name);
57
31
  return {
58
32
  resource: Adapter$ReventlessInfra.make(Pulumi.output(""), Pulumi.output(""), Pulumi.output(""), Pulumi.output("memory:InMemory"), undefined, undefined, undefined, undefined, undefined, undefined),
59
33
  operations: Pulumi.output({
@@ -62,16 +36,9 @@ function Make(Bus) {
62
36
  })
63
37
  };
64
38
  };
65
- let reset = () => {
66
- Object.values(activeTimers.contents).forEach(prim => {
67
- clearTimeout(prim);
68
- });
69
- activeTimers.contents = {};
70
- };
39
+ let reset = () => LocalTimers$ReventlessLocal.reset(timers);
71
40
  return {
72
- activeTimers: activeTimers,
73
- rateToMs: rateToMs,
74
- isSingleShot: isSingleShot,
41
+ timers: timers,
75
42
  scheduleMeta: scheduleMeta,
76
43
  make: make,
77
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,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, and its schedules start.
3
4
 
4
5
  module Make = (Bus: LocalBus.T) => {
5
6
  module RuntimeEnvironment = LocalRuntimeEnvironment
@@ -24,6 +25,7 @@ module Make = (Bus: LocalBus.T) => {
24
25
  EventCollectorChannel,
25
26
  EventCollectorRuntimeBuilder,
26
27
  Api,
28
+ LocalAutomationSchedules,
27
29
  )
28
30
 
29
31
  module Make = (
@@ -35,7 +37,34 @@ module Make = (Bus: LocalBus.T) => {
35
37
  module Automation = Automation
36
38
  type component = Inner.component
37
39
  let queryDbName = Inner.queryDbName
40
+ let schedulesDbName = Inner.schedulesDbName
38
41
  let sourceNames = Inner.sourceNames
39
- let make = Inner.make
42
+ let make = (~allEventTopics, ~publishJsons, ~context, ~runtime=?, ~opts=?) => {
43
+ let component = Inner.make(~allEventTopics, ~publishJsons, ~context, ~runtime?, ~opts?)
44
+ let operations = () => component->ReventlessCore.Component.operations->TestRunner.resolve
45
+ LocalAutomationSweep.register(~name=Spec.name, async () => {
46
+ let ops = await operations()
47
+ await ops.processPending()
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
+ })
67
+ component
68
+ }
40
69
  }
41
70
  }
@@ -1,8 +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";
7
+ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
8
+ import * as TestRunner$ReventlessLocal from "../test/TestRunner.res.mjs";
9
+ import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
3
10
  import * as LocalQueryDbStorage$ReventlessLocal from "../adapter/QueryDb/LocalQueryDbStorage.res.mjs";
11
+ import * as LocalAutomationSweep$ReventlessLocal from "./LocalAutomationSweep.res.mjs";
4
12
  import * as AutomationSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs";
5
13
  import * as LocalRuntimeEnvironment$ReventlessLocal from "../adapter/Runtime/LocalRuntimeEnvironment.res.mjs";
14
+ import * as LocalAutomationSchedules$ReventlessLocal from "./LocalAutomationSchedules.res.mjs";
6
15
  import * as QueryDbResolvers_GraphQL$ReventlessLocal from "../adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs";
7
16
  import * as LocalEventCollectorChannel$ReventlessLocal from "../adapter/EventCollector/LocalEventCollectorChannel.res.mjs";
8
17
  import * as LocalEventCollectorRuntime_Builder$ReventlessLocal from "../adapter/Runtime/LocalEventCollectorRuntime_Builder.res.mjs";
@@ -31,15 +40,34 @@ function Make(Bus) {
31
40
  make: QueryDbStorage.make
32
41
  })({
33
42
  make: QueryDbResolvers.make
34
- })(EventCollectorChannel)(EventCollectorRuntimeBuilder)(Api);
43
+ })(EventCollectorChannel)(EventCollectorRuntimeBuilder)(Api)({
44
+ platformFor: LocalAutomationSchedules$ReventlessLocal.platformFor
45
+ });
35
46
  let Make$1 = Spec => (Automation => {
36
47
  let Inner = CoreMaker.Make(Spec)(Automation);
48
+ let make = (allEventTopics, publishJsons, context, runtime, opts) => {
49
+ let component = Inner.make(allEventTopics, publishJsons, context, runtime, opts);
50
+ LocalAutomationSweep$ReventlessLocal.register(Spec.name, async () => {
51
+ let ops = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(component));
52
+ return await ops.processPending();
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
+ });
62
+ return component;
63
+ };
37
64
  return {
38
65
  Spec: Spec,
39
66
  Automation: Automation,
40
67
  queryDbName: Inner.queryDbName,
68
+ schedulesDbName: Inner.schedulesDbName,
41
69
  sourceNames: Inner.sourceNames,
42
- make: Inner.make
70
+ make: make
43
71
  };
44
72
  });
45
73
  return {
@@ -57,4 +85,4 @@ function Make(Bus) {
57
85
  export {
58
86
  Make,
59
87
  }
60
- /* LocalQueryDbStorage-ReventlessLocal Not a pure module */
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 */