@rikalabs/akter 0.1.0-alpha.0 → 0.1.0-alpha.3

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 (308) hide show
  1. package/CHANGELOG.md +46 -3
  2. package/README.md +2 -2
  3. package/dist/actor/definition.d.ts +13 -9
  4. package/dist/actor/descriptor.d.ts +2 -2
  5. package/dist/actor/descriptor.js +4 -4
  6. package/dist/client/calls.js +1 -1
  7. package/dist/client/index.d.ts +1 -1
  8. package/dist/client/index.js +1 -1
  9. package/dist/errors/actor.d.ts +12 -42
  10. package/dist/errors/actor.js +13 -42
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/members/payload.d.ts +1 -1
  14. package/dist/protocol/inspection.d.ts +221 -1
  15. package/dist/protocol/inspection.js +151 -2
  16. package/dist/protocol/wire.d.ts +6 -49
  17. package/dist/protocol/wire.js +1 -10
  18. package/dist/runtime/actors.d.ts +3 -0
  19. package/dist/runtime/adoption/backfill.js +1 -1
  20. package/dist/runtime/adoption/enforce.js +2 -2
  21. package/dist/runtime/adoption/observe.js +1 -1
  22. package/dist/runtime/adoption/plan.d.ts +1 -1
  23. package/dist/runtime/adoption/startup.js +1 -1
  24. package/dist/runtime/adoption/status.d.ts +1 -1
  25. package/dist/runtime/app.d.ts +65 -0
  26. package/dist/runtime/app.js +38 -0
  27. package/dist/runtime/connections/holder.d.ts +11 -1
  28. package/dist/runtime/connections/holder.js +8 -4
  29. package/dist/runtime/connections/streams.d.ts +4 -4
  30. package/dist/runtime/content/store.js +15 -11
  31. package/dist/runtime/database/bounded.d.ts +4 -18
  32. package/dist/runtime/database/bounded.js +36 -22
  33. package/dist/runtime/database/coordination.d.ts +12 -3
  34. package/dist/runtime/database/coordination.js +29 -36
  35. package/dist/runtime/database/migrations.d.ts +61 -29
  36. package/dist/runtime/database/migrations.js +375 -139
  37. package/dist/runtime/database/replica.d.ts +4 -1
  38. package/dist/runtime/database/replica.js +4 -1
  39. package/dist/runtime/database/schema.d.ts +13 -0
  40. package/dist/runtime/database/schema.js +13 -0
  41. package/dist/runtime/database/shards.d.ts +31 -13
  42. package/dist/runtime/database/shards.js +38 -26
  43. package/dist/runtime/entity/register.d.ts +8 -1
  44. package/dist/runtime/entity/register.js +58 -7
  45. package/dist/runtime/fleet/checks.js +4 -4
  46. package/dist/runtime/fleet/maintainer.d.ts +1 -1
  47. package/dist/runtime/fleet/maintainer.js +1 -1
  48. package/dist/runtime/index.d.ts +2 -0
  49. package/dist/runtime/index.js +1 -0
  50. package/dist/runtime/inspector/layer.d.ts +20 -0
  51. package/dist/runtime/inspector/layer.js +224 -5
  52. package/dist/runtime/inspector/queries.d.ts +54 -8
  53. package/dist/runtime/inspector/queries.js +84 -15
  54. package/dist/runtime/layer.d.ts +48 -39
  55. package/dist/runtime/layer.js +124 -56
  56. package/dist/runtime/operators/audit.d.ts +3 -0
  57. package/dist/runtime/operators/audit.js +6 -2
  58. package/dist/runtime/operators/repair.js +30 -10
  59. package/dist/runtime/operators/seed.js +4 -2
  60. package/dist/runtime/payloads/versions.d.ts +4 -4
  61. package/dist/runtime/payloads/versions.js +24 -15
  62. package/dist/runtime/queries.js +4 -2
  63. package/dist/runtime/registration.d.ts +5 -2
  64. package/dist/runtime/registration.js +3 -3
  65. package/dist/runtime/runner.d.ts +12 -0
  66. package/dist/runtime/runner.js +12 -0
  67. package/dist/runtime/storage/codec.d.ts +19 -7
  68. package/dist/runtime/storage/codec.js +23 -7
  69. package/dist/runtime/storage/placements.d.ts +6 -1
  70. package/dist/runtime/storage/placements.js +98 -6
  71. package/dist/runtime/storage/retention.d.ts +6 -0
  72. package/dist/runtime/storage/retention.js +25 -13
  73. package/dist/runtime/subscriptions/relay.js +47 -44
  74. package/dist/runtime/subscriptions/storage.d.ts +3 -2
  75. package/dist/runtime/subscriptions/storage.js +9 -6
  76. package/dist/runtime/telemetry/live.d.ts +255 -0
  77. package/dist/runtime/telemetry/live.js +540 -0
  78. package/dist/runtime/telemetry/metrics.d.ts +2 -0
  79. package/dist/runtime/telemetry/metrics.js +4 -1
  80. package/dist/runtime/telemetry/sampler.js +33 -13
  81. package/dist/runtime/telemetry/spans.d.ts +1 -1
  82. package/dist/runtime/telemetry/spans.js +1 -1
  83. package/dist/runtime/topology/locks.d.ts +6 -3
  84. package/dist/runtime/topology/locks.js +27 -4
  85. package/dist/runtime/turn/execute.d.ts +2 -0
  86. package/dist/runtime/turn/execute.js +5 -2
  87. package/dist/runtime/turn/pipeline.d.ts +5 -12
  88. package/dist/runtime/turn/pipeline.js +11 -11
  89. package/dist/runtime/turn/relay.d.ts +13 -3
  90. package/dist/runtime/turn/relay.js +27 -14
  91. package/dist/runtime/turn/rows.js +4 -4
  92. package/dist/runtime/workflows/compatibility.d.ts +1 -1
  93. package/dist/runtime/workflows/compatibility.js +85 -31
  94. package/dist/runtime/workflows/engine.js +20 -17
  95. package/dist/serve/layer.js +3 -2
  96. package/dist/tables/owned.d.ts +1 -1
  97. package/dist/tables/owned.js +1 -1
  98. package/dist/testing/actor-test.d.ts +2 -24
  99. package/dist/testing/actor-test.js +1 -27
  100. package/dist/testing/index.d.ts +0 -12
  101. package/dist/testing/index.js +0 -6
  102. package/package.json +1 -1
  103. package/dist/runtime/database/neki/migrations.d.ts +0 -31
  104. package/dist/runtime/database/neki/migrations.js +0 -276
  105. package/dist/runtime/database/neki/session.d.ts +0 -23
  106. package/dist/runtime/database/neki/session.js +0 -31
  107. package/dist/testing/cluster.d.ts +0 -93
  108. package/dist/testing/cluster.js +0 -296
  109. package/dist/testing/conformance/access.d.ts +0 -5
  110. package/dist/testing/conformance/access.js +0 -531
  111. package/dist/testing/conformance/admission.d.ts +0 -11
  112. package/dist/testing/conformance/admission.js +0 -182
  113. package/dist/testing/conformance/adoption.d.ts +0 -2
  114. package/dist/testing/conformance/adoption.js +0 -1019
  115. package/dist/testing/conformance/assertions.d.ts +0 -202
  116. package/dist/testing/conformance/assertions.js +0 -849
  117. package/dist/testing/conformance/batches.d.ts +0 -41
  118. package/dist/testing/conformance/batches.js +0 -388
  119. package/dist/testing/conformance/blobs.d.ts +0 -14
  120. package/dist/testing/conformance/blobs.js +0 -405
  121. package/dist/testing/conformance/capacity.d.ts +0 -8
  122. package/dist/testing/conformance/capacity.js +0 -433
  123. package/dist/testing/conformance/client.d.ts +0 -5
  124. package/dist/testing/conformance/client.js +0 -719
  125. package/dist/testing/conformance/cold-serve.d.ts +0 -16
  126. package/dist/testing/conformance/cold-serve.js +0 -254
  127. package/dist/testing/conformance/connections/actors.d.ts +0 -1143
  128. package/dist/testing/conformance/connections/actors.js +0 -180
  129. package/dist/testing/conformance/connections/harness.d.ts +0 -1754
  130. package/dist/testing/conformance/connections/harness.js +0 -142
  131. package/dist/testing/conformance/connections/holders.d.ts +0 -4
  132. package/dist/testing/conformance/connections/holders.js +0 -165
  133. package/dist/testing/conformance/connections/lifecycle.d.ts +0 -4
  134. package/dist/testing/conformance/connections/lifecycle.js +0 -113
  135. package/dist/testing/conformance/connections/limits.d.ts +0 -4
  136. package/dist/testing/conformance/connections/limits.js +0 -127
  137. package/dist/testing/conformance/connections/reauthorization.d.ts +0 -4
  138. package/dist/testing/conformance/connections/reauthorization.js +0 -170
  139. package/dist/testing/conformance/connections/resync.d.ts +0 -4
  140. package/dist/testing/conformance/connections/resync.js +0 -360
  141. package/dist/testing/conformance/connections/takeover.d.ts +0 -4
  142. package/dist/testing/conformance/connections/takeover.js +0 -287
  143. package/dist/testing/conformance/connections.d.ts +0 -4
  144. package/dist/testing/conformance/connections.js +0 -15
  145. package/dist/testing/conformance/content-blobs.d.ts +0 -16
  146. package/dist/testing/conformance/content-blobs.js +0 -534
  147. package/dist/testing/conformance/counter.d.ts +0 -16
  148. package/dist/testing/conformance/counter.js +0 -813
  149. package/dist/testing/conformance/cron.d.ts +0 -5
  150. package/dist/testing/conformance/cron.js +0 -665
  151. package/dist/testing/conformance/drain.d.ts +0 -26
  152. package/dist/testing/conformance/drain.js +0 -540
  153. package/dist/testing/conformance/effect-control.d.ts +0 -38
  154. package/dist/testing/conformance/effect-control.js +0 -1059
  155. package/dist/testing/conformance/effects.d.ts +0 -23
  156. package/dist/testing/conformance/effects.js +0 -432
  157. package/dist/testing/conformance/events.d.ts +0 -28
  158. package/dist/testing/conformance/events.js +0 -335
  159. package/dist/testing/conformance/export.d.ts +0 -2
  160. package/dist/testing/conformance/export.js +0 -368
  161. package/dist/testing/conformance/fleet.d.ts +0 -444
  162. package/dist/testing/conformance/fleet.js +0 -1009
  163. package/dist/testing/conformance/groups.d.ts +0 -7
  164. package/dist/testing/conformance/groups.js +0 -353
  165. package/dist/testing/conformance/heap.d.ts +0 -3
  166. package/dist/testing/conformance/heap.js +0 -149
  167. package/dist/testing/conformance/http.d.ts +0 -2809
  168. package/dist/testing/conformance/http.js +0 -1234
  169. package/dist/testing/conformance/inspection-views.d.ts +0 -8
  170. package/dist/testing/conformance/inspection-views.js +0 -272
  171. package/dist/testing/conformance/inspector.d.ts +0 -7
  172. package/dist/testing/conformance/inspector.js +0 -650
  173. package/dist/testing/conformance/mint.d.ts +0 -58
  174. package/dist/testing/conformance/mint.js +0 -692
  175. package/dist/testing/conformance/multi-runner.d.ts +0 -8
  176. package/dist/testing/conformance/multi-runner.js +0 -403
  177. package/dist/testing/conformance/neki/plan.d.ts +0 -19
  178. package/dist/testing/conformance/neki/plan.js +0 -30
  179. package/dist/testing/conformance/observability.d.ts +0 -3
  180. package/dist/testing/conformance/observability.js +0 -343
  181. package/dist/testing/conformance/offline.d.ts +0 -4
  182. package/dist/testing/conformance/offline.js +0 -474
  183. package/dist/testing/conformance/operator-harness.d.ts +0 -57
  184. package/dist/testing/conformance/operator-harness.js +0 -78
  185. package/dist/testing/conformance/operator.d.ts +0 -3
  186. package/dist/testing/conformance/operator.js +0 -379
  187. package/dist/testing/conformance/outbox-cross-shard.d.ts +0 -14
  188. package/dist/testing/conformance/outbox-cross-shard.js +0 -160
  189. package/dist/testing/conformance/outbox.d.ts +0 -36
  190. package/dist/testing/conformance/outbox.js +0 -437
  191. package/dist/testing/conformance/payload-migrations.d.ts +0 -3
  192. package/dist/testing/conformance/payload-migrations.js +0 -612
  193. package/dist/testing/conformance/pipeline.d.ts +0 -3
  194. package/dist/testing/conformance/pipeline.js +0 -1040
  195. package/dist/testing/conformance/placement.d.ts +0 -22
  196. package/dist/testing/conformance/placement.js +0 -539
  197. package/dist/testing/conformance/platform.d.ts +0 -40
  198. package/dist/testing/conformance/platform.js +0 -101
  199. package/dist/testing/conformance/progress.d.ts +0 -39
  200. package/dist/testing/conformance/progress.js +0 -666
  201. package/dist/testing/conformance/properties.d.ts +0 -6
  202. package/dist/testing/conformance/properties.js +0 -295
  203. package/dist/testing/conformance/protocols.d.ts +0 -4
  204. package/dist/testing/conformance/protocols.js +0 -618
  205. package/dist/testing/conformance/provider/advertise.d.ts +0 -21
  206. package/dist/testing/conformance/provider/advertise.js +0 -44
  207. package/dist/testing/conformance/read-your-writes.d.ts +0 -20
  208. package/dist/testing/conformance/read-your-writes.js +0 -192
  209. package/dist/testing/conformance/reducers.d.ts +0 -8
  210. package/dist/testing/conformance/reducers.js +0 -258
  211. package/dist/testing/conformance/relay.d.ts +0 -42
  212. package/dist/testing/conformance/relay.js +0 -1115
  213. package/dist/testing/conformance/restore.d.ts +0 -43
  214. package/dist/testing/conformance/restore.js +0 -341
  215. package/dist/testing/conformance/retention.d.ts +0 -14
  216. package/dist/testing/conformance/retention.js +0 -463
  217. package/dist/testing/conformance/rls.d.ts +0 -24
  218. package/dist/testing/conformance/rls.js +0 -576
  219. package/dist/testing/conformance/simulation.d.ts +0 -31
  220. package/dist/testing/conformance/simulation.js +0 -167
  221. package/dist/testing/conformance/single-shard.d.ts +0 -2
  222. package/dist/testing/conformance/single-shard.js +0 -125
  223. package/dist/testing/conformance/singleton.d.ts +0 -3
  224. package/dist/testing/conformance/singleton.js +0 -293
  225. package/dist/testing/conformance/statements.d.ts +0 -60
  226. package/dist/testing/conformance/statements.js +0 -108
  227. package/dist/testing/conformance/streams.d.ts +0 -7
  228. package/dist/testing/conformance/streams.js +0 -280
  229. package/dist/testing/conformance/subscriptions/access.d.ts +0 -4
  230. package/dist/testing/conformance/subscriptions/access.js +0 -98
  231. package/dist/testing/conformance/subscriptions/actors.d.ts +0 -4910
  232. package/dist/testing/conformance/subscriptions/actors.js +0 -372
  233. package/dist/testing/conformance/subscriptions/cluster.d.ts +0 -4
  234. package/dist/testing/conformance/subscriptions/cluster.js +0 -89
  235. package/dist/testing/conformance/subscriptions/delivery.d.ts +0 -4
  236. package/dist/testing/conformance/subscriptions/delivery.js +0 -253
  237. package/dist/testing/conformance/subscriptions/deploys.d.ts +0 -4
  238. package/dist/testing/conformance/subscriptions/deploys.js +0 -162
  239. package/dist/testing/conformance/subscriptions/epochs.d.ts +0 -4
  240. package/dist/testing/conformance/subscriptions/epochs.js +0 -169
  241. package/dist/testing/conformance/subscriptions/failures.d.ts +0 -4
  242. package/dist/testing/conformance/subscriptions/failures.js +0 -108
  243. package/dist/testing/conformance/subscriptions/feeds.d.ts +0 -4
  244. package/dist/testing/conformance/subscriptions/feeds.js +0 -218
  245. package/dist/testing/conformance/subscriptions/harness.d.ts +0 -124
  246. package/dist/testing/conformance/subscriptions/harness.js +0 -143
  247. package/dist/testing/conformance/subscriptions/operator.d.ts +0 -4
  248. package/dist/testing/conformance/subscriptions/operator.js +0 -128
  249. package/dist/testing/conformance/subscriptions/retention.d.ts +0 -4
  250. package/dist/testing/conformance/subscriptions/retention.js +0 -288
  251. package/dist/testing/conformance/subscriptions/routing.d.ts +0 -4
  252. package/dist/testing/conformance/subscriptions/routing.js +0 -179
  253. package/dist/testing/conformance/subscriptions.d.ts +0 -4
  254. package/dist/testing/conformance/subscriptions.js +0 -19
  255. package/dist/testing/conformance/tables.d.ts +0 -125
  256. package/dist/testing/conformance/tables.js +0 -664
  257. package/dist/testing/conformance/transports/actors.d.ts +0 -1907
  258. package/dist/testing/conformance/transports/actors.js +0 -163
  259. package/dist/testing/conformance/transports/client.d.ts +0 -3
  260. package/dist/testing/conformance/transports/client.js +0 -237
  261. package/dist/testing/conformance/transports/feeds.d.ts +0 -3
  262. package/dist/testing/conformance/transports/feeds.js +0 -232
  263. package/dist/testing/conformance/transports/harness.d.ts +0 -93
  264. package/dist/testing/conformance/transports/harness.js +0 -84
  265. package/dist/testing/conformance/transports/streams.d.ts +0 -3
  266. package/dist/testing/conformance/transports/streams.js +0 -96
  267. package/dist/testing/conformance/transports/websocket.d.ts +0 -3
  268. package/dist/testing/conformance/transports/websocket.js +0 -384
  269. package/dist/testing/conformance/transports/wire.d.ts +0 -141
  270. package/dist/testing/conformance/transports/wire.js +0 -204
  271. package/dist/testing/conformance/transports.d.ts +0 -3
  272. package/dist/testing/conformance/transports.js +0 -11
  273. package/dist/testing/conformance/watch.d.ts +0 -59
  274. package/dist/testing/conformance/watch.js +0 -879
  275. package/dist/testing/conformance/workflow-engine.d.ts +0 -97
  276. package/dist/testing/conformance/workflow-engine.js +0 -253
  277. package/dist/testing/conformance/workflow-versions.d.ts +0 -3
  278. package/dist/testing/conformance/workflow-versions.js +0 -545
  279. package/dist/testing/conformance/workflows/access.d.ts +0 -4
  280. package/dist/testing/conformance/workflows/access.js +0 -147
  281. package/dist/testing/conformance/workflows/actors.d.ts +0 -2317
  282. package/dist/testing/conformance/workflows/actors.js +0 -305
  283. package/dist/testing/conformance/workflows/basics.d.ts +0 -4
  284. package/dist/testing/conformance/workflows/basics.js +0 -144
  285. package/dist/testing/conformance/workflows/engine.d.ts +0 -4
  286. package/dist/testing/conformance/workflows/engine.js +0 -80
  287. package/dist/testing/conformance/workflows/guards.d.ts +0 -4
  288. package/dist/testing/conformance/workflows/guards.js +0 -172
  289. package/dist/testing/conformance/workflows/harness.d.ts +0 -14
  290. package/dist/testing/conformance/workflows/harness.js +0 -44
  291. package/dist/testing/conformance/workflows/recovery.d.ts +0 -4
  292. package/dist/testing/conformance/workflows/recovery.js +0 -181
  293. package/dist/testing/conformance/workflows/starts.d.ts +0 -4
  294. package/dist/testing/conformance/workflows/starts.js +0 -57
  295. package/dist/testing/conformance/workflows/suspension.d.ts +0 -4
  296. package/dist/testing/conformance/workflows/suspension.js +0 -106
  297. package/dist/testing/conformance/workflows/waits.d.ts +0 -4
  298. package/dist/testing/conformance/workflows/waits.js +0 -284
  299. package/dist/testing/conformance/workflows.d.ts +0 -4
  300. package/dist/testing/conformance/workflows.js +0 -19
  301. package/dist/testing/conformance.d.ts +0 -330
  302. package/dist/testing/conformance.js +0 -364
  303. package/dist/testing/foundation.d.ts +0 -41
  304. package/dist/testing/foundation.js +0 -517
  305. package/dist/testing/simulate-cluster.d.ts +0 -112
  306. package/dist/testing/simulate-cluster.js +0 -257
  307. package/dist/testing/simulate.d.ts +0 -90
  308. package/dist/testing/simulate.js +0 -127
