@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
|
@@ -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 */
|
package/src/types/Schedule.res
CHANGED
|
@@ -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
|
-
|
|
57
|
+
When a schedule fires: `Every` an interval, `Calendar` a wall-clock time that recurs,
|
|
58
|
+
or `Once` a single moment.
|
|
14
59
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
//
|
|
24
|
-
let
|
|
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
|
-
//
|
|
27
|
-
let
|
|
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
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
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
|
|
85
|
+
A named schedule.
|
|
42
86
|
|
|
43
|
-
`payload` is an opaque string forwarded to the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
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: "
|
|
43
|
-
|
|
44
|
-
|
|
126
|
+
TAG: "Calendar",
|
|
127
|
+
calendar: s.m(calendarSchema),
|
|
128
|
+
zone: s.m(Sury.$option(zoneSchema))
|
|
45
129
|
})),
|
|
46
130
|
Sury.$schema(s => ({
|
|
47
|
-
TAG: "
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 */
|