@reventlessdev/reventless-spec 3.0.0-alpha.163 → 3.0.0-alpha.164

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 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.164 (2026-10-15)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **spec:** generate-plugin writes Plugin.res as the formatter prints it ([3fcfd2b](https://github.com/ReventlessDev/reventless-core/commit/3fcfd2b38632bb9550413f88f410766934b6f609))
11
+ * feat!: an automation's process takes ~now, and collect the event's recorded time ([8c8f3f5](https://github.com/ReventlessDev/reventless-core/commit/8c8f3f570474b1658b605b726077debc7d6a1dc7))
12
+ * feat!: schedules take a Schedule.time, run on EventBridge Scheduler on AWS ([2d74557](https://github.com/ReventlessDev/reventless-core/commit/2d74557a6304917ded8f2b9f4107af9298c91ac0))
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.163 (2026-10-12)
7
26
 
8
27
  **Note:** Version bump only for package @reventlessdev/reventless-spec
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.163",
3
+ "version": "3.0.0-alpha.164",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -111,6 +111,10 @@ type context = {
111
111
  platformName: string,
112
112
  pluginName: string,
113
113
  sliceName: string,
114
+ /** When the source event was recorded, in milliseconds since 1970 (UTC): what a
115
+ due time is counted from. The runtime sets it for every event it delivers;
116
+ it is absent only in a context built by hand. */
117
+ recordedAt?: float,
114
118
  }
115
119
 
116
120
  /**
@@ -166,9 +170,14 @@ module type Automation = {
166
170
  /**
167
171
  Process: given a pending TODO item, produce a command.
168
172
  The processor calls this for each pending item. Returns the target ID and command.
169
- May return `None` to skip processing (e.g., wait for more data).
173
+ May return `None` to skip processing (e.g., wait for more data, or a due time
174
+ not yet reached); the item stays pending and the sweep offers it again.
175
+
176
+ `~now` is the current time in milliseconds since 1970 (UTC), from Effect's
177
+ `Clock`, so a test can fix it. The `@@reventless.automation` PPX adds `~now as _`
178
+ to a `process` written without it.
170
179
  */
171
- let process: (string, Spec.todoItem) => option<(string, Spec.command)>
180
+ let process: (string, Spec.todoItem, ~now: float) => option<(string, Spec.command)>
172
181
 
173
182
  /**
174
183
  The retry budget is spent: this item will never be processed again.
@@ -24,6 +24,18 @@ let parseArgs = (argv: array<string>): result<args, string> =>
24
24
 
25
25
  let cli: CliArgs.cli<args> = {bin: "generate-plugin", usage, parse: parseArgs}
26
26
 
27
+ /** The generated files sit in the plugin's source tree, so they face its format
28
+ gate like any other source. Codegen lays text out for reading, not for the
29
+ formatter, so the formatter settles the layout before the file is written.
30
+ Raising beats falling back: unformatted output would build and then fail the
31
+ format gate on every regeneration. */
32
+ let formatted = (source: string, ~cwd: string): string =>
33
+ NodeChildProcess.execFileSync(
34
+ "pnpm",
35
+ ["exec", "rescript", "format", "--stdin", ".res"],
36
+ {cwd, encoding: "utf8", input: source},
37
+ )
38
+
27
39
  // A usage mistake exits non-zero, so `prebuild` (and CI) fail instead of
28
40
  // continuing green with no Plugin.res generated.
29
41
  let main = () =>
@@ -46,7 +58,7 @@ let main = () =>
46
58
  }
47
59
 
48
60
  let pluginPath = NodePath.join([outputDir, "Plugin.res"])
49
- NodeFs.writeFileSync(pluginPath, source)
61
+ NodeFs.writeFileSync(pluginPath, source->formatted(~cwd=outputDir))
50
62
  Console.log("Generated: " ++ pluginPath)
51
63
 
52
64
  switch variant {
@@ -54,7 +66,7 @@ let main = () =>
54
66
  | Config.Aws(_) =>
55
67
  let mainSource = Codegen.renderMain(~config)
56
68
  let mainPath = NodePath.join([outputDir, "Main.res"])
57
- NodeFs.writeFileSync(mainPath, mainSource)
69
+ NodeFs.writeFileSync(mainPath, mainSource->formatted(~cwd=outputDir))
58
70
  Console.log("Generated: " ++ mainPath)
59
71
  }
60
72
  })
@@ -7,6 +7,7 @@ import * as Config$Reventless from "./Config.res.mjs";
7
7
  import * as CliArgs$Reventless from "../CliArgs.res.mjs";
8
8
  import * as Codegen$Reventless from "./Codegen.res.mjs";
9
9
  import * as Pairing$Reventless from "./Pairing.res.mjs";
10
+ import * as Nodechild_process from "node:child_process";
10
11
  import * as Discovery$Reventless from "./Discovery.res.mjs";
11
12
 
12
13
  let usage = `Usage: generate-plugin [--aws <Namespace>] <srcDir>
@@ -62,6 +63,20 @@ let cli = {
62
63
  parse: parseArgs
63
64
  };
64
65
 
66
+ function formatted(source, cwd) {
67
+ return Nodechild_process.execFileSync("pnpm", [
68
+ "exec",
69
+ "rescript",
70
+ "format",
71
+ "--stdin",
72
+ ".res"
73
+ ], {
74
+ cwd: cwd,
75
+ encoding: "utf8",
76
+ input: source
77
+ });
78
+ }
79
+
65
80
  function main() {
66
81
  return CliArgs$Reventless.run(cli, undefined, undefined, async param => {
67
82
  let variant = param.variant;
@@ -86,14 +101,14 @@ function main() {
86
101
  let outputDir;
87
102
  outputDir = typeof variant !== "object" ? srcDir : Nodepath.join(process.cwd(), "src");
88
103
  let pluginPath = Nodepath.join(outputDir, "Plugin.res");
89
- Nodefs.writeFileSync(pluginPath, source, "utf8");
104
+ Nodefs.writeFileSync(pluginPath, formatted(source, outputDir), "utf8");
90
105
  console.log("Generated: " + pluginPath);
91
106
  if (typeof variant !== "object") {
92
107
  return;
93
108
  }
94
109
  let mainSource = Codegen$Reventless.renderMain(config);
95
110
  let mainPath = Nodepath.join(outputDir, "Main.res");
96
- Nodefs.writeFileSync(mainPath, mainSource, "utf8");
111
+ Nodefs.writeFileSync(mainPath, formatted(mainSource, outputDir), "utf8");
97
112
  console.log("Generated: " + mainPath);
98
113
  });
99
114
  }
@@ -102,6 +117,7 @@ export {
102
117
  usage,
103
118
  parseArgs,
104
119
  cli,
120
+ formatted,
105
121
  main,
106
122
  }
107
123
  /* node:fs Not a pure module */
@@ -9,44 +9,88 @@ type hour = int
9
9
  @schema
10
10
  type minute = int
11
11
 
12
+ /** A fixed length, measured from the previous firing. */
13
+ @schema
14
+ type interval =
15
+ | Minutes(int)
16
+ | Hours(int)
17
+ | Days(int)
18
+ | Weeks(int)
19
+
20
+ @schema
21
+ type weekday = Mon | Tue | Wed | Thu | Fri | Sat | Sun
22
+
23
+ /**
24
+ A wall-clock time that recurs. `Monthly` with `every: 3` fires in January, April, July
25
+ and October; a `day` a month does not have (the 31st) is skipped in that month.
26
+ */
27
+ @schema
28
+ type calendar =
29
+ | Daily({hour: hour, minute: minute})
30
+ | Weekly({days: array<weekday>, hour: hour, minute: minute})
31
+ | Monthly({day: day, hour: hour, minute: minute, every?: int})
32
+ | Yearly({month: month, day: day, hour: hour, minute: minute})
33
+
34
+ /** A span counted once, from the moment a schedule is set; months and years by the calendar. */
35
+ @schema
36
+ type period =
37
+ | Minutes(int)
38
+ | Hours(int)
39
+ | Days(int)
40
+ | Weeks(int)
41
+ | Months(int)
42
+ | Years(int)
43
+
44
+ /** A wall-clock date and time, read in the schedule's zone. */
45
+ @schema
46
+ type localTime = {year: year, month: month, day: day, hour: hour, minute: minute}
47
+
48
+ /** `After` becomes `At` when the schedule is set. */
49
+ @schema
50
+ type moment = At(localTime) | After(period)
51
+
52
+ /** An IANA zone name such as `Europe/Berlin`; absent means UTC. */
53
+ @schema
54
+ type zone = string
55
+
12
56
  /**
13
- The repetition rule for a scheduled event.
57
+ When a schedule fires: `Every` an interval, `Calendar` a wall-clock time that recurs,
58
+ or `Once` a single moment.
14
59
 
15
- - `Single(year, month, day, hour, minute)` — fires once at an absolute UTC time
16
- - `Minutes(n)` / `Hours(n)` / `Days(n)` — fires every n minutes / hours / days
17
- - `Daily(hour, minute)` — fires every day at the given UTC time
18
- - `Weekdays(hour, minute)` — fires Monday–Friday at the given UTC time
19
- - `WeekdaysAndSaturday(hour, minute)` — fires Monday–Saturday at the given UTC time
60
+ A calendar time in the hour skipped by daylight saving does not fire that day; one in
61
+ the repeated hour fires once, at the first.
20
62
 
21
63
  @example
22
64
  ```rescript
23
- // Fire every 15 minutes
24
- let rate = Schedule.Minutes(15)
65
+ // every 15 minutes
66
+ let every = Schedule.Every(Minutes(15))
67
+
68
+ // weekdays at 07:30 in Berlin
69
+ let weekdays = Schedule.Calendar({
70
+ calendar: Weekly({days: [Mon, Tue, Wed, Thu, Fri], hour: 7, minute: 30}),
71
+ zone: "Europe/Berlin",
72
+ })
25
73
 
26
- // Fire once on 2024-12-31 at 23:59 UTC
27
- let oneTime = Schedule.Single(2024, 12, 31, 23, 59)
74
+ // once, ten minutes after it is set
75
+ let reminder = Schedule.Once({moment: After(Minutes(10))})
28
76
  ```
29
77
  */
30
78
  @schema
31
- type rate =
32
- | Single(year, month, day, hour, minute)
33
- | Minutes(int)
34
- | Hours(int)
35
- | Days(int)
36
- | Daily(hour, minute)
37
- | Weekdays(hour, minute)
38
- | WeekdaysAndSaturday(hour, minute)
79
+ type time =
80
+ | Every(interval)
81
+ | Calendar({calendar: calendar, zone?: zone})
82
+ | Once({moment: moment, zone?: zone})
39
83
 
40
84
  /**
41
- A named recurring or one-time schedule.
85
+ A named schedule.
42
86
 
43
- `payload` is an opaque string forwarded to the task handler when the schedule fires.
87
+ `payload` is an opaque string forwarded to the target when the schedule fires.
44
88
 
45
89
  @example
46
90
  ```rescript
47
91
  let dailySync: Schedule.schedule = {
48
92
  name: "DailyCatalogSync",
49
- rate: Daily(2, 0), // 02:00 UTC every day
93
+ time: Calendar({calendar: Daily({hour: 2, minute: 0})}),
50
94
  payload: `{"action": "sync"}`,
51
95
  }
52
96
  ```
@@ -54,7 +98,7 @@ let dailySync: Schedule.schedule = {
54
98
  @schema
55
99
  type schedule = {
56
100
  name: string,
57
- rate: rate,
101
+ time: time,
58
102
  payload: string,
59
103
  }
60
104
 
@@ -12,15 +12,7 @@ let hourSchema = Sury.int32;
12
12
 
13
13
  let minuteSchema = Sury.int32;
14
14
 
15
- let rateSchema = Sury.union([
16
- Sury.$schema(s => ({
17
- TAG: "Single",
18
- _0: s.m(yearSchema),
19
- _1: s.m(monthSchema),
20
- _2: s.m(daySchema),
21
- _3: s.m(hourSchema),
22
- _4: s.m(minuteSchema)
23
- })),
15
+ let intervalSchema = Sury.union([
24
16
  Sury.$schema(s => ({
25
17
  TAG: "Minutes",
26
18
  _0: s.m(Sury.int32)
@@ -33,26 +25,118 @@ let rateSchema = Sury.union([
33
25
  TAG: "Days",
34
26
  _0: s.m(Sury.int32)
35
27
  })),
28
+ Sury.$schema(s => ({
29
+ TAG: "Weeks",
30
+ _0: s.m(Sury.int32)
31
+ }))
32
+ ]);
33
+
34
+ let weekdaySchema = Sury.union([
35
+ Sury.literal("Mon"),
36
+ Sury.literal("Tue"),
37
+ Sury.literal("Wed"),
38
+ Sury.literal("Thu"),
39
+ Sury.literal("Fri"),
40
+ Sury.literal("Sat"),
41
+ Sury.literal("Sun")
42
+ ]);
43
+
44
+ let calendarSchema = Sury.union([
36
45
  Sury.$schema(s => ({
37
46
  TAG: "Daily",
38
- _0: s.m(hourSchema),
39
- _1: s.m(minuteSchema)
47
+ hour: s.m(hourSchema),
48
+ minute: s.m(minuteSchema)
49
+ })),
50
+ Sury.$schema(s => ({
51
+ TAG: "Weekly",
52
+ days: s.m(Sury.array(weekdaySchema)),
53
+ hour: s.m(hourSchema),
54
+ minute: s.m(minuteSchema)
55
+ })),
56
+ Sury.$schema(s => ({
57
+ TAG: "Monthly",
58
+ day: s.m(daySchema),
59
+ hour: s.m(hourSchema),
60
+ minute: s.m(minuteSchema),
61
+ every: s.m(Sury.$option(Sury.int32))
62
+ })),
63
+ Sury.$schema(s => ({
64
+ TAG: "Yearly",
65
+ month: s.m(monthSchema),
66
+ day: s.m(daySchema),
67
+ hour: s.m(hourSchema),
68
+ minute: s.m(minuteSchema)
69
+ }))
70
+ ]);
71
+
72
+ let periodSchema = Sury.union([
73
+ Sury.$schema(s => ({
74
+ TAG: "Minutes",
75
+ _0: s.m(Sury.int32)
76
+ })),
77
+ Sury.$schema(s => ({
78
+ TAG: "Hours",
79
+ _0: s.m(Sury.int32)
80
+ })),
81
+ Sury.$schema(s => ({
82
+ TAG: "Days",
83
+ _0: s.m(Sury.int32)
84
+ })),
85
+ Sury.$schema(s => ({
86
+ TAG: "Weeks",
87
+ _0: s.m(Sury.int32)
88
+ })),
89
+ Sury.$schema(s => ({
90
+ TAG: "Months",
91
+ _0: s.m(Sury.int32)
92
+ })),
93
+ Sury.$schema(s => ({
94
+ TAG: "Years",
95
+ _0: s.m(Sury.int32)
96
+ }))
97
+ ]);
98
+
99
+ let localTimeSchema = Sury.$schema(s => ({
100
+ year: s.m(yearSchema),
101
+ month: s.m(monthSchema),
102
+ day: s.m(daySchema),
103
+ hour: s.m(hourSchema),
104
+ minute: s.m(minuteSchema)
105
+ }));
106
+
107
+ let momentSchema = Sury.union([
108
+ Sury.$schema(s => ({
109
+ TAG: "At",
110
+ _0: s.m(localTimeSchema)
111
+ })),
112
+ Sury.$schema(s => ({
113
+ TAG: "After",
114
+ _0: s.m(periodSchema)
115
+ }))
116
+ ]);
117
+
118
+ let zoneSchema = Sury.string;
119
+
120
+ let timeSchema = Sury.union([
121
+ Sury.$schema(s => ({
122
+ TAG: "Every",
123
+ _0: s.m(intervalSchema)
40
124
  })),
41
125
  Sury.$schema(s => ({
42
- TAG: "Weekdays",
43
- _0: s.m(hourSchema),
44
- _1: s.m(minuteSchema)
126
+ TAG: "Calendar",
127
+ calendar: s.m(calendarSchema),
128
+ zone: s.m(Sury.$option(zoneSchema))
45
129
  })),
46
130
  Sury.$schema(s => ({
47
- TAG: "WeekdaysAndSaturday",
48
- _0: s.m(hourSchema),
49
- _1: s.m(minuteSchema)
131
+ TAG: "Once",
132
+ moment: s.m(momentSchema),
133
+ zone: s.m(Sury.$option(zoneSchema))
50
134
  }))
51
135
  ]);
52
136
 
53
137
  let scheduleSchema = Sury.$schema(s => ({
54
138
  name: s.m(Sury.string),
55
- rate: s.m(rateSchema),
139
+ time: s.m(timeSchema),
56
140
  payload: s.m(Sury.string)
57
141
  }));
58
142
 
@@ -62,7 +146,14 @@ export {
62
146
  daySchema,
63
147
  hourSchema,
64
148
  minuteSchema,
65
- rateSchema,
149
+ intervalSchema,
150
+ weekdaySchema,
151
+ calendarSchema,
152
+ periodSchema,
153
+ localTimeSchema,
154
+ momentSchema,
155
+ zoneSchema,
156
+ timeSchema,
66
157
  scheduleSchema,
67
158
  }
68
159
  /* yearSchema Not a pure module */