@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
|
@@ -382,7 +382,7 @@ module Make = (
|
|
|
382
382
|
// and idResolver paths). Slice output dicts are keyed by Spec.name; the
|
|
383
383
|
// queryDb name appends a suffix per slice kind:
|
|
384
384
|
// StateViewSlice — no suffix
|
|
385
|
-
// AutomationSlice — "Todo"
|
|
385
|
+
// AutomationSlice — "Todo", and "Schedule" when it maps the Clock
|
|
386
386
|
// OutboundTranslationSlice — "Todo"
|
|
387
387
|
// InboundTranslationSlice — "Audit"
|
|
388
388
|
dcbResult.stateViewSlicesOutputs
|
|
@@ -393,7 +393,10 @@ module Make = (
|
|
|
393
393
|
->Array.forEach(((k, v)) => allQueryDbs->Dict.set(k ++ "Audit", v.queryDb))
|
|
394
394
|
dcbResult.automationSlicesOutputs
|
|
395
395
|
->Dict.toArray
|
|
396
|
-
->Array.forEach(((k, v)) =>
|
|
396
|
+
->Array.forEach(((k, v)) => {
|
|
397
|
+
allQueryDbs->Dict.set(k ++ "Todo", v.queryDb)
|
|
398
|
+
v.schedulesDb->Option.forEach(db => allQueryDbs->Dict.set(k ++ "Schedule", db))
|
|
399
|
+
})
|
|
397
400
|
dcbResult.outboundTranslationSlicesOutputs
|
|
398
401
|
->Dict.toArray
|
|
399
402
|
->Array.forEach(((k, v)) => allQueryDbs->Dict.set(k ++ "Todo", v.queryDb))
|
|
@@ -466,9 +469,13 @@ module Make = (
|
|
|
466
469
|
let extractEventTypes = schema => Reventless.DcbTag.extractVariantNames(schema)
|
|
467
470
|
let routingSchemas: Dict.t<Plugin_Helpers.pluginDeployedSchema> = Dict.make()
|
|
468
471
|
automationSlices->Array.forEach((module(As: ReventlessInfra.AutomationSlice.T)) => {
|
|
469
|
-
// Union of source-event variants across every per-source mapping.
|
|
472
|
+
// Union of source-event variants across every per-source mapping. The
|
|
473
|
+
// Clock's tick is no event of the plugin's; its schedule says when it comes.
|
|
470
474
|
let consumedEventTypes =
|
|
471
475
|
As.Automation.mappings
|
|
476
|
+
->Array.filter((module(M: As.Automation.Mapping)) =>
|
|
477
|
+
M.sourceName != Reventless.Clock.name
|
|
478
|
+
)
|
|
472
479
|
->Array.flatMap((module(M: As.Automation.Mapping)) =>
|
|
473
480
|
extractEventTypes(M.sourceEventSchema->S.castToUnknown)
|
|
474
481
|
)
|
|
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
7
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
8
8
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
|
+
import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
|
|
9
10
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
11
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
11
12
|
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
@@ -284,7 +285,12 @@ function Make(Spec) {
|
|
|
284
285
|
allQueryDbs[param[0] + "Audit"] = param[1].queryDb;
|
|
285
286
|
});
|
|
286
287
|
Object.entries(dcbResult.automationSlicesOutputs).forEach(param => {
|
|
287
|
-
|
|
288
|
+
let v = param[1];
|
|
289
|
+
let k = param[0];
|
|
290
|
+
allQueryDbs[k + "Todo"] = v.queryDb;
|
|
291
|
+
Stdlib_Option.forEach(v.schedulesDb, db => {
|
|
292
|
+
allQueryDbs[k + "Schedule"] = db;
|
|
293
|
+
});
|
|
288
294
|
});
|
|
289
295
|
Object.entries(dcbResult.outboundTranslationSlicesOutputs).forEach(param => {
|
|
290
296
|
allQueryDbs[param[0] + "Todo"] = param[1].queryDb;
|
|
@@ -348,7 +354,7 @@ function Make(Spec) {
|
|
|
348
354
|
});
|
|
349
355
|
let routingSchemas = {};
|
|
350
356
|
automationSlices.forEach(As => {
|
|
351
|
-
let consumedEventTypes = Belt_SetString.toArray(Belt_SetString.fromArray(As.Automation.mappings.flatMap(M => DcbTag$Reventless.extractVariantNames(M.sourceEventSchema))));
|
|
357
|
+
let consumedEventTypes = Belt_SetString.toArray(Belt_SetString.fromArray(As.Automation.mappings.filter(M => M.sourceName !== Clock$Reventless.name).flatMap(M => DcbTag$Reventless.extractVariantNames(M.sourceEventSchema))));
|
|
352
358
|
routingSchemas[As.Spec.name] = {
|
|
353
359
|
consumedEventTypes: consumedEventTypes,
|
|
354
360
|
producedCommandTypes: DcbTag$Reventless.extractAllVariantNames(As.Spec.commandSchema),
|
|
@@ -1775,6 +1775,8 @@ let make = (
|
|
|
1775
1775
|
// contributes that one source's variants.
|
|
1776
1776
|
let allConsumedVariants =
|
|
1777
1777
|
AS.Automation.mappings
|
|
1778
|
+
// The Clock's tick is no event of the plugin's; `schedule` says when it comes.
|
|
1779
|
+
->Array.filter((module(M: AS.Automation.Mapping)) => M.sourceName != Reventless.Clock.name)
|
|
1778
1780
|
->Array.flatMap((module(M: AS.Automation.Mapping)) =>
|
|
1779
1781
|
eventVariantNames(M.sourceEventSchema->S.castToUnknown)
|
|
1780
1782
|
)
|
|
@@ -1788,6 +1790,11 @@ let make = (
|
|
|
1788
1790
|
producedCommandTypes: qualify(~prefix=name, commandVariantNames(AS.Spec.commandSchema)),
|
|
1789
1791
|
targetName: AS.Spec.targetName,
|
|
1790
1792
|
chapter: chapterOf(AS.Spec.name),
|
|
1793
|
+
schedule: ?(
|
|
1794
|
+
AS.Spec.schedule->Option.map(time =>
|
|
1795
|
+
time->Reventless.Util_Sury.toJson(Reventless.Schedule.timeSchema)->JSON.stringify
|
|
1796
|
+
)
|
|
1797
|
+
),
|
|
1791
1798
|
}: Reventless.Plugin.automationSliceDef
|
|
1792
1799
|
)
|
|
1793
1800
|
})
|
|
@@ -6,12 +6,14 @@ import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
|
6
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
7
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
8
8
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
|
+
import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
|
|
9
10
|
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
10
11
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
11
12
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
12
13
|
import * as Trait$Reventless from "@reventlessdev/reventless-spec/src/types/Trait.res.mjs";
|
|
13
14
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
14
15
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
16
|
+
import * as Schedule$Reventless from "@reventlessdev/reventless-spec/src/types/Schedule.res.mjs";
|
|
15
17
|
import * as Semantic$Reventless from "@reventlessdev/reventless-spec/src/semantic/Semantic.res.mjs";
|
|
16
18
|
import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/Lifecycle.res.mjs";
|
|
17
19
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
@@ -1252,13 +1254,14 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1252
1254
|
};
|
|
1253
1255
|
});
|
|
1254
1256
|
let automationSliceDefs = automationSlices.map(AS => {
|
|
1255
|
-
let allConsumedVariants = Belt_SetString.toArray(Belt_SetString.fromArray(AS.Automation.mappings.flatMap(M => DcbTag$Reventless.extractVariantNames(M.sourceEventSchema))));
|
|
1257
|
+
let allConsumedVariants = Belt_SetString.toArray(Belt_SetString.fromArray(AS.Automation.mappings.filter(M => M.sourceName !== Clock$Reventless.name).flatMap(M => DcbTag$Reventless.extractVariantNames(M.sourceEventSchema))));
|
|
1256
1258
|
return {
|
|
1257
1259
|
name: AS.Spec.name,
|
|
1258
1260
|
consumedEventTypes: qualify(name, allConsumedVariants),
|
|
1259
1261
|
producedCommandTypes: qualify(name, DcbTag$Reventless.extractAllVariantNames(AS.Spec.commandSchema)),
|
|
1260
1262
|
targetName: AS.Spec.targetName,
|
|
1261
|
-
chapter: componentChapters[AS.Spec.name]
|
|
1263
|
+
chapter: componentChapters[AS.Spec.name],
|
|
1264
|
+
schedule: Stdlib_Option.map(AS.Spec.schedule, time => JSON.stringify(Util_Sury$Reventless.toJson(time, Schedule$Reventless.timeSchema)))
|
|
1262
1265
|
};
|
|
1263
1266
|
});
|
|
1264
1267
|
let outboundTranslationSliceDefs = outboundTranslationSlices.map(OTS => ({
|
|
@@ -120,6 +120,7 @@ let automation: automationSliceDef = {
|
|
|
120
120
|
producedCommandTypes: ["Restock"],
|
|
121
121
|
targetName: "Product",
|
|
122
122
|
chapter: Some("Replenishment"),
|
|
123
|
+
schedule: `{"TAG":"Every","_0":{"TAG":"Minutes","_0":15}}`,
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
let outbound: outboundTranslationSliceDef = {
|
|
@@ -184,12 +184,15 @@ let automation_producedCommandTypes = ["Restock"];
|
|
|
184
184
|
|
|
185
185
|
let automation_chapter = "Replenishment";
|
|
186
186
|
|
|
187
|
+
let automation_schedule = `{"TAG":"Every","_0":{"TAG":"Minutes","_0":15}}`;
|
|
188
|
+
|
|
187
189
|
let automation = {
|
|
188
190
|
name: "Restocker",
|
|
189
191
|
consumedEventTypes: automation_consumedEventTypes,
|
|
190
192
|
producedCommandTypes: automation_producedCommandTypes,
|
|
191
193
|
targetName: "Product",
|
|
192
|
-
chapter: automation_chapter
|
|
194
|
+
chapter: automation_chapter,
|
|
195
|
+
schedule: automation_schedule
|
|
193
196
|
};
|
|
194
197
|
|
|
195
198
|
let outbound_consumedEventTypes = ["OrderPlaced"];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// A slice's schedule rows, and how the platform is brought in line with them: the
|
|
2
|
+
// declared time is written unless an event set or cleared the slice-wide key, the
|
|
3
|
+
// platform gets what it is missing and loses what the rows do not hold, and a
|
|
4
|
+
// once-only time already past is not put again.
|
|
5
|
+
|
|
6
|
+
open JestGlobals
|
|
7
|
+
|
|
8
|
+
module Schedules = AutomationSlice_Schedules
|
|
9
|
+
|
|
10
|
+
let at = iso => Date.fromString(iso)->Date.getTime
|
|
11
|
+
let daily6 = Reventless.Schedule.Calendar({calendar: Daily({hour: 6, minute: 0})})
|
|
12
|
+
let daily18 = Reventless.Schedule.Calendar({calendar: Daily({hour: 18, minute: 0})})
|
|
13
|
+
let now = at("2026-10-16T12:00:00Z")
|
|
14
|
+
|
|
15
|
+
// A platform that holds its schedules in a dict and records what it was told.
|
|
16
|
+
let fakePlatform = (~held: array<string>=[]) => {
|
|
17
|
+
let schedules = Dict.fromArray(held->Array.map(name => (name, daily6)))
|
|
18
|
+
let calls = []
|
|
19
|
+
let platform: ReventlessInfra.AutomationSlice.platformSchedules = {
|
|
20
|
+
put: async (~key, time) => {
|
|
21
|
+
calls->Array.push(`put ${key}`)
|
|
22
|
+
schedules->Dict.set(key, time)
|
|
23
|
+
},
|
|
24
|
+
removeName: async name => {
|
|
25
|
+
calls->Array.push(`remove ${name}`)
|
|
26
|
+
schedules->Dict.delete(name)
|
|
27
|
+
},
|
|
28
|
+
names: async () => schedules->Dict.keysToArray,
|
|
29
|
+
nameOf: key => key,
|
|
30
|
+
}
|
|
31
|
+
(platform, schedules, calls)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let liveKeys = rows =>
|
|
35
|
+
rows
|
|
36
|
+
->Dict.valuesToArray
|
|
37
|
+
->Array.filter(row => row.Schedules.time->Option.isSome)
|
|
38
|
+
->Array.map(row => row.key)
|
|
39
|
+
->Array.toSorted(String.compare)
|
|
40
|
+
|
|
41
|
+
let set = (rows, key, time, ~recordedAt=now) =>
|
|
42
|
+
rows->Schedules.apply(Set({key, time}), ~setBy="event", ~recordedAt)
|
|
43
|
+
let clear = (rows, key) => rows->Schedules.apply(Clear({key: key}), ~setBy="event", ~recordedAt=now)
|
|
44
|
+
|
|
45
|
+
describe("the declared time", () => {
|
|
46
|
+
testSync("is written when no event has touched the slice-wide key", () => {
|
|
47
|
+
let rows = Dict.make()
|
|
48
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
49
|
+
expect(rows->Dict.get("default")->Option.flatMap(Schedules.timeOf))->toEqual(Some(daily6))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
testSync("does not replace a time an event set", () => {
|
|
53
|
+
let rows = Dict.make()
|
|
54
|
+
let _ = rows->set("default", daily18)
|
|
55
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
56
|
+
expect(rows->Dict.get("default")->Option.flatMap(Schedules.timeOf))->toEqual(Some(daily18))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
testSync("does not come back after an event cleared it", () => {
|
|
60
|
+
let rows = Dict.make()
|
|
61
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
62
|
+
let _ = rows->clear("default")
|
|
63
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
64
|
+
expect(rows->liveKeys)->toEqual([])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
testSync("follows a changed declaration, and goes with it", () => {
|
|
68
|
+
let rows = Dict.make()
|
|
69
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
70
|
+
rows->Schedules.declare(Some(daily18), ~now)
|
|
71
|
+
let changed = rows->Dict.get("default")->Option.flatMap(Schedules.timeOf)
|
|
72
|
+
rows->Schedules.declare(None, ~now)
|
|
73
|
+
expect((changed, rows->liveKeys))->toEqual((Some(daily18), []))
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe("a change an event makes", () => {
|
|
78
|
+
testSync("counts an After from when the event was recorded", () => {
|
|
79
|
+
let rows = Dict.make()
|
|
80
|
+
let _ =
|
|
81
|
+
rows->set("o1", Once({moment: After(Minutes(10))}), ~recordedAt=at("2026-10-16T09:00:00Z"))
|
|
82
|
+
expect(rows->Dict.get("o1")->Option.flatMap(Schedules.timeOf))->toEqual(
|
|
83
|
+
Some(
|
|
84
|
+
Reventless.Schedule.Once({
|
|
85
|
+
moment: At({year: 2026, month: 10, day: 16, hour: 9, minute: 10}),
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
testSync("refuses a time that cannot fire, and changes nothing", () => {
|
|
92
|
+
let rows = Dict.make()
|
|
93
|
+
let refused = rows->set("r1", Calendar({calendar: Daily({hour: 25, minute: 0})}))
|
|
94
|
+
expect((refused->Result.isError, rows->liveKeys))->toEqual((true, []))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
testSync("removes a once-only schedule when it fires, not a recurring one", () => {
|
|
98
|
+
let rows = Dict.make()
|
|
99
|
+
let _ = rows->set("o1", Once({moment: After(Minutes(10))}))
|
|
100
|
+
let _ = rows->set("r1", daily18)
|
|
101
|
+
rows->Schedules.fired(~key="o1")
|
|
102
|
+
rows->Schedules.fired(~key="r1")
|
|
103
|
+
expect(rows->liveKeys)->toEqual(["r1"])
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("the reconcile", () => {
|
|
108
|
+
testPromise(
|
|
109
|
+
"puts what the platform is missing and removes what the rows do not hold",
|
|
110
|
+
async () => {
|
|
111
|
+
let rows = Dict.make()
|
|
112
|
+
let _ = rows->set("r1", daily18)
|
|
113
|
+
let _ = rows->set("r2", daily18)
|
|
114
|
+
let (platform, held, _) = fakePlatform(~held=["r2", "stray"])
|
|
115
|
+
await rows->Schedules.reconcile(platform, ~declaredTime=None, ~now)
|
|
116
|
+
expect(held->Dict.keysToArray->Array.toSorted(String.compare))->toEqual(["r1", "r2"])
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
testPromise("does not put again a schedule an event set and the platform holds", async () => {
|
|
121
|
+
let rows = Dict.make()
|
|
122
|
+
let _ = rows->set("r1", daily18)
|
|
123
|
+
let (platform, _, calls) = fakePlatform(~held=["r1"])
|
|
124
|
+
await rows->Schedules.reconcile(platform, ~declaredTime=None, ~now)
|
|
125
|
+
expect(calls)->toEqual([])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
testPromise("puts the declared time again, in case it changed", async () => {
|
|
129
|
+
let rows = Dict.make()
|
|
130
|
+
let (platform, _, calls) = fakePlatform(~held=["default"])
|
|
131
|
+
await rows->Schedules.reconcile(platform, ~declaredTime=Some(daily6), ~now)
|
|
132
|
+
expect(calls)->toEqual(["put default"])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
testPromise("drops a once-only time already past, from the rows and the platform", async () => {
|
|
136
|
+
let rows = Dict.make()
|
|
137
|
+
let _ =
|
|
138
|
+
rows->set("o1", Once({moment: After(Minutes(10))}), ~recordedAt=at("2026-10-16T09:00:00Z"))
|
|
139
|
+
let (platform, held, _) = fakePlatform(~held=["o1"])
|
|
140
|
+
await rows->Schedules.reconcile(platform, ~declaredTime=None, ~now)
|
|
141
|
+
expect((rows->liveKeys, held->Dict.keysToArray))->toEqual(([], []))
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
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_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
7
|
+
import * as AutomationSlice_Schedules$ReventlessCore from "../../src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs";
|
|
8
|
+
|
|
9
|
+
function at(iso) {
|
|
10
|
+
return new Date(iso).getTime();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let daily6 = {
|
|
14
|
+
TAG: "Calendar",
|
|
15
|
+
calendar: {
|
|
16
|
+
TAG: "Daily",
|
|
17
|
+
hour: 6,
|
|
18
|
+
minute: 0
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let daily18 = {
|
|
23
|
+
TAG: "Calendar",
|
|
24
|
+
calendar: {
|
|
25
|
+
TAG: "Daily",
|
|
26
|
+
hour: 18,
|
|
27
|
+
minute: 0
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let now = new Date("2026-10-16T12:00:00Z").getTime();
|
|
32
|
+
|
|
33
|
+
function fakePlatform(heldOpt) {
|
|
34
|
+
let held = heldOpt !== undefined ? heldOpt : [];
|
|
35
|
+
let schedules = Object.fromEntries(held.map(name => [
|
|
36
|
+
name,
|
|
37
|
+
daily6
|
|
38
|
+
]));
|
|
39
|
+
let calls = [];
|
|
40
|
+
let platform_put = async (key, time) => {
|
|
41
|
+
calls.push(`put ` + key);
|
|
42
|
+
schedules[key] = time;
|
|
43
|
+
};
|
|
44
|
+
let platform_removeName = async name => {
|
|
45
|
+
calls.push(`remove ` + name);
|
|
46
|
+
return Stdlib_Dict.$$delete(schedules, name);
|
|
47
|
+
};
|
|
48
|
+
let platform_names = async () => Object.keys(schedules);
|
|
49
|
+
let platform_nameOf = key => key;
|
|
50
|
+
let platform = {
|
|
51
|
+
put: platform_put,
|
|
52
|
+
removeName: platform_removeName,
|
|
53
|
+
names: platform_names,
|
|
54
|
+
nameOf: platform_nameOf
|
|
55
|
+
};
|
|
56
|
+
return [
|
|
57
|
+
platform,
|
|
58
|
+
schedules,
|
|
59
|
+
calls
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function liveKeys(rows) {
|
|
64
|
+
return Object.values(rows).filter(row => Stdlib_Option.isSome(row.time)).map(row => row.key).toSorted(Primitive_string.compare);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function set(rows, key, time, recordedAtOpt) {
|
|
68
|
+
let recordedAt = recordedAtOpt !== undefined ? recordedAtOpt : now;
|
|
69
|
+
return AutomationSlice_Schedules$ReventlessCore.apply(rows, {
|
|
70
|
+
TAG: "Set",
|
|
71
|
+
key: key,
|
|
72
|
+
time: time
|
|
73
|
+
}, "event", recordedAt);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function clear(rows, key) {
|
|
77
|
+
return AutomationSlice_Schedules$ReventlessCore.apply(rows, {
|
|
78
|
+
TAG: "Clear",
|
|
79
|
+
key: key
|
|
80
|
+
}, "event", now);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
globalThis.describe("the declared time", () => {
|
|
84
|
+
globalThis.test("is written when no event has touched the slice-wide key", () => {
|
|
85
|
+
let rows = {};
|
|
86
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
87
|
+
globalThis.expect(Stdlib_Option.flatMap(rows["default"], AutomationSlice_Schedules$ReventlessCore.timeOf)).toEqual(daily6);
|
|
88
|
+
});
|
|
89
|
+
globalThis.test("does not replace a time an event set", () => {
|
|
90
|
+
let rows = {};
|
|
91
|
+
set(rows, "default", daily18, undefined);
|
|
92
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
93
|
+
globalThis.expect(Stdlib_Option.flatMap(rows["default"], AutomationSlice_Schedules$ReventlessCore.timeOf)).toEqual(daily18);
|
|
94
|
+
});
|
|
95
|
+
globalThis.test("does not come back after an event cleared it", () => {
|
|
96
|
+
let rows = {};
|
|
97
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
98
|
+
clear(rows, "default");
|
|
99
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
100
|
+
globalThis.expect(liveKeys(rows)).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
globalThis.test("follows a changed declaration, and goes with it", () => {
|
|
103
|
+
let rows = {};
|
|
104
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
105
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily18, now);
|
|
106
|
+
let changed = Stdlib_Option.flatMap(rows["default"], AutomationSlice_Schedules$ReventlessCore.timeOf);
|
|
107
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, undefined, now);
|
|
108
|
+
globalThis.expect([
|
|
109
|
+
changed,
|
|
110
|
+
liveKeys(rows)
|
|
111
|
+
]).toEqual([
|
|
112
|
+
daily18,
|
|
113
|
+
[]
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
globalThis.describe("a change an event makes", () => {
|
|
119
|
+
globalThis.test("counts an After from when the event was recorded", () => {
|
|
120
|
+
let rows = {};
|
|
121
|
+
set(rows, "o1", {
|
|
122
|
+
TAG: "Once",
|
|
123
|
+
moment: {
|
|
124
|
+
TAG: "After",
|
|
125
|
+
_0: {
|
|
126
|
+
TAG: "Minutes",
|
|
127
|
+
_0: 10
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, new Date("2026-10-16T09:00:00Z").getTime());
|
|
131
|
+
globalThis.expect(Stdlib_Option.flatMap(rows["o1"], AutomationSlice_Schedules$ReventlessCore.timeOf)).toEqual({
|
|
132
|
+
TAG: "Once",
|
|
133
|
+
moment: {
|
|
134
|
+
TAG: "At",
|
|
135
|
+
_0: {
|
|
136
|
+
year: 2026,
|
|
137
|
+
month: 10,
|
|
138
|
+
day: 16,
|
|
139
|
+
hour: 9,
|
|
140
|
+
minute: 10
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
globalThis.test("refuses a time that cannot fire, and changes nothing", () => {
|
|
146
|
+
let rows = {};
|
|
147
|
+
let refused = set(rows, "r1", {
|
|
148
|
+
TAG: "Calendar",
|
|
149
|
+
calendar: {
|
|
150
|
+
TAG: "Daily",
|
|
151
|
+
hour: 25,
|
|
152
|
+
minute: 0
|
|
153
|
+
}
|
|
154
|
+
}, undefined);
|
|
155
|
+
globalThis.expect([
|
|
156
|
+
Stdlib_Result.isError(refused),
|
|
157
|
+
liveKeys(rows)
|
|
158
|
+
]).toEqual([
|
|
159
|
+
true,
|
|
160
|
+
[]
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
globalThis.test("removes a once-only schedule when it fires, not a recurring one", () => {
|
|
164
|
+
let rows = {};
|
|
165
|
+
set(rows, "o1", {
|
|
166
|
+
TAG: "Once",
|
|
167
|
+
moment: {
|
|
168
|
+
TAG: "After",
|
|
169
|
+
_0: {
|
|
170
|
+
TAG: "Minutes",
|
|
171
|
+
_0: 10
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, undefined);
|
|
175
|
+
set(rows, "r1", daily18, undefined);
|
|
176
|
+
AutomationSlice_Schedules$ReventlessCore.fired(rows, "o1");
|
|
177
|
+
AutomationSlice_Schedules$ReventlessCore.fired(rows, "r1");
|
|
178
|
+
globalThis.expect(liveKeys(rows)).toEqual(["r1"]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
globalThis.describe("the reconcile", () => {
|
|
183
|
+
globalThis.test("puts what the platform is missing and removes what the rows do not hold", async () => {
|
|
184
|
+
let rows = {};
|
|
185
|
+
set(rows, "r1", daily18, undefined);
|
|
186
|
+
set(rows, "r2", daily18, undefined);
|
|
187
|
+
let match = fakePlatform([
|
|
188
|
+
"r2",
|
|
189
|
+
"stray"
|
|
190
|
+
]);
|
|
191
|
+
await AutomationSlice_Schedules$ReventlessCore.reconcile(rows, match[0], undefined, now);
|
|
192
|
+
globalThis.expect(Object.keys(match[1]).toSorted(Primitive_string.compare)).toEqual([
|
|
193
|
+
"r1",
|
|
194
|
+
"r2"
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
globalThis.test("does not put again a schedule an event set and the platform holds", async () => {
|
|
198
|
+
let rows = {};
|
|
199
|
+
set(rows, "r1", daily18, undefined);
|
|
200
|
+
let match = fakePlatform(["r1"]);
|
|
201
|
+
await AutomationSlice_Schedules$ReventlessCore.reconcile(rows, match[0], undefined, now);
|
|
202
|
+
globalThis.expect(match[2]).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
globalThis.test("puts the declared time again, in case it changed", async () => {
|
|
205
|
+
let rows = {};
|
|
206
|
+
let match = fakePlatform(["default"]);
|
|
207
|
+
await AutomationSlice_Schedules$ReventlessCore.reconcile(rows, match[0], daily6, now);
|
|
208
|
+
globalThis.expect(match[2]).toEqual(["put default"]);
|
|
209
|
+
});
|
|
210
|
+
globalThis.test("drops a once-only time already past, from the rows and the platform", async () => {
|
|
211
|
+
let rows = {};
|
|
212
|
+
set(rows, "o1", {
|
|
213
|
+
TAG: "Once",
|
|
214
|
+
moment: {
|
|
215
|
+
TAG: "After",
|
|
216
|
+
_0: {
|
|
217
|
+
TAG: "Minutes",
|
|
218
|
+
_0: 10
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}, new Date("2026-10-16T09:00:00Z").getTime());
|
|
222
|
+
let match = fakePlatform(["o1"]);
|
|
223
|
+
await AutomationSlice_Schedules$ReventlessCore.reconcile(rows, match[0], undefined, now);
|
|
224
|
+
globalThis.expect([
|
|
225
|
+
liveKeys(rows),
|
|
226
|
+
Object.keys(match[1])
|
|
227
|
+
]).toEqual([
|
|
228
|
+
[],
|
|
229
|
+
[]
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let Schedules;
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
Schedules,
|
|
238
|
+
at,
|
|
239
|
+
daily6,
|
|
240
|
+
daily18,
|
|
241
|
+
now,
|
|
242
|
+
fakePlatform,
|
|
243
|
+
liveKeys,
|
|
244
|
+
set,
|
|
245
|
+
clear,
|
|
246
|
+
}
|
|
247
|
+
/* now Not a pure module */
|