@@ -1,5 +0,0 @@
1
- import type { ConformanceCase } from "../conformance.ts";
2
- /** Cron cases: tick rows, firing as System, catch-up and skip rules, time zones, and stale ticks from earlier deployments. */
3
- export declare const cronConformance: ReadonlyArray<ConformanceCase>;
4
- /** Multi-runner cases: real Postgres only, each on a fresh database and cluster. */
5
- export declare const cronClusterConformance: ReadonlyArray<ConformanceCase>;
@@ -1,665 +0,0 @@
1
- import { Cause, DateTime, Duration, Effect, Exit, Fiber, Layer, Schedule, Schema } from "effect";
2
- import { SqlClient } from "effect/sql";
3
- import { Actor, Intent, NotCreated, System } from "../../index.js";
4
- import { CallerJson } from "../../runtime/turn/outbox.js";
5
- import { claimIntents } from "../../runtime/turn/relay.js";
6
- import { ActorTest } from "../actor-test.js";
7
- import { ActorCluster } from "../cluster.js";
8
- import { CLAIM_LEASE } from "./outbox.js";
9
- const fired = [];
10
- const firedFor = (id) => fired.filter((run) => run.id === id);
11
- class Refused extends Schema.TaggedError()("Refused", {}) {
12
- }
13
- const Open = Actor.command("Open");
14
- const Refuse = Actor.command("Refuse", { error: Refused });
15
- const Stop = Actor.command("Stop");
16
- const Hijack = Actor.command("Hijack", { payload: Schema.Literals(["key", "cancel"]) });
17
- const Beat = Actor.command("Beat");
18
- const Yearly = Actor.command("Yearly");
19
- const EVERY_MINUTE = "$cron:UTC * * * * *";
20
- const YEARLY = "$cron:UTC 0 0 1 1 *";
21
- const SKIP = "10 minutes";
22
- const Heartbeat = Actor.make("CronHeartbeat", {
23
- key: Schema.String,
24
- state: Actor.state({
25
- beats: Schema.Int.pipe(Schema.withDecodingDefault(Effect.succeed(0))),
26
- ignored: Schema.Int.pipe(Schema.withDecodingDefault(Effect.succeed(0))),
27
- stopped: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
28
- }),
29
- api: { Open, Refuse, Stop, Hijack },
30
- internal: { Beat, Yearly },
31
- policy: {
32
- maxScheduleLag: SKIP,
33
- },
34
- schedules: { "* * * * *": Beat, "0 0 1 1 *": Yearly },
35
- });
36
- const Peek = Actor.command("Peek");
37
- const Gated = Actor.make("CronGated", {
38
- key: Schema.String,
39
- api: { Open, Peek },
40
- internal: { Beat },
41
- createdBy: Open,
42
- schedules: { "* * * * *": Beat },
43
- });
44
- const Secondly = Actor.make("CronSecondly", {
45
- key: Schema.String,
46
- api: { Open },
47
- internal: { Beat },
48
- schedules: { "* * * * * *": Beat },
49
- });
50
- const Pulse = Actor.command("Pulse");
51
- const Beacon = Actor.make("CronBeacon", {
52
- key: Actor.singleton,
53
- state: Actor.state({ pulses: Schema.Int.pipe(Schema.withDecodingDefault(Effect.succeed(0))) }),
54
- api: {},
55
- internal: { Pulse },
56
- schedules: { "* * * * *": Pulse },
57
- });
58
- const Gap = Actor.command("Gap");
59
- const Fold = Actor.command("Fold");
60
- const FoldUtc = Actor.command("FoldUtc");
61
- const Interval = Actor.command("Interval");
62
- const GAP = "$cron:America/New_York 30 2 * * *";
63
- const FOLD = "$cron:America/New_York 30 1 * * *";
64
- const FOLD_UTC = "$cron:UTC 30 1 * * *";
65
- const INTERVAL = "$cron:@every 300000ms";
66
- /** Ticks in America/New_York across its real daylight-saving transitions, beside UTC and an interval. */
67
- const Zoned = Actor.make("CronZoned", {
68
- key: Schema.String,
69
- api: { Open },
70
- internal: { Gap, Fold, FoldUtc, Interval },
71
- policy: {
72
- maxScheduleLag: "1 hour",
73
- },
74
- schedules: {
75
- "CRON_TZ=America/New_York 30 2 * * *": Gap,
76
- "CRON_TZ=America/New_York 30 1 * * *": Fold,
77
- "30 1 * * *": FoldUtc,
78
- "@every 5 minutes": Interval,
79
- },
80
- });
81
- const record = (ref, commandId, caller) => Effect.sync(() => {
82
- fired.push({
83
- actor: ref.actor,
84
- id: ref.id,
85
- commandId,
86
- source: Schema.is(System)(caller) ? caller.source : undefined,
87
- });
88
- });
89
- const HeartbeatLive = Heartbeat.toLayer(Effect.succeed({
90
- Open: () => Effect.void,
91
- Refuse: () => Effect.fail(Refused.make()),
92
- Stop: Effect.fnUntraced(function* () {
93
- yield* (yield* Heartbeat.Turn).state.set({ stopped: true });
94
- }),
95
- Hijack: Effect.fnUntraced(function* (how) {
96
- const turn = yield* Heartbeat.Turn;
97
- if (how === "cancel")
98
- return yield* Intent.cancel(EVERY_MINUTE);
99
- yield* (yield* Heartbeat.intents(turn.ref.id)).Open().pipe(Intent.key(EVERY_MINUTE));
100
- }),
101
- Beat: Effect.fnUntraced(function* () {
102
- const turn = yield* Heartbeat.Turn;
103
- yield* record(turn.ref, turn.commandId, turn.caller);
104
- if (turn.state.stopped)
105
- yield* turn.state.set({ ignored: turn.state.ignored + 1 });
106
- else
107
- yield* turn.state.set({ beats: turn.state.beats + 1 });
108
- }),
109
- Yearly: () => Effect.void,
110
- }));
111
- const GatedLive = Gated.toLayer(Effect.succeed({
112
- Open: () => Effect.void,
113
- Peek: () => Effect.void,
114
- Beat: Effect.fnUntraced(function* () {
115
- const turn = yield* Gated.Turn;
116
- yield* record(turn.ref, turn.commandId, turn.caller);
117
- }),
118
- }));
119
- const SecondlyLive = Secondly.toLayer(Effect.succeed({
120
- Open: () => Effect.void,
121
- Beat: Effect.fnUntraced(function* () {
122
- const turn = yield* Secondly.Turn;
123
- yield* record(turn.ref, turn.commandId, turn.caller);
124
- }),
125
- }));
126
- const BeaconLive = Beacon.toLayer(Effect.succeed({
127
- Pulse: Effect.fnUntraced(function* () {
128
- const turn = yield* Beacon.Turn;
129
- yield* record(turn.ref, turn.commandId, turn.caller);
130
- yield* turn.state.set({ pulses: turn.state.pulses + 1 });
131
- }),
132
- }));
133
- const recordZoned = Effect.gen(function* () {
134
- const turn = yield* Zoned.Turn;
135
- yield* record(turn.ref, turn.commandId, turn.caller);
136
- });
137
- const ZonedLive = Zoned.toLayer(Effect.succeed({
138
- Open: () => Effect.void,
139
- Gap: () => recordZoned,
140
- Fold: () => recordZoned,
141
- FoldUtc: () => recordZoned,
142
- Interval: () => recordZoned,
143
- }));
144
- const CronLive = Layer.mergeAll(HeartbeatLive, GatedLive, SecondlyLive, ZonedLive);
145
- /**
146
- * One runtime of the cron actors for one case. The singleton's minutely tick
147
- * joins only when asked, so it cannot take another case's fault injection, and
148
- * the case's ticks are removed after it, since PGlite shares one database.
149
- */
150
- const RETRY_WINDOW_MS = 60_000;
151
- const withRuntime = (environment, body, options = {}) => environment.run(Effect.gen(function* () {
152
- const database = yield* environment.freshDatabase;
153
- const context = yield* Layer.build(Layer.fresh((options.singleton === true ? Layer.merge(CronLive, BeaconLive) : CronLive).pipe(Layer.provideMerge(ActorTest.layer({
154
- database,
155
- retryWindowMs: RETRY_WINDOW_MS,
156
- })), Layer.orDie)));
157
- return yield* body.pipe(Effect.ensuring(Effect.gen(function* () {
158
- const sql = yield* SqlClient.SqlClient;
159
- const test = yield* ActorTest;
160
- yield* sql `DELETE FROM actor_outbox WHERE timer_key LIKE '$cron:%'
161
- AND (tenant_id = ${test.tenant} OR actor_type = 'CronBeacon')`;
162
- }).pipe(Effect.orDie)), Effect.provideContext(context));
163
- }));
164
- const ticksOf = (ref) => Effect.gen(function* () {
165
- const sql = yield* SqlClient.SqlClient;
166
- return yield* sql `SELECT timer_key, intent_id, command, caller, attempts,
167
- due_at_ms::text AS due, scheduled_at_ms::text AS scheduled
168
- FROM actor_outbox WHERE tenant_id = ${ref.tenant} AND actor_type = ${ref.actor}
169
- AND actor_id = ${ref.id} AND timer_key LIKE '$cron:%'
170
- ORDER BY timer_key COLLATE "C" DESC`;
171
- }).pipe(Effect.orDie);
172
- const nowMs = ActorTest.use((test) => test.now).pipe(Effect.map((now) => now.epochMilliseconds));
173
- const MINUTE = 60_000;
174
- /** Holds when `row` is the first whole minute after `now`. */
175
- const nextMinuteAfter = (row, now) => row !== undefined &&
176
- Number(row.scheduled) === Number(row.due) &&
177
- Number(row.scheduled) === (Math.floor(now / MINUTE) + 1) * MINUTE;
178
- const HOUR = 60 * MINUTE;
179
- const DAY = 24 * HOUR;
180
- /** Midnight UTC of the `nth` Sunday of `month` (0-based) in `year`. */
181
- const nthSunday = (year, month, nth) => {
182
- const weekday = DateTime.getPartUtc(DateTime.makeUnsafe(Date.UTC(year, month, 1)), "weekDay");
183
- return Date.UTC(year, month, 1 + ((7 - weekday) % 7) + 7 * (nth - 1));
184
- };
185
- const springForward = (year) => nthSunday(year, 2, 2) + 7 * HOUR;
186
- const fallBack = (year) => nthSunday(year, 10, 1) + 6 * HOUR;
187
- /** The first transition at least two days after `now`, so a case can open its actor before it. */
188
- const upcoming = (transition, now) => {
189
- let year = DateTime.getPartUtc(DateTime.makeUnsafe(now), "year");
190
- while (transition(year) < now + 2 * DAY)
191
- year++;
192
- return transition(year);
193
- };
194
- const tickOf = (rows, key) => {
195
- const row = rows.find((candidate) => candidate.timer_key === key);
196
- return row === undefined ? undefined : { id: row.intent_id, scheduled: Number(row.scheduled) };
197
- };
198
- const advanceTo = (at) => Effect.gen(function* () {
199
- const test = yield* ActorTest;
200
- yield* test.advance(Math.max(0, at - (yield* nowMs)));
201
- });
202
- /**
203
- * Moves the framework clock to one second past the next minute boundary, so a
204
- * minutely tick written now is due about 59 seconds ahead. A case that later
205
- * advances exactly to that tick then advances by a positive amount however
206
- * slowly the database clock moves between its reads.
207
- */
208
- const startOfMinute = Effect.gen(function* () {
209
- const now = yield* nowMs;
210
- yield* ActorTest.use((test) => test.advance((Math.floor(now / MINUTE) + 1) * MINUTE + 1000 - now));
211
- });
212
- const expiresAt = (row) => Number(row.intent_id.split(".")[2]);
213
- const receipts = (ref, command) => ActorTest.use((test) => test.receiptsFor(ref, command));
214
- const stateOf = (ref) => ActorTest.use((test) => test.inspect(ref)).pipe(Effect.map(({ state }) => state));
215
- /** Cron cases: tick rows, firing as System, catch-up and skip rules, time zones, and stale ticks from earlier deployments. */
216
- export const cronConformance = [
217
- {
218
- name: "writes one tick per cron entry on the first committed turn, including a declared failure",
219
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
220
- const heartbeat = yield* Heartbeat.get("first-turn");
221
- const before = yield* nowMs;
222
- expect(Exit.isFailure(yield* heartbeat.Refuse().pipe(Effect.exit))).toBe(true);
223
- const rows = yield* ticksOf(heartbeat.ref);
224
- expect(rows.map((row) => [row.timer_key, row.command, row.attempts])).toEqual([
225
- [YEARLY, "Yearly", 0],
226
- [EVERY_MINUTE, "Beat", 0],
227
- ]);
228
- expect(nextMinuteAfter(rows[1], before) || nextMinuteAfter(rows[1], before + MINUTE)).toBe(true);
229
- const caller = yield* Schema.decodeEffect(CallerJson)(rows[1].caller);
230
- expect(Schema.is(System)(caller) ? [caller.source, caller.onBehalfOf] : caller).toEqual([
231
- "cron",
232
- undefined,
233
- ]);
234
- yield* heartbeat.Open();
235
- yield* ActorTest.use((test) => test.invalidate(heartbeat.ref));
236
- yield* heartbeat.Open();
237
- expect(yield* ticksOf(heartbeat.ref)).toEqual(rows);
238
- })),
239
- },
240
- {
241
- name: "writes no tick for a turn rejected NotCreated",
242
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
243
- const gated = yield* Gated.get("gated");
244
- expect(yield* gated.Peek().pipe(Effect.flip)).toMatchObject({
245
- reason: NotCreated.make({}),
246
- });
247
- expect(yield* ticksOf(gated.ref)).toEqual([]);
248
- yield* gated.Open();
249
- expect((yield* ticksOf(gated.ref)).map((row) => row.timer_key)).toEqual([EVERY_MINUTE]);
250
- })),
251
- },
252
- {
253
- name: "fires a due tick once as System cron and rewrites its row to the next scheduled time",
254
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
255
- const test = yield* ActorTest;
256
- const heartbeat = yield* Heartbeat.get("fires");
257
- yield* heartbeat.Open();
258
- const [, first] = yield* ticksOf(heartbeat.ref);
259
- yield* test.advance("1 minute");
260
- const runs = firedFor("fires");
261
- expect(runs).toEqual([
262
- { actor: "CronHeartbeat", id: "fires", commandId: first.intent_id, source: "cron" },
263
- ]);
264
- expect(yield* receipts(heartbeat.ref, "Beat")).toBe(1);
265
- const [, next] = yield* ticksOf(heartbeat.ref);
266
- expect(next.intent_id).not.toBe(first.intent_id);
267
- expect(next.attempts).toBe(0);
268
- expect(nextMinuteAfter(next, yield* nowMs)).toBe(true);
269
- expect(expiresAt(first)).toBe(Number(first.scheduled) + RETRY_WINDOW_MS);
270
- expect(expiresAt(next)).toBe(Number(next.scheduled) + RETRY_WINDOW_MS);
271
- yield* test.advance("1 minute");
272
- expect(yield* stateOf(heartbeat.ref)).toMatchObject({ beats: 2 });
273
- expect(new Set(firedFor("fires").map((run) => run.commandId)).size).toBe(2);
274
- expect((yield* ticksOf(heartbeat.ref)).length).toBe(2);
275
- })),
276
- },
277
- {
278
- name: "schedules a first tick from the time the first turn writes it, not from its admission",
279
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
280
- const test = yield* ActorTest;
281
- const secondly = yield* Secondly.get("slow-first-turn");
282
- const admitted = yield* nowMs;
283
- const handling = yield* test.pauseNext("beforeHandler");
284
- const opening = yield* secondly.Open().pipe(Effect.forkChild);
285
- yield* handling.reached;
286
- yield* Effect.sleep("1200 millis");
287
- const claimed = yield* test.pauseNext("afterClaim");
288
- yield* handling.release;
289
- yield* Fiber.join(opening);
290
- const [tick] = yield* ticksOf(secondly.ref);
291
- yield* claimed.release;
292
- expect(Number(tick.scheduled) > admitted + 1200).toBe(true);
293
- })),
294
- },
295
- {
296
- name: "retargets a pending tick to the entry's current command and fires it once",
297
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
298
- const sql = yield* SqlClient.SqlClient;
299
- const test = yield* ActorTest;
300
- const heartbeat = yield* Heartbeat.get("retarget");
301
- yield* heartbeat.Open();
302
- const [, first] = yield* ticksOf(heartbeat.ref);
303
- yield* sql `UPDATE actor_outbox SET command = 'Retired'
304
- WHERE intent_id = ${first.intent_id}`;
305
- yield* test.advance("1 minute");
306
- expect(firedFor("retarget").map((run) => run.commandId)).toEqual([first.intent_id]);
307
- expect(yield* receipts(heartbeat.ref, "Beat")).toBe(1);
308
- const [, next] = yield* ticksOf(heartbeat.ref);
309
- expect(next.command).toBe("Beat");
310
- expect(next.intent_id).not.toBe(first.intent_id);
311
- })),
312
- },
313
- {
314
- name: "advances a retargeted tick that already fired under its old command without firing it again",
315
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
316
- const sql = yield* SqlClient.SqlClient;
317
- const test = yield* ActorTest;
318
- const heartbeat = yield* Heartbeat.get("retarget-fired");
319
- yield* startOfMinute;
320
- yield* heartbeat.Open();
321
- const [, first] = yield* ticksOf(heartbeat.ref);
322
- yield* test.crashNext("beforeOutboxDelete");
323
- yield* test.advance(Number(first.scheduled) - (yield* nowMs));
324
- expect(yield* receipts(heartbeat.ref, "Beat")).toBe(1);
325
- yield* sql `UPDATE actor_outbox SET command = 'Retired'
326
- WHERE intent_id = ${first.intent_id}`;
327
- yield* test.advance(CLAIM_LEASE);
328
- expect(firedFor("retarget-fired").map((run) => run.commandId)).toEqual([first.intent_id]);
329
- expect(yield* receipts(heartbeat.ref, "Beat")).toBe(1);
330
- const [, next] = yield* ticksOf(heartbeat.ref);
331
- expect(next).toMatchObject({ command: "Beat", attempts: 0 });
332
- expect(next.intent_id).not.toBe(first.intent_id);
333
- expect(nextMinuteAfter(next, yield* nowMs)).toBe(true);
334
- })),
335
- },
336
- {
337
- name: "fires once after downtime inside the skip window and skips a tick older than it",
338
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
339
- const test = yield* ActorTest;
340
- const heartbeat = yield* Heartbeat.get("downtime");
341
- yield* heartbeat.Open();
342
- yield* test.advance("5 minutes");
343
- expect(yield* stateOf(heartbeat.ref)).toMatchObject({ beats: 1 });
344
- expect(nextMinuteAfter((yield* ticksOf(heartbeat.ref))[1], yield* nowMs)).toBe(true);
345
- yield* test.advance("15 minutes");
346
- expect(yield* stateOf(heartbeat.ref)).toMatchObject({ beats: 1 });
347
- expect(yield* receipts(heartbeat.ref, "Beat")).toBe(1);
348
- expect(nextMinuteAfter((yield* ticksOf(heartbeat.ref))[1], yield* nowMs)).toBe(true);
349
- yield* test.advance("1 minute");
350
- expect(yield* stateOf(heartbeat.ref)).toMatchObject({ beats: 2 });
351
- })),
352
- },
353
- {
354
- name: "rewrites a tick once after a crash between its receipt and the rewrite",
355
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
356
- const test = yield* ActorTest;
357
- const heartbeat = yield* Heartbeat.get("crash-rewrite");
358
- yield* startOfMinute;
359
- yield* heartbeat.Open();
360
- const [, first] = yield* ticksOf(heartbeat.ref);
361
- yield* test.crashNext("beforeOutboxDelete");
362
- yield* test.advance(Number(first.scheduled) - (yield* nowMs));
363
- expect(yield* receipts(heartbeat.ref, "Beat")).toBe(1);
364
- const [, claimed] = yield* ticksOf(heartbeat.ref);
365
- expect(claimed).toMatchObject({ intent_id: first.intent_id, attempts: 1 });
366
- yield* test.advance(CLAIM_LEASE);
367
- expect(yield* receipts(heartbeat.ref, "Beat")).toBe(1);
368
- expect(yield* stateOf(heartbeat.ref)).toMatchObject({ beats: 1 });
369
- const rows = yield* ticksOf(heartbeat.ref);
370
- expect(rows.length).toBe(2);
371
- expect(rows[1].intent_id).not.toBe(first.intent_id);
372
- expect(nextMinuteAfter(rows[1], yield* nowMs)).toBe(true);
373
- })),
374
- },
375
- {
376
- name: "fires a claimed tick once even after the actor stopped, and its handler rechecks state",
377
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
378
- const test = yield* ActorTest;
379
- const heartbeat = yield* Heartbeat.get("claimed-then-stopped");
380
- yield* heartbeat.Open();
381
- const pause = yield* test.pauseNext("afterClaim");
382
- const advancing = yield* test.advance("1 minute").pipe(Effect.forkChild);
383
- yield* pause.reached;
384
- yield* heartbeat.Stop();
385
- yield* pause.release;
386
- yield* Fiber.join(advancing);
387
- expect(yield* stateOf(heartbeat.ref)).toEqual({ stopped: true, ignored: 1 });
388
- expect(firedFor("claimed-then-stopped").length).toBe(1);
389
- })),
390
- },
391
- {
392
- name: "rejects application intents that stage or cancel a $cron: key",
393
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
394
- const heartbeat = yield* Heartbeat.get("reserved");
395
- yield* heartbeat.Open();
396
- const rows = yield* ticksOf(heartbeat.ref);
397
- for (const how of ["key", "cancel"]) {
398
- const exit = yield* heartbeat.Hijack(how).pipe(Effect.exit);
399
- expect(Exit.isFailure(exit) && Cause.pretty(exit.cause)).toContain("Intent.key values starting with $ are reserved");
400
- }
401
- expect(yield* ticksOf(heartbeat.ref)).toEqual(rows);
402
- })),
403
- },
404
- {
405
- name: "restores a missing entry's tick on the actor's next activation",
406
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
407
- const test = yield* ActorTest;
408
- const sql = yield* SqlClient.SqlClient;
409
- const heartbeat = yield* Heartbeat.get("restored");
410
- yield* heartbeat.Open();
411
- const { ref } = heartbeat;
412
- yield* sql `DELETE FROM actor_outbox WHERE tenant_id = ${ref.tenant}
413
- AND actor_type = ${ref.actor} AND actor_id = ${ref.id} AND timer_key = ${YEARLY}`.pipe(Effect.orDie);
414
- yield* heartbeat.Open();
415
- expect((yield* ticksOf(ref)).map((row) => row.timer_key)).toEqual([EVERY_MINUTE]);
416
- yield* test.invalidate(ref);
417
- yield* heartbeat.Open();
418
- expect((yield* ticksOf(ref)).map((row) => row.timer_key)).toEqual([YEARLY, EVERY_MINUTE]);
419
- })),
420
- },
421
- {
422
- name: "releases a tick whose entry left the policy and deletes it past the skip window",
423
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
424
- const test = yield* ActorTest;
425
- const sql = yield* SqlClient.SqlClient;
426
- yield* startOfMinute;
427
- const heartbeat = yield* Heartbeat.get("removed");
428
- yield* heartbeat.Open();
429
- const { ref } = heartbeat;
430
- const removed = "$cron:UTC 5 4 * * *";
431
- yield* sql `INSERT INTO actor_outbox (routing_key, intent_id, kind, bucket, due_at_ms,
432
- scheduled_at_ms, tenant_id, actor_type, actor_id, timer_key, target_type, target_id,
433
- command, payload, caller)
434
- SELECT routing_key, intent_id || '-removed', kind, bucket, ${yield* nowMs},
435
- ${yield* nowMs}, tenant_id, actor_type, actor_id, ${removed}, target_type,
436
- target_id, command, payload, caller
437
- FROM actor_outbox WHERE tenant_id = ${ref.tenant} AND actor_type = ${ref.actor}
438
- AND actor_id = ${ref.id} AND timer_key = ${EVERY_MINUTE}`.pipe(Effect.orDie);
439
- yield* test.advance(0);
440
- const released = (yield* ticksOf(ref)).find((row) => row.timer_key === removed);
441
- expect(released).toMatchObject({ attempts: 1 });
442
- expect(yield* receipts(ref, "Beat")).toBe(0);
443
- yield* test.advance("9 minutes");
444
- expect((yield* ticksOf(ref)).map((row) => row.timer_key)).toEqual([
445
- removed,
446
- YEARLY,
447
- EVERY_MINUTE,
448
- ]);
449
- expect(yield* receipts(ref, "Beat")).toBe(1);
450
- yield* test.advance("2 minutes");
451
- expect((yield* ticksOf(ref)).map((row) => row.timer_key)).toEqual([YEARLY, EVERY_MINUTE]);
452
- expect(yield* receipts(ref, "Beat")).toBe(2);
453
- })),
454
- },
455
- {
456
- name: "writes a singleton's ticks in the default tenant at startup",
457
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
458
- const test = yield* ActorTest;
459
- const ref = (yield* Beacon.get()).ref;
460
- expect(ref.tenant).toBe(test.tenant);
461
- const [row] = yield* ticksOf(ref);
462
- expect(row).toMatchObject({ timer_key: EVERY_MINUTE, command: "Pulse" });
463
- expect(yield* test.inspect(ref)).toMatchObject({ receipts: 0 });
464
- yield* test.advance("1 minute");
465
- expect(yield* receipts(ref, "Pulse")).toBe(1);
466
- expect(yield* stateOf(ref)).toMatchObject({ pulses: 1 });
467
- }), { singleton: true }),
468
- },
469
- {
470
- name: "claims $cron: ticks only for actor types registered on the claiming runner",
471
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
472
- const sql = yield* SqlClient.SqlClient;
473
- const heartbeat = yield* Heartbeat.get("claim-filter");
474
- yield* heartbeat.Open();
475
- const later = (yield* nowMs) + 400 * 24 * 60 * MINUTE;
476
- const claim = (cronActors) => claimIntents({
477
- sql,
478
- now: later,
479
- limit: 1000,
480
- leaseMs: 1000,
481
- maxBackoffMs: 1000,
482
- cronActors,
483
- }).pipe(Effect.orDie, Effect.map((rows) => rows.filter((row) => row.kind === "intent" && row.target_id === "claim-filter")));
484
- expect(yield* claim(["CronGated"])).toEqual([]);
485
- const claimed = yield* claim(["CronHeartbeat"]);
486
- expect(claimed.map((row) => row.command).toSorted()).toEqual(["Beat", "Yearly"]);
487
- })),
488
- },
489
- {
490
- name: "fires a wall-clock time skipped by a spring-forward gap once, at the first instant after the gap",
491
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
492
- const zoned = yield* Zoned.get("spring-gap");
493
- yield* zoned.Open();
494
- const { ref } = zoned;
495
- const gapEnd = upcoming(springForward, yield* nowMs);
496
- yield* advanceTo(gapEnd - MINUTE);
497
- const pending = tickOf(yield* ticksOf(ref), GAP);
498
- expect(pending?.scheduled).toBe(gapEnd);
499
- expect(yield* receipts(ref, "Gap")).toBe(0);
500
- yield* advanceTo(gapEnd);
501
- expect(firedFor("spring-gap").filter((run) => run.commandId === pending.id)).toEqual([
502
- { actor: "CronZoned", id: "spring-gap", commandId: pending.id, source: "cron" },
503
- ]);
504
- expect(tickOf(yield* ticksOf(ref), GAP)?.scheduled).toBe(gapEnd - 30 * MINUTE + DAY);
505
- yield* advanceTo(gapEnd + HOUR + MINUTE);
506
- expect(yield* receipts(ref, "Gap")).toBe(1);
507
- })),
508
- },
509
- {
510
- name: "fires a wall-clock time repeated by a fall-back transition once, at its first occurrence",
511
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
512
- const zoned = yield* Zoned.get("fall-fold");
513
- yield* zoned.Open();
514
- const { ref } = zoned;
515
- const transition = upcoming(fallBack, yield* nowMs);
516
- const firstOccurrence = transition - 30 * MINUTE;
517
- const secondOccurrence = transition + 30 * MINUTE;
518
- yield* advanceTo(firstOccurrence - MINUTE);
519
- const pending = tickOf(yield* ticksOf(ref), FOLD);
520
- expect(pending?.scheduled).toBe(firstOccurrence);
521
- yield* advanceTo(transition + 10 * MINUTE);
522
- expect(yield* receipts(ref, "Fold")).toBe(1);
523
- expect(firedFor("fall-fold").filter((run) => run.commandId === pending.id).length).toBe(1);
524
- expect(tickOf(yield* ticksOf(ref), FOLD)?.scheduled).toBe(secondOccurrence + DAY);
525
- yield* advanceTo(secondOccurrence + MINUTE);
526
- expect(yield* receipts(ref, "Fold")).toBe(1);
527
- })),
528
- },
529
- {
530
- name: "keeps one expression in two zones as two entries that fire at their own times",
531
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
532
- const zoned = yield* Zoned.get("two-zones");
533
- yield* zoned.Open();
534
- const { ref } = zoned;
535
- const transition = upcoming(fallBack, yield* nowMs);
536
- const nextUtc = transition - 6 * HOUR + DAY + 90 * MINUTE;
537
- yield* advanceTo(transition - 31 * MINUTE);
538
- const rows = yield* ticksOf(ref);
539
- const newYork = tickOf(rows, FOLD);
540
- const utc = tickOf(rows, FOLD_UTC);
541
- expect(newYork?.scheduled).toBe(transition - 30 * MINUTE);
542
- expect(utc?.scheduled).toBe(nextUtc);
543
- expect(newYork?.id).not.toBe(utc?.id);
544
- yield* advanceTo(transition - 30 * MINUTE);
545
- expect([yield* receipts(ref, "Fold"), yield* receipts(ref, "FoldUtc")]).toEqual([1, 0]);
546
- yield* advanceTo(nextUtc);
547
- expect([yield* receipts(ref, "Fold"), yield* receipts(ref, "FoldUtc")]).toEqual([1, 1]);
548
- expect(firedFor("two-zones")
549
- .map((run) => run.commandId)
550
- .filter((id) => id === newYork.id || id === utc.id)
551
- .toSorted()).toEqual([newYork.id, utc.id].toSorted());
552
- })),
553
- },
554
- {
555
- name: "fires a fixed interval at multiples of its length and once after downtime",
556
- run: ({ expect, environment }) => withRuntime(environment, Effect.gen(function* () {
557
- const zoned = yield* Zoned.get("interval");
558
- const opened = yield* nowMs;
559
- yield* zoned.Open();
560
- const { ref } = zoned;
561
- const interval = 5 * MINUTE;
562
- const first = tickOf(yield* ticksOf(ref), INTERVAL);
563
- expect(first.scheduled % interval).toBe(0);
564
- expect(first.scheduled > opened && first.scheduled <= opened + interval + 30_000).toBe(true);
565
- yield* advanceTo(first.scheduled);
566
- expect(yield* receipts(ref, "Interval")).toBe(1);
567
- const second = tickOf(yield* ticksOf(ref), INTERVAL);
568
- expect(second.scheduled).toBe(first.scheduled + interval);
569
- yield* advanceTo(second.scheduled + 4 * interval + interval / 2);
570
- expect(yield* receipts(ref, "Interval")).toBe(2);
571
- expect(tickOf(yield* ticksOf(ref), INTERVAL)?.scheduled).toBe(second.scheduled + 5 * interval);
572
- })),
573
- },
574
- ];
575
- const EXPIRATION_SECONDS = 3;
576
- /** A poll far longer than any case, so only a runner's own `advance` claims its rows. */
577
- const NO_POLL = { poll: "1 hour" };
578
- const withCluster = (environment, runners, body) => environment.run(Effect.gen(function* () {
579
- const database = yield* environment.freshDatabase;
580
- const context = yield* Layer.build(ActorTest.cluster({
581
- database,
582
- runners,
583
- shardLockExpiration: `${EXPIRATION_SECONDS} seconds`,
584
- actors: BeaconLive,
585
- relay: NO_POLL,
586
- }));
587
- return yield* body.pipe(Effect.provideContext(context));
588
- }));
589
- const on = (runner, effect) => ActorCluster.use((cluster) => cluster.on(runner)(effect));
590
- const advanceOn = (runner, duration) => on(runner, ActorTest.use((test) => test.advance(duration)));
591
- const beaconRef = on(0, Beacon.get().pipe(Effect.map((beacon) => beacon.ref)));
592
- const pulsesOn = (runner, ref) => on(runner, receipts(ref, "Pulse"));
593
- const eventually = (check, what) => check.pipe(Effect.repeat({ schedule: Schedule.spaced("25 millis"), until: (held) => held }), Effect.timeoutOrElse({
594
- duration: "30 seconds",
595
- orElse: () => Effect.die(new Error(`Timed out waiting for ${what}`)),
596
- }), Effect.asVoid);
597
- const kill = (runner) => ActorCluster.use((cluster) => cluster.kill(runner).pipe(Effect.andThen(cluster.ready)));
598
- /** Multi-runner cases: real Postgres only, each on a fresh database and cluster. */
599
- export const cronClusterConformance = [
600
- {
601
- name: "keeps one singleton tick row and fires one logical tick per scheduled time on three runners",
602
- requiresIndependentConnections: true,
603
- timeoutMs: 90_000,
604
- run: ({ expect, environment }) => withCluster(environment, 3, Effect.gen(function* () {
605
- const cluster = yield* ActorCluster;
606
- yield* cluster.ready;
607
- const ref = yield* beaconRef;
608
- const before = fired.length;
609
- expect((yield* on(1, ticksOf(ref))).length).toBe(1);
610
- for (const round of [1, 2, 3]) {
611
- yield* Effect.forEach([0, 1, 2], (runner) => advanceOn(runner, "1 minute"), {
612
- concurrency: "unbounded",
613
- discard: true,
614
- });
615
- yield* eventually(pulsesOn(2, ref).pipe(Effect.map((count) => count === round)), `tick ${round}`);
616
- }
617
- const runs = fired.slice(before).filter((run) => run.id === ref.id);
618
- expect(new Set(runs.map((run) => run.commandId)).size).toBe(3);
619
- expect(yield* pulsesOn(0, ref)).toBe(3);
620
- expect((yield* on(0, ticksOf(ref))).length).toBe(1);
621
- })),
622
- },
623
- ...["afterClaim", "beforeOutboxDelete"].map((point) => ({
624
- name: `fires a tick once when the runner delivering it is killed at ${point}`,
625
- requiresIndependentConnections: true,
626
- timeoutMs: 90_000,
627
- run: ({ expect, environment }) => withCluster(environment, 3, Effect.gen(function* () {
628
- const cluster = yield* ActorCluster;
629
- yield* cluster.ready;
630
- const ref = yield* beaconRef;
631
- const [first] = yield* on(1, ticksOf(ref));
632
- const pause = yield* on(0, ActorTest.use((test) => test.pauseNext(point)));
633
- yield* advanceOn(0, "1 minute").pipe(Effect.forkChild);
634
- yield* pause.reached;
635
- yield* kill(0);
636
- yield* advanceOn(1, "1 minute");
637
- yield* advanceOn(2, "1 minute");
638
- const held = yield* on(1, ticksOf(ref));
639
- expect(held).toMatchObject([{ intent_id: first.intent_id, attempts: 1 }]);
640
- expect(yield* pulsesOn(1, ref)).toBe(point === "afterClaim" ? 0 : 1);
641
- yield* advanceOn(1, CLAIM_LEASE);
642
- yield* advanceOn(2, CLAIM_LEASE);
643
- yield* eventually(on(1, ticksOf(ref)).pipe(Effect.map((rows) => rows.length === 1 && rows[0].intent_id !== first.intent_id)), "the tick's rewrite");
644
- expect(yield* pulsesOn(1, ref)).toBe(1);
645
- expect(yield* on(2, stateOf(ref))).toMatchObject({ pulses: 1 });
646
- expect((yield* on(1, ticksOf(ref)))[0]).toMatchObject({ attempts: 0 });
647
- })),
648
- })),
649
- {
650
- name: "keeps a singleton's ticks firing after its runner is killed",
651
- requiresIndependentConnections: true,
652
- timeoutMs: 90_000,
653
- run: ({ expect, environment }) => withCluster(environment, 3, Effect.gen(function* () {
654
- const cluster = yield* ActorCluster;
655
- yield* cluster.ready;
656
- const ref = yield* beaconRef;
657
- const owner = (yield* cluster.owner(ref));
658
- const survivor = (owner + 1) % 3;
659
- yield* kill(owner);
660
- yield* advanceOn(survivor, "1 minute");
661
- yield* eventually(pulsesOn(survivor, ref).pipe(Effect.map((count) => count === 1)), "a tick on a survivor");
662
- expect((yield* on(survivor, ticksOf(ref))).length).toBe(1);
663
- })),
664
- },
665
- ];