@reventlessdev/reventless-local 3.0.0-alpha.286 → 3.0.0-alpha.288

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.
Files changed (28) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +9 -9
  3. package/src/adapter/Scheduler/LocalScheduledPublisher.res +5 -52
  4. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -47
  5. package/src/adapter/Scheduler/LocalTimers.res +62 -0
  6. package/src/adapter/Scheduler/LocalTimers.res.mjs +63 -0
  7. package/src/components/AutomationSlice_Builder.res +23 -2
  8. package/src/components/AutomationSlice_Builder.res.mjs +19 -2
  9. package/src/components/LocalAutomationSchedules.res +59 -0
  10. package/src/components/LocalAutomationSchedules.res.mjs +72 -0
  11. package/src/test/TestRunner.res +1 -0
  12. package/src/test/TestRunner.res.mjs +6 -0
  13. package/tests/components/automationslice/AutomationRescheduleFixtures.res +142 -0
  14. package/tests/components/automationslice/AutomationRescheduleFixtures.res.mjs +259 -0
  15. package/tests/components/automationslice/AutomationRescheduleTest.res +96 -0
  16. package/tests/components/automationslice/AutomationRescheduleTest.res.mjs +177 -0
  17. package/tests/components/automationslice/AutomationScheduleFixtures.res +118 -0
  18. package/tests/components/automationslice/AutomationScheduleFixtures.res.mjs +263 -0
  19. package/tests/components/automationslice/AutomationScheduleTest.res +88 -0
  20. package/tests/components/automationslice/AutomationScheduleTest.res.mjs +108 -0
  21. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -0
  22. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +14 -2
  23. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +2 -0
  24. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +7 -1
  25. package/tests/components/automationslice/AutomationSweepFixtures.res +2 -0
  26. package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +7 -1
  27. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +3 -0
  28. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +13 -2
