@reventlessdev/reventless-core 3.0.0-alpha.295 → 3.0.0-alpha.297
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 +33 -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/Api/GraphQL_FragmentGenerator.res +15 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +13 -0
- package/src/components/AutomationSlice/AutomationSlice.res +2 -0
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +126 -9
- package/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs +90 -10
- 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 +35 -4
- package/src/components/Dcb/Dcb_Builder.res.mjs +33 -5
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +1 -1
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res.mjs +3 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +1 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res.mjs +3 -1
- 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
|
@@ -93,8 +93,35 @@ type runtime = {
|
|
|
93
93
|
todoItems: Dict.t<todoRow>,
|
|
94
94
|
phase1: phase1,
|
|
95
95
|
phase2: phase2,
|
|
96
|
+
/** The slice's schedules, by key: what its schedules store holds. */
|
|
97
|
+
schedules: Dict.t<AutomationSlice_Schedules.row>,
|
|
98
|
+
/** What phase 1 changed in `schedules` since the last call, for the platform to
|
|
99
|
+
follow once the rows are stored. */
|
|
100
|
+
takeScheduleChanges: unit => array<AutomationSlice_Schedules.platformChange>,
|
|
101
|
+
/** Folds the given events' `reschedule` into empty rows, in order: a rebuild. */
|
|
102
|
+
rebuildSchedules: array<JSON.t> => unit,
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
/** A tick as the runtime receives an event: an envelope from the `Clock`
|
|
106
|
+
source, recorded at the scheduled time. */
|
|
107
|
+
let tickEvent = (~key, ~at) =>
|
|
108
|
+
JSON.Encode.object(
|
|
109
|
+
Dict.fromArray([
|
|
110
|
+
("id", key->JSON.Encode.string),
|
|
111
|
+
(
|
|
112
|
+
"meta",
|
|
113
|
+
{
|
|
114
|
+
...Message.generateMeta(~service=Reventless.Clock.name),
|
|
115
|
+
time: at->Date.fromTime->Date.toISOString,
|
|
116
|
+
}->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema),
|
|
117
|
+
),
|
|
118
|
+
(
|
|
119
|
+
"event",
|
|
120
|
+
Reventless.Clock.Tick({key, at})->Reventless.Util_Sury.toJson(Reventless.Clock.eventSchema),
|
|
121
|
+
),
|
|
122
|
+
]),
|
|
123
|
+
)
|
|
124
|
+
|
|
98
125
|
module type T = {
|
|
99
126
|
module Spec: Reventless.AutomationSlice.Spec
|
|
100
127
|
module Automation: Reventless.AutomationSlice.Automation with module Spec := Spec
|
|
@@ -108,6 +135,10 @@ module type T = {
|
|
|
108
135
|
|
|
109
136
|
/** Phase 2: process all pending items, publishing commands via publishJsons. */
|
|
110
137
|
let phase2: phase2
|
|
138
|
+
|
|
139
|
+
let schedules: Dict.t<AutomationSlice_Schedules.row>
|
|
140
|
+
let takeScheduleChanges: unit => array<AutomationSlice_Schedules.platformChange>
|
|
141
|
+
let rebuildSchedules: array<JSON.t> => unit
|
|
111
142
|
}
|
|
112
143
|
|
|
113
144
|
module Make = (
|
|
@@ -118,6 +149,20 @@ module Make = (
|
|
|
118
149
|
module Automation = Automation
|
|
119
150
|
|
|
120
151
|
let todoItems: Dict.t<todoRow> = Dict.make()
|
|
152
|
+
let schedules: Dict.t<AutomationSlice_Schedules.row> = Dict.make()
|
|
153
|
+
let scheduleChanges: ref<array<AutomationSlice_Schedules.platformChange>> = ref([])
|
|
154
|
+
|
|
155
|
+
let takeScheduleChanges = () => {
|
|
156
|
+
let changes = scheduleChanges.contents
|
|
157
|
+
scheduleChanges := []
|
|
158
|
+
changes
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Only a slice that maps the Clock can receive the ticks a schedule sends.
|
|
162
|
+
let readsClock =
|
|
163
|
+
Automation.mappings->Array.some((module(M: Automation.Mapping)) =>
|
|
164
|
+
M.sourceName == Reventless.Clock.name
|
|
165
|
+
)
|
|
121
166
|
|
|
122
167
|
let now = () => Date.make()->Date.toISOString
|
|
123
168
|
|
|
@@ -133,6 +178,10 @@ module Make = (
|
|
|
133
178
|
// the slice here makes the target's own events unmatchable, silently.
|
|
134
179
|
let makeMeta = (): Reventless.Message.meta => Message.generateMeta(~service=Spec.targetName)
|
|
135
180
|
|
|
181
|
+
/** How long a row may stay `Processing` before its command is sent again. */
|
|
182
|
+
let processingTimeoutMs =
|
|
183
|
+
15. *. 60. *. 1000.
|
|
184
|
+
|
|
136
185
|
/** Record a failed attempt. The row is marked `Abandoned` at the moment the
|
|
137
186
|
budget runs out rather than being left to fall out of the next pass's
|
|
138
187
|
filter, so a reader never has to compare `retryCount` against a constant
|
|
@@ -201,6 +250,32 @@ module Make = (
|
|
|
201
250
|
)
|
|
202
251
|
}
|
|
203
252
|
|
|
253
|
+
let reschedule = (
|
|
254
|
+
routed: array<AutomationSlice_Route.routed<Spec.todoItem>>,
|
|
255
|
+
~setBy,
|
|
256
|
+
~recordedAt,
|
|
257
|
+
) =>
|
|
258
|
+
routed->Array.forEach(r =>
|
|
259
|
+
r.rescheduled->Array.forEach(change =>
|
|
260
|
+
if !readsClock {
|
|
261
|
+
EffectLogger.logError(
|
|
262
|
+
~comp=`AutomationSlice(${Spec.name})`,
|
|
263
|
+
`a mapping changed a schedule, but no mapping reads the Clock, so no tick would reach the slice; the change is skipped`,
|
|
264
|
+
)->Effect.runSync
|
|
265
|
+
} else {
|
|
266
|
+
switch schedules->AutomationSlice_Schedules.apply(change, ~setBy, ~recordedAt) {
|
|
267
|
+
| Ok(platformChange) =>
|
|
268
|
+
scheduleChanges := scheduleChanges.contents->Array.concat([platformChange])
|
|
269
|
+
| Error(message) =>
|
|
270
|
+
EffectLogger.logError(
|
|
271
|
+
~comp=`AutomationSlice(${Spec.name})`,
|
|
272
|
+
`${message}; the change is skipped`,
|
|
273
|
+
)->Effect.runSync
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
204
279
|
let phase1 = (events: array<JSON.t>, sliceCtx: Reventless.AutomationSlice.context) => {
|
|
205
280
|
events->Array.forEach(json => {
|
|
206
281
|
let context = json->Message.decode(Reventless.Message.contextSchema)
|
|
@@ -217,8 +292,38 @@ module Make = (
|
|
|
217
292
|
| None => json
|
|
218
293
|
}
|
|
219
294
|
// Multiple mappings can share a sourceName; all matching dispatches run.
|
|
220
|
-
Route.route(eventPayload, ~sourceName, ~sourceId=context.id, ctx)
|
|
295
|
+
let routed = Route.route(eventPayload, ~sourceName, ~sourceId=context.id, ctx)
|
|
296
|
+
routed->Array.forEach(apply)
|
|
297
|
+
reschedule(
|
|
298
|
+
routed,
|
|
299
|
+
~setBy=context.meta.msgId,
|
|
300
|
+
~recordedAt=ctx.recordedAt->Option.getOr(Date.now()),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
// A tick's envelope is addressed by its key.
|
|
304
|
+
if sourceName == Reventless.Clock.name {
|
|
305
|
+
schedules->AutomationSlice_Schedules.fired(~key=context.id)
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let rebuildSchedules = (events: array<JSON.t>) => {
|
|
311
|
+
schedules->Dict.keysToArray->Array.forEach(key => schedules->Dict.delete(key))
|
|
312
|
+
events->Array.forEach(json => {
|
|
313
|
+
let context = json->Message.decode(Reventless.Message.contextSchema)
|
|
314
|
+
let eventPayload = switch json->JSON.Decode.object {
|
|
315
|
+
| Some(dict) => dict->Dict.get("event")->Option.getOr(json)
|
|
316
|
+
| None => json
|
|
317
|
+
}
|
|
318
|
+
let recordedAt = recordedAtOf(context.meta.time)->Option.getOr(Date.now())
|
|
319
|
+
Route.route(
|
|
320
|
+
eventPayload,
|
|
321
|
+
~sourceName=context.meta.service,
|
|
322
|
+
~sourceId=context.id,
|
|
323
|
+
{environment: "", platformName: "", pluginName: "", sliceName: Spec.name, recordedAt},
|
|
324
|
+
)->reschedule(~setBy=context.meta.msgId, ~recordedAt)
|
|
221
325
|
})
|
|
326
|
+
scheduleChanges := []
|
|
222
327
|
}
|
|
223
328
|
|
|
224
329
|
let phase2 = async (publishJsons: ReventlessInfra.CommandTopic.publishJsons) => {
|
|
@@ -228,6 +333,22 @@ module Make = (
|
|
|
228
333
|
// in fact, so say so rather than letting it sit in a status that promises a
|
|
229
334
|
// retry no pass will make.
|
|
230
335
|
let newlyAbandoned: array<string> = []
|
|
336
|
+
// A command published but never answered leaves its row `Processing`. After
|
|
337
|
+
// the timeout that counts as a failed attempt, so this pass sends it again
|
|
338
|
+
// while the budget lasts.
|
|
339
|
+
todoItems
|
|
340
|
+
->Dict.toArray
|
|
341
|
+
->Array.forEach(((id, row)) =>
|
|
342
|
+
switch (row.status, row.processedAt->Option.map(t => Date.fromString(t)->Date.getTime)) {
|
|
343
|
+
| (Processing, Some(processedAt)) if currentTime -. processedAt >= processingTimeoutMs =>
|
|
344
|
+
let updated = row->afterFailedAttempt
|
|
345
|
+
todoItems->Dict.set(id, updated)
|
|
346
|
+
if updated.status == Abandoned {
|
|
347
|
+
newlyAbandoned->Array.push(id)
|
|
348
|
+
}
|
|
349
|
+
| _ => ()
|
|
350
|
+
}
|
|
351
|
+
)
|
|
231
352
|
todoItems
|
|
232
353
|
->Dict.toArray
|
|
233
354
|
->Array.forEach(((id, row)) =>
|
|
@@ -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
|
+
}
|