@reventlessdev/reventless-local 3.0.0-alpha.285 → 3.0.0-alpha.287

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 (36) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +11 -11
  3. package/src/adapter/Scheduler/LocalScheduledPublisher.res +11 -51
  4. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +7 -40
  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 +31 -2
  8. package/src/components/AutomationSlice_Builder.res.mjs +31 -3
  9. package/src/components/LocalAutomationSchedules.res +59 -0
  10. package/src/components/LocalAutomationSchedules.res.mjs +72 -0
  11. package/src/components/LocalAutomationSweep.res +61 -0
  12. package/src/components/LocalAutomationSweep.res.mjs +83 -0
  13. package/src/test/TestRunner.res +2 -0
  14. package/src/test/TestRunner.res.mjs +12 -0
  15. package/tests/adapter/ScheduledPublisherTest.res +156 -155
  16. package/tests/adapter/ScheduledPublisherTest.res.mjs +239 -137
  17. package/tests/components/automationslice/AutomationRescheduleFixtures.res +142 -0
  18. package/tests/components/automationslice/AutomationRescheduleFixtures.res.mjs +259 -0
  19. package/tests/components/automationslice/AutomationRescheduleTest.res +96 -0
  20. package/tests/components/automationslice/AutomationRescheduleTest.res.mjs +177 -0
  21. package/tests/components/automationslice/AutomationScheduleFixtures.res +118 -0
  22. package/tests/components/automationslice/AutomationScheduleFixtures.res.mjs +263 -0
  23. package/tests/components/automationslice/AutomationScheduleTest.res +88 -0
  24. package/tests/components/automationslice/AutomationScheduleTest.res.mjs +108 -0
  25. package/tests/components/automationslice/AutomationSliceFixtures.res +6 -2
  26. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +16 -4
  27. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +6 -1
  28. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +8 -2
  29. package/tests/components/automationslice/AutomationSweepFixtures.res +131 -0
  30. package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +223 -0
  31. package/tests/components/automationslice/AutomationSweepTest.res +107 -0
  32. package/tests/components/automationslice/AutomationSweepTest.res.mjs +116 -0
  33. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +4 -1
  34. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +14 -3
  35. package/tests/components/scheduler/SchedulerTest.res +5 -5
  36. package/tests/components/scheduler/SchedulerTest.res.mjs +22 -14
@@ -1,8 +1,10 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Globals from "@jest/globals";
4
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
4
5
  import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
5
6
  import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
7
+ import * as ScheduleTime$ReventlessCore from "@reventlessdev/reventless-core/src/util/ScheduleTime.res.mjs";
6
8
  import * as LocalScheduledPublisher$ReventlessLocal from "../../src/adapter/Scheduler/LocalScheduledPublisher.res.mjs";
7
9
 
