@reventlessdev/reventless-local 3.0.0-alpha.248 → 3.0.0-alpha.250

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 (170) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +14 -14
  3. package/src/BakedManifest.res +5 -6
  4. package/src/EmitCapabilities.res +3 -1
  5. package/src/LocalPlatformRegistry.res +3 -1
  6. package/src/LocalPlatformStart.res +7 -6
  7. package/src/LocalSeedTarget.res +3 -1
  8. package/src/Platform.res +291 -215
  9. package/src/ShellConfig.res +10 -17
  10. package/src/UiHints.res +8 -14
  11. package/src/UiSlots.res +10 -17
  12. package/src/adapter/Api/LocalEvents_Server.res +5 -3
  13. package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
  14. package/src/adapter/Auth/LocalAuth.res +11 -15
  15. package/src/adapter/Auth/UserStore.res +5 -7
  16. package/src/adapter/Backend.res +1 -2
  17. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  18. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  19. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  20. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  21. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  22. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  23. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  24. package/src/adapter/DomainGraphQL_Server.res +35 -34
  25. package/src/adapter/DomainMCP_Server.res +45 -41
  26. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  27. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  28. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  29. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  30. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  31. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  32. package/src/adapter/LocalBus.res +12 -12
  33. package/src/adapter/LocalCapabilities.res.mjs +1 -1
  34. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  35. package/src/adapter/LocalSideEffectHandler.res +1 -1
  36. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  37. package/src/adapter/MCP_ServerInstance.res +51 -39
  38. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  39. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  40. package/src/adapter/PlatformGraphQL_Server.res +9 -5
  41. package/src/adapter/ProjectionCheckpoint.res +8 -1
  42. package/src/adapter/ProjectionPending.res +1 -3
  43. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  44. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  45. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  46. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  47. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  48. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  49. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  50. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  51. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  52. package/src/adapter/SqliteDriver.res +16 -4
  53. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  54. package/src/components/Aggregate_Builder.res +6 -1
  55. package/src/components/Counter_Builder.res +1 -5
  56. package/src/components/DcbEventLog_Builder.res +5 -3
  57. package/src/components/ExtensionPoint_Builder.res +13 -15
  58. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  59. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  60. package/src/components/ReadModel_Builder.res +18 -20
  61. package/src/components/StateViewSlice_Builder.res +3 -2
  62. package/src/components/Task_Builder.res +12 -13
  63. package/src/reset/LocalSeedReset.res +2 -5
  64. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  65. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  66. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  67. package/src/test/TestRunner.res +1 -1
  68. package/tests/BakedManifestFilesTest.res +7 -10
  69. package/tests/LocalEventTapTest.res +26 -21
  70. package/tests/LocalPlatformStartTest.res +33 -28
  71. package/tests/PluginEventDecodeTest.res +3 -3
  72. package/tests/ShellConfigTest.res +30 -21
  73. package/tests/SplitApiFixtures.res +34 -10
  74. package/tests/SplitApiTest.res +92 -62
  75. package/tests/adapter/BackendParityTest.res +180 -130
  76. package/tests/adapter/CollectNEventsTest.res +6 -4
  77. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  78. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  79. package/tests/adapter/CounterHandlerTest.res +95 -77
  80. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  81. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  82. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  83. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  84. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  85. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  86. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  87. package/tests/adapter/EventTapTest.res +37 -20
  88. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  89. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  90. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  91. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  92. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  93. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  94. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  95. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  96. package/tests/adapter/LocalAuthTest.res +8 -16
  97. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  98. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  99. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  100. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  101. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  102. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  103. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  104. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  105. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  106. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  107. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  108. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  109. package/tests/adapter/QueryDbStorageTest.res +163 -117
  110. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  111. package/tests/adapter/QueryEngineTest.res +179 -126
  112. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  113. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  114. package/tests/adapter/SortKey_FilterTest.res +19 -3
  115. package/tests/adapter/SqliteDriverTest.res +13 -10
  116. package/tests/adapter/TaskBucketTest.res +94 -80
  117. package/tests/components/aggregate/AggregateTest.res +31 -18
  118. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  119. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  120. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  121. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  122. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  123. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  124. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  125. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  126. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  127. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  128. package/tests/components/counter/CounterFixtures.res +6 -8
  129. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  130. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  131. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  132. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  133. package/tests/components/dcb/DcbFixtures.res +4 -1
  134. package/tests/components/dcb/DcbTest.res +9 -6
  135. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  136. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  137. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  138. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  139. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  140. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  141. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  142. package/tests/components/eventlog/EventLogTest.res +20 -4
  143. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  144. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  145. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  146. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  147. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  148. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  149. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  150. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  151. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  152. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  153. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  154. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  155. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  156. package/tests/components/querydb/QueryDbTest.res +2 -4
  157. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  158. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  159. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  160. package/tests/components/scheduler/SchedulerTest.res +76 -58
  161. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  162. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  163. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  164. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  165. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  166. package/tests/components/task/TaskFixtures.res +5 -5
  167. package/tests/components/task/TaskTest.res +95 -80
  168. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  169. package/tests/plugin/UiFragments_GWT.res +6 -1
  170. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -51,149 +51,185 @@ let makeTopicResource = (topicName: string): ReventlessInfra.Adapter.resolvedRes
