@reventlessdev/reventless-local 3.0.0-alpha.286 → 3.0.0-alpha.288

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 (28) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +9 -9
  3. package/src/adapter/Scheduler/LocalScheduledPublisher.res +5 -52
  4. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -47
  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 +23 -2
  8. package/src/components/AutomationSlice_Builder.res.mjs +19 -2
  9. package/src/components/LocalAutomationSchedules.res +59 -0
  10. package/src/components/LocalAutomationSchedules.res.mjs +72 -0
  11. package/src/test/TestRunner.res +1 -0
  12. package/src/test/TestRunner.res.mjs +6 -0
  13. package/tests/components/automationslice/AutomationRescheduleFixtures.res +142 -0
  14. package/tests/components/automationslice/AutomationRescheduleFixtures.res.mjs +259 -0
  15. package/tests/components/automationslice/AutomationRescheduleTest.res +96 -0
  16. package/tests/components/automationslice/AutomationRescheduleTest.res.mjs +177 -0
  17. package/tests/components/automationslice/AutomationScheduleFixtures.res +118 -0
  18. package/tests/components/automationslice/AutomationScheduleFixtures.res.mjs +263 -0
  19. package/tests/components/automationslice/AutomationScheduleTest.res +88 -0
  20. package/tests/components/automationslice/AutomationScheduleTest.res.mjs +108 -0
  21. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -0
  22. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +14 -2
  23. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +2 -0
  24. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +7 -1
  25. package/tests/components/automationslice/AutomationSweepFixtures.res +2 -0
  26. package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +7 -1
  27. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +3 -0
  28. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +13 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@
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.288 (2026-10-16)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-local
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.287 (2026-10-16)
15
+
16
+ * feat!(automation): schedules set by events ([53bc436](https://github.com/ReventlessDev/reventless-core/commit/53bc4363dffce81ef36475728bf0dae704716021))
17
+ * feat!(automation): declared schedules tick an automation ([420acd2](https://github.com/ReventlessDev/reventless-core/commit/420acd2233aadf700141a31d9d3e0fdc8074bef7))
18
+ ### Features
19
+
20
+ * **examples:** a marketing digest at the time each customer chooses ([3521b10](https://github.com/ReventlessDev/reventless-core/commit/3521b10c4f3ff64f3e4561e94e051892cdfe5d5f))
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ * a hand-written AutomationSlice.Mapping.Make the PPX does
25
+ not see must define `let reschedule = _ => []`.
26
+ * a hand-written AutomationSlice spec the PPX does not see must
27
+ declare `let schedule = None`.
28
+
29
+
30
+
6
31
  # 3.0.0-alpha.286 (2026-10-15)
7
32
 
8
33
  * 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.286",
3
+ "version": "3.0.0-alpha.288",
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": {
@@ -36,22 +36,22 @@
36
36
  "ws": "^8.18.0",
37
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",
40
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
41
40
  "@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",
41
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
45
42
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
46
- "@reventlessdev/reventless-infra": "3.0.0-alpha.192",
43
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.145",
44
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.245",
45
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.194",
46
+ "@reventlessdev/reventless-core": "3.0.0-alpha.297",
47
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.161",
47
48
  "@reventlessdev/reventless-seed": "1.0.0-alpha.34",
48
- "@reventlessdev/reventless-spec": "3.0.0-alpha.164",
49
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.159"
49
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.166"
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.102"
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
- type timerHandle
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
- let publish = () =>
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) => stop(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 ScheduleTime$ReventlessCore from "@reventlessdev/reventless-core/src/util/ScheduleTime.res.mjs";
5
+ import * as LocalTimers$ReventlessLocal from "./LocalTimers.res.mjs";
8
6
 
9
7
  function Make(Bus) {
10
- let activeTimers = {
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
- let publish = () => {
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
- activeTimers: activeTimers,
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 component->ReventlessCore.Component.operations->TestRunner.resolve
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
- /* Component-ReventlessCore 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 */
@@ -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 */