8
10
  globalThis.beforeAll(() => {
@@ -30,174 +32,274 @@ function makeTopicResource(topicName) {
30
32
  };
31
33
  }
32
34
 
35
+ let hour = 60 * 60000;
36
+
37
+ let day = 24 * hour;
38
+
39
+ function at(iso) {
40
+ return new Date(iso).getTime();
41
+ }
42
+
43
+ async function flush() {
44
+ await Promise.resolve();
45
+ await Promise.resolve();
46
+ }
47
+
48
+ async function advance(ms) {
49
+ Globals.jest.advanceTimersByTime(ms);
50
+ return await flush();
51
+ }
52
+
53
+ let TestBus = LocalBus$ReventlessLocal.Make({});
54
+
55
+ let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
56
+
57
+ let topicCount = {
58
+ contents: 0
59
+ };
60
+
61
+ function fired(topic) {
62
+ let count = {
63
+ contents: 0
64
+ };
65
+ TestBus.subscribeToEvents(topic, async (param, param$1, param$2) => {
66
+ count.contents = count.contents + 1 | 0;
67
+ });
68
+ return count;
69
+ }
70
+
71
+ async function schedule(now, time) {
72
+ Globals.jest.setSystemTime(new Date(now).getTime());
73
+ topicCount.contents = topicCount.contents + 1 | 0;
74
+ let topic = `sched-topic-` + topicCount.contents.toString();
75
+ let count = fired(topic);
76
+ let ops = await TestRunner$ReventlessLocal.resolve(SP.make("scheduler", {}).operations);
77
+ await ops.createSchedule([makeTopicResource(topic)], {
78
+ name: topic,
79
+ time: time,
80
+ payload: "{}"
81
+ });
82
+ return [
83
+ ops,
84
+ topic,
85
+ count
86
+ ];
87
+ }
88
+
33
89
  globalThis.describe("LocalScheduledPublisher", () => {
34
- globalThis.describe("createSchedule", () => {
35
- globalThis.test("single-shot schedule fires event after runAllTimers", async () => {
36
- let TestBus = LocalBus$ReventlessLocal.Make({});
37
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
38
- let count = {
39
- contents: 0
40
- };
41
- TestBus.subscribeToEvents("sched-topic", async (param, param$1, param$2) => {
42
- count.contents = count.contents + 1 | 0;
90
+ globalThis.afterEach(() => SP.reset());
91
+ globalThis.describe("Every", () => {
92
+ globalThis.test("fires one interval after it is set, and again each interval", async () => {
93
+ let match = await schedule("2026-10-16T10:00:00Z", {
94
+ TAG: "Every",
95
+ _0: {
96
+ TAG: "Minutes",
97
+ _0: 3
98
+ }
99
+ });
100
+ let count = match[2];
101
+ await advance(3 * 60000 - 1);
102
+ globalThis.expect(count.contents).toBe(0);
103
+ await advance(1);
104
+ globalThis.expect(count.contents).toBe(1);
105
+ await advance(3 * 60000);
106
+ globalThis.expect(count.contents).toBe(2);
107
+ });
108
+ globalThis.test("Hours(n) fires after n hours", async () => {
109
+ let match = await schedule("2026-10-16T10:00:00Z", {
110
+ TAG: "Every",
111
+ _0: {
112
+ TAG: "Hours",
113
+ _0: 2
114
+ }
115
+ });
116
+ await advance(2 * hour);
117
+ globalThis.expect(match[2].contents).toBe(1);
118
+ });
119
+ });
120
+ globalThis.describe("Calendar", () => {
121
+ globalThis.test("fires at the wall-clock time each day, not 24 hours after it is set", async () => {
122
+ let match = await schedule("2026-10-16T05:58:00Z", {
123
+ TAG: "Calendar",
124
+ calendar: {
125
+ TAG: "Daily",
126
+ hour: 6,
127
+ minute: 0
128
+ }
43
129
  });
44
- let pub = SP.make("scheduler", {});
45
- let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
46
- await ops.createSchedule([makeTopicResource("sched-topic")], {
47
- name: "one-shot",
48
- rate: {
49
- TAG: "Single",
50
- _0: 2024,
51
- _1: 1,
52
- _2: 1,
53
- _3: 0,
54
- _4: 0
130
+ let count = match[2];
131
+ await advance(2 * 60000);
132
+ globalThis.expect(count.contents).toBe(1);
133
+ await advance(day);
134
+ globalThis.expect(count.contents).toBe(2);
135
+ });
136
+ globalThis.test("skips a day whose time falls in the hour daylight saving skips", async () => {
137
+ let match = await schedule("2027-03-27T12:00:00Z", {
138
+ TAG: "Calendar",
139
+ calendar: {
140
+ TAG: "Daily",
141
+ hour: 2,
142
+ minute: 30
55
143
  },
56
- payload: "{}"
144
+ zone: "Europe/Berlin"
145
+ });
146
+ let count = match[2];
147
+ await advance(new Date("2027-03-29T12:00:00Z").getTime() - new Date("2027-03-27T12:00:00Z").getTime());
148
+ globalThis.expect(count.contents).toBe(1);
149
+ await advance(new Date("2027-03-30T12:00:00Z").getTime() - new Date("2027-03-29T12:00:00Z").getTime());
150
+ globalThis.expect(count.contents).toBe(2);
151
+ });
152
+ globalThis.test("reaches a firing further away than a timer can wait", async () => {
153
+ let match = await schedule("2026-10-16T10:00:00Z", {
154
+ TAG: "Calendar",
155
+ calendar: {
156
+ TAG: "Yearly",
157
+ month: 1,
158
+ day: 1,
159
+ hour: 6,
160
+ minute: 0
161
+ }
57
162
  });
58
- Globals.jest.runAllTimers();
59
- await Promise.resolve();
60
- await Promise.resolve();
163
+ let count = match[2];
164
+ await advance(new Date("2027-01-01T05:59:00Z").getTime() - new Date("2026-10-16T10:00:00Z").getTime());
165
+ globalThis.expect(count.contents).toBe(0);
166
+ await advance(60000);
167
+ globalThis.expect(count.contents).toBe(1);
168
+ });
169
+ });
170
+ globalThis.describe("Once", () => {
171
+ globalThis.test("fires once, a period after it is set", async () => {
172
+ let match = await schedule("2026-10-16T10:00:00Z", {
173
+ TAG: "Once",
174
+ moment: {
175
+ TAG: "After",
176
+ _0: {
177
+ TAG: "Minutes",
178
+ _0: 10
179
+ }
180
+ }
181
+ });
182
+ let count = match[2];
183
+ await advance(10 * 60000);
184
+ globalThis.expect(count.contents).toBe(1);
185
+ await advance(day);
61
186
  globalThis.expect(count.contents).toBe(1);
62
- return SP.reset();
63
187
  });
64
- globalThis.test("recurring schedule fires on each interval advance", async () => {
65
- let TestBus = LocalBus$ReventlessLocal.Make({});
66
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
67
- let count = {
68
- contents: 0
69
- };
70
- TestBus.subscribeToEvents("repeat-topic", async (param, param$1, param$2) => {
71
- count.contents = count.contents + 1 | 0;
188
+ globalThis.test("never fires at a moment already past", async () => {
189
+ let match = await schedule("2026-10-16T10:00:00Z", {
190
+ TAG: "Once",
191
+ moment: {
192
+ TAG: "At",
193
+ _0: {
194
+ year: 2024,
195
+ month: 1,
196
+ day: 1,
197
+ hour: 0,
198
+ minute: 0
199
+ }
200
+ }
72
201
  });
73
- let pub = SP.make("scheduler2", {});
74
- let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
75
- await ops.createSchedule([makeTopicResource("repeat-topic")], {
76
- name: "every-minute",
77
- rate: {
202
+ await advance(day);
203
+ globalThis.expect(match[2].contents).toBe(0);
204
+ });
205
+ });
206
+ globalThis.describe("createSchedule", () => {
207
+ globalThis.test("replaces a schedule of the same name", async () => {
208
+ let match = await schedule("2026-10-16T10:00:00Z", {
209
+ TAG: "Every",
210
+ _0: {
78
211
  TAG: "Minutes",
79
212
  _0: 1
213
+ }
214
+ });
215
+ let topic = match[1];
216
+ await match[0].createSchedule([makeTopicResource(topic)], {
217
+ name: topic,
218
+ time: {
219
+ TAG: "Every",
220
+ _0: {
221
+ TAG: "Minutes",
222
+ _0: 5
223
+ }
80
224
  },
81
225
  payload: "{}"
82
226
  });
83
- Globals.jest.advanceTimersByTime(60000);
84
- await Promise.resolve();
85
- await Promise.resolve();
86
- Globals.jest.advanceTimersByTime(60000);
87
- await Promise.resolve();
88
- await Promise.resolve();
89
- globalThis.expect(count.contents).toBe(2);
90
- return SP.reset();
227
+ await advance(5 * 60000);
228
+ globalThis.expect(match[2].contents).toBe(1);
229
+ });
230
+ globalThis.test("refuses a time that cannot fire", async () => {
231
+ let ops = await TestRunner$ReventlessLocal.resolve(SP.make("scheduler", {}).operations);
232
+ let refused;
233
+ try {
234
+ await ops.createSchedule([makeTopicResource("refused")], {
235
+ name: "refused",
236
+ time: {
237
+ TAG: "Calendar",
238
+ calendar: {
239
+ TAG: "Daily",
240
+ hour: 25,
241
+ minute: 0
242
+ },
243
+ zone: "Mars/Base"
244
+ },
245
+ payload: "{}"
246
+ });
247
+ refused = false;
248
+ } catch (raw_exn) {
249
+ let exn = Primitive_exceptions.internalToException(raw_exn);
250
+ if (exn.RE_EXN_ID === ScheduleTime$ReventlessCore.InvalidTime) {
251
+ refused = true;
252
+ } else {
253
+ throw exn;
254
+ }
255
+ }
256
+ globalThis.expect(refused).toBe(true);
91
257
  });
92
258
  });
93
259
  globalThis.describe("deleteSchedule", () => {
94
- globalThis.test("clears a named schedule; advancing timers does not fire it", async () => {
95
- let TestBus = LocalBus$ReventlessLocal.Make({});
96
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
97
- let count = {
98
- contents: 0
99
- };
100
- TestBus.subscribeToEvents("del-topic", async (param, param$1, param$2) => {
101
- count.contents = count.contents + 1 | 0;
102
- });
103
- let pub = SP.make("scheduler3", {});
104
- let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
105
- await ops.createSchedule([makeTopicResource("del-topic")], {
106
- name: "to-delete",
107
- rate: {
260
+ globalThis.test("stops a schedule; advancing timers does not fire it", async () => {
261
+ let match = await schedule("2026-10-16T10:00:00Z", {
262
+ TAG: "Every",
263
+ _0: {
108
264
  TAG: "Minutes",
109
265
  _0: 1
110
- },
111
- payload: "{}"
266
+ }
112
267
  });
113
- await ops.deleteSchedule([], "to-delete");
114
- Globals.jest.advanceTimersByTime(60000);
115
- globalThis.expect(count.contents).toBe(0);
116
- return SP.reset();
268
+ await match[0].deleteSchedule([], match[1]);
269
+ await advance(60000);
270
+ globalThis.expect(match[2].contents).toBe(0);
117
271
  });
118
272
  });
119
273
  globalThis.describe("reset", () => {
120
274
  globalThis.test("clears all active timers; subsequent advance does not fire", async () => {
121
- let TestBus = LocalBus$ReventlessLocal.Make({});
122
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
123
- let count = {
124
- contents: 0
125
- };
126
- TestBus.subscribeToEvents("reset-topic", async (param, param$1, param$2) => {
127
- count.contents = count.contents + 1 | 0;
128
- });
129
- let pub = SP.make("scheduler4", {});
130
- let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
131
- await ops.createSchedule([makeTopicResource("reset-topic")], {
132
- name: "reset-sched",
133
- rate: {
275
+ let match = await schedule("2026-10-16T10:00:00Z", {
276
+ TAG: "Every",
277
+ _0: {
134
278
  TAG: "Minutes",
135
279
  _0: 5
136
- },
137
- payload: "{}"
280
+ }
138
281
  });
139
282
  SP.reset();
140
- Globals.jest.advanceTimersByTime(300000);
141
- globalThis.expect(count.contents).toBe(0);
142
- });
143
- });
144
- globalThis.describe("rateToMs", () => {
145
- globalThis.test("Minutes(n) fires after n minutes", async () => {
146
- let TestBus = LocalBus$ReventlessLocal.Make({});
147
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
148
- let count = {
149
- contents: 0
150
- };
151
- TestBus.subscribeToEvents("rate-topic", async (param, param$1, param$2) => {
152
- count.contents = count.contents + 1 | 0;
153
- });
154
- let pub = SP.make("rate-scheduler", {});
155
- let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
156
- await ops.createSchedule([makeTopicResource("rate-topic")], {
157
- name: "rate-sched",
158
- rate: {
159
- TAG: "Minutes",
160
- _0: 3
161
- },
162
- payload: "{}"
163
- });
164
- Globals.jest.advanceTimersByTime(179999);
165
- globalThis.expect(count.contents).toBe(0);
166
- Globals.jest.advanceTimersByTime(1);
167
- await Promise.resolve();
168
- await Promise.resolve();
169
- globalThis.expect(count.contents).toBe(1);
170
- return SP.reset();
171
- });
172
- globalThis.test("Hours(n) fires after n hours", async () => {
173
- let TestBus = LocalBus$ReventlessLocal.Make({});
174
- let SP = LocalScheduledPublisher$ReventlessLocal.Make(TestBus);
175
- let count = {
176
- contents: 0
177
- };
178
- TestBus.subscribeToEvents("hours-topic", async (param, param$1, param$2) => {
179
- count.contents = count.contents + 1 | 0;
180
- });
181
- let pub = SP.make("hours-scheduler", {});
182
- let ops = await TestRunner$ReventlessLocal.resolve(pub.operations);
183
- await ops.createSchedule([makeTopicResource("hours-topic")], {
184
- name: "hours-sched",
185
- rate: {
186
- TAG: "Hours",
187
- _0: 2
188
- },
189
- payload: "{}"
190
- });
191
- Globals.jest.advanceTimersByTime(7200000);
192
- await Promise.resolve();
193
- await Promise.resolve();
194
- globalThis.expect(count.contents).toBe(1);
195
- return SP.reset();
283
+ await advance(5 * 60000);
284
+ globalThis.expect(match[2].contents).toBe(0);
196
285
  });
197
286
  });
198
287
  });
199
288
 
289
+ let minute = 60000;
290
+
200
291
  export {
201
292
  makeTopicResource,
293
+ minute,
294
+ hour,
295
+ day,
296
+ at,
297
+ flush,
298
+ advance,
299
+ TestBus,
300
+ SP,
301
+ topicCount,
302
+ fired,
303
+ schedule,
202
304
  }
203
305
  /* Not a pure module */
@@ -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()