51
51
 
52
52
  describe("LocalScheduledPublisher", () => {
53
53
  describe("createSchedule", () => {
54
- testPromise("single-shot schedule fires event after runAllTimers", async () => {
55
- module TestBus = LocalBus.Make()
56
- module SP = LocalScheduledPublisher.Make(TestBus)
57
- let count: ref<int> = ref(0)
58
- TestBus.subscribeToEvents("sched-topic", async (_, _, _) => {
59
- count := count.contents + 1
60
- })
61
- let pub = SP.make(~name="scheduler", ~opts={})
62
- let ops = await pub.operations->TestRunner.resolve
63
- let schedule: Reventless.Schedule.schedule = {
64
- name: "one-shot",
65
- rate: Reventless.Schedule.Single(2024, 1, 1, 0, 0),
66
- payload: "{}",
67
- }
68
- await ops.createSchedule([makeTopicResource("sched-topic")], schedule)
69
- jest->runAllTimers
70
- // Two microtask ticks: publishEvent uses Effect.all(concurrency=unbounded) which forks
71
- // child fibers via startFork — one tick for the child fiber to offer to the Queue,
72
- // a second tick for the drain fiber to run the callback.
73
- let _ = await Promise.resolve(())
74
- let _ = await Promise.resolve(())
75
- expect(count.contents)->toBe(1)
76
- SP.reset()
77
- })
78
-
79
- testPromise("recurring schedule fires on each interval advance", async () => {
80
- module TestBus = LocalBus.Make()
81
- module SP = LocalScheduledPublisher.Make(TestBus)
82
- let count: ref<int> = ref(0)
83
- TestBus.subscribeToEvents("repeat-topic", async (_, _, _) => {
84
- count := count.contents + 1
85
- })
86
- let pub = SP.make(~name="scheduler2", ~opts={})
87
- let ops = await pub.operations->TestRunner.resolve
88
- let schedule: Reventless.Schedule.schedule = {
89
- name: "every-minute",
90
- rate: Reventless.Schedule.Minutes(1),
91
- payload: "{}",
92
- }
93
- await ops.createSchedule([makeTopicResource("repeat-topic")], schedule)
94
- jest->advanceTimersByTime(60 * 1000)
95
- let _ = await Promise.resolve(())
96
- let _ = await Promise.resolve(())
97
- jest->advanceTimersByTime(60 * 1000)
98
- let _ = await Promise.resolve(())
99
- let _ = await Promise.resolve(())
100
- expect(count.contents)->toBe(2)
101
- SP.reset()
102
- })
54
+ testPromise(
55
+ "single-shot schedule fires event after runAllTimers",
56
+ async () => {
57
+ module TestBus = LocalBus.Make()
58
+ module SP = LocalScheduledPublisher.Make(TestBus)
59
+ let count: ref<int> = ref(0)
60
+ TestBus.subscribeToEvents(
61
+ "sched-topic",
62
+ async (_, _, _) => {
63
+ count := count.contents + 1
64
+ },
65
+ )
66
+ let pub = SP.make(~name="scheduler", ~opts={})
67
+ let ops = await pub.operations->TestRunner.resolve
68
+ let schedule: Reventless.Schedule.schedule = {
69
+ name: "one-shot",
70
+ rate: Reventless.Schedule.Single(2024, 1, 1, 0, 0),
71
+ payload: "{}",
72
+ }
73
+ await ops.createSchedule([makeTopicResource("sched-topic")], schedule)
74
+ jest->runAllTimers
75
+ // Two microtask ticks: publishEvent uses Effect.all(concurrency=unbounded) which forks
76
+ // child fibers via startFork — one tick for the child fiber to offer to the Queue,
77
+ // a second tick for the drain fiber to run the callback.
78
+ let _ = await Promise.resolve()
79
+ let _ = await Promise.resolve()
80
+ expect(count.contents)->toBe(1)
81
+ SP.reset()
82
+ },
83
+ )
84
+
85
+ testPromise(
86
+ "recurring schedule fires on each interval advance",
87
+ async () => {
88
+ module TestBus = LocalBus.Make()
89
+ module SP = LocalScheduledPublisher.Make(TestBus)
90
+ let count: ref<int> = ref(0)
91
+ TestBus.subscribeToEvents(
92
+ "repeat-topic",
93
+ async (_, _, _) => {
94
+ count := count.contents + 1
95
+ },
96
+ )
97
+ let pub = SP.make(~name="scheduler2", ~opts={})
98
+ let ops = await pub.operations->TestRunner.resolve
99
+ let schedule: Reventless.Schedule.schedule = {
100
+ name: "every-minute",
101
+ rate: Reventless.Schedule.Minutes(1),
102
+ payload: "{}",
103
+ }
104
+ await ops.createSchedule([makeTopicResource("repeat-topic")], schedule)
105
+ jest->advanceTimersByTime(60 * 1000)
106
+ let _ = await Promise.resolve()
107
+ let _ = await Promise.resolve()
108
+ jest->advanceTimersByTime(60 * 1000)
109
+ let _ = await Promise.resolve()
110
+ let _ = await Promise.resolve()
111
+ expect(count.contents)->toBe(2)
112
+ SP.reset()
113
+ },
114
+ )
103
115
  })
104
116
 
105
117
  describe("deleteSchedule", () => {
106
- testPromise("clears a named schedule; advancing timers does not fire it", async () => {
107
- module TestBus = LocalBus.Make()
108
- module SP = LocalScheduledPublisher.Make(TestBus)
109
- let count: ref<int> = ref(0)
110
- TestBus.subscribeToEvents("del-topic", async (_, _, _) => {
111
- count := count.contents + 1
112
- })
113
- let pub = SP.make(~name="scheduler3", ~opts={})
114
- let ops = await pub.operations->TestRunner.resolve
115
- let schedule: Reventless.Schedule.schedule = {
116
- name: "to-delete",
117
- rate: Reventless.Schedule.Minutes(1),
118
- payload: "{}",
119
- }
120
- await ops.createSchedule([makeTopicResource("del-topic")], schedule)
121
- await ops.deleteSchedule([], "to-delete")
122
- jest->advanceTimersByTime(60 * 1000)
123
- expect(count.contents)->toBe(0)
124
- SP.reset()
125
- })
118
+ testPromise(
119
+ "clears a named schedule; advancing timers does not fire it",
120
+ async () => {
121
+ module TestBus = LocalBus.Make()
122
+ module SP = LocalScheduledPublisher.Make(TestBus)
123
+ let count: ref<int> = ref(0)
124
+ TestBus.subscribeToEvents(
125
+ "del-topic",
126
+ async (_, _, _) => {
127
+ count := count.contents + 1
128
+ },
129
+ )
130
+ let pub = SP.make(~name="scheduler3", ~opts={})
131
+ let ops = await pub.operations->TestRunner.resolve
132
+ let schedule: Reventless.Schedule.schedule = {
133
+ name: "to-delete",
134
+ rate: Reventless.Schedule.Minutes(1),
135
+ payload: "{}",
136
+ }
137
+ await ops.createSchedule([makeTopicResource("del-topic")], schedule)
138
+ await ops.deleteSchedule([], "to-delete")
139
+ jest->advanceTimersByTime(60 * 1000)
140
+ expect(count.contents)->toBe(0)
141
+ SP.reset()
142
+ },
143
+ )
126
144
  })
127
145
 
128
146
  describe("reset", () => {
129
- testPromise("clears all active timers; subsequent advance does not fire", async () => {
130
- module TestBus = LocalBus.Make()
131
- module SP = LocalScheduledPublisher.Make(TestBus)
132
- let count: ref<int> = ref(0)
133
- TestBus.subscribeToEvents("reset-topic", async (_, _, _) => {
134
- count := count.contents + 1
135
- })
136
- let pub = SP.make(~name="scheduler4", ~opts={})
137
- let ops = await pub.operations->TestRunner.resolve
138
- let schedule: Reventless.Schedule.schedule = {
139
- name: "reset-sched",
140
- rate: Reventless.Schedule.Minutes(5),
141
- payload: "{}",
142
- }
143
- await ops.createSchedule([makeTopicResource("reset-topic")], schedule)
144
- SP.reset()
145
- jest->advanceTimersByTime(5 * 60 * 1000)
146
- expect(count.contents)->toBe(0)
147
- })
147
+ testPromise(
148
+ "clears all active timers; subsequent advance does not fire",
149
+ async () => {
150
+ module TestBus = LocalBus.Make()
151
+ module SP = LocalScheduledPublisher.Make(TestBus)
152
+ let count: ref<int> = ref(0)
153
+ TestBus.subscribeToEvents(
154
+ "reset-topic",
155
+ async (_, _, _) => {
156
+ count := count.contents + 1
157
+ },
158
+ )
159
+ let pub = SP.make(~name="scheduler4", ~opts={})
160
+ let ops = await pub.operations->TestRunner.resolve
161
+ let schedule: Reventless.Schedule.schedule = {
162
+ name: "reset-sched",
163
+ rate: Reventless.Schedule.Minutes(5),
164
+ payload: "{}",
165
+ }
166
+ await ops.createSchedule([makeTopicResource("reset-topic")], schedule)
167
+ SP.reset()
168
+ jest->advanceTimersByTime(5 * 60 * 1000)
169
+ expect(count.contents)->toBe(0)
170
+ },
171
+ )
148
172
  })
149
173
 
150
174
  describe("rateToMs", () => {
151
- testPromise("Minutes(n) fires after n minutes", async () => {
152
- module TestBus = LocalBus.Make()
153
- module SP = LocalScheduledPublisher.Make(TestBus)
154
- let count: ref<int> = ref(0)
155
- TestBus.subscribeToEvents("rate-topic", async (_, _, _) => {
156
- count := count.contents + 1
157
- })
158
- let pub = SP.make(~name="rate-scheduler", ~opts={})
159
- let ops = await pub.operations->TestRunner.resolve
160
- let schedule: Reventless.Schedule.schedule = {
161
- name: "rate-sched",
162
- rate: Reventless.Schedule.Minutes(3),
163
- payload: "{}",
164
- }
165
- await ops.createSchedule([makeTopicResource("rate-topic")], schedule)
166
- // Advance by slightly less than 3 minutes — should not fire
167
- jest->advanceTimersByTime(3 * 60 * 1000 - 1)
168
- expect(count.contents)->toBe(0)
169
- // Advance the remaining millisecond — now fires; two ticks for Effect.all drain
170
- jest->advanceTimersByTime(1)
171
- let _ = await Promise.resolve(())
172
- let _ = await Promise.resolve(())
173
- expect(count.contents)->toBe(1)
174
- SP.reset()
175
- })
176
-
177
- testPromise("Hours(n) fires after n hours", async () => {
178
- module TestBus = LocalBus.Make()
179
- module SP = LocalScheduledPublisher.Make(TestBus)
180
- let count: ref<int> = ref(0)
181
- TestBus.subscribeToEvents("hours-topic", async (_, _, _) => {
182
- count := count.contents + 1
183
- })
184
- let pub = SP.make(~name="hours-scheduler", ~opts={})
185
- let ops = await pub.operations->TestRunner.resolve
186
- let schedule: Reventless.Schedule.schedule = {
187
- name: "hours-sched",
188
- rate: Reventless.Schedule.Hours(2),
189
- payload: "{}",
190
- }
191
- await ops.createSchedule([makeTopicResource("hours-topic")], schedule)
192
- jest->advanceTimersByTime(2 * 60 * 60 * 1000)
193
- let _ = await Promise.resolve(())
194
- let _ = await Promise.resolve(())
195
- expect(count.contents)->toBe(1)
196
- SP.reset()
197
- })
175
+ testPromise(
176
+ "Minutes(n) fires after n minutes",
177
+ async () => {
178
+ module TestBus = LocalBus.Make()
179
+ module SP = LocalScheduledPublisher.Make(TestBus)
180
+ let count: ref<int> = ref(0)
181
+ TestBus.subscribeToEvents(
182
+ "rate-topic",
183
+ async (_, _, _) => {
184
+ count := count.contents + 1
185
+ },
186
+ )
187
+ let pub = SP.make(~name="rate-scheduler", ~opts={})
188
+ let ops = await pub.operations->TestRunner.resolve
189
+ let schedule: Reventless.Schedule.schedule = {
190
+ name: "rate-sched",
191
+ rate: Reventless.Schedule.Minutes(3),
192
+ payload: "{}",
193
+ }
194
+ await ops.createSchedule([makeTopicResource("rate-topic")], schedule)
195
+ // Advance by slightly less than 3 minutes — should not fire
196
+ jest->advanceTimersByTime(3 * 60 * 1000 - 1)
197
+ expect(count.contents)->toBe(0)
198
+ // Advance the remaining millisecond — now fires; two ticks for Effect.all drain
199
+ jest->advanceTimersByTime(1)
200
+ let _ = await Promise.resolve()
201
+ let _ = await Promise.resolve()
202
+ expect(count.contents)->toBe(1)
203
+ SP.reset()
204
+ },
205
+ )
206
+
207
+ testPromise(
208
+ "Hours(n) fires after n hours",
209
+ async () => {
210
+ module TestBus = LocalBus.Make()
211
+ module SP = LocalScheduledPublisher.Make(TestBus)
212
+ let count: ref<int> = ref(0)
213
+ TestBus.subscribeToEvents(
214
+ "hours-topic",
215
+ async (_, _, _) => {
216
+ count := count.contents + 1
217
+ },
218
+ )
219
+ let pub = SP.make(~name="hours-scheduler", ~opts={})
220
+ let ops = await pub.operations->TestRunner.resolve
221
+ let schedule: Reventless.Schedule.schedule = {
222
+ name: "hours-sched",
223
+ rate: Reventless.Schedule.Hours(2),
224
+ payload: "{}",
225
+ }
226
+ await ops.createSchedule([makeTopicResource("hours-topic")], schedule)
227
+ jest->advanceTimersByTime(2 * 60 * 60 * 1000)
228
+ let _ = await Promise.resolve()
229
+ let _ = await Promise.resolve()
230
+ expect(count.contents)->toBe(1)
231
+ SP.reset()
232
+ },
233
+ )
198
234
  })
199
235
  })