@@ -0,0 +1,142 @@
1
+ // A digest per recipient on the local platform: the slice declares no time; a
2
+ // recipient's choice sets their schedule, unsubscribing clears it, and each tick
3
+ // sends that recipient's digest. The slice is made by `makeSlice`, so a test can fix
4
+ // the time first.
5
+
6
+ open TestFixtures
7
+ open Reventless
8
+
9
+ module PreferencesSource = {
10
+ module Id = Id.String
11
+ let name = "DigestPreferencesEventTopic"
12
+ @schema
13
+ type event =
14
+ | DigestTimeChosen({recipientId: string, time: Schedule.time})
15
+ | Unsubscribed({recipientId: string})
16
+ }
17
+
18
+ module DigestSpec = {
19
+ let name = "LocalDigest"
20
+ let moduleUrl: string = %raw(`import.meta.url`)
21
+
22
+ @schema
23
+ type todoItem = {recipientId: string}
24
+
25
+ @schema
26
+ type command = SendDigest({recipientId: @s.matches(DcbTag.string) string})
27
+
28
+ let maxRetries = 2
29
+ let heartbeatInterval = 60
30
+ let targetName = "SendDigest"
31
+ let schedule = None
32
+ }
33
+
34
+ module FromClock = AutomationSlice.Mapping.Make(
35
+ Clock,
36
+ DigestSpec,
37
+ {
38
+ let collect = (Clock.Tick({key, at}), ~sourceId as _, _ctx) => [
39
+ (Clock.itemId(key, at), ({recipientId: key}: DigestSpec.todoItem)),
40
+ ]
41
+ let resolve = _ => None
42
+ let reschedule = _ => []
43
+ },
44
+ )
45
+
46
+ module FromPreferences = AutomationSlice.Mapping.Make(
47
+ PreferencesSource,
48
+ DigestSpec,
49
+ {
50
+ let collect = (_, ~sourceId as _, _ctx) => []
51
+ let resolve = _ => None
52
+ let reschedule = event =>
53
+ switch event {
54
+ | PreferencesSource.DigestTimeChosen({recipientId, time}) => [
55
+ AutomationSlice.Set({key: recipientId, time}),
56
+ ]
57
+ | Unsubscribed({recipientId}) => [Clear({key: recipientId})]
58
+ }
59
+ },
60
+ )
61
+
62
+ module DigestAutomation: AutomationSlice.Automation with module Spec := DigestSpec = {
63
+ let process = (_id, item: DigestSpec.todoItem, ~now as _) => Some((
64
+ item.recipientId,
65
+ DigestSpec.SendDigest({recipientId: item.recipientId}),
66
+ ))
67
+ let onExhausted = (_id, _item: DigestSpec.todoItem) => None
68
+ let moduleUrl: string = %raw(`import.meta.url`)
69
+ module M = AutomationSlice.Mappings.Make(DigestSpec)
70
+ module type Mapping = M.Mapping
71
+ let mappings: array<module(Mapping)> = [module(FromClock), module(FromPreferences)]
72
+ }
73
+
74
+ let preferencesResource: ReventlessInfra.Adapter.resource = ReventlessInfra.Adapter.make(
75
+ ~name=PreferencesSource.name->Pulumi.Output.make,
76
+ ~id=PreferencesSource.name->Pulumi.Output.make,
77
+ ~urn=PreferencesSource.name->Pulumi.Output.make,
78
+ ~service="memory:InMemory"->Pulumi.Output.make,
79
+ )
80
+
81
+ module Bus = LocalBus.Make()
82
+ let _ = TestRunner.setup()
83
+
84
+ module AutomationSliceMaker = AutomationSlice_Builder.Make(Bus)
85
+ module Digest = AutomationSliceMaker.Make(DigestSpec, DigestAutomation)
86
+
87
+ let publishedCommands: ref<array<Reventless.Message.commandJson>> = ref([])
88
+ let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
89
+ publishedCommands := publishedCommands.contents->Array.concat(cmds)
90
+
91
+ let makeSlice = () =>
92
+ Digest.make(
93
+ ~allEventTopics=Dict.fromArray([
94
+ (PreferencesSource.name, {ReventlessInfra.EventTopic.resources: [preferencesResource]}),
95
+ ]),
96
+ ~publishJsons=publishJsons->Pulumi.Output.make,
97
+ ~context={
98
+ environment: "local",
99
+ platformName: "local",
100
+ pluginName: "TestPlugin",
101
+ sliceName: DigestSpec.name,
102
+ },
103
+ )
104
+
105
+ /** An event as the log would hold it, recorded now as the fake Date has it. */
106
+ let eventJson = (event: PreferencesSource.event, ~recipientId) => {
107
+ let meta = {
108
+ ...makeTestMeta(~service=PreferencesSource.name),
109
+ time: Date.now()->Date.fromTime->Date.toISOString,
110
+ }
111
+ (
112
+ meta,
113
+ JSON.Encode.object(
114
+ Dict.fromArray([
115
+ ("id", recipientId->JSON.Encode.string),
116
+ ("meta", meta->Reventless.Util_Sury.toJson(Message.metaSchema)),
117
+ ("event", event->Reventless.Util_Sury.toJson(PreferencesSource.eventSchema)),
118
+ ]),
119
+ ),
120
+ )
121
+ }
122
+
123
+ let publish = async (event, ~recipientId) => {
124
+ let (meta, json) = eventJson(event, ~recipientId)
125
+ await Bus.publishEvent(PreferencesSource.name, "test", meta, json)
126
+ }
127
+
128
+ /** The keys and times the schedules store holds. */
129
+ let storedSchedules = () =>
130
+ Bus.getQueryDbScan(Digest.schedulesDbName->Option.getOr(""))
131
+ ->Option.map(scan => scan())
132
+ ->Option.getOr([])
133
+ ->Array.map(row =>
134
+ row->Reventless.Util_Sury.fromJson(ReventlessCore.AutomationSlice_Schedules.rowSchema)
135
+ )
136
+ ->Array.filterMap(row =>
137
+ row->ReventlessCore.AutomationSlice_Schedules.timeOf->Option.map(t => (row.key, t))
138
+ )
139
+ ->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
140
+
141
+ /** The keys whose timers the local platform holds for the slice. */
142
+ let timers = () => LocalAutomationSchedules.platformFor(~slice=DigestSpec.name).names()
@@ -0,0 +1,259 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Pulumi from "@pulumi/pulumi";
8
+ import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
9
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
10
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
11
+ import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
12
+ import * as Schedule$Reventless from "@reventlessdev/reventless-spec/src/types/Schedule.res.mjs";
13
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
14
+ import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
15
+ import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
16
+ import * as AutomationSlice$Reventless from "@reventlessdev/reventless-spec/src/components/AutomationSlice.res.mjs";
17
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
18
+ import * as TestFixtures$ReventlessLocal from "../../TestFixtures.res.mjs";
19
+ import * as AutomationSlice_Builder$ReventlessLocal from "../../../src/components/AutomationSlice_Builder.res.mjs";
20
+ import * as AutomationSlice_Schedules$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs";
21
+ import * as LocalAutomationSchedules$ReventlessLocal from "../../../src/components/LocalAutomationSchedules.res.mjs";
22
+
23
+ let name = "DigestPreferencesEventTopic";
24
+
25
+ let eventSchema = Sury.union([
26
+ Sury.$schema(s => ({
27
+ TAG: "DigestTimeChosen",
28
+ recipientId: s.m(Sury.string),
29
+ time: s.m(Schedule$Reventless.timeSchema)
30
+ })),
31
+ Sury.$schema(s => ({
32
+ TAG: "Unsubscribed",
33
+ recipientId: s.m(Sury.string)
34
+ }))
35
+ ]);
36
+
37
+ let PreferencesSource = {
38
+ Id: undefined,
39
+ name: name,
40
+ eventSchema: eventSchema
41
+ };
42
+
43
+ let name$1 = "LocalDigest";
44
+
45
+ let moduleUrl = import.meta.url;
46
+
47
+ let todoItemSchema = Sury.$schema(s => ({
48
+ recipientId: s.m(Sury.string)
49
+ }));
50
+
51
+ let commandSchema = Sury.$schema(s => ({
52
+ TAG: "SendDigest",
53
+ recipientId: s.m(DcbTag$Reventless.string)
54
+ }));
55
+
56
+ function authorizationOf(param) {
57
+ return "AllowAuthenticated";
58
+ }
59
+
60
+ function commandTransition(param) {
61
+ return "Undeclared";
62
+ }
63
+
64
+ let traits = [];
65
+
66
+ let DigestSpec = {
67
+ name: name$1,
68
+ moduleUrl: moduleUrl,
69
+ todoItemSchema: todoItemSchema,
70
+ commandSchema: commandSchema,
71
+ maxRetries: 2,
72
+ heartbeatInterval: 60,
73
+ targetName: "SendDigest",
74
+ schedule: undefined,
75
+ authorizationOf: authorizationOf,
76
+ commandTransition: commandTransition,
77
+ traits: traits
78
+ };
79
+
80
+ function collect(param, param$1, _ctx) {
81
+ let key = param.key;
82
+ return [[
83
+ Clock$Reventless.itemId(key, param.at),
84
+ {
85
+ recipientId: key
86
+ }
87
+ ]];
88
+ }
89
+
90
+ function resolve(param) {
91
+
92
+ }
93
+
94
+ function reschedule(param) {
95
+ return [];
96
+ }
97
+
98
+ let FromClock = AutomationSlice$Reventless.Mapping.Make({
99
+ Id: Id$Reventless.$$String,
100
+ name: Clock$Reventless.name,
101
+ eventSchema: Clock$Reventless.eventSchema
102
+ })(DigestSpec)({
103
+ collect: collect,
104
+ resolve: resolve,
105
+ reschedule: reschedule
106
+ });
107
+
108
+ function collect$1(param, param$1, _ctx) {
109
+ return [];
110
+ }
111
+
112
+ function resolve$1(param) {
113
+
114
+ }
115
+
116
+ function reschedule$1(event) {
117
+ if (event.TAG === "DigestTimeChosen") {
118
+ return [{
119
+ TAG: "Set",
120
+ key: event.recipientId,
121
+ time: event.time
122
+ }];
123
+ } else {
124
+ return [{
125
+ TAG: "Clear",
126
+ key: event.recipientId
127
+ }];
128
+ }
129
+ }
130
+
131
+ let FromPreferences = AutomationSlice$Reventless.Mapping.Make({
132
+ Id: Id$Reventless.$$String,
133
+ name: name,
134
+ eventSchema: eventSchema
135
+ })(DigestSpec)({
136
+ collect: collect$1,
137
+ resolve: resolve$1,
138
+ reschedule: reschedule$1
139
+ });
140
+
141
+ function process(_id, item, param) {
142
+ return [
143
+ item.recipientId,
144
+ {
145
+ TAG: "SendDigest",
146
+ recipientId: item.recipientId
147
+ }
148
+ ];
149
+ }
150
+
151
+ function onExhausted(_id, _item) {
152
+
153
+ }
154
+
155
+ let moduleUrl$1 = import.meta.url;
156
+
157
+ AutomationSlice$Reventless.Mappings.Make(DigestSpec);
158
+
159
+ let mappings = [
160
+ FromClock,
161
+ FromPreferences
162
+ ];
163
+
164
+ let DigestAutomation = {
165
+ process: process,
166
+ onExhausted: onExhausted,
167
+ moduleUrl: moduleUrl$1,
168
+ mappings: mappings
169
+ };
170
+
171
+ let preferencesResource = Adapter$ReventlessInfra.make(Pulumi.output(name), Pulumi.output(name), Pulumi.output(name), Pulumi.output("memory:InMemory"), undefined, undefined, undefined, undefined, undefined, undefined);
172
+
173
+ let Bus = LocalBus$ReventlessLocal.Make({});
174
+
175
+ TestRunner$ReventlessLocal.setup();
176
+
177
+ let AutomationSliceMaker = AutomationSlice_Builder$ReventlessLocal.Make(Bus);
178
+
179
+ let Digest = AutomationSliceMaker.Make(DigestSpec)(DigestAutomation);
180
+
181
+ let publishedCommands = {
182
+ contents: []
183
+ };
184
+
185
+ async function publishJsons(cmds) {
186
+ publishedCommands.contents = publishedCommands.contents.concat(cmds);
187
+ }
188
+
189
+ function makeSlice() {
190
+ return Digest.make(Object.fromEntries([[
191
+ name,
192
+ {
193
+ resources: [preferencesResource]
194
+ }
195
+ ]]), Pulumi.output(publishJsons), {
196
+ environment: "local",
197
+ platformName: "local",
198
+ pluginName: "TestPlugin",
199
+ sliceName: name$1
200
+ }, undefined, undefined);
201
+ }
202
+
203
+ function eventJson(event, recipientId) {
204
+ let newrecord = {...TestFixtures$ReventlessLocal.makeTestMeta(name)};
205
+ newrecord.time = new Date(Date.now()).toISOString();
206
+ return [
207
+ newrecord,
208
+ Object.fromEntries([
209
+ [
210
+ "id",
211
+ recipientId
212
+ ],
213
+ [
214
+ "meta",
215
+ Util_Sury$Reventless.toJson(newrecord, Message$Reventless.metaSchema)
216
+ ],
217
+ [
218
+ "event",
219
+ Util_Sury$Reventless.toJson(event, eventSchema)
220
+ ]
221
+ ])
222
+ ];
223
+ }
224
+
225
+ async function publish(event, recipientId) {
226
+ let match = eventJson(event, recipientId);
227
+ return await Bus.publishEvent(name, "test", match[0], match[1]);
228
+ }
229
+
230
+ function storedSchedules() {
231
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.map(Bus.getQueryDbScan(Stdlib_Option.getOr(Digest.schedulesDbName, "")), scan => scan()), []).map(row => Util_Sury$Reventless.fromJson(row, AutomationSlice_Schedules$ReventlessCore.rowSchema)), row => Stdlib_Option.map(AutomationSlice_Schedules$ReventlessCore.timeOf(row), t => [
232
+ row.key,
233
+ t
234
+ ])).toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
235
+ }
236
+
237
+ function timers() {
238
+ return LocalAutomationSchedules$ReventlessLocal.platformFor(name$1).names();
239
+ }
240
+
241
+ export {
242
+ PreferencesSource,
243
+ DigestSpec,
244
+ FromClock,
245
+ FromPreferences,
246
+ DigestAutomation,
247
+ preferencesResource,
248
+ Bus,
249
+ AutomationSliceMaker,
250
+ Digest,
251
+ publishedCommands,
252
+ publishJsons,
253
+ makeSlice,
254
+ eventJson,
255
+ publish,
256
+ storedSchedules,
257
+ timers,
258
+ }
259
+ /* eventSchema Not a pure module */
@@ -0,0 +1,96 @@
1
+ // Schedules set by events, on the local platform: a recipient's choice is stored and
2
+ // starts their timer, the timer's tick sends their digest, unsubscribing removes
3
+ // both, and a rebuild from the events reproduces the rows and removes a stray timer.
4
+
5
+ open JestGlobals
6
+ open AutomationRescheduleFixtures
7
+
8
+ type jestObj
9
+ @module("@jest/globals") external jest: jestObj = "jest"
10
+ @send external useFakeTimers: jestObj => unit = "useFakeTimers"
11
+ @send external useRealTimers: jestObj => unit = "useRealTimers"
12
+ @send external setSystemTime: (jestObj, float) => unit = "setSystemTime"
13
+ @send
14
+ external advanceTimersByTimeAsync: (jestObj, float) => promise<unit> = "advanceTimersByTimeAsync"
15
+
16
+ let time = iso => Date.fromString(iso)->Date.getTime
17
+ let at18 = Reventless.Schedule.Calendar({calendar: Daily({hour: 18, minute: 0})})
18
+ let at19Berlin = Reventless.Schedule.Calendar({
19
+ calendar: Daily({hour: 19, minute: 0}),
20
+ zone: "Europe/Berlin",
21
+ })
22
+
23
+ let settle = async () =>
24
+ for _ in 1 to 20 {
25
+ await jest->advanceTimersByTimeAsync(0.)
26
+ }
27
+
28
+ let sorted = keys => keys->Array.toSorted(String.compare)
29
+
30
+ describe("schedules set by events, locally", () => {
31
+ let slice = ref(None)
32
+ let ops = async () =>
33
+ await slice.contents->Option.getOrThrow->ReventlessCore.Component.operations->TestRunner.resolve
34
+
35
+ let _ = beforeAllAsync(async () => {
36
+ jest->useFakeTimers
37
+ jest->setSystemTime(time("2026-10-16T12:00:00Z"))
38
+ slice := Some(makeSlice())
39
+ let _ = await ops()
40
+ await settle()
41
+ })
42
+ let _ = afterAll(() => {
43
+ LocalAutomationSchedules.reset()
44
+ jest->useRealTimers
45
+ })
46
+
47
+ testPromise("a choice is stored and starts the recipient's timer", async () => {
48
+ await publish(DigestTimeChosen({recipientId: "r1", time: at18}), ~recipientId="r1")
49
+ await publish(DigestTimeChosen({recipientId: "r2", time: at19Berlin}), ~recipientId="r2")
50
+ await settle()
51
+ expect((storedSchedules(), (await timers())->sorted))->toEqual((
52
+ [("r1", at18), ("r2", at19Berlin)],
53
+ ["r1", "r2"],
54
+ ))
55
+ })
56
+
57
+ testPromise("each recipient's tick comes at their own time and sends their digest", async () => {
58
+ // 18:00 UTC is r1's; 19:00 in Berlin is 17:00 UTC, r2's.
59
+ await jest->advanceTimersByTimeAsync(5. *. 3_600_000. +. 1_000.)
60
+ await settle()
61
+ expect(publishedCommands.contents->Array.map(c => c.id))->toEqual(["r2"])
62
+ await jest->advanceTimersByTimeAsync(3_600_000.)
63
+ await settle()
64
+ // Nothing answers r2's command here, so 18:00's pass, an hour on, sends it again.
65
+ expect(publishedCommands.contents->Array.map(c => c.id))->toEqual(["r2", "r2", "r1"])
66
+ })
67
+
68
+ testPromise("unsubscribing removes the recipient's row and timer, and only theirs", async () => {
69
+ await publish(Unsubscribed({recipientId: "r1"}), ~recipientId="r1")
70
+ await settle()
71
+ expect((storedSchedules(), await timers()))->toEqual(([("r2", at19Berlin)], ["r2"]))
72
+ })
73
+
74
+ testPromise("a later reconcile keeps the times events set", async () => {
75
+ let ops = await ops()
76
+ await ops.reconcileSchedules()
77
+ expect((storedSchedules(), await timers()))->toEqual(([("r2", at19Berlin)], ["r2"]))
78
+ })
79
+
80
+ testPromise(
81
+ "a rebuild reproduces the rows from the events and removes a stray timer",
82
+ async () => {
83
+ let platform = LocalAutomationSchedules.platformFor(~slice=DigestSpec.name)
84
+ await platform.put(~key="stray", at18)
85
+ let events =
86
+ [
87
+ eventJson(DigestTimeChosen({recipientId: "r1", time: at18}), ~recipientId="r1"),
88
+ eventJson(DigestTimeChosen({recipientId: "r3", time: at19Berlin}), ~recipientId="r3"),
89
+ eventJson(Unsubscribed({recipientId: "r1"}), ~recipientId="r1"),
90
+ ]->Array.map(((_, json)) => json)
91
+ let ops = await ops()
92
+ await ops.rebuildSchedules(events)
93
+ expect((storedSchedules(), (await timers())->sorted))->toEqual(([("r3", at19Berlin)], ["r3"]))
94
+ },
95
+ )
96
+ })
@@ -0,0 +1,177 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Globals from "@jest/globals";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
+ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
8
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
9
+ import * as LocalAutomationSchedules$ReventlessLocal from "../../../src/components/LocalAutomationSchedules.res.mjs";
10
+ import * as AutomationRescheduleFixtures$ReventlessLocal from "./AutomationRescheduleFixtures.res.mjs";
11
+
12
+ function time(iso) {
13
+ return new Date(iso).getTime();
14
+ }
15
+
16
+ let at18 = {
17
+ TAG: "Calendar",
18
+ calendar: {
19
+ TAG: "Daily",
20
+ hour: 18,
21
+ minute: 0
22
+ }
23
+ };
24
+
25
+ let at19Berlin = {
26
+ TAG: "Calendar",
27
+ calendar: {
28
+ TAG: "Daily",
29
+ hour: 19,
30
+ minute: 0
31
+ },
32
+ zone: "Europe/Berlin"
33
+ };
34
+
35
+ async function settle() {
36
+ for (let _for = 1; _for <= 20; ++_for) {
37
+ await Globals.jest.advanceTimersByTimeAsync(0);
38
+ }
39
+ }
40
+
41
+ function sorted(keys) {
42
+ return keys.toSorted(Primitive_string.compare);
43
+ }
44
+
45
+ globalThis.describe("schedules set by events, locally", () => {
46
+ let slice = {
47
+ contents: undefined
48
+ };
49
+ let ops = async () => await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(Stdlib_Option.getOrThrow(slice.contents, undefined)));
50
+ globalThis.beforeAll(async () => {
51
+ Globals.jest.useFakeTimers();
52
+ Globals.jest.setSystemTime(new Date("2026-10-16T12:00:00Z").getTime());
53
+ slice.contents = Primitive_option.some(AutomationRescheduleFixtures$ReventlessLocal.makeSlice());
54
+ await ops();
55
+ return await settle();
56
+ });
57
+ globalThis.afterAll(() => {
58
+ LocalAutomationSchedules$ReventlessLocal.reset();
59
+ Globals.jest.useRealTimers();
60
+ });
61
+ globalThis.test("a choice is stored and starts the recipient's timer", async () => {
62
+ await AutomationRescheduleFixtures$ReventlessLocal.publish({
63
+ TAG: "DigestTimeChosen",
64
+ recipientId: "r1",
65
+ time: at18
66
+ }, "r1");
67
+ await AutomationRescheduleFixtures$ReventlessLocal.publish({
68
+ TAG: "DigestTimeChosen",
69
+ recipientId: "r2",
70
+ time: at19Berlin
71
+ }, "r2");
72
+ await settle();
73
+ globalThis.expect([
74
+ AutomationRescheduleFixtures$ReventlessLocal.storedSchedules(),
75
+ sorted(await AutomationRescheduleFixtures$ReventlessLocal.timers())
76
+ ]).toEqual([
77
+ [
78
+ [
79
+ "r1",
80
+ at18
81
+ ],
82
+ [
83
+ "r2",
84
+ at19Berlin
85
+ ]
86
+ ],
87
+ [
88
+ "r1",
89
+ "r2"
90
+ ]
91
+ ]);
92
+ });
93
+ globalThis.test("each recipient's tick comes at their own time and sends their digest", async () => {
94
+ await Globals.jest.advanceTimersByTimeAsync(5 * 3600000 + 1000);
95
+ await settle();
96
+ globalThis.expect(AutomationRescheduleFixtures$ReventlessLocal.publishedCommands.contents.map(c => c.id)).toEqual(["r2"]);
97
+ await Globals.jest.advanceTimersByTimeAsync(3600000);
98
+ await settle();
99
+ globalThis.expect(AutomationRescheduleFixtures$ReventlessLocal.publishedCommands.contents.map(c => c.id)).toEqual([
100
+ "r2",
101
+ "r2",
102
+ "r1"
103
+ ]);
104
+ });
105
+ globalThis.test("unsubscribing removes the recipient's row and timer, and only theirs", async () => {
106
+ await AutomationRescheduleFixtures$ReventlessLocal.publish({
107
+ TAG: "Unsubscribed",
108
+ recipientId: "r1"
109
+ }, "r1");
110
+ await settle();
111
+ globalThis.expect([
112
+ AutomationRescheduleFixtures$ReventlessLocal.storedSchedules(),
113
+ await AutomationRescheduleFixtures$ReventlessLocal.timers()
114
+ ]).toEqual([
115
+ [[
116
+ "r2",
117
+ at19Berlin
118
+ ]],
119
+ ["r2"]
120
+ ]);
121
+ });
122
+ globalThis.test("a later reconcile keeps the times events set", async () => {
123
+ let ops$1 = await ops();
124
+ await ops$1.reconcileSchedules();
125
+ globalThis.expect([
126
+ AutomationRescheduleFixtures$ReventlessLocal.storedSchedules(),
127
+ await AutomationRescheduleFixtures$ReventlessLocal.timers()
128
+ ]).toEqual([
129
+ [[
130
+ "r2",
131
+ at19Berlin
132
+ ]],
133
+ ["r2"]
134
+ ]);
135
+ });
136
+ globalThis.test("a rebuild reproduces the rows from the events and removes a stray timer", async () => {
137
+ let platform = LocalAutomationSchedules$ReventlessLocal.platformFor(AutomationRescheduleFixtures$ReventlessLocal.DigestSpec.name);
138
+ await platform.put("stray", at18);
139
+ let events = [
140
+ AutomationRescheduleFixtures$ReventlessLocal.eventJson({
141
+ TAG: "DigestTimeChosen",
142
+ recipientId: "r1",
143
+ time: at18
144
+ }, "r1"),
145
+ AutomationRescheduleFixtures$ReventlessLocal.eventJson({
146
+ TAG: "DigestTimeChosen",
147
+ recipientId: "r3",
148
+ time: at19Berlin
149
+ }, "r3"),
150
+ AutomationRescheduleFixtures$ReventlessLocal.eventJson({
151
+ TAG: "Unsubscribed",
152
+ recipientId: "r1"
153
+ }, "r1")
154
+ ].map(param => param[1]);
155
+ let ops$1 = await ops();
156
+ await ops$1.rebuildSchedules(events);
157
+ globalThis.expect([
158
+ AutomationRescheduleFixtures$ReventlessLocal.storedSchedules(),
159
+ sorted(await AutomationRescheduleFixtures$ReventlessLocal.timers())
160
+ ]).toEqual([
161
+ [[
162
+ "r3",
163
+ at19Berlin
164
+ ]],
165
+ ["r3"]
166
+ ]);
167
+ });
168
+ });
169
+
170
+ export {
171
+ time,
172
+ at18,
173
+ at19Berlin,
174
+ settle,
175
+ sorted,
176
+ }
177
+ /* Not a pure module */