@reventlessdev/reventless-core 3.0.0-alpha.295 → 3.0.0-alpha.296
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 +20 -0
- package/package.json +6 -6
- package/src/admin/Platform_ComponentDefinitionsApi.res +3 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/components/AutomationSlice/AutomationSlice.res +2 -0
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +125 -8
- package/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs +86 -9
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +122 -1
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +101 -2
- package/src/components/AutomationSlice/AutomationSlice_Route.res +3 -1
- package/src/components/AutomationSlice/AutomationSlice_Route.res.mjs +2 -1
- package/src/components/AutomationSlice/AutomationSlice_Schedules.res +149 -0
- package/src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs +193 -0
- package/src/components/Dcb/Dcb_Builder.res +28 -4
- package/src/components/Dcb/Dcb_Builder.res.mjs +27 -5
- package/src/plugin/component/Plugin_Builder.res +10 -3
- package/src/plugin/component/Plugin_Builder.res.mjs +8 -2
- package/src/plugin/component/Plugin_Structure.res +7 -0
- package/src/plugin/component/Plugin_Structure.res.mjs +5 -2
- package/tests/admin/AdminApiSchemaDriftTest.res +1 -0
- package/tests/admin/AdminApiSchemaDriftTest.res.mjs +4 -1
- package/tests/automationslice/AutomationSliceSchedulesTest.res +143 -0
- package/tests/automationslice/AutomationSliceSchedulesTest.res.mjs +247 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
5
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
6
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
7
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
7
8
|
import * as Clock from "effect/Clock";
|
|
8
9
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
9
10
|
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
|
|
10
12
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
13
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
12
14
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
@@ -14,6 +16,7 @@ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/U
|
|
|
14
16
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
15
17
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
16
18
|
import * as AutomationSlice_Route$ReventlessCore from "./AutomationSlice_Route.res.mjs";
|
|
19
|
+
import * as AutomationSlice_Schedules$ReventlessCore from "./AutomationSlice_Schedules.res.mjs";
|
|
17
20
|
|
|
18
21
|
let todoStatusSchema = Sury.union([
|
|
19
22
|
Sury.literal("Pending"),
|
|
@@ -45,10 +48,43 @@ function todoRowSchemaFor(itemSchema) {
|
|
|
45
48
|
}));
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
function tickEvent(key, at) {
|
|
52
|
+
let newrecord = {...Message$ReventlessCore.generateMeta(Clock$Reventless.name, undefined, undefined, undefined, undefined, undefined, undefined, undefined)};
|
|
53
|
+
return Object.fromEntries([
|
|
54
|
+
[
|
|
55
|
+
"id",
|
|
56
|
+
key
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
"meta",
|
|
60
|
+
Util_Sury$Reventless.toJson((newrecord.time = new Date(at).toISOString(), newrecord), Message$Reventless.metaSchema)
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"event",
|
|
64
|
+
Util_Sury$Reventless.toJson({
|
|
65
|
+
TAG: "Tick",
|
|
66
|
+
key: key,
|
|
67
|
+
at: at
|
|
68
|
+
}, Clock$Reventless.eventSchema)
|
|
69
|
+
]
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
function Make(Spec) {
|
|
49
74
|
return Automation => {
|
|
50
75
|
let todoItems = {};
|
|
76
|
+
let schedules = {};
|
|
77
|
+
let scheduleChanges = {
|
|
78
|
+
contents: []
|
|
79
|
+
};
|
|
80
|
+
let takeScheduleChanges = () => {
|
|
81
|
+
let changes = scheduleChanges.contents;
|
|
82
|
+
scheduleChanges.contents = [];
|
|
83
|
+
return changes;
|
|
84
|
+
};
|
|
85
|
+
let readsClock = Automation.mappings.some(M => M.sourceName === Clock$Reventless.name);
|
|
51
86
|
let makeMeta = () => Message$ReventlessCore.generateMeta(Spec.targetName, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
87
|
+
let processingTimeoutMs = 15 * 60 * 1000;
|
|
52
88
|
let afterFailedAttempt = row => {
|
|
53
89
|
let retryCount = row.retryCount + 1 | 0;
|
|
54
90
|
let newrecord = {...row};
|
|
@@ -91,6 +127,22 @@ function Make(Spec) {
|
|
|
91
127
|
todoItems[id] = newrecord;
|
|
92
128
|
}));
|
|
93
129
|
};
|
|
130
|
+
let reschedule = (routed, setBy, recordedAt) => {
|
|
131
|
+
routed.forEach(r => {
|
|
132
|
+
r.rescheduled.forEach(change => {
|
|
133
|
+
if (!readsClock) {
|
|
134
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `a mapping changed a schedule, but no mapping reads the Clock, so no tick would reach the slice; the change is skipped`));
|
|
135
|
+
}
|
|
136
|
+
let platformChange = AutomationSlice_Schedules$ReventlessCore.apply(schedules, change, setBy, recordedAt);
|
|
137
|
+
if (platformChange.TAG === "Ok") {
|
|
138
|
+
scheduleChanges.contents = scheduleChanges.contents.concat([platformChange._0]);
|
|
139
|
+
return;
|
|
140
|
+
} else {
|
|
141
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, platformChange._0 + `; the change is skipped`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
};
|
|
94
146
|
let phase1 = (events, sliceCtx) => {
|
|
95
147
|
events.forEach(json => {
|
|
96
148
|
let context = Message$ReventlessCore.decode(json, Message$Reventless.contextSchema);
|
|
@@ -106,12 +158,55 @@ function Make(Spec) {
|
|
|
106
158
|
let sourceName = context.meta.service;
|
|
107
159
|
let dict = Stdlib_JSON.Decode.object(json);
|
|
108
160
|
let eventPayload = dict !== undefined ? Stdlib_Option.getOr(dict["event"], json) : json;
|
|
109
|
-
Route.route(eventPayload, sourceName, context.id, ctx)
|
|
161
|
+
let routed = Route.route(eventPayload, sourceName, context.id, ctx);
|
|
162
|
+
routed.forEach(apply);
|
|
163
|
+
reschedule(routed, context.meta.msgId, Stdlib_Option.getOr(ctx.recordedAt, Date.now()));
|
|
164
|
+
if (sourceName === Clock$Reventless.name) {
|
|
165
|
+
return AutomationSlice_Schedules$ReventlessCore.fired(schedules, context.id);
|
|
166
|
+
}
|
|
110
167
|
});
|
|
111
168
|
};
|
|
169
|
+
let rebuildSchedules = events => {
|
|
170
|
+
Object.keys(schedules).forEach(key => Stdlib_Dict.$$delete(schedules, key));
|
|
171
|
+
events.forEach(json => {
|
|
172
|
+
let context = Message$ReventlessCore.decode(json, Message$Reventless.contextSchema);
|
|
173
|
+
let dict = Stdlib_JSON.Decode.object(json);
|
|
174
|
+
let eventPayload = dict !== undefined ? Stdlib_Option.getOr(dict["event"], json) : json;
|
|
175
|
+
let recordedAt = Stdlib_Option.getOr(recordedAtOf(context.meta.time), Date.now());
|
|
176
|
+
reschedule(Route.route(eventPayload, context.meta.service, context.id, {
|
|
177
|
+
environment: "",
|
|
178
|
+
platformName: "",
|
|
179
|
+
pluginName: "",
|
|
180
|
+
sliceName: Spec.name,
|
|
181
|
+
recordedAt: recordedAt
|
|
182
|
+
}), context.meta.msgId, recordedAt);
|
|
183
|
+
});
|
|
184
|
+
scheduleChanges.contents = [];
|
|
185
|
+
};
|
|
112
186
|
let phase2 = async publishJsons => {
|
|
113
187
|
let currentTime = await Effect.runPromise(Clock.currentTimeMillis);
|
|
114
188
|
let newlyAbandoned = [];
|
|
189
|
+
Object.entries(todoItems).forEach(param => {
|
|
190
|
+
let row = param[1];
|
|
191
|
+
let id = param[0];
|
|
192
|
+
let match = row.status;
|
|
193
|
+
let match$1 = Stdlib_Option.map(row.processedAt, t => new Date(t).getTime());
|
|
194
|
+
if (match !== "Processing") {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (match$1 === undefined) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (currentTime - match$1 < processingTimeoutMs) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let updated = afterFailedAttempt(row);
|
|
204
|
+
todoItems[id] = updated;
|
|
205
|
+
if (updated.status === "Abandoned") {
|
|
206
|
+
newlyAbandoned.push(id);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
115
210
|
Object.entries(todoItems).forEach(param => {
|
|
116
211
|
let row = param[1];
|
|
117
212
|
if (!(row.status === "Failed" && row.retryCount >= Spec.maxRetries)) {
|
|
@@ -244,7 +339,10 @@ function Make(Spec) {
|
|
|
244
339
|
Spec: Spec,
|
|
245
340
|
todoItems: todoItems,
|
|
246
341
|
phase1: phase1,
|
|
247
|
-
phase2: phase2
|
|
342
|
+
phase2: phase2,
|
|
343
|
+
schedules: schedules,
|
|
344
|
+
takeScheduleChanges: takeScheduleChanges,
|
|
345
|
+
rebuildSchedules: rebuildSchedules
|
|
248
346
|
};
|
|
249
347
|
};
|
|
250
348
|
}
|
|
@@ -253,6 +351,7 @@ export {
|
|
|
253
351
|
todoStatusSchema,
|
|
254
352
|
todoRowSchema,
|
|
255
353
|
todoRowSchemaFor,
|
|
354
|
+
tickEvent,
|
|
256
355
|
Make,
|
|
257
356
|
}
|
|
258
357
|
/* todoStatusSchema Not a pure module */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Decode one source event and run its mappings' `collect`/`resolve`, with no
|
|
1
|
+
// Decode one source event and run its mappings' `collect`/`resolve`/`reschedule`, with no
|
|
2
2
|
// side effects. Shared by the runtime callback, which applies the result to its
|
|
3
3
|
// to-do list, and by the GWT sweep, which must route exactly as the runtime does.
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
type routed<'item> = {
|
|
7
7
|
collected: array<(string, 'item)>,
|
|
8
8
|
resolved: option<string>,
|
|
9
|
+
rescheduled: array<Reventless.AutomationSlice.scheduleChange>,
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
module Make = (
|
|
@@ -31,6 +32,7 @@ module Make = (
|
|
|
31
32
|
decoder.decode(~eventType, ~data=dataDict)->Option.map(event => {
|
|
32
33
|
collected: M.collect(event, ~sourceId, ctx),
|
|
33
34
|
resolved: M.resolve(event),
|
|
35
|
+
rescheduled: M.reschedule(event),
|
|
34
36
|
})
|
|
35
37
|
}
|
|
36
38
|
{sourceName: M.sourceName, handle}
|
|
@@ -13,7 +13,8 @@ function Make(Spec) {
|
|
|
13
13
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
14
14
|
return Stdlib_Option.map(decoder.decode(match[0], match[1]), event => ({
|
|
15
15
|
collected: M.collect(event, sourceId, ctx),
|
|
16
|
-
resolved: M.resolve(event)
|
|
16
|
+
resolved: M.resolve(event),
|
|
17
|
+
rescheduled: M.reschedule(event)
|
|
17
18
|
}));
|
|
18
19
|
};
|
|
19
20
|
return {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The schedules of one automation slice: a row per key, kept in the slice's schedules
|
|
2
|
+
// store. The rows are the truth and the platform's schedules follow them. Shared by
|
|
3
|
+
// the runtime and the GWT DSL, so a scenario folds `reschedule` as the runtime does.
|
|
4
|
+
|
|
5
|
+
/** The `setBy` of the row a spec's declared `schedule` writes. */
|
|
6
|
+
let declared = "declared"
|
|
7
|
+
|
|
8
|
+
@schema
|
|
9
|
+
type row = {
|
|
10
|
+
key: string,
|
|
11
|
+
/** The time as the JSON text of `Reventless.Schedule.timeSchema`, an `After` set by an event
|
|
12
|
+
already counted from when the event was recorded. Absent when an event cleared
|
|
13
|
+
the slice-wide key, so a deploy does not put the declared time back. */
|
|
14
|
+
time?: string,
|
|
15
|
+
/** The id of the event that set or cleared it, or `declared`. */
|
|
16
|
+
setBy: string,
|
|
17
|
+
setAt: string,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What the platform is told after a row changed. */
|
|
21
|
+
type platformChange =
|
|
22
|
+
| Put({key: string, time: Reventless.Schedule.time})
|
|
23
|
+
| Remove({key: string})
|
|
24
|
+
|
|
25
|
+
let encodeTime = (time: Reventless.Schedule.time) =>
|
|
26
|
+
time->Reventless.Util_Sury.toJson(Reventless.Schedule.timeSchema)->JSON.stringify
|
|
27
|
+
let decodeTime = (text: string): Reventless.Schedule.time =>
|
|
28
|
+
text->JSON.parseOrThrow->Reventless.Util_Sury.fromJson(Reventless.Schedule.timeSchema)
|
|
29
|
+
let timeOf = (row: row) => row.time->Option.map(decodeTime)
|
|
30
|
+
|
|
31
|
+
let iso = millis => millis->Date.fromTime->Date.toISOString
|
|
32
|
+
|
|
33
|
+
// The slice-wide key keeps a row saying it was cleared; any other key's row goes.
|
|
34
|
+
let clearRow = (rows: dict<row>, ~key, ~setBy, ~setAt) =>
|
|
35
|
+
if key == Reventless.Clock.defaultKey {
|
|
36
|
+
rows->Dict.set(key, {key, setBy, setAt})
|
|
37
|
+
} else {
|
|
38
|
+
rows->Dict.delete(key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
Applies one change an event made, recorded at `recordedAt`, and returns what the
|
|
43
|
+
platform is told. A time that cannot fire is refused with every problem it has, and
|
|
44
|
+
changes nothing.
|
|
45
|
+
*/
|
|
46
|
+
let apply = (
|
|
47
|
+
rows: dict<row>,
|
|
48
|
+
change: Reventless.AutomationSlice.scheduleChange,
|
|
49
|
+
~setBy,
|
|
50
|
+
~recordedAt,
|
|
51
|
+
): result<platformChange, string> =>
|
|
52
|
+
switch change {
|
|
53
|
+
| Set({key, time: wanted}) =>
|
|
54
|
+
switch ScheduleTime.errors(wanted) {
|
|
55
|
+
| [] =>
|
|
56
|
+
let resolved = wanted->ScheduleTime.resolveOnce(~from=recordedAt)
|
|
57
|
+
rows->Dict.set(key, {key, time: encodeTime(resolved), setBy, setAt: iso(recordedAt)})
|
|
58
|
+
Ok(Put({key, time: resolved}))
|
|
59
|
+
| errs => Error(`schedule ${key}: ${errs->Array.join("; ")}`)
|
|
60
|
+
}
|
|
61
|
+
| Clear({key: cleared}) =>
|
|
62
|
+
clearRow(rows, ~key=cleared, ~setBy, ~setAt=iso(recordedAt))
|
|
63
|
+
Ok(Remove({key: cleared}))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A tick of `key` arrived: a once-only schedule has fired, so its row goes. */
|
|
67
|
+
let fired = (rows: dict<row>, ~key) =>
|
|
68
|
+
rows
|
|
69
|
+
->Dict.get(key)
|
|
70
|
+
->Option.forEach(row =>
|
|
71
|
+
switch row->timeOf {
|
|
72
|
+
| Some(Once(_)) => clearRow(rows, ~key, ~setBy=row.setBy, ~setAt=row.setAt)
|
|
73
|
+
| _ => ()
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
Brings the spec's declared time into the rows under `Reventless.Clock.defaultKey`: written when
|
|
79
|
+
no event has set or cleared that key, replaced when the declaration changed, and
|
|
80
|
+
removed when the spec no longer declares one.
|
|
81
|
+
*/
|
|
82
|
+
let declare = (rows: dict<row>, declaredTime: option<Reventless.Schedule.time>, ~now) => {
|
|
83
|
+
let key = Reventless.Clock.defaultKey
|
|
84
|
+
let write = time =>
|
|
85
|
+
rows->Dict.set(key, {key, time: encodeTime(time), setBy: declared, setAt: iso(now)})
|
|
86
|
+
switch (declaredTime, rows->Dict.get(key)) {
|
|
87
|
+
| (Some(time), None) => write(time)
|
|
88
|
+
| (Some(time), Some(row)) if row.setBy == declared && row.time != Some(encodeTime(time)) =>
|
|
89
|
+
write(time)
|
|
90
|
+
| (None, Some(row)) if row.setBy == declared => rows->Dict.delete(key)
|
|
91
|
+
| _ => ()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A once-only time already past fires no more.
|
|
96
|
+
let isPast = (time: Reventless.Schedule.time, ~now) =>
|
|
97
|
+
switch time {
|
|
98
|
+
| Once(_) => time->ScheduleTime.nextFiring(~after=now)->Option.isNone
|
|
99
|
+
| _ => false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
Brings the platform in line with the rows, after the declared time is brought in: a
|
|
104
|
+
schedule missing from the platform is put, the declared one is put again in case its
|
|
105
|
+
time changed, and one the rows do not hold is removed. A once-only time already past
|
|
106
|
+
is not put again, and its row goes. A row an event changed reached the platform when
|
|
107
|
+
it changed, so it is not put again.
|
|
108
|
+
*/
|
|
109
|
+
let reconcile = async (
|
|
110
|
+
rows: dict<row>,
|
|
111
|
+
platform: ReventlessInfra.AutomationSlice.platformSchedules,
|
|
112
|
+
~declaredTime,
|
|
113
|
+
~now,
|
|
114
|
+
) => {
|
|
115
|
+
declare(rows, declaredTime, ~now)
|
|
116
|
+
let live =
|
|
117
|
+
rows
|
|
118
|
+
->Dict.valuesToArray
|
|
119
|
+
->Array.filterMap(row =>
|
|
120
|
+
switch row->timeOf {
|
|
121
|
+
| Some(time) if isPast(time, ~now) =>
|
|
122
|
+
clearRow(rows, ~key=row.key, ~setBy=row.setBy, ~setAt=row.setAt)
|
|
123
|
+
None
|
|
124
|
+
| Some(time) => Some((row, time))
|
|
125
|
+
| None => None
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
let held = await platform.names()
|
|
129
|
+
for i in 0 to live->Array.length - 1 {
|
|
130
|
+
let (row, time) = live->Array.getUnsafe(i)
|
|
131
|
+
if row.setBy == declared || !(held->Array.includes(platform.nameOf(row.key))) {
|
|
132
|
+
await platform.put(~key=row.key, time)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
let wanted = live->Array.map(((row, _)) => platform.nameOf(row.key))
|
|
136
|
+
let extra = held->Array.filter(name => !(wanted->Array.includes(name)))
|
|
137
|
+
for i in 0 to extra->Array.length - 1 {
|
|
138
|
+
await platform.removeName(extra->Array.getUnsafe(i))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Tells the platform what the rows' changes were, in order. */
|
|
143
|
+
let tell = async (platform: ReventlessInfra.AutomationSlice.platformSchedules, changes) =>
|
|
144
|
+
for i in 0 to changes->Array.length - 1 {
|
|
145
|
+
switch changes->Array.getUnsafe(i) {
|
|
146
|
+
| Put({key, time}) => await platform.put(~key, time)
|
|
147
|
+
| Remove({key}) => await platform.removeName(platform.nameOf(key))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
|
|
8
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
9
|
+
import * as Schedule$Reventless from "@reventlessdev/reventless-spec/src/types/Schedule.res.mjs";
|
|
10
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
11
|
+
import * as ScheduleTime$ReventlessCore from "../../util/ScheduleTime.res.mjs";
|
|
12
|
+
|
|
13
|
+
let declared = "declared";
|
|
14
|
+
|
|
15
|
+
let rowSchema = Sury.$schema(s => ({
|
|
16
|
+
key: s.m(Sury.string),
|
|
17
|
+
time: s.m(Sury.$option(Sury.string)),
|
|
18
|
+
setBy: s.m(Sury.string),
|
|
19
|
+
setAt: s.m(Sury.string)
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
function encodeTime(time) {
|
|
23
|
+
return JSON.stringify(Util_Sury$Reventless.toJson(time, Schedule$Reventless.timeSchema));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function decodeTime(text) {
|
|
27
|
+
return Util_Sury$Reventless.fromJson(JSON.parse(text), Schedule$Reventless.timeSchema);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function timeOf(row) {
|
|
31
|
+
return Stdlib_Option.map(row.time, decodeTime);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function iso(millis) {
|
|
35
|
+
return new Date(millis).toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clearRow(rows, key, setBy, setAt) {
|
|
39
|
+
if (key === Clock$Reventless.defaultKey) {
|
|
40
|
+
rows[key] = {
|
|
41
|
+
key: key,
|
|
42
|
+
setBy: setBy,
|
|
43
|
+
setAt: setAt
|
|
44
|
+
};
|
|
45
|
+
return;
|
|
46
|
+
} else {
|
|
47
|
+
return Stdlib_Dict.$$delete(rows, key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function apply(rows, change, setBy, recordedAt) {
|
|
52
|
+
if (change.TAG === "Set") {
|
|
53
|
+
let wanted = change.time;
|
|
54
|
+
let key = change.key;
|
|
55
|
+
let errs = ScheduleTime$ReventlessCore.errors(wanted);
|
|
56
|
+
if (errs.length !== 0) {
|
|
57
|
+
return {
|
|
58
|
+
TAG: "Error",
|
|
59
|
+
_0: `schedule ` + key + `: ` + errs.join("; ")
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
let resolved = ScheduleTime$ReventlessCore.resolveOnce(wanted, recordedAt);
|
|
63
|
+
rows[key] = {
|
|
64
|
+
key: key,
|
|
65
|
+
time: encodeTime(resolved),
|
|
66
|
+
setBy: setBy,
|
|
67
|
+
setAt: new Date(recordedAt).toISOString()
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
TAG: "Ok",
|
|
71
|
+
_0: {
|
|
72
|
+
TAG: "Put",
|
|
73
|
+
key: key,
|
|
74
|
+
time: resolved
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
let cleared = change.key;
|
|
79
|
+
clearRow(rows, cleared, setBy, new Date(recordedAt).toISOString());
|
|
80
|
+
return {
|
|
81
|
+
TAG: "Ok",
|
|
82
|
+
_0: {
|
|
83
|
+
TAG: "Remove",
|
|
84
|
+
key: cleared
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fired(rows, key) {
|
|
90
|
+
Stdlib_Option.forEach(rows[key], row => {
|
|
91
|
+
let match = Stdlib_Option.map(row.time, decodeTime);
|
|
92
|
+
if (match === undefined) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
switch (match.TAG) {
|
|
96
|
+
case "Every" :
|
|
97
|
+
case "Calendar" :
|
|
98
|
+
return;
|
|
99
|
+
case "Once" :
|
|
100
|
+
return clearRow(rows, key, row.setBy, row.setAt);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function declare(rows, declaredTime, now) {
|
|
106
|
+
let match = rows[Clock$Reventless.defaultKey];
|
|
107
|
+
if (declaredTime !== undefined) {
|
|
108
|
+
if (match !== undefined && !(match.setBy === declared && Primitive_object.notequal(match.time, encodeTime(declaredTime)))) {
|
|
109
|
+
return;
|
|
110
|
+
} else {
|
|
111
|
+
rows[Clock$Reventless.defaultKey] = {
|
|
112
|
+
key: Clock$Reventless.defaultKey,
|
|
113
|
+
time: encodeTime(declaredTime),
|
|
114
|
+
setBy: declared,
|
|
115
|
+
setAt: new Date(now).toISOString()
|
|
116
|
+
};
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
} else if (match !== undefined && match.setBy === declared) {
|
|
120
|
+
return Stdlib_Dict.$$delete(rows, Clock$Reventless.defaultKey);
|
|
121
|
+
} else {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isPast(time, now) {
|
|
127
|
+
switch (time.TAG) {
|
|
128
|
+
case "Every" :
|
|
129
|
+
case "Calendar" :
|
|
130
|
+
return false;
|
|
131
|
+
case "Once" :
|
|
132
|
+
return Stdlib_Option.isNone(ScheduleTime$ReventlessCore.nextFiring(time, now));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function reconcile(rows, platform, declaredTime, now) {
|
|
137
|
+
declare(rows, declaredTime, now);
|
|
138
|
+
let live = Stdlib_Array.filterMap(Object.values(rows), row => {
|
|
139
|
+
let time = Stdlib_Option.map(row.time, decodeTime);
|
|
140
|
+
if (time !== undefined) {
|
|
141
|
+
if (isPast(time, now)) {
|
|
142
|
+
clearRow(rows, row.key, row.setBy, row.setAt);
|
|
143
|
+
return;
|
|
144
|
+
} else {
|
|
145
|
+
return [
|
|
146
|
+
row,
|
|
147
|
+
time
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
let held = await platform.names();
|
|
153
|
+
for (let i = 0, i_finish = live.length; i < i_finish; ++i) {
|
|
154
|
+
let match = live[i];
|
|
155
|
+
let row = match[0];
|
|
156
|
+
if (row.setBy === declared || !held.includes(platform.nameOf(row.key))) {
|
|
157
|
+
await platform.put(row.key, match[1]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let wanted = live.map(param => platform.nameOf(param[0].key));
|
|
161
|
+
let extra = held.filter(name => !wanted.includes(name));
|
|
162
|
+
for (let i$1 = 0, i_finish$1 = extra.length; i$1 < i_finish$1; ++i$1) {
|
|
163
|
+
await platform.removeName(extra[i$1]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function tell(platform, changes) {
|
|
168
|
+
for (let i = 0, i_finish = changes.length; i < i_finish; ++i) {
|
|
169
|
+
let match = changes[i];
|
|
170
|
+
if (match.TAG === "Put") {
|
|
171
|
+
await platform.put(match.key, match.time);
|
|
172
|
+
} else {
|
|
173
|
+
await platform.removeName(platform.nameOf(match.key));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export {
|
|
179
|
+
declared,
|
|
180
|
+
rowSchema,
|
|
181
|
+
encodeTime,
|
|
182
|
+
decodeTime,
|
|
183
|
+
timeOf,
|
|
184
|
+
iso,
|
|
185
|
+
clearRow,
|
|
186
|
+
apply,
|
|
187
|
+
fired,
|
|
188
|
+
declare,
|
|
189
|
+
isPast,
|
|
190
|
+
reconcile,
|
|
191
|
+
tell,
|
|
192
|
+
}
|
|
193
|
+
/* rowSchema Not a pure module */
|
|
@@ -188,11 +188,13 @@ module Make = (
|
|
|
188
188
|
// its own `sourceEventSchema`. Validation contributes one entry per
|
|
189
189
|
// (slice, source) pair so unknown variants surface against the right
|
|
190
190
|
// mapping context.
|
|
191
|
+
// The Clock's ticks come from the slice's schedules, not from a slice.
|
|
191
192
|
automationSlices->Array.flatMap((module(A: AutomationSlice.T)) =>
|
|
192
|
-
A.Automation.mappings->Array.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
A.Automation.mappings->Array.filterMap((module(M: A.Automation.Mapping)) =>
|
|
194
|
+
M.sourceName == Reventless.Clock.name
|
|
195
|
+
? None
|
|
196
|
+
: Some((A.Spec.name, M.sourceEventSchema->S.castToUnknown))
|
|
197
|
+
)
|
|
196
198
|
),
|
|
197
199
|
)
|
|
198
200
|
->Array.concat(
|
|
@@ -664,6 +666,12 @@ module Make = (
|
|
|
664
666
|
automationSlices->Array.forEach((module(A: AutomationSlice.T)) => {
|
|
665
667
|
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName=A.queryDbName)
|
|
666
668
|
Plugin_Helpers.queryFieldNamesRegistry->Dict.set(A.queryDbName, qn)
|
|
669
|
+
A.schedulesDbName->Option.forEach(queryDbName =>
|
|
670
|
+
Plugin_Helpers.queryFieldNamesRegistry->Dict.set(
|
|
671
|
+
queryDbName,
|
|
672
|
+
Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName),
|
|
673
|
+
)
|
|
674
|
+
)
|
|
667
675
|
})
|
|
668
676
|
|
|
669
677
|
outboundTranslationSlices->Array.forEach((module(O: OutboundTranslationSlice.T)) => {
|
|
@@ -1144,6 +1152,21 @@ module Make = (
|
|
|
1144
1152
|
}
|
|
1145
1153
|
})
|
|
1146
1154
|
|
|
1155
|
+
// A scheduled slice's schedules: when it runs, and who set it.
|
|
1156
|
+
let scheduleEntries = automationSlices->Array.filterMap((module(A: AutomationSlice.T)) =>
|
|
1157
|
+
A.schedulesDbName->Option.map(queryDbName => {
|
|
1158
|
+
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName)
|
|
1159
|
+
{
|
|
1160
|
+
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1161
|
+
listFieldName: qn.listFieldName,
|
|
1162
|
+
returnTypeName: qn.returnTypeName,
|
|
1163
|
+
stateSchema: AutomationSlice_Schedules.rowSchema->S.castToUnknown,
|
|
1164
|
+
authorization: None,
|
|
1165
|
+
connectionSpec: true,
|
|
1166
|
+
}
|
|
1167
|
+
})
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1147
1170
|
let outboundEntries =
|
|
1148
1171
|
outboundTranslationSlices->Array.map((module(O: OutboundTranslationSlice.T)) => {
|
|
1149
1172
|
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(
|
|
@@ -1234,6 +1257,7 @@ module Make = (
|
|
|
1234
1257
|
mutationEntries: Array.concat(mutationEntriesFromSlices, mutationEntriesFromInboundSlices),
|
|
1235
1258
|
queryEntries: stateViewEntries
|
|
1236
1259
|
->Array.concat(automationEntries)
|
|
1260
|
+
->Array.concat(scheduleEntries)
|
|
1237
1261
|
->Array.concat(outboundEntries)
|
|
1238
1262
|
->Array.concat(inboundEntries),
|
|
1239
1263
|
eventLogEntries: if allProducedSchemas->Array.length > 0 {
|
|
@@ -8,6 +8,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
import * as Pulumi from "@pulumi/pulumi";
|
|
10
10
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
11
|
+
import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
|
|
11
12
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
12
13
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
13
14
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
@@ -25,6 +26,7 @@ import * as Plugin_Structure$ReventlessCore from "../../plugin/component/Plugin_
|
|
|
25
26
|
import * as DcbEventLog_Builder$ReventlessCore from "../DcbEventLog/DcbEventLog_Builder.res.mjs";
|
|
26
27
|
import * as CommandTopic_Builder$ReventlessCore from "../CommandTopic/CommandTopic_Builder.res.mjs";
|
|
27
28
|
import * as AutomationSlice_Callback$ReventlessCore from "../AutomationSlice/AutomationSlice_Callback.res.mjs";
|
|
29
|
+
import * as AutomationSlice_Schedules$ReventlessCore from "../AutomationSlice/AutomationSlice_Schedules.res.mjs";
|
|
28
30
|
import * as CommandGenerator_Callback$ReventlessCore from "../CommandGenerator/CommandGenerator_Callback.res.mjs";
|
|
29
31
|
import * as InboundTranslationSlice_Callback$ReventlessCore from "../InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
|
|
30
32
|
import * as OutboundTranslationSlice_Callback$ReventlessCore from "../OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
|
|
@@ -150,10 +152,16 @@ function Make(DcbEventLogStorage) {
|
|
|
150
152
|
]).concat(stateViewSlices.map(V => [
|
|
151
153
|
V.Spec.name,
|
|
152
154
|
V.Spec.consumedEventSchema
|
|
153
|
-
])).concat(automationSlices.flatMap(A => A.Automation.mappings
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
])).concat(automationSlices.flatMap(A => Stdlib_Array.filterMap(A.Automation.mappings, M => {
|
|
156
|
+
if (M.sourceName === Clock$Reventless.name) {
|
|
157
|
+
return;
|
|
158
|
+
} else {
|
|
159
|
+
return [
|
|
160
|
+
A.Spec.name,
|
|
161
|
+
M.sourceEventSchema
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
}))).concat(outboundTranslationSlices.map(O => [
|
|
157
165
|
O.Spec.name,
|
|
158
166
|
O.Spec.consumedEventSchema
|
|
159
167
|
]));
|
|
@@ -352,6 +360,9 @@ function Make(DcbEventLogStorage) {
|
|
|
352
360
|
automationSlices.forEach(A => {
|
|
353
361
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForSliceQueryDb(name, A.queryDbName, undefined);
|
|
354
362
|
Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[A.queryDbName] = qn;
|
|
363
|
+
Stdlib_Option.forEach(A.schedulesDbName, queryDbName => {
|
|
364
|
+
Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[queryDbName] = Api_Naming$ReventlessCore.queryFieldNamesForSliceQueryDb(name, queryDbName, undefined);
|
|
365
|
+
});
|
|
355
366
|
});
|
|
356
367
|
outboundTranslationSlices.forEach(O => {
|
|
357
368
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForSliceQueryDb(name, O.queryDbName, undefined);
|
|
@@ -612,6 +623,17 @@ function Make(DcbEventLogStorage) {
|
|
|
612
623
|
connectionSpec: true
|
|
613
624
|
};
|
|
614
625
|
});
|
|
626
|
+
let scheduleEntries = Stdlib_Array.filterMap(automationSlices, A => Stdlib_Option.map(A.schedulesDbName, queryDbName => {
|
|
627
|
+
let qn = Api_Naming$ReventlessCore.queryFieldNamesForSliceQueryDb(name, queryDbName, undefined);
|
|
628
|
+
return {
|
|
629
|
+
singleFieldName: qn.singleFieldName,
|
|
630
|
+
listFieldName: qn.listFieldName,
|
|
631
|
+
returnTypeName: qn.returnTypeName,
|
|
632
|
+
stateSchema: AutomationSlice_Schedules$ReventlessCore.rowSchema,
|
|
633
|
+
authorization: undefined,
|
|
634
|
+
connectionSpec: true
|
|
635
|
+
};
|
|
636
|
+
}));
|
|
615
637
|
let outboundEntries = outboundTranslationSlices.map(O => {
|
|
616
638
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForSliceQueryDb(name, O.queryDbName, undefined);
|
|
617
639
|
return {
|
|
@@ -655,7 +677,7 @@ function Make(DcbEventLogStorage) {
|
|
|
655
677
|
dcbPublishJsons: publishJsons,
|
|
656
678
|
dcbCommandTopicQueueUrl: dcbCommandTopicQueueUrl,
|
|
657
679
|
mutationEntries: mutationEntriesFromSlices.concat(mutationEntriesFromInboundSlices),
|
|
658
|
-
queryEntries: stateViewEntries.concat(automationEntries).concat(outboundEntries).concat(inboundEntries),
|
|
680
|
+
queryEntries: stateViewEntries.concat(automationEntries).concat(scheduleEntries).concat(outboundEntries).concat(inboundEntries),
|
|
659
681
|
eventLogEntries: allProducedSchemas.length !== 0 ? [{
|
|
660
682
|
busKey: name + "DcbEventLog",
|
|
661
683
|
displayName: name,
|