@@ -44,31 +44,32 @@ type httpRes
44
44
  let postJson = (path: string, body: dict<JSON.t>): promise<(int, JSON.t)> =>
45
45
  Promise.make((resolve, reject) => {
46
46
  let bodyStr = body->JSON.Encode.object->JSON.stringify
47
- let req =
48
- _request(
49
- {
50
- hostname: "localhost",
51
- port,
52
- path,
53
- method: "POST",
54
- headers: Dict.fromArray([
55
- ("content-type", "application/json"),
56
- ("content-length", bodyStr->String.length->Int.toString),
57
- ]),
58
- },
59
- res => {
60
- let status = _resStatusCode(res)
61
- let buf = ref("")
62
- res->_resSetEncoding("utf8")
63
- res->_resOnData(chunk => buf := buf.contents ++ Obj.magic(chunk))
64
- res->_resOnEnd(() => {
47
+ let req = _request(
48
+ {
49
+ hostname: "localhost",
50
+ port,
51
+ path,
52
+ method: "POST",
53
+ headers: Dict.fromArray([
54
+ ("content-type", "application/json"),
55
+ ("content-length", bodyStr->String.length->Int.toString),
56
+ ]),
57
+ },
58
+ res => {
59
+ let status = _resStatusCode(res)
60
+ let buf = ref("")
61
+ res->_resSetEncoding("utf8")
62
+ res->_resOnData(chunk => buf := buf.contents ++ Obj.magic(chunk))
63
+ res->_resOnEnd(
64
+ () => {
65
65
  let parsed = try buf.contents->JSON.parseOrThrow catch {
66
66
  | _ => JSON.Encode.null
67
67
  }
68
68
  resolve((status, parsed))
69
- })
70
- },
71
- )
69
+ },
70
+ )
71
+ },
72
+ )
72
73
  req->_reqOnError(err => reject(Obj.magic(err)))
73
74
  req->_reqWrite(bodyStr)
74
75
  req->_reqEnd
@@ -78,24 +79,23 @@ let postJson = (path: string, body: dict<JSON.t>): promise<(int, JSON.t)> =>
78
79
  // only for ASCII payloads, which the SVG fixture below is.
79
80
  let putRaw = (path: string, body: string, ~contentType: string): promise<int> =>
80
81
  Promise.make((resolve, reject) => {
81
- let req =
82
- _request(
83
- {
84
- hostname: "localhost",
85
- port,
86
- path,
87
- method: "PUT",
88
- headers: Dict.fromArray([
89
- ("content-type", contentType),
90
- ("content-length", body->String.length->Int.toString),
91
- ]),
92
- },
93
- res => {
94
- let status = _resStatusCode(res)
95
- res->_resOnData(_ => ())
96
- res->_resOnEnd(() => resolve(status))
97
- },
98
- )
82
+ let req = _request(
83
+ {
84
+ hostname: "localhost",
85
+ port,
86
+ path,
87
+ method: "PUT",
88
+ headers: Dict.fromArray([
89
+ ("content-type", contentType),
90
+ ("content-length", body->String.length->Int.toString),
91
+ ]),
92
+ },
93
+ res => {
94
+ let status = _resStatusCode(res)
95
+ res->_resOnData(_ => ())
96
+ res->_resOnEnd(() => resolve(status))
97
+ },
98
+ )
99
99
  req->_reqOnError(err => reject(Obj.magic(err)))
100
100
  req->_reqWrite(body)
101
101
  req->_reqEnd
@@ -103,24 +103,27 @@ let putRaw = (path: string, body: string, ~contentType: string): promise<int> =>
103
103
 
104
104
  let getRaw = (path: string): promise<(int, string, string)> =>
105
105
  Promise.make((resolve, reject) => {
106
- let req =
107
- _request(
108
- {hostname: "localhost", port, path, method: "GET", headers: Dict.make()},
109
- res => {
110
- let status = _resStatusCode(res)
111
- let contentType = _resHeaders(res)->Dict.get("content-type")->Option.getOr("")
112
- let buf = ref("")
113
- res->_resSetEncoding("utf8")
114
- res->_resOnData(chunk => buf := buf.contents ++ Obj.magic(chunk))
115
- res->_resOnEnd(() => resolve((status, buf.contents, contentType)))
116
- },
117
- )
106
+ let req = _request(
107
+ {hostname: "localhost", port, path, method: "GET", headers: Dict.make()},
108
+ res => {
109
+ let status = _resStatusCode(res)
110
+ let contentType = _resHeaders(res)->Dict.get("content-type")->Option.getOr("")
111
+ let buf = ref("")
112
+ res->_resSetEncoding("utf8")
113
+ res->_resOnData(chunk => buf := buf.contents ++ Obj.magic(chunk))
114
+ res->_resOnEnd(() => resolve((status, buf.contents, contentType)))
115
+ },
116
+ )
118
117
  req->_reqOnError(err => reject(Obj.magic(err)))
119
118
  req->_reqEnd
120
119
  })
121
120
 
122
121
  let getString = (j: JSON.t, k: string): string =>
123
- j->JSON.Decode.object->Option.flatMap(d => d->Dict.get(k))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
122
+ j
123
+ ->JSON.Decode.object
124
+ ->Option.flatMap(d => d->Dict.get(k))
125
+ ->Option.flatMap(JSON.Decode.string)
126
+ ->Option.getOr("")
124
127
 
125
128
  // ── Lifecycle ────────────────────────────────────────────────────────────────
126
129
 
@@ -198,13 +201,16 @@ testPromise("presign → PUT → GET → release → GET(404) is the full loop",
198
201
  expect(goneStatus)->toEqual(404)
199
202
  })
200
203
 
201
- testPromise("Upload_Release of a ref outside a served prefix is refused with a reason", async () => {
202
- let r = await releaseRef(~storageRef="/not-a-store/x/y.svg")
203
- expect(r->JSON.Decode.object->Option.flatMap(d => d->Dict.get("released")))->toEqual(
204
- Some(JSON.Encode.bool(false)),
205
- )
206
- expect(getString(r, "reason"))->toEqual("not_in_store")
207
- })
204
+ testPromise(
205
+ "Upload_Release of a ref outside a served prefix is refused with a reason",
206
+ async () => {
207
+ let r = await releaseRef(~storageRef="/not-a-store/x/y.svg")
208
+ expect(r->JSON.Decode.object->Option.flatMap(d => d->Dict.get("released")))->toEqual(
209
+ Some(JSON.Encode.bool(false)),
210
+ )
211
+ expect(getString(r, "reason"))->toEqual("not_in_store")
212
+ },
213
+ )
208
214
 
209
215
  testPromise("GET a missing served object returns 404", async () => {
210
216
  let (status, _, _) = await getRaw("/uploads/missing/none.svg")
@@ -20,14 +20,24 @@ let skField = "_subId"
20
20
 
21
21
  let getSks = result =>
22
22
  result.SortKey_Filter.items->Array.map(item =>
23
- item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(skField))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
23
+ item
24
+ ->JSON.Decode.object
25
+ ->Option.flatMap(d => d->Dict.get(skField))
26
+ ->Option.flatMap(JSON.Decode.string)
27
+ ->Option.getOr("")
24
28
  )
25
29
 
26
30
  describe("SortKey_Filter.apply:", () => {
27
31
  testPromise("no filters returns all items in order", async () => {
28
32
  let r = SortKey_Filter.apply(~items, ~skField)
29
33
  expect(r.items->Array.length)->toBe(5)
30
- expect(getSks(r))->toEqual(["art/2026-01", "art/2026-02", "math/2026-01", "math/2026-02", "science/2026-01"])
34
+ expect(getSks(r))->toEqual([
35
+ "art/2026-01",
36
+ "art/2026-02",
37
+ "math/2026-01",
38
+ "math/2026-02",
39
+ "science/2026-01",
40
+ ])
31
41
  expect(r.nextToken)->toBe(None)
32
42
  })
33
43
 
@@ -59,7 +69,13 @@ describe("SortKey_Filter.apply:", () => {
59
69
 
60
70
  testPromise("reverse returns items in descending order", async () => {
61
71
  let r = SortKey_Filter.apply(~items, ~skField, ~reverse=true)
62
- expect(getSks(r))->toEqual(["science/2026-01", "math/2026-02", "math/2026-01", "art/2026-02", "art/2026-01"])
72
+ expect(getSks(r))->toEqual([
73
+ "science/2026-01",
74
+ "math/2026-02",
75
+ "math/2026-01",
76
+ "art/2026-02",
77
+ "art/2026-01",
78
+ ])
63
79
  })
64
80
 
65
81
  testPromise("limit returns first N items", async () => {
@@ -37,20 +37,24 @@ describe("SqliteDriver", () => {
37
37
  let insert = db->SqliteDriver.prepare("INSERT INTO t(id) VALUES(?)")
38
38
  let countAll = db->SqliteDriver.prepare("SELECT COUNT(*) AS c FROM t")
39
39
 
40
- db->SqliteDriver.transaction(() => {
41
- insert->SqliteDriver.run([JSON.Encode.int(1)])
42
- insert->SqliteDriver.run([JSON.Encode.int(2)])
43
- })
40
+ db->SqliteDriver.transaction(
41
+ () => {
42
+ insert->SqliteDriver.run([JSON.Encode.int(1)])
43
+ insert->SqliteDriver.run([JSON.Encode.int(2)])
44
+ },
45
+ )
44
46
 
45
47
  let row = countAll->SqliteDriver.get([])->Option.getOrThrow
46
48
  expect(row->Dict.get("c")->Option.getOr(JSON.Encode.null))->toEqual(JSON.Encode.int(2))
47
49
 
48
50
  let didThrow = ref(false)
49
51
  try {
50
- db->SqliteDriver.transaction(() => {
51
- insert->SqliteDriver.run([JSON.Encode.int(3)])
52
- throw(Failure("boom"))
53
- })
52
+ db->SqliteDriver.transaction(
53
+ () => {
54
+ insert->SqliteDriver.run([JSON.Encode.int(3)])
55
+ throw(Failure("boom"))
56
+ },
57
+ )
54
58
  } catch {
55
59
  | Failure(_) => didThrow := true
56
60
  }
@@ -63,8 +67,7 @@ describe("SqliteDriver", () => {
63
67
  })
64
68
 
65
69
  testPromise("Backend.fromEnv parses sqlite path and reset flag", async () => {
66
- let originalEnv =
67
- NodeProcess.env->Dict.get("REVENTLESS_LOCAL_BACKEND")
70
+ let originalEnv = NodeProcess.env->Dict.get("REVENTLESS_LOCAL_BACKEND")
68
71
 
69
72
  NodeProcess.env->Dict.set("REVENTLESS_LOCAL_BACKEND", "sqlite:./local.db")
70
73
  let parsed = Backend.fromEnv()