@rikalabs/akter 0.1.0-alpha.0

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 (603) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +8 -0
  4. package/README.md +116 -0
  5. package/dist/actor/codecs.d.ts +77 -0
  6. package/dist/actor/codecs.js +87 -0
  7. package/dist/actor/connections.d.ts +34 -0
  8. package/dist/actor/connections.js +145 -0
  9. package/dist/actor/definition.d.ts +893 -0
  10. package/dist/actor/definition.js +147 -0
  11. package/dist/actor/descriptor.d.ts +172 -0
  12. package/dist/actor/descriptor.js +367 -0
  13. package/dist/actor/handles.d.ts +77 -0
  14. package/dist/actor/handles.js +265 -0
  15. package/dist/actor/jobs.d.ts +10 -0
  16. package/dist/actor/jobs.js +137 -0
  17. package/dist/actor/layers.d.ts +41 -0
  18. package/dist/actor/layers.js +177 -0
  19. package/dist/actor/reads.d.ts +47 -0
  20. package/dist/actor/reads.js +224 -0
  21. package/dist/actor/served.d.ts +120 -0
  22. package/dist/actor/served.js +72 -0
  23. package/dist/actor/turns.d.ts +13 -0
  24. package/dist/actor/turns.js +332 -0
  25. package/dist/actor/workflows.d.ts +16 -0
  26. package/dist/actor/workflows.js +37 -0
  27. package/dist/client/calls.d.ts +47 -0
  28. package/dist/client/calls.js +425 -0
  29. package/dist/client/clock.d.ts +56 -0
  30. package/dist/client/clock.js +119 -0
  31. package/dist/client/fleet-page.d.ts +15 -0
  32. package/dist/client/fleet-page.js +13 -0
  33. package/dist/client/fleet.d.ts +29 -0
  34. package/dist/client/fleet.js +51 -0
  35. package/dist/client/index.d.ts +17 -0
  36. package/dist/client/index.js +6 -0
  37. package/dist/client/make.d.ts +157 -0
  38. package/dist/client/make.js +221 -0
  39. package/dist/client/offline/queue.d.ts +118 -0
  40. package/dist/client/offline/queue.js +292 -0
  41. package/dist/client/offline/store.d.ts +74 -0
  42. package/dist/client/offline/store.js +78 -0
  43. package/dist/client/optimistic.d.ts +57 -0
  44. package/dist/client/optimistic.js +136 -0
  45. package/dist/client/report.d.ts +2 -0
  46. package/dist/client/report.js +8 -0
  47. package/dist/client/sessions/connection.d.ts +118 -0
  48. package/dist/client/sessions/connection.js +162 -0
  49. package/dist/client/sessions/feed.d.ts +42 -0
  50. package/dist/client/sessions/feed.js +43 -0
  51. package/dist/client/sessions/sse.d.ts +47 -0
  52. package/dist/client/sessions/sse.js +112 -0
  53. package/dist/client/sessions/stream.d.ts +28 -0
  54. package/dist/client/sessions/stream.js +35 -0
  55. package/dist/client/sessions/watch.d.ts +34 -0
  56. package/dist/client/sessions/watch.js +40 -0
  57. package/dist/client/transport.d.ts +37 -0
  58. package/dist/client/transport.js +66 -0
  59. package/dist/contexts/command.d.ts +132 -0
  60. package/dist/contexts/command.js +20 -0
  61. package/dist/contexts/connection.d.ts +68 -0
  62. package/dist/contexts/connection.js +0 -0
  63. package/dist/contexts/job.d.ts +49 -0
  64. package/dist/contexts/job.js +0 -0
  65. package/dist/contexts/workflow.d.ts +147 -0
  66. package/dist/contexts/workflow.js +22 -0
  67. package/dist/errors/actor.d.ts +200 -0
  68. package/dist/errors/actor.js +271 -0
  69. package/dist/errors/content.d.ts +19 -0
  70. package/dist/errors/content.js +14 -0
  71. package/dist/errors/database.d.ts +22 -0
  72. package/dist/errors/database.js +23 -0
  73. package/dist/errors/events.d.ts +17 -0
  74. package/dist/errors/events.js +14 -0
  75. package/dist/errors/subscription.d.ts +12 -0
  76. package/dist/errors/subscription.js +8 -0
  77. package/dist/errors/workflow.d.ts +28 -0
  78. package/dist/errors/workflow.js +17 -0
  79. package/dist/handles/actors.d.ts +17 -0
  80. package/dist/handles/actors.js +7 -0
  81. package/dist/handles/content.d.ts +59 -0
  82. package/dist/handles/content.js +36 -0
  83. package/dist/handles/intents.d.ts +164 -0
  84. package/dist/handles/intents.js +214 -0
  85. package/dist/handles/run.d.ts +31 -0
  86. package/dist/handles/run.js +21 -0
  87. package/dist/handles/workflow.d.ts +27 -0
  88. package/dist/handles/workflow.js +23 -0
  89. package/dist/identity/caller.d.ts +105 -0
  90. package/dist/identity/caller.js +68 -0
  91. package/dist/identity/child.d.ts +18 -0
  92. package/dist/identity/child.js +39 -0
  93. package/dist/identity/command.d.ts +19 -0
  94. package/dist/identity/command.js +21 -0
  95. package/dist/identity/connection.d.ts +24 -0
  96. package/dist/identity/connection.js +29 -0
  97. package/dist/identity/content.d.ts +18 -0
  98. package/dist/identity/content.js +12 -0
  99. package/dist/identity/digest.d.ts +4 -0
  100. package/dist/identity/digest.js +5 -0
  101. package/dist/identity/execution.d.ts +32 -0
  102. package/dist/identity/execution.js +63 -0
  103. package/dist/identity/mint.d.ts +51 -0
  104. package/dist/identity/mint.js +76 -0
  105. package/dist/identity/version.d.ts +2 -0
  106. package/dist/identity/version.js +3 -0
  107. package/dist/index.d.ts +159 -0
  108. package/dist/index.js +97 -0
  109. package/dist/members/blob.d.ts +43 -0
  110. package/dist/members/blob.js +29 -0
  111. package/dist/members/command.d.ts +109 -0
  112. package/dist/members/command.js +61 -0
  113. package/dist/members/connection.d.ts +55 -0
  114. package/dist/members/connection.js +49 -0
  115. package/dist/members/event.d.ts +36 -0
  116. package/dist/members/event.js +24 -0
  117. package/dist/members/job.d.ts +157 -0
  118. package/dist/members/job.js +67 -0
  119. package/dist/members/payload.d.ts +93 -0
  120. package/dist/members/payload.js +145 -0
  121. package/dist/members/reducer.d.ts +64 -0
  122. package/dist/members/reducer.js +27 -0
  123. package/dist/members/stream.d.ts +36 -0
  124. package/dist/members/stream.js +14 -0
  125. package/dist/members/subscription.d.ts +182 -0
  126. package/dist/members/subscription.js +91 -0
  127. package/dist/members/workflow.d.ts +99 -0
  128. package/dist/members/workflow.js +2 -0
  129. package/dist/policies/access.d.ts +35 -0
  130. package/dist/policies/access.js +6 -0
  131. package/dist/policies/command.d.ts +127 -0
  132. package/dist/policies/command.js +45 -0
  133. package/dist/policies/job.d.ts +28 -0
  134. package/dist/policies/job.js +60 -0
  135. package/dist/policies/schedules.d.ts +33 -0
  136. package/dist/policies/schedules.js +131 -0
  137. package/dist/protocol/frames.d.ts +66 -0
  138. package/dist/protocol/frames.js +55 -0
  139. package/dist/protocol/inspection.d.ts +807 -0
  140. package/dist/protocol/inspection.js +243 -0
  141. package/dist/protocol/wire.d.ts +184 -0
  142. package/dist/protocol/wire.js +150 -0
  143. package/dist/runtime/actors.d.ts +155 -0
  144. package/dist/runtime/actors.js +4 -0
  145. package/dist/runtime/admission.d.ts +34 -0
  146. package/dist/runtime/admission.js +72 -0
  147. package/dist/runtime/adoption/backfill.d.ts +27 -0
  148. package/dist/runtime/adoption/backfill.js +144 -0
  149. package/dist/runtime/adoption/enforce.d.ts +55 -0
  150. package/dist/runtime/adoption/enforce.js +241 -0
  151. package/dist/runtime/adoption/observe.d.ts +43 -0
  152. package/dist/runtime/adoption/observe.js +139 -0
  153. package/dist/runtime/adoption/plan.d.ts +45 -0
  154. package/dist/runtime/adoption/plan.js +192 -0
  155. package/dist/runtime/adoption/startup.d.ts +32 -0
  156. package/dist/runtime/adoption/startup.js +76 -0
  157. package/dist/runtime/adoption/status.d.ts +23 -0
  158. package/dist/runtime/adoption/status.js +47 -0
  159. package/dist/runtime/adoption/target.d.ts +33 -0
  160. package/dist/runtime/adoption/target.js +49 -0
  161. package/dist/runtime/connections/holder.d.ts +221 -0
  162. package/dist/runtime/connections/holder.js +816 -0
  163. package/dist/runtime/connections/owner.d.ts +263 -0
  164. package/dist/runtime/connections/owner.js +896 -0
  165. package/dist/runtime/connections/protocol.d.ts +441 -0
  166. package/dist/runtime/connections/protocol.js +222 -0
  167. package/dist/runtime/connections/reads.d.ts +24 -0
  168. package/dist/runtime/connections/reads.js +19 -0
  169. package/dist/runtime/connections/streams.d.ts +62 -0
  170. package/dist/runtime/connections/streams.js +128 -0
  171. package/dist/runtime/connections/transport.d.ts +40 -0
  172. package/dist/runtime/connections/transport.js +68 -0
  173. package/dist/runtime/connections/watch.d.ts +52 -0
  174. package/dist/runtime/connections/watch.js +159 -0
  175. package/dist/runtime/content/grant.d.ts +34 -0
  176. package/dist/runtime/content/grant.js +58 -0
  177. package/dist/runtime/content/store.d.ts +63 -0
  178. package/dist/runtime/content/store.js +294 -0
  179. package/dist/runtime/cron/key.d.ts +2 -0
  180. package/dist/runtime/cron/key.js +2 -0
  181. package/dist/runtime/cron/schedule.d.ts +69 -0
  182. package/dist/runtime/cron/schedule.js +144 -0
  183. package/dist/runtime/database/bounded.d.ts +43 -0
  184. package/dist/runtime/database/bounded.js +108 -0
  185. package/dist/runtime/database/connection.d.ts +8 -0
  186. package/dist/runtime/database/connection.js +18 -0
  187. package/dist/runtime/database/coordination.d.ts +19 -0
  188. package/dist/runtime/database/coordination.js +72 -0
  189. package/dist/runtime/database/flock.d.ts +10 -0
  190. package/dist/runtime/database/flock.js +60 -0
  191. package/dist/runtime/database/gate.d.ts +26 -0
  192. package/dist/runtime/database/gate.js +54 -0
  193. package/dist/runtime/database/keepalive.d.ts +11 -0
  194. package/dist/runtime/database/keepalive.js +22 -0
  195. package/dist/runtime/database/migrations.d.ts +177 -0
  196. package/dist/runtime/database/migrations.js +1018 -0
  197. package/dist/runtime/database/neki/migrations.d.ts +31 -0
  198. package/dist/runtime/database/neki/migrations.js +276 -0
  199. package/dist/runtime/database/neki/session.d.ts +23 -0
  200. package/dist/runtime/database/neki/session.js +31 -0
  201. package/dist/runtime/database/pglite.d.ts +21 -0
  202. package/dist/runtime/database/pglite.js +74 -0
  203. package/dist/runtime/database/replica.d.ts +41 -0
  204. package/dist/runtime/database/replica.js +51 -0
  205. package/dist/runtime/database/shards.d.ts +21 -0
  206. package/dist/runtime/database/shards.js +37 -0
  207. package/dist/runtime/database/snapshot.d.ts +7 -0
  208. package/dist/runtime/database/snapshot.js +7 -0
  209. package/dist/runtime/database/tenancy.d.ts +69 -0
  210. package/dist/runtime/database/tenancy.js +145 -0
  211. package/dist/runtime/database/transient.d.ts +10 -0
  212. package/dist/runtime/database/transient.js +25 -0
  213. package/dist/runtime/drain.d.ts +132 -0
  214. package/dist/runtime/drain.js +109 -0
  215. package/dist/runtime/entity/mailbox.d.ts +86 -0
  216. package/dist/runtime/entity/mailbox.js +121 -0
  217. package/dist/runtime/entity/register.d.ts +424 -0
  218. package/dist/runtime/entity/register.js +669 -0
  219. package/dist/runtime/events/append.d.ts +192 -0
  220. package/dist/runtime/events/append.js +128 -0
  221. package/dist/runtime/events/cursor.d.ts +2 -0
  222. package/dist/runtime/events/cursor.js +5 -0
  223. package/dist/runtime/events/replay.d.ts +20 -0
  224. package/dist/runtime/events/replay.js +45 -0
  225. package/dist/runtime/feeds.d.ts +26 -0
  226. package/dist/runtime/feeds.js +100 -0
  227. package/dist/runtime/fleet/checks.d.ts +17 -0
  228. package/dist/runtime/fleet/checks.js +106 -0
  229. package/dist/runtime/fleet/maintainer.d.ts +38 -0
  230. package/dist/runtime/fleet/maintainer.js +383 -0
  231. package/dist/runtime/fleet/pgoutput.d.ts +41 -0
  232. package/dist/runtime/fleet/pgoutput.js +102 -0
  233. package/dist/runtime/fleet/setup.d.ts +36 -0
  234. package/dist/runtime/fleet/setup.js +58 -0
  235. package/dist/runtime/fleet/subscribe.d.ts +42 -0
  236. package/dist/runtime/fleet/subscribe.js +147 -0
  237. package/dist/runtime/index.d.ts +66 -0
  238. package/dist/runtime/index.js +37 -0
  239. package/dist/runtime/inspector/http.d.ts +19 -0
  240. package/dist/runtime/inspector/http.js +36 -0
  241. package/dist/runtime/inspector/layer.d.ts +46 -0
  242. package/dist/runtime/inspector/layer.js +180 -0
  243. package/dist/runtime/inspector/queries.d.ts +711 -0
  244. package/dist/runtime/inspector/queries.js +494 -0
  245. package/dist/runtime/jobs/attempt.d.ts +254 -0
  246. package/dist/runtime/jobs/attempt.js +413 -0
  247. package/dist/runtime/jobs/isolation.d.ts +20 -0
  248. package/dist/runtime/jobs/isolation.js +10 -0
  249. package/dist/runtime/jobs/progress.d.ts +95 -0
  250. package/dist/runtime/jobs/progress.js +144 -0
  251. package/dist/runtime/layer.d.ts +260 -0
  252. package/dist/runtime/layer.js +876 -0
  253. package/dist/runtime/members.d.ts +381 -0
  254. package/dist/runtime/members.js +8 -0
  255. package/dist/runtime/operators/audit.d.ts +64 -0
  256. package/dist/runtime/operators/audit.js +56 -0
  257. package/dist/runtime/operators/auth.d.ts +29 -0
  258. package/dist/runtime/operators/auth.js +42 -0
  259. package/dist/runtime/operators/export.d.ts +68 -0
  260. package/dist/runtime/operators/export.js +137 -0
  261. package/dist/runtime/operators/grants.d.ts +45 -0
  262. package/dist/runtime/operators/grants.js +40 -0
  263. package/dist/runtime/operators/repair.d.ts +206 -0
  264. package/dist/runtime/operators/repair.js +226 -0
  265. package/dist/runtime/operators/routes.d.ts +35 -0
  266. package/dist/runtime/operators/routes.js +190 -0
  267. package/dist/runtime/operators/seed.d.ts +123 -0
  268. package/dist/runtime/operators/seed.js +123 -0
  269. package/dist/runtime/payloads/versions.d.ts +65 -0
  270. package/dist/runtime/payloads/versions.js +240 -0
  271. package/dist/runtime/peering/authority.d.ts +42 -0
  272. package/dist/runtime/peering/authority.js +132 -0
  273. package/dist/runtime/peering/credentials.d.ts +21 -0
  274. package/dist/runtime/peering/credentials.js +13 -0
  275. package/dist/runtime/peering/transport.d.ts +51 -0
  276. package/dist/runtime/peering/transport.js +175 -0
  277. package/dist/runtime/queries.d.ts +18 -0
  278. package/dist/runtime/queries.js +169 -0
  279. package/dist/runtime/readiness.d.ts +36 -0
  280. package/dist/runtime/readiness.js +44 -0
  281. package/dist/runtime/registration.d.ts +58 -0
  282. package/dist/runtime/registration.js +186 -0
  283. package/dist/runtime/request.d.ts +158 -0
  284. package/dist/runtime/request.js +102 -0
  285. package/dist/runtime/retry.d.ts +8 -0
  286. package/dist/runtime/retry.js +11 -0
  287. package/dist/runtime/runner.d.ts +93 -0
  288. package/dist/runtime/runner.js +122 -0
  289. package/dist/runtime/storage/codec.d.ts +39 -0
  290. package/dist/runtime/storage/codec.js +41 -0
  291. package/dist/runtime/storage/generation.d.ts +69 -0
  292. package/dist/runtime/storage/generation.js +49 -0
  293. package/dist/runtime/storage/placements.d.ts +15 -0
  294. package/dist/runtime/storage/placements.js +38 -0
  295. package/dist/runtime/storage/platform.d.ts +6 -0
  296. package/dist/runtime/storage/platform.js +9 -0
  297. package/dist/runtime/storage/retention.d.ts +73 -0
  298. package/dist/runtime/storage/retention.js +158 -0
  299. package/dist/runtime/subscriptions/identity.d.ts +31 -0
  300. package/dist/runtime/subscriptions/identity.js +45 -0
  301. package/dist/runtime/subscriptions/relay.d.ts +175 -0
  302. package/dist/runtime/subscriptions/relay.js +763 -0
  303. package/dist/runtime/subscriptions/storage.d.ts +40 -0
  304. package/dist/runtime/subscriptions/storage.js +98 -0
  305. package/dist/runtime/sweeps.d.ts +39 -0
  306. package/dist/runtime/sweeps.js +54 -0
  307. package/dist/runtime/telemetry/defects.d.ts +57 -0
  308. package/dist/runtime/telemetry/defects.js +43 -0
  309. package/dist/runtime/telemetry/metrics.d.ts +52 -0
  310. package/dist/runtime/telemetry/metrics.js +138 -0
  311. package/dist/runtime/telemetry/routes.d.ts +19 -0
  312. package/dist/runtime/telemetry/routes.js +18 -0
  313. package/dist/runtime/telemetry/sampler.d.ts +26 -0
  314. package/dist/runtime/telemetry/sampler.js +101 -0
  315. package/dist/runtime/telemetry/spans.d.ts +38 -0
  316. package/dist/runtime/telemetry/spans.js +44 -0
  317. package/dist/runtime/telemetry/usage.d.ts +52 -0
  318. package/dist/runtime/telemetry/usage.js +30 -0
  319. package/dist/runtime/topology/admission.d.ts +13 -0
  320. package/dist/runtime/topology/admission.js +36 -0
  321. package/dist/runtime/topology/locks.d.ts +56 -0
  322. package/dist/runtime/topology/locks.js +81 -0
  323. package/dist/runtime/topology/messages.d.ts +15 -0
  324. package/dist/runtime/topology/messages.js +32 -0
  325. package/dist/runtime/turn/admission.d.ts +22 -0
  326. package/dist/runtime/turn/admission.js +41 -0
  327. package/dist/runtime/turn/blobs.d.ts +38 -0
  328. package/dist/runtime/turn/blobs.js +215 -0
  329. package/dist/runtime/turn/execute.d.ts +208 -0
  330. package/dist/runtime/turn/execute.js +972 -0
  331. package/dist/runtime/turn/group.d.ts +165 -0
  332. package/dist/runtime/turn/group.js +326 -0
  333. package/dist/runtime/turn/hooks.d.ts +44 -0
  334. package/dist/runtime/turn/hooks.js +28 -0
  335. package/dist/runtime/turn/outbox.d.ts +159 -0
  336. package/dist/runtime/turn/outbox.js +269 -0
  337. package/dist/runtime/turn/pipeline.d.ts +68 -0
  338. package/dist/runtime/turn/pipeline.js +83 -0
  339. package/dist/runtime/turn/receipt.d.ts +81 -0
  340. package/dist/runtime/turn/receipt.js +32 -0
  341. package/dist/runtime/turn/relay.d.ts +179 -0
  342. package/dist/runtime/turn/relay.js +685 -0
  343. package/dist/runtime/turn/rows.d.ts +43 -0
  344. package/dist/runtime/turn/rows.js +592 -0
  345. package/dist/runtime/workflows/compatibility.d.ts +125 -0
  346. package/dist/runtime/workflows/compatibility.js +285 -0
  347. package/dist/runtime/workflows/engine.d.ts +107 -0
  348. package/dist/runtime/workflows/engine.js +826 -0
  349. package/dist/runtime/workflows/manifest.d.ts +34 -0
  350. package/dist/runtime/workflows/manifest.js +39 -0
  351. package/dist/runtime/workflows/steps.d.ts +33 -0
  352. package/dist/runtime/workflows/steps.js +152 -0
  353. package/dist/serve/api.d.ts +99 -0
  354. package/dist/serve/api.js +403 -0
  355. package/dist/serve/assertion/binding.d.ts +41 -0
  356. package/dist/serve/assertion/binding.js +92 -0
  357. package/dist/serve/assertion/verify.d.ts +93 -0
  358. package/dist/serve/assertion/verify.js +189 -0
  359. package/dist/serve/auth.d.ts +200 -0
  360. package/dist/serve/auth.js +92 -0
  361. package/dist/serve/clock.d.ts +10 -0
  362. package/dist/serve/clock.js +25 -0
  363. package/dist/serve/jwt.d.ts +65 -0
  364. package/dist/serve/jwt.js +202 -0
  365. package/dist/serve/layer.d.ts +98 -0
  366. package/dist/serve/layer.js +809 -0
  367. package/dist/serve/mcp/endpoint.d.ts +39 -0
  368. package/dist/serve/mcp/endpoint.js +187 -0
  369. package/dist/serve/mcp/tools.d.ts +37 -0
  370. package/dist/serve/mcp/tools.js +125 -0
  371. package/dist/serve/origin.d.ts +6 -0
  372. package/dist/serve/origin.js +21 -0
  373. package/dist/serve/sessions/feed.d.ts +79 -0
  374. package/dist/serve/sessions/feed.js +115 -0
  375. package/dist/serve/sessions/socket.d.ts +56 -0
  376. package/dist/serve/sessions/socket.js +212 -0
  377. package/dist/serve/sessions/sse.d.ts +27 -0
  378. package/dist/serve/sessions/sse.js +26 -0
  379. package/dist/serve/sessions/stream.d.ts +9 -0
  380. package/dist/serve/sessions/stream.js +9 -0
  381. package/dist/serve/sessions/watch.d.ts +12 -0
  382. package/dist/serve/sessions/watch.js +11 -0
  383. package/dist/serve/wire.d.ts +16 -0
  384. package/dist/serve/wire.js +50 -0
  385. package/dist/state/blob.d.ts +71 -0
  386. package/dist/state/blob.js +0 -0
  387. package/dist/state/migration.d.ts +53 -0
  388. package/dist/state/migration.js +29 -0
  389. package/dist/tables/fleet.d.ts +170 -0
  390. package/dist/tables/fleet.js +214 -0
  391. package/dist/tables/owned.d.ts +250 -0
  392. package/dist/tables/owned.js +233 -0
  393. package/dist/testing/actor-test.d.ts +360 -0
  394. package/dist/testing/actor-test.js +337 -0
  395. package/dist/testing/cluster.d.ts +93 -0
  396. package/dist/testing/cluster.js +296 -0
  397. package/dist/testing/conformance/access.d.ts +5 -0
  398. package/dist/testing/conformance/access.js +531 -0
  399. package/dist/testing/conformance/admission.d.ts +11 -0
  400. package/dist/testing/conformance/admission.js +182 -0
  401. package/dist/testing/conformance/adoption.d.ts +2 -0
  402. package/dist/testing/conformance/adoption.js +1019 -0
  403. package/dist/testing/conformance/assertions.d.ts +202 -0
  404. package/dist/testing/conformance/assertions.js +849 -0
  405. package/dist/testing/conformance/batches.d.ts +41 -0
  406. package/dist/testing/conformance/batches.js +388 -0
  407. package/dist/testing/conformance/blobs.d.ts +14 -0
  408. package/dist/testing/conformance/blobs.js +405 -0
  409. package/dist/testing/conformance/capacity.d.ts +8 -0
  410. package/dist/testing/conformance/capacity.js +433 -0
  411. package/dist/testing/conformance/client.d.ts +5 -0
  412. package/dist/testing/conformance/client.js +719 -0
  413. package/dist/testing/conformance/cold-serve.d.ts +16 -0
  414. package/dist/testing/conformance/cold-serve.js +254 -0
  415. package/dist/testing/conformance/connections/actors.d.ts +1143 -0
  416. package/dist/testing/conformance/connections/actors.js +180 -0
  417. package/dist/testing/conformance/connections/harness.d.ts +1754 -0
  418. package/dist/testing/conformance/connections/harness.js +142 -0
  419. package/dist/testing/conformance/connections/holders.d.ts +4 -0
  420. package/dist/testing/conformance/connections/holders.js +165 -0
  421. package/dist/testing/conformance/connections/lifecycle.d.ts +4 -0
  422. package/dist/testing/conformance/connections/lifecycle.js +113 -0
  423. package/dist/testing/conformance/connections/limits.d.ts +4 -0
  424. package/dist/testing/conformance/connections/limits.js +127 -0
  425. package/dist/testing/conformance/connections/reauthorization.d.ts +4 -0
  426. package/dist/testing/conformance/connections/reauthorization.js +170 -0
  427. package/dist/testing/conformance/connections/resync.d.ts +4 -0
  428. package/dist/testing/conformance/connections/resync.js +360 -0
  429. package/dist/testing/conformance/connections/takeover.d.ts +4 -0
  430. package/dist/testing/conformance/connections/takeover.js +287 -0
  431. package/dist/testing/conformance/connections.d.ts +4 -0
  432. package/dist/testing/conformance/connections.js +15 -0
  433. package/dist/testing/conformance/content-blobs.d.ts +16 -0
  434. package/dist/testing/conformance/content-blobs.js +534 -0
  435. package/dist/testing/conformance/counter.d.ts +16 -0
  436. package/dist/testing/conformance/counter.js +813 -0
  437. package/dist/testing/conformance/cron.d.ts +5 -0
  438. package/dist/testing/conformance/cron.js +665 -0
  439. package/dist/testing/conformance/drain.d.ts +26 -0
  440. package/dist/testing/conformance/drain.js +540 -0
  441. package/dist/testing/conformance/effect-control.d.ts +38 -0
  442. package/dist/testing/conformance/effect-control.js +1059 -0
  443. package/dist/testing/conformance/effects.d.ts +23 -0
  444. package/dist/testing/conformance/effects.js +432 -0
  445. package/dist/testing/conformance/events.d.ts +28 -0
  446. package/dist/testing/conformance/events.js +335 -0
  447. package/dist/testing/conformance/export.d.ts +2 -0
  448. package/dist/testing/conformance/export.js +368 -0
  449. package/dist/testing/conformance/fleet.d.ts +444 -0
  450. package/dist/testing/conformance/fleet.js +1009 -0
  451. package/dist/testing/conformance/groups.d.ts +7 -0
  452. package/dist/testing/conformance/groups.js +353 -0
  453. package/dist/testing/conformance/heap.d.ts +3 -0
  454. package/dist/testing/conformance/heap.js +149 -0
  455. package/dist/testing/conformance/http.d.ts +2809 -0
  456. package/dist/testing/conformance/http.js +1234 -0
  457. package/dist/testing/conformance/inspection-views.d.ts +8 -0
  458. package/dist/testing/conformance/inspection-views.js +272 -0
  459. package/dist/testing/conformance/inspector.d.ts +7 -0
  460. package/dist/testing/conformance/inspector.js +650 -0
  461. package/dist/testing/conformance/mint.d.ts +58 -0
  462. package/dist/testing/conformance/mint.js +692 -0
  463. package/dist/testing/conformance/multi-runner.d.ts +8 -0
  464. package/dist/testing/conformance/multi-runner.js +403 -0
  465. package/dist/testing/conformance/neki/plan.d.ts +19 -0
  466. package/dist/testing/conformance/neki/plan.js +30 -0
  467. package/dist/testing/conformance/observability.d.ts +3 -0
  468. package/dist/testing/conformance/observability.js +343 -0
  469. package/dist/testing/conformance/offline.d.ts +4 -0
  470. package/dist/testing/conformance/offline.js +474 -0
  471. package/dist/testing/conformance/operator-harness.d.ts +57 -0
  472. package/dist/testing/conformance/operator-harness.js +78 -0
  473. package/dist/testing/conformance/operator.d.ts +3 -0
  474. package/dist/testing/conformance/operator.js +379 -0
  475. package/dist/testing/conformance/outbox-cross-shard.d.ts +14 -0
  476. package/dist/testing/conformance/outbox-cross-shard.js +160 -0
  477. package/dist/testing/conformance/outbox.d.ts +36 -0
  478. package/dist/testing/conformance/outbox.js +437 -0
  479. package/dist/testing/conformance/payload-migrations.d.ts +3 -0
  480. package/dist/testing/conformance/payload-migrations.js +612 -0
  481. package/dist/testing/conformance/pipeline.d.ts +3 -0
  482. package/dist/testing/conformance/pipeline.js +1040 -0
  483. package/dist/testing/conformance/placement.d.ts +22 -0
  484. package/dist/testing/conformance/placement.js +539 -0
  485. package/dist/testing/conformance/platform.d.ts +40 -0
  486. package/dist/testing/conformance/platform.js +101 -0
  487. package/dist/testing/conformance/progress.d.ts +39 -0
  488. package/dist/testing/conformance/progress.js +666 -0
  489. package/dist/testing/conformance/properties.d.ts +6 -0
  490. package/dist/testing/conformance/properties.js +295 -0
  491. package/dist/testing/conformance/protocols.d.ts +4 -0
  492. package/dist/testing/conformance/protocols.js +618 -0
  493. package/dist/testing/conformance/provider/advertise.d.ts +21 -0
  494. package/dist/testing/conformance/provider/advertise.js +44 -0
  495. package/dist/testing/conformance/read-your-writes.d.ts +20 -0
  496. package/dist/testing/conformance/read-your-writes.js +192 -0
  497. package/dist/testing/conformance/reducers.d.ts +8 -0
  498. package/dist/testing/conformance/reducers.js +258 -0
  499. package/dist/testing/conformance/relay.d.ts +42 -0
  500. package/dist/testing/conformance/relay.js +1115 -0
  501. package/dist/testing/conformance/restore.d.ts +43 -0
  502. package/dist/testing/conformance/restore.js +341 -0
  503. package/dist/testing/conformance/retention.d.ts +14 -0
  504. package/dist/testing/conformance/retention.js +463 -0
  505. package/dist/testing/conformance/rls.d.ts +24 -0
  506. package/dist/testing/conformance/rls.js +576 -0
  507. package/dist/testing/conformance/simulation.d.ts +31 -0
  508. package/dist/testing/conformance/simulation.js +167 -0
  509. package/dist/testing/conformance/single-shard.d.ts +2 -0
  510. package/dist/testing/conformance/single-shard.js +125 -0
  511. package/dist/testing/conformance/singleton.d.ts +3 -0
  512. package/dist/testing/conformance/singleton.js +293 -0
  513. package/dist/testing/conformance/statements.d.ts +60 -0
  514. package/dist/testing/conformance/statements.js +108 -0
  515. package/dist/testing/conformance/streams.d.ts +7 -0
  516. package/dist/testing/conformance/streams.js +280 -0
  517. package/dist/testing/conformance/subscriptions/access.d.ts +4 -0
  518. package/dist/testing/conformance/subscriptions/access.js +98 -0
  519. package/dist/testing/conformance/subscriptions/actors.d.ts +4910 -0
  520. package/dist/testing/conformance/subscriptions/actors.js +372 -0
  521. package/dist/testing/conformance/subscriptions/cluster.d.ts +4 -0
  522. package/dist/testing/conformance/subscriptions/cluster.js +89 -0
  523. package/dist/testing/conformance/subscriptions/delivery.d.ts +4 -0
  524. package/dist/testing/conformance/subscriptions/delivery.js +253 -0
  525. package/dist/testing/conformance/subscriptions/deploys.d.ts +4 -0
  526. package/dist/testing/conformance/subscriptions/deploys.js +162 -0
  527. package/dist/testing/conformance/subscriptions/epochs.d.ts +4 -0
  528. package/dist/testing/conformance/subscriptions/epochs.js +169 -0
  529. package/dist/testing/conformance/subscriptions/failures.d.ts +4 -0
  530. package/dist/testing/conformance/subscriptions/failures.js +108 -0
  531. package/dist/testing/conformance/subscriptions/feeds.d.ts +4 -0
  532. package/dist/testing/conformance/subscriptions/feeds.js +218 -0
  533. package/dist/testing/conformance/subscriptions/harness.d.ts +124 -0
  534. package/dist/testing/conformance/subscriptions/harness.js +143 -0
  535. package/dist/testing/conformance/subscriptions/operator.d.ts +4 -0
  536. package/dist/testing/conformance/subscriptions/operator.js +128 -0
  537. package/dist/testing/conformance/subscriptions/retention.d.ts +4 -0
  538. package/dist/testing/conformance/subscriptions/retention.js +288 -0
  539. package/dist/testing/conformance/subscriptions/routing.d.ts +4 -0
  540. package/dist/testing/conformance/subscriptions/routing.js +179 -0
  541. package/dist/testing/conformance/subscriptions.d.ts +4 -0
  542. package/dist/testing/conformance/subscriptions.js +19 -0
  543. package/dist/testing/conformance/tables.d.ts +125 -0
  544. package/dist/testing/conformance/tables.js +664 -0
  545. package/dist/testing/conformance/transports/actors.d.ts +1907 -0
  546. package/dist/testing/conformance/transports/actors.js +163 -0
  547. package/dist/testing/conformance/transports/client.d.ts +3 -0
  548. package/dist/testing/conformance/transports/client.js +237 -0
  549. package/dist/testing/conformance/transports/feeds.d.ts +3 -0
  550. package/dist/testing/conformance/transports/feeds.js +232 -0
  551. package/dist/testing/conformance/transports/harness.d.ts +93 -0
  552. package/dist/testing/conformance/transports/harness.js +84 -0
  553. package/dist/testing/conformance/transports/streams.d.ts +3 -0
  554. package/dist/testing/conformance/transports/streams.js +96 -0
  555. package/dist/testing/conformance/transports/websocket.d.ts +3 -0
  556. package/dist/testing/conformance/transports/websocket.js +384 -0
  557. package/dist/testing/conformance/transports/wire.d.ts +141 -0
  558. package/dist/testing/conformance/transports/wire.js +204 -0
  559. package/dist/testing/conformance/transports.d.ts +3 -0
  560. package/dist/testing/conformance/transports.js +11 -0
  561. package/dist/testing/conformance/watch.d.ts +59 -0
  562. package/dist/testing/conformance/watch.js +879 -0
  563. package/dist/testing/conformance/workflow-engine.d.ts +97 -0
  564. package/dist/testing/conformance/workflow-engine.js +253 -0
  565. package/dist/testing/conformance/workflow-versions.d.ts +3 -0
  566. package/dist/testing/conformance/workflow-versions.js +545 -0
  567. package/dist/testing/conformance/workflows/access.d.ts +4 -0
  568. package/dist/testing/conformance/workflows/access.js +147 -0
  569. package/dist/testing/conformance/workflows/actors.d.ts +2317 -0
  570. package/dist/testing/conformance/workflows/actors.js +305 -0
  571. package/dist/testing/conformance/workflows/basics.d.ts +4 -0
  572. package/dist/testing/conformance/workflows/basics.js +144 -0
  573. package/dist/testing/conformance/workflows/engine.d.ts +4 -0
  574. package/dist/testing/conformance/workflows/engine.js +80 -0
  575. package/dist/testing/conformance/workflows/guards.d.ts +4 -0
  576. package/dist/testing/conformance/workflows/guards.js +172 -0
  577. package/dist/testing/conformance/workflows/harness.d.ts +14 -0
  578. package/dist/testing/conformance/workflows/harness.js +44 -0
  579. package/dist/testing/conformance/workflows/recovery.d.ts +4 -0
  580. package/dist/testing/conformance/workflows/recovery.js +181 -0
  581. package/dist/testing/conformance/workflows/starts.d.ts +4 -0
  582. package/dist/testing/conformance/workflows/starts.js +57 -0
  583. package/dist/testing/conformance/workflows/suspension.d.ts +4 -0
  584. package/dist/testing/conformance/workflows/suspension.js +106 -0
  585. package/dist/testing/conformance/workflows/waits.d.ts +4 -0
  586. package/dist/testing/conformance/workflows/waits.js +284 -0
  587. package/dist/testing/conformance/workflows.d.ts +4 -0
  588. package/dist/testing/conformance/workflows.js +19 -0
  589. package/dist/testing/conformance.d.ts +330 -0
  590. package/dist/testing/conformance.js +364 -0
  591. package/dist/testing/database.d.ts +31 -0
  592. package/dist/testing/database.js +70 -0
  593. package/dist/testing/foundation.d.ts +41 -0
  594. package/dist/testing/foundation.js +517 -0
  595. package/dist/testing/index.d.ts +20 -0
  596. package/dist/testing/index.js +12 -0
  597. package/dist/testing/property.d.ts +40 -0
  598. package/dist/testing/property.js +95 -0
  599. package/dist/testing/simulate-cluster.d.ts +112 -0
  600. package/dist/testing/simulate-cluster.js +257 -0
  601. package/dist/testing/simulate.d.ts +90 -0
  602. package/dist/testing/simulate.js +127 -0
  603. package/package.json +1 -0
@@ -0,0 +1,1115 @@
1
+ import { Clock, DateTime, Deferred, Duration, Effect, Exit, Fiber, Layer, Predicate, Schedule, Schema, Tracer, } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import { Actor, Intent } from "../../index.js";
4
+ import { layer as runtimeLayer } from "../../runtime/layer.js";
5
+ import { SpanNames } from "../../runtime/telemetry/spans.js";
6
+ import { TurnHooks } from "../../runtime/turn/hooks.js";
7
+ import { claimIntents } from "../../runtime/turn/relay.js";
8
+ import { ActorTest } from "../actor-test.js";
9
+ import { ActorCluster } from "../cluster.js";
10
+ import { mintedTitle, mintLayer, planLaterTask } from "./mint.js";
11
+ import { CLAIM_LEASE, ExplainOutput, planNodes } from "./outbox.js";
12
+ export const relayFixture = () => ({
13
+ taken: new Map(),
14
+ claims: new Map(),
15
+ attempts: [],
16
+ onTake: () => Effect.void,
17
+ provider: (attempt) => Effect.succeed(attempt.key),
18
+ hook: () => Effect.void,
19
+ });
20
+ const reset = (fixture) => Effect.sync(() => {
21
+ fixture.taken.clear();
22
+ fixture.claims.clear();
23
+ fixture.attempts.length = 0;
24
+ fixture.onTake = () => Effect.void;
25
+ fixture.provider = (attempt) => Effect.succeed(attempt.key);
26
+ fixture.hook = () => Effect.void;
27
+ });
28
+ class ProviderDown extends Schema.TaggedError()("ProviderDown", {}) {
29
+ }
30
+ const Take = Actor.command("Take", { payload: Schema.String });
31
+ const RelayMailbox = Actor.make("RelayMailbox", {
32
+ key: Schema.String,
33
+ api: {},
34
+ internal: { Take },
35
+ });
36
+ const Stage = Actor.command("Stage", {
37
+ payload: Schema.Struct({
38
+ ids: Schema.Array(Schema.String),
39
+ afterMs: Schema.optional(Schema.Int),
40
+ atMs: Schema.optional(Schema.Int),
41
+ }),
42
+ });
43
+ const Relayer = Actor.make("Relayer", { key: Schema.String, api: { Stage } });
44
+ const RelayCall = Actor.job("RelayCall", {
45
+ payload: { key: Schema.String },
46
+ success: Schema.String,
47
+ });
48
+ const RelayCallOnce = Actor.job("RelayCallOnce", {
49
+ payload: { key: Schema.String },
50
+ success: Schema.String,
51
+ });
52
+ const RelayTimed = Actor.job("RelayTimed", {
53
+ payload: { key: Schema.String },
54
+ success: Schema.String,
55
+ });
56
+ const Perform = Actor.command("Perform", {
57
+ payload: Schema.Struct({
58
+ key: Schema.String,
59
+ effect: Schema.Literals(["RelayCall", "RelayCallOnce", "RelayTimed"]),
60
+ }),
61
+ });
62
+ const Called = Actor.command("Called", { payload: Schema.String });
63
+ const CallFailed = Actor.command("CallFailed", { payload: Actor.DeadLetter(RelayCall) });
64
+ const OnceFailed = Actor.command("OnceFailed", { payload: Actor.DeadLetter(RelayCallOnce) });
65
+ const Letter = Schema.Struct({
66
+ effectId: Schema.String,
67
+ attempts: Schema.Int,
68
+ ambiguous: Schema.Boolean,
69
+ });
70
+ const RelayCaller = Actor.make("RelayCaller", {
71
+ key: Schema.String,
72
+ state: Actor.state({
73
+ called: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
74
+ letters: Schema.Array(Letter).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
75
+ }),
76
+ jobs: {
77
+ RelayCall: { job: RelayCall, retry: { times: 1 }, onSuccess: Called, onDeadLetter: CallFailed },
78
+ RelayCallOnce: {
79
+ job: RelayCallOnce,
80
+ retry: { times: 0 },
81
+ onSuccess: Called,
82
+ onDeadLetter: OnceFailed,
83
+ },
84
+ RelayTimed: {
85
+ job: RelayTimed,
86
+ timeout: "100 millis",
87
+ retry: { times: 3, backoff: { base: "10 millis", max: "40 millis" } },
88
+ },
89
+ },
90
+ api: { Perform },
91
+ internal: { Called, CallFailed, OnceFailed },
92
+ });
93
+ const MAILBOXES = 32;
94
+ /** A `Take` intent's stored payload: its encoded input under `value`. */
95
+ const decodeTaken = (payload) => Schema.decodeEffect(Schema.fromJsonString(Schema.Struct({ value: Schema.String })))(payload).pipe(Effect.map(({ value }) => value), Effect.orDie);
96
+ const mailboxOf = (id) => {
97
+ let hash = 0;
98
+ for (const char of id)
99
+ hash = (hash * 31 + char.charCodeAt(0)) % MAILBOXES;
100
+ return `mailbox-${hash}`;
101
+ };
102
+ const recordLetter = (letter) => Effect.gen(function* () {
103
+ const turn = yield* RelayCaller.Turn;
104
+ const { jobId, attempts, ambiguous } = letter;
105
+ yield* turn.state.set({
106
+ letters: [...turn.state.letters, { effectId: jobId, attempts, ambiguous }],
107
+ });
108
+ });
109
+ /** The relay actors' command layers, which every runner builds. */
110
+ export const relayLayer = (fixture) => Layer.mergeAll(RelayMailbox.toLayer(Effect.succeed({
111
+ Take: (id) => Effect.suspend(() => {
112
+ fixture.taken.set(id, (fixture.taken.get(id) ?? 0) + 1);
113
+ return fixture.onTake(id);
114
+ }),
115
+ })), Relayer.toLayer(Effect.succeed({
116
+ Stage: Effect.fnUntraced(function* ({ ids, afterMs, atMs }) {
117
+ for (const id of ids) {
118
+ let intent = (yield* RelayMailbox.intents(mailboxOf(id))).Take(id);
119
+ if (afterMs !== undefined)
120
+ intent = intent.pipe(Intent.after(Duration.millis(afterMs)));
121
+ if (atMs !== undefined)
122
+ intent = intent.pipe(Intent.at(DateTime.makeUnsafe(atMs)));
123
+ yield* intent;
124
+ }
125
+ }),
126
+ })), RelayCaller.toLayer(Effect.succeed({
127
+ Perform: Effect.fnUntraced(function* ({ key, effect }) {
128
+ const turn = yield* RelayCaller.Turn;
129
+ const performed = {
130
+ RelayCall: () => RelayCall.make({ key }),
131
+ RelayCallOnce: () => RelayCallOnce.make({ key }),
132
+ RelayTimed: () => RelayTimed.make({ key }),
133
+ };
134
+ yield* turn.enqueue(performed[effect]());
135
+ }),
136
+ Called: Effect.fnUntraced(function* (value) {
137
+ const turn = yield* RelayCaller.Turn;
138
+ yield* turn.state.set({ called: [...turn.state.called, value] });
139
+ }),
140
+ CallFailed: recordLetter,
141
+ OnceFailed: recordLetter,
142
+ })));
143
+ /** The executors of `runner`; a case can leave a runner without them. */
144
+ const runnerEffects = (fixture, runner) => {
145
+ const execute = (key) => Effect.gen(function* () {
146
+ const exec = yield* RelayCaller.Executor;
147
+ const attempt = {
148
+ key,
149
+ effectId: exec.jobId,
150
+ attempt: exec.attempt,
151
+ runner,
152
+ startedAt: yield* Clock.currentTimeMillis,
153
+ endedAt: undefined,
154
+ interrupted: false,
155
+ };
156
+ fixture.attempts.push(attempt);
157
+ return yield* Effect.suspend(() => fixture.provider(attempt)).pipe(Effect.onExit((exit) => Effect.map(Clock.currentTimeMillis, (now) => {
158
+ attempt.endedAt = now;
159
+ attempt.interrupted = Exit.hasInterrupts(exit);
160
+ })));
161
+ });
162
+ return RelayCaller.toJobLayer(Effect.succeed({
163
+ RelayCall: ({ key }) => execute(key),
164
+ RelayCallOnce: ({ key }) => execute(key),
165
+ RelayTimed: ({ key }) => execute(key),
166
+ }));
167
+ };
168
+ /** The executors of the conformance environment's single runtime. */
169
+ export const relayEffects = (fixture) => runnerEffects(fixture, 0);
170
+ const EXPIRATION_SECONDS = 3;
171
+ /** A poll far longer than any case, so only wakes after commit and `advance` claim rows. */
172
+ const NO_POLL = { poll: "1 hour" };
173
+ const SHORT_LEASE = { lease: "3 seconds" };
174
+ /** Builds a fresh database and `runners` runners on it for one case. */
175
+ const withCluster = (environment, fixture, runners, settings, body) => environment.run(Effect.gen(function* () {
176
+ yield* reset(fixture);
177
+ const database = yield* environment.freshDatabase;
178
+ const spans = settings.spans;
179
+ const cluster = ActorTest.cluster({
180
+ database,
181
+ runners,
182
+ shardLockExpiration: `${EXPIRATION_SECONDS} seconds`,
183
+ actors: Layer.merge(relayLayer(fixture), mintLayer),
184
+ runnerActors: (runner) => (settings.withoutExecutors ?? []).includes(runner)
185
+ ? Layer.empty
186
+ : runnerEffects(fixture, runner),
187
+ relay: settings.relay,
188
+ executors: settings.executors,
189
+ });
190
+ const context = yield* Layer.build(spans === undefined
191
+ ? cluster
192
+ : Layer.provide(cluster, Layer.succeed(Tracer.Tracer, Tracer.make({
193
+ span: (options) => {
194
+ const span = Tracer.nativeTracer.span(options);
195
+ spans.push(span);
196
+ return span;
197
+ },
198
+ })))).pipe(Effect.provideService(TurnHooks, {
199
+ at: (point, request) => Effect.suspend(() => {
200
+ if (point === "afterClaim")
201
+ fixture.claims.set(request.commandId, (fixture.claims.get(request.commandId) ?? 0) + 1);
202
+ return fixture.hook(point, request);
203
+ }),
204
+ }));
205
+ return yield* body.pipe(Effect.provideContext(context));
206
+ }));
207
+ const on = (runner, effect) => ActorCluster.use((cluster) => cluster.on(runner)(effect));
208
+ const stage = (runner, ids, options = {}) => on(runner, Effect.forEach(Array.from({ length: Math.ceil(ids.length / 50) }, (_, index) => ids.slice(index * 50, (index + 1) * 50)), (chunk) => Relayer.get(`relayer-${runner}`).pipe(Effect.flatMap((relayer) => relayer.Stage({ ids: chunk, ...options }))), { discard: true })).pipe(Effect.orDie);
209
+ const perform = (runner, id, effect = "RelayCall") => on(runner, RelayCaller.get(id).pipe(Effect.flatMap((caller) => caller.Perform({ key: id, effect })))).pipe(Effect.orDie);
210
+ const refOf = (id) => on(0, RelayCaller.get(id).pipe(Effect.map((caller) => caller.ref)));
211
+ /** Runs a query on `runner`'s database pool. */
212
+ const query = (runner, statement) => on(runner, Effect.gen(function* () {
213
+ return yield* statement(yield* SqlClient.SqlClient);
214
+ })).pipe(Effect.orDie);
215
+ const receipts = (runner, command) => query(runner, (sql) => sql `SELECT count(*)::int AS count FROM actor_receipts
216
+ WHERE command = ${command}`.pipe(Effect.map((rows) => rows[0].count)));
217
+ const outboxRows = (runner) => query(runner, (sql) => sql `SELECT kind, attempts, ambiguous, last_error, payload, due_at_ms::text AS due,
218
+ floor(extract(epoch FROM clock_timestamp()) * 1000)::text AS now
219
+ FROM actor_outbox ORDER BY intent_id`);
220
+ const deadLetters = (runner) => query(runner, (sql) => sql `SELECT attempts, ambiguous
221
+ FROM actor_dead_letters`);
222
+ const callerState = (runner, id) => on(runner, Effect.gen(function* () {
223
+ const caller = yield* RelayCaller.get(id);
224
+ const { state } = yield* (yield* ActorTest).inspect(caller.ref);
225
+ return state;
226
+ }));
227
+ /** Polls `check` until it holds, dying after `timeout`. */
228
+ const eventually = (check, timeout = "20 seconds", what = "a condition") => check.pipe(Effect.repeat({ schedule: Schedule.spaced("25 millis"), until: (held) => held }), Effect.timeoutOrElse({
229
+ duration: timeout,
230
+ orElse: () => Effect.die(new Error(`Timed out waiting for ${what}`)),
231
+ }), Effect.asVoid);
232
+ const advance = (runner, duration) => on(runner, ActorTest.use((test) => test.advance(duration)));
233
+ const faults = (runner, use) => on(runner, ActorTest.use(use));
234
+ const takenOnce = (fixture, ids) => ids.every((id) => fixture.taken.get(id) === 1);
235
+ /**
236
+ * Whether every claim's `akter.relay.intent` span has ended, so a
237
+ * read of `brokenClaims` sees each claim's whole hold.
238
+ */
239
+ const claimsSettled = (spans) => spans.every((span) => span.name !== SpanNames.relayIntent || Predicate.isTagged(span.status, "Ended"));
240
+ /**
241
+ * Describes each way a row's claims broke "one runner at a time". The relay
242
+ * opens one `akter.relay.intent` span per claim and holds it until
243
+ * the row is deleted or backed off, and the claim returns the row's `attempts`
244
+ * after counting itself. A row claimed again after a failed delivery is a
245
+ * later attempt that starts once the first ended; a second runner holding it
246
+ * concurrently overlaps the first, and two claims taken from one row state
247
+ * carry the same attempt. The runtime's claim count alone cannot tell these
248
+ * apart, because a legitimate retry raises it too.
249
+ */
250
+ const brokenClaims = (spans) => {
251
+ const held = new Map();
252
+ for (const span of spans) {
253
+ if (span.name !== SpanNames.relayIntent || !Predicate.isTagged(span.status, "Ended"))
254
+ continue;
255
+ const row = String(span.attributes.get("command.id"));
256
+ held.set(row, [
257
+ ...(held.get(row) ?? []),
258
+ {
259
+ attempt: span.attributes.get("relay.attempt"),
260
+ startedAt: span.status.startTime,
261
+ endedAt: span.status.endTime,
262
+ },
263
+ ]);
264
+ }
265
+ return [...held].flatMap(([row, claims]) => claims
266
+ .sort((a, b) => (a.startedAt < b.startedAt ? -1 : a.startedAt > b.startedAt ? 1 : 0))
267
+ .flatMap((claim, index) => [
268
+ ...(claim.attempt === index + 1
269
+ ? []
270
+ : [`${row}: claim ${index + 1} returned attempt ${String(claim.attempt)}`]),
271
+ ...(index === 0 || claims[index - 1].endedAt <= claim.startedAt
272
+ ? []
273
+ : [`${row}: claim ${index + 1} started before claim ${index} ended`]),
274
+ ]));
275
+ };
276
+ const percentile = (samples, p) => {
277
+ const sorted = [...samples].sort((a, b) => a - b);
278
+ return sorted[Math.min(sorted.length - 1, Math.ceil(p * sorted.length) - 1)];
279
+ };
280
+ /** The runner that owns `ref`'s shard, and another one. */
281
+ const ownerAndOther = (ref) => Effect.gen(function* () {
282
+ const cluster = yield* ActorCluster;
283
+ const owner = (yield* cluster.owner(ref));
284
+ return { owner, other: (owner + 1) % cluster.runners };
285
+ });
286
+ /** Kills `runner` and waits until the survivors hold its shards. */
287
+ const kill = (runner) => ActorCluster.use((cluster) => cluster.kill(runner).pipe(Effect.andThen(cluster.ready)));
288
+ /** Multi-runner cases: real Postgres only, each on a fresh database and cluster. */
289
+ export const relayClusterConformance = [
290
+ {
291
+ name: "claims each due row on exactly one runner",
292
+ requiresIndependentConnections: true,
293
+ timeoutMs: 120_000,
294
+ run: ({ expect, environment, fixture }) => {
295
+ const spans = [];
296
+ return withCluster(environment, fixture, 3, { spans }, Effect.gen(function* () {
297
+ const ids = Array.from({ length: 300 }, (_, index) => `claimed-${index}`);
298
+ yield* stage(0, ids);
299
+ yield* eventually(Effect.sync(() => takenOnce(fixture, ids)), "60 seconds", "every intent's delivery");
300
+ yield* eventually(outboxRows(1).pipe(Effect.map((rows) => rows.length === 0)), "10 seconds", "the outbox to empty");
301
+ yield* eventually(Effect.sync(() => claimsSettled(spans)), "10 seconds", "every claim to end");
302
+ expect(fixture.claims.size).toBe(300);
303
+ expect(brokenClaims(spans)).toEqual([]);
304
+ expect(yield* receipts(2, "Take")).toBe(300);
305
+ }));
306
+ },
307
+ },
308
+ {
309
+ name: "counts a delivery retried after a failure as the row's next attempt, not a second holder",
310
+ requiresIndependentConnections: true,
311
+ timeoutMs: 120_000,
312
+ run: ({ expect, environment, fixture }) => {
313
+ const spans = [];
314
+ return withCluster(environment, fixture, 3, { spans }, Effect.gen(function* () {
315
+ const ids = Array.from({ length: 30 }, (_, index) => `retried-${index}`);
316
+ const failing = new Set(ids.filter((_, index) => index % 10 === 0));
317
+ const delivering = new Map();
318
+ fixture.hook = (point, request) => point === "afterClaim" && request.command === "Take"
319
+ ? decodeTaken(request.payload).pipe(Effect.map((id) => {
320
+ delivering.set(id, fixture.claims.get(request.commandId));
321
+ }))
322
+ : Effect.void;
323
+ fixture.onTake = (id) => failing.has(id) && delivering.get(id) === 1
324
+ ? Effect.die(new Error("Receiver defect"))
325
+ : Effect.void;
326
+ yield* stage(0, ids);
327
+ yield* eventually(outboxRows(1).pipe(Effect.map((rows) => rows.length === 0)), "60 seconds", "the outbox to empty");
328
+ yield* eventually(Effect.sync(() => claimsSettled(spans)), "10 seconds", "every claim to end");
329
+ expect(ids.filter((id) => delivering.get(id) === 2)).toEqual([
330
+ "retried-0",
331
+ "retried-10",
332
+ "retried-20",
333
+ ]);
334
+ expect(fixture.claims.size).toBe(30);
335
+ expect([...fixture.claims.values()].filter((count) => count === 2).length).toBe(3);
336
+ expect([...fixture.claims.values()].filter((count) => count === 1).length).toBe(27);
337
+ expect(brokenClaims(spans)).toEqual([]);
338
+ expect(yield* receipts(2, "Take")).toBe(30);
339
+ }));
340
+ },
341
+ },
342
+ {
343
+ name: "redelivers a row after its claim lease when the claiming runner is killed",
344
+ requiresIndependentConnections: true,
345
+ timeoutMs: 90_000,
346
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 3, { relay: NO_POLL }, Effect.gen(function* () {
347
+ yield* stage(1, ["killed-before-delivery"], { afterMs: 60_000 });
348
+ const pause = yield* faults(0, (test) => test.pauseNext("beforeDelivery"));
349
+ yield* advance(0, "1 minute").pipe(Effect.forkChild);
350
+ yield* pause.reached;
351
+ yield* kill(0);
352
+ yield* advance(1, "1 minute");
353
+ yield* advance(2, "1 minute");
354
+ expect(fixture.taken.get("killed-before-delivery")).toBe(undefined);
355
+ expect(yield* outboxRows(1)).toMatchObject([{ kind: "intent", attempts: 1 }]);
356
+ yield* advance(1, CLAIM_LEASE);
357
+ expect(fixture.taken.get("killed-before-delivery")).toBe(1);
358
+ expect(yield* receipts(1, "Take")).toBe(1);
359
+ expect(yield* outboxRows(1)).toEqual([]);
360
+ expect([...fixture.claims.values()]).toEqual([2]);
361
+ })),
362
+ },
363
+ {
364
+ name: "redelivers after a runner kill between receiver commit and row deletion with one receiver transition",
365
+ requiresIndependentConnections: true,
366
+ timeoutMs: 90_000,
367
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 3, { relay: NO_POLL }, Effect.gen(function* () {
368
+ yield* stage(1, ["killed-before-delete"], { afterMs: 60_000 });
369
+ const pause = yield* faults(0, (test) => test.pauseNext("beforeOutboxDelete"));
370
+ yield* advance(0, "1 minute").pipe(Effect.forkChild);
371
+ yield* pause.reached;
372
+ expect(yield* receipts(1, "Take")).toBe(1);
373
+ yield* kill(0);
374
+ yield* advance(2, "1 minute");
375
+ expect(yield* outboxRows(2)).toMatchObject([{ kind: "intent", attempts: 1 }]);
376
+ yield* advance(2, CLAIM_LEASE);
377
+ expect(fixture.taken.get("killed-before-delete")).toBe(1);
378
+ expect(yield* receipts(2, "Take")).toBe(1);
379
+ expect(yield* outboxRows(2)).toEqual([]);
380
+ })),
381
+ },
382
+ {
383
+ name: "creates a minted child once when the runner claiming its creating intent is killed",
384
+ requiresIndependentConnections: true,
385
+ timeoutMs: 90_000,
386
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 3, { relay: NO_POLL }, Effect.gen(function* () {
387
+ const id = yield* on(1, planLaterTask("killed-mint")).pipe(Effect.orDie);
388
+ const pause = yield* faults(0, (test) => test.pauseNext("beforeDelivery"));
389
+ yield* advance(0, "1 hour").pipe(Effect.forkChild);
390
+ yield* pause.reached;
391
+ yield* kill(0);
392
+ yield* advance(1, "1 hour");
393
+ yield* advance(2, "1 hour");
394
+ expect(yield* receipts(1, "Open")).toBe(0);
395
+ yield* advance(1, CLAIM_LEASE);
396
+ expect(yield* receipts(1, "Open")).toBe(1);
397
+ expect(yield* outboxRows(1)).toEqual([]);
398
+ expect(yield* on(1, mintedTitle(id)).pipe(Effect.orDie)).toBe("later");
399
+ })),
400
+ },
401
+ {
402
+ name: "does not let rows that die unsettled delay newer due rows",
403
+ requiresIndependentConnections: true,
404
+ timeoutMs: 120_000,
405
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, {}, Effect.gen(function* () {
406
+ const dead = Array.from({ length: 300 }, (_, index) => `dead-${index}`);
407
+ fixture.hook = (point, request) => point === "beforeOutboxDelete" && request.payload.includes("dead-")
408
+ ? Effect.die(new Error("Injected settle crash"))
409
+ : Effect.void;
410
+ yield* stage(0, dead);
411
+ yield* eventually(Effect.sync(() => takenOnce(fixture, dead)), "60 seconds", "every dead row's first delivery");
412
+ const rows = yield* outboxRows(1);
413
+ expect(rows.length).toBe(300);
414
+ expect(rows.every((row) => row.attempts === 1)).toBe(true);
415
+ expect(rows.every((row) => Number(row.due) - Number(row.now) > 30_000)).toBe(true);
416
+ const sent = yield* Clock.currentTimeMillis;
417
+ yield* stage(1, ["fresh"]);
418
+ yield* eventually(Effect.sync(() => fixture.taken.get("fresh") === 1), "5 seconds", "the fresh intent");
419
+ expect((yield* Clock.currentTimeMillis) - sent <= 1100).toBe(true);
420
+ yield* query(1, (sql) => sql `UPDATE actor_outbox SET attempts = 9,
421
+ due_at_ms = floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
422
+ WHERE payload LIKE '%"dead-0"%'`);
423
+ const deadZero = outboxRows(1).pipe(Effect.map((rows) => rows.find((row) => row.payload.includes('"dead-0"'))));
424
+ yield* eventually(deadZero.pipe(Effect.map((row) => row.attempts === 10)), "5 seconds", "the backed-off row's next claim");
425
+ const capped = yield* deadZero;
426
+ const wait = Number(capped.due) - Number(capped.now);
427
+ expect(capped.attempts).toBe(10);
428
+ expect(wait > 250_000 && wait <= 256_000).toBe(true);
429
+ })),
430
+ },
431
+ {
432
+ name: "keeps a stale runner's settle from changing a row another runner claimed",
433
+ requiresIndependentConnections: true,
434
+ timeoutMs: 90_000,
435
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL }, Effect.gen(function* () {
436
+ yield* stage(1, ["stale"], { afterMs: 60_000 });
437
+ const pause = yield* faults(0, (test) => test.pauseNext("afterClaim"));
438
+ const stale = yield* advance(0, "1 minute").pipe(Effect.forkChild);
439
+ yield* pause.reached;
440
+ let defects = 1;
441
+ fixture.onTake = () => defects-- > 0 ? Effect.die(new Error("Receiver defect")) : Effect.void;
442
+ yield* advance(1, "1 minute");
443
+ yield* advance(1, CLAIM_LEASE);
444
+ const [rescheduled] = yield* outboxRows(1);
445
+ expect(rescheduled).toMatchObject({ kind: "intent", attempts: 2 });
446
+ yield* pause.release;
447
+ yield* Fiber.join(stale);
448
+ expect(yield* receipts(1, "Take")).toBe(1);
449
+ const [after] = yield* outboxRows(1);
450
+ expect({ attempts: after.attempts, due: after.due }).toEqual({
451
+ attempts: rescheduled.attempts,
452
+ due: rescheduled.due,
453
+ });
454
+ yield* advance(1, "2 seconds");
455
+ expect(yield* outboxRows(1)).toEqual([]);
456
+ expect(yield* receipts(1, "Take")).toBe(1);
457
+ })),
458
+ },
459
+ {
460
+ name: "delivers intents while every executor slot runs a slow effect",
461
+ requiresIndependentConnections: true,
462
+ timeoutMs: 120_000,
463
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { executors: { concurrency: 4 } }, Effect.gen(function* () {
464
+ let next = 0;
465
+ const deliver = Effect.gen(function* () {
466
+ const id = `timed-${next++}`;
467
+ const started = yield* Clock.currentTimeMillis;
468
+ yield* stage(0, [id]);
469
+ yield* eventually(Effect.sync(() => fixture.taken.get(id) === 1), "10 seconds", `delivery of ${id}`);
470
+ return (yield* Clock.currentTimeMillis) - started;
471
+ });
472
+ const baseline = yield* Effect.replicateEffect(deliver, 30);
473
+ const running = () => fixture.attempts.filter((a) => a.endedAt === undefined).length;
474
+ const gate = yield* Deferred.make();
475
+ fixture.provider = (attempt) => Deferred.await(gate).pipe(Effect.timeout("10 seconds"), Effect.ignore, Effect.as(attempt.key));
476
+ yield* Effect.forEach(Array.from({ length: 16 }, (_, index) => index), (index) => perform(index % 2, `slow-${index}`), { concurrency: 4, discard: true });
477
+ yield* eventually(Effect.sync(() => running() === 8), "10 seconds", "every slot to be busy");
478
+ const blocked = yield* Effect.replicateEffect(deliver, 30);
479
+ expect(running()).toBe(8);
480
+ yield* Deferred.succeed(gate, undefined);
481
+ expect(percentile(blocked, 0.99) <= percentile(baseline, 0.99) + 1000).toBe(true);
482
+ })),
483
+ },
484
+ {
485
+ name: "renews an executor lease so a long attempt is not taken over",
486
+ requiresIndependentConnections: true,
487
+ timeoutMs: 90_000,
488
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { executors: SHORT_LEASE }, Effect.gen(function* () {
489
+ fixture.provider = (attempt) => Effect.sleep("9 seconds").pipe(Effect.as(attempt.key));
490
+ yield* perform(0, "long");
491
+ yield* eventually(callerState(1, "long").pipe(Effect.map((state) => (state.called ?? []).length === 1)), "30 seconds", "the long attempt's route");
492
+ expect(fixture.attempts.map(({ attempt, interrupted }) => [attempt, interrupted])).toEqual([[1, false]]);
493
+ expect(yield* receipts(1, "Called")).toBe(1);
494
+ })),
495
+ },
496
+ {
497
+ name: "interrupts an attempt that loses its lease and routes at most one result",
498
+ requiresIndependentConnections: true,
499
+ timeoutMs: 90_000,
500
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
501
+ const { owner, other } = yield* ownerAndOther(yield* refOf("lost"));
502
+ fixture.provider = (attempt) => attempt.attempt === 1 ? Effect.never : Effect.succeed(attempt.key);
503
+ const renewal = yield* faults(owner, (test) => test.pauseNext("beforeRenew"));
504
+ yield* perform(owner, "lost");
505
+ yield* renewal.reached;
506
+ const takeover = yield* faults(other, (test) => test.pauseNext("beforeExecute"));
507
+ const draining = yield* advance(other, "4 seconds").pipe(Effect.forkChild);
508
+ yield* takeover.reached;
509
+ expect(yield* outboxRows(other)).toMatchObject([
510
+ { kind: "job", attempts: 2, ambiguous: true },
511
+ ]);
512
+ yield* takeover.release;
513
+ yield* Fiber.join(draining);
514
+ yield* renewal.release;
515
+ yield* eventually(Effect.sync(() => fixture.attempts[0]?.interrupted === true), "10 seconds", "attempt 1's interruption");
516
+ const [lost] = fixture.attempts;
517
+ expect(lost.endedAt - lost.startedAt < 2900).toBe(true);
518
+ expect(fixture.attempts.map(({ attempt, runner }) => [attempt, runner])).toEqual([
519
+ [1, owner],
520
+ [2, other],
521
+ ]);
522
+ expect(yield* receipts(other, "Called")).toBe(1);
523
+ expect((yield* callerState(other, "lost")).called).toEqual(["lost"]);
524
+ })),
525
+ },
526
+ {
527
+ name: "dead-letters as ambiguous when the last attempt's lease expires",
528
+ requiresIndependentConnections: true,
529
+ timeoutMs: 90_000,
530
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
531
+ const { owner, other } = yield* ownerAndOther(yield* refOf("mid-call"));
532
+ fixture.provider = () => Effect.never;
533
+ yield* perform(owner, "mid-call", "RelayCallOnce");
534
+ yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
535
+ yield* kill(owner);
536
+ yield* advance(other, "4 seconds");
537
+ expect((yield* callerState(other, "mid-call")).letters).toMatchObject([
538
+ { attempts: 1, ambiguous: true },
539
+ ]);
540
+ expect(yield* deadLetters(other)).toEqual([{ attempts: 1, ambiguous: true }]);
541
+ expect(yield* receipts(other, "OnceFailed")).toBe(1);
542
+ expect(fixture.attempts.length).toBe(1);
543
+ })),
544
+ },
545
+ {
546
+ name: "claims effects only on runners that have their executor",
547
+ requiresIndependentConnections: true,
548
+ timeoutMs: 90_000,
549
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { withoutExecutors: [0] }, Effect.gen(function* () {
550
+ const cluster = yield* ActorCluster;
551
+ let index = 0;
552
+ let effectClaims = 0;
553
+ fixture.hook = (point, request) => Effect.sync(() => {
554
+ if (point === "afterClaim" && request.command === "RelayCall")
555
+ effectClaims++;
556
+ });
557
+ while ((yield* cluster.owner(yield* refOf(`elsewhere-${index}`))) !== 0)
558
+ index++;
559
+ const id = `elsewhere-${index}`;
560
+ yield* perform(0, id);
561
+ yield* eventually(callerState(1, id).pipe(Effect.map((state) => (state.called ?? []).length === 1)), "20 seconds", "the route");
562
+ expect(fixture.attempts.map(({ attempt, runner }) => [attempt, runner])).toEqual([[1, 1]]);
563
+ expect(effectClaims).toBe(1);
564
+ expect(yield* receipts(0, "Called")).toBe(1);
565
+ })),
566
+ },
567
+ {
568
+ name: "uses per-effect timeout and backoff from policy.effects",
569
+ requiresIndependentConnections: true,
570
+ timeoutMs: 60_000,
571
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 1, { relay: NO_POLL }, Effect.gen(function* () {
572
+ fixture.provider = (attempt) => Effect.sleep("1 second").pipe(Effect.as(attempt.key));
573
+ yield* query(0, (sql) => Effect.gen(function* () {
574
+ yield* sql `CREATE TABLE relay_waits (attempts int, wait bigint)`;
575
+ yield* sql `CREATE FUNCTION relay_record_wait() RETURNS trigger LANGUAGE plpgsql AS $$
576
+ BEGIN
577
+ INSERT INTO relay_waits VALUES (NEW.attempts,
578
+ NEW.due_at_ms - floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint);
579
+ RETURN NEW;
580
+ END $$`;
581
+ yield* sql `CREATE TRIGGER relay_record_wait AFTER UPDATE ON actor_outbox FOR EACH ROW
582
+ WHEN (NEW.kind = 'job' AND NEW.last_error NOT LIKE 'Attempt % ended without%')
583
+ EXECUTE FUNCTION relay_record_wait()`;
584
+ }));
585
+ yield* perform(0, "timed", "RelayTimed");
586
+ yield* eventually(advance(0, 0).pipe(Effect.andThen(deadLetters(0)), Effect.map((letters) => letters.length === 1)), "20 seconds", "the dead letter");
587
+ const waits = yield* query(0, (sql) => sql `SELECT attempts, wait::text AS wait
588
+ FROM relay_waits ORDER BY attempts`);
589
+ expect(waits.map(({ attempts }) => attempts)).toEqual([1, 2, 3, 4]);
590
+ for (const [index, backoff] of [10, 20, 40].entries()) {
591
+ const wait = Number(waits[index].wait);
592
+ expect(wait <= backoff && wait > backoff - 50).toBe(true);
593
+ }
594
+ expect(fixture.attempts.length).toBe(4);
595
+ for (const attempt of fixture.attempts) {
596
+ const ran = attempt.endedAt - attempt.startedAt;
597
+ expect(attempt.interrupted && ran >= 95 && ran < 900).toBe(true);
598
+ }
599
+ expect(yield* outboxRows(0)).toEqual([]);
600
+ expect(yield* deadLetters(0)).toEqual([{ attempts: 4, ambiguous: true }]);
601
+ })),
602
+ },
603
+ {
604
+ name: "claims no more intents than free delivery slots",
605
+ requiresIndependentConnections: true,
606
+ timeoutMs: 120_000,
607
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 1, { relay: { deliveryConcurrency: 8, claimLease: "3 seconds" } }, Effect.gen(function* () {
608
+ const ids = Array.from({ length: 64 }, (_, index) => `slow-${index}`);
609
+ const claimedAt = new Map();
610
+ const startedAt = new Map();
611
+ fixture.hook = (point, request) => Effect.map(Clock.currentTimeMillis, (now) => {
612
+ if (point === "afterClaim")
613
+ claimedAt.set(request.payload, now);
614
+ if (point === "beforeDelivery")
615
+ startedAt.set(request.payload, now);
616
+ });
617
+ fixture.onTake = () => Effect.sleep("1 second");
618
+ const samples = [];
619
+ const sampler = yield* query(0, (sql) => sql `SELECT count(*)::int AS claimed FROM actor_outbox
620
+ WHERE attempts > 0`).pipe(Effect.tap((rows) => Effect.sync(() => samples.push(rows[0].claimed))), Effect.repeat(Schedule.spaced("50 millis")), Effect.forkChild);
621
+ yield* stage(0, ids);
622
+ yield* eventually(Effect.sync(() => takenOnce(fixture, ids)), "60 seconds", "every slow delivery");
623
+ yield* Fiber.interrupt(sampler);
624
+ expect(samples.length > 20).toBe(true);
625
+ expect(Math.max(...samples) <= 8).toBe(true);
626
+ expect([...fixture.claims.values()].every((count) => count === 1)).toBe(true);
627
+ expect(claimedAt.size).toBe(64);
628
+ for (const [payload, claimed] of claimedAt)
629
+ expect(startedAt.get(payload) - claimed < 1000).toBe(true);
630
+ })),
631
+ },
632
+ {
633
+ name: "routes a lease-expired attempt's success when it beats the takeover",
634
+ requiresIndependentConnections: true,
635
+ timeoutMs: 90_000,
636
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
637
+ const { owner, other } = yield* ownerAndOther(yield* refOf("beats"));
638
+ const first = yield* Deferred.make();
639
+ const second = yield* Deferred.make();
640
+ fixture.provider = (attempt) => attempt.attempt === 1
641
+ ? Deferred.await(first).pipe(Effect.as(attempt.key))
642
+ : Deferred.await(second).pipe(Effect.andThen(Effect.fail(ProviderDown.make({}))));
643
+ const renewal = yield* faults(owner, (test) => test.pauseNext("beforeRenew"));
644
+ yield* perform(owner, "beats");
645
+ yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
646
+ const takeover = yield* advance(other, "4 seconds").pipe(Effect.forkChild);
647
+ yield* eventually(Effect.sync(() => fixture.attempts.length === 2));
648
+ yield* Deferred.succeed(first, undefined);
649
+ yield* eventually(receipts(other, "Called").pipe(Effect.map((count) => count === 1)), "10 seconds", "attempt 1's route");
650
+ yield* Deferred.succeed(second, undefined);
651
+ yield* Fiber.join(takeover);
652
+ yield* renewal.release;
653
+ yield* eventually(Effect.sync(() => fixture.attempts[1]?.endedAt !== undefined));
654
+ expect((yield* callerState(other, "beats")).called).toEqual(["beats"]);
655
+ expect(yield* deadLetters(other)).toEqual([]);
656
+ expect(yield* receipts(other, "CallFailed")).toBe(0);
657
+ })),
658
+ },
659
+ {
660
+ name: "keeps a newer attempt's row when an attempt that lost its lease fails late",
661
+ requiresIndependentConnections: true,
662
+ timeoutMs: 90_000,
663
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
664
+ const { owner, other } = yield* ownerAndOther(yield* refOf("stale"));
665
+ const first = yield* Deferred.make();
666
+ const second = yield* Deferred.make();
667
+ fixture.provider = (attempt) => attempt.attempt === 1
668
+ ? Deferred.await(first).pipe(Effect.andThen(Effect.fail(ProviderDown.make({}))))
669
+ : Deferred.await(second).pipe(Effect.as(attempt.key));
670
+ const renewal = yield* faults(owner, (test) => test.pauseNext("beforeRenew"));
671
+ yield* perform(owner, "stale");
672
+ yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
673
+ const takeover = yield* advance(other, "4 seconds").pipe(Effect.forkChild);
674
+ yield* eventually(Effect.sync(() => fixture.attempts.length === 2));
675
+ yield* Deferred.succeed(first, undefined);
676
+ yield* eventually(Effect.sync(() => fixture.attempts[0]?.endedAt !== undefined));
677
+ expect(fixture.attempts[0].interrupted).toBe(false);
678
+ yield* Effect.sleep("1 second");
679
+ const [held] = yield* outboxRows(other);
680
+ expect(held).toMatchObject({ kind: "job", attempts: 2, ambiguous: true });
681
+ expect(held.last_error).toContain("Attempt 2");
682
+ yield* Deferred.succeed(second, undefined);
683
+ yield* Fiber.join(takeover);
684
+ yield* renewal.release;
685
+ yield* eventually(receipts(other, "Called").pipe(Effect.map((count) => count === 1)), "10 seconds", "attempt 2's route");
686
+ expect(fixture.attempts.map(({ attempt, runner }) => [attempt, runner])).toEqual([
687
+ [1, owner],
688
+ [2, other],
689
+ ]);
690
+ expect((yield* callerState(other, "stale")).called).toEqual(["stale"]);
691
+ expect(yield* deadLetters(other)).toEqual([]);
692
+ expect(yield* receipts(other, "CallFailed")).toBe(0);
693
+ })),
694
+ },
695
+ {
696
+ name: "marks the dead letter ambiguous when a stale success arrives after it",
697
+ requiresIndependentConnections: true,
698
+ timeoutMs: 90_000,
699
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
700
+ const { owner, other } = yield* ownerAndOther(yield* refOf("late"));
701
+ const first = yield* Deferred.make();
702
+ fixture.provider = (attempt) => attempt.attempt === 1
703
+ ? Deferred.await(first).pipe(Effect.as(attempt.key))
704
+ : Effect.fail(ProviderDown.make({}));
705
+ const renewal = yield* faults(owner, (test) => test.pauseNext("beforeRenew"));
706
+ yield* perform(owner, "late");
707
+ yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
708
+ yield* advance(other, "4 seconds");
709
+ expect(yield* deadLetters(other)).toEqual([{ attempts: 2, ambiguous: false }]);
710
+ expect((yield* callerState(other, "late")).letters).toMatchObject([
711
+ { attempts: 2, ambiguous: false },
712
+ ]);
713
+ yield* Deferred.succeed(first, undefined);
714
+ yield* eventually(deadLetters(other).pipe(Effect.map((letters) => letters[0]?.ambiguous === true)), "10 seconds", "the late success");
715
+ yield* renewal.release;
716
+ expect(yield* receipts(other, "Called")).toBe(0);
717
+ expect(yield* receipts(other, "CallFailed")).toBe(1);
718
+ expect((yield* callerState(other, "late")).letters).toMatchObject([
719
+ { attempts: 2, ambiguous: false },
720
+ ]);
721
+ })),
722
+ },
723
+ {
724
+ name: "interrupts an attempt at its local deadline when renewals cannot reach the database",
725
+ requiresIndependentConnections: true,
726
+ timeoutMs: 90_000,
727
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
728
+ const { owner, other } = yield* ownerAndOther(yield* refOf("partitioned"));
729
+ fixture.provider = (attempt) => attempt.attempt === 1 ? Effect.never : Effect.succeed(attempt.key);
730
+ fixture.hook = (point) => point === "beforeRenew" ? Effect.die(new Error("Database unreachable")) : Effect.void;
731
+ yield* perform(owner, "partitioned");
732
+ yield* eventually(Effect.sync(() => fixture.attempts[0]?.interrupted === true), "10 seconds", "the local deadline");
733
+ const [first] = fixture.attempts;
734
+ const ran = first.endedAt - first.startedAt;
735
+ const [held] = yield* outboxRows(other);
736
+ expect(ran >= 2900 && ran < 4000).toBe(true);
737
+ expect(fixture.attempts.length).toBe(1);
738
+ expect(held).toMatchObject({ kind: "job", attempts: 1, ambiguous: true });
739
+ expect(first.endedAt <= Number(held.due) + 100).toBe(true);
740
+ fixture.hook = () => Effect.void;
741
+ yield* advance(other, "4 seconds");
742
+ expect(fixture.attempts.map(({ attempt }) => attempt)).toEqual([1, 2]);
743
+ expect(yield* receipts(other, "Called")).toBe(1);
744
+ })),
745
+ },
746
+ {
747
+ name: "does not start an attempt whose claim outlived its lease before execution",
748
+ requiresIndependentConnections: true,
749
+ timeoutMs: 90_000,
750
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
751
+ const { owner, other } = yield* ownerAndOther(yield* refOf("stalled"));
752
+ fixture.provider = (attempt) => Effect.succeed(attempt.key);
753
+ const stalled = yield* faults(owner, (test) => test.pauseNext("beforeExecute"));
754
+ yield* perform(owner, "stalled");
755
+ yield* stalled.reached;
756
+ yield* Effect.sleep("3100 millis");
757
+ yield* advance(other, "4 seconds");
758
+ expect(fixture.attempts.map(({ attempt, runner }) => [attempt, runner])).toEqual([
759
+ [2, other],
760
+ ]);
761
+ yield* stalled.release;
762
+ yield* advance(owner, "0 seconds");
763
+ expect(fixture.attempts.map(({ attempt, runner }) => [attempt, runner])).toEqual([
764
+ [2, other],
765
+ ]);
766
+ expect(yield* receipts(other, "Called")).toBe(1);
767
+ expect((yield* callerState(other, "stalled")).called).toEqual(["stalled"]);
768
+ })),
769
+ },
770
+ {
771
+ name: "keeps a failure's backoff when a renewal races the settle",
772
+ requiresIndependentConnections: true,
773
+ timeoutMs: 90_000,
774
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 1, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
775
+ const failing = yield* Deferred.make();
776
+ fixture.provider = () => Deferred.await(failing).pipe(Effect.andThen(Effect.fail(ProviderDown.make({}))));
777
+ const renewal = yield* faults(0, (test) => test.pauseNext("beforeRenew"));
778
+ yield* perform(0, "raced");
779
+ yield* renewal.reached;
780
+ yield* Deferred.succeed(failing, undefined);
781
+ yield* eventually(outboxRows(0).pipe(Effect.map((rows) => rows[0]?.last_error?.includes("ProviderDown") === true)));
782
+ yield* renewal.release;
783
+ yield* Effect.sleep("200 millis");
784
+ const [row] = yield* outboxRows(0);
785
+ expect(row).toMatchObject({ kind: "job", attempts: 1, ambiguous: false });
786
+ expect(Number(row.due) - Number(row.now) <= 1000).toBe(true);
787
+ })),
788
+ },
789
+ {
790
+ name: "routes one result when the runner is killed after the provider succeeded",
791
+ requiresIndependentConnections: true,
792
+ timeoutMs: 90_000,
793
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
794
+ const { owner, other } = yield* ownerAndOther(yield* refOf("acked"));
795
+ const pause = yield* faults(owner, (test) => test.pauseNext("afterExecute"));
796
+ yield* perform(owner, "acked");
797
+ yield* pause.reached;
798
+ yield* kill(owner);
799
+ yield* advance(other, "4 seconds");
800
+ expect(fixture.attempts.map(({ attempt, runner }) => [attempt, runner])).toEqual([
801
+ [1, owner],
802
+ [2, other],
803
+ ]);
804
+ expect(yield* receipts(other, "Called")).toBe(1);
805
+ expect((yield* callerState(other, "acked")).called).toEqual(["acked"]);
806
+ })),
807
+ },
808
+ ];
809
+ const releasesOnShutdown = (point, name) => ({
810
+ name,
811
+ run: ({ expect, environment, fixture }) => Effect.runPromise(Effect.gen(function* () {
812
+ const claimedAt = yield* Effect.promise(() => environment.run(Effect.gen(function* () {
813
+ yield* reset(fixture);
814
+ const test = yield* ActorTest;
815
+ const sender = yield* Relayer.get("shutdown");
816
+ yield* sender.Stage({ ids: ["shutdown"], afterMs: 60_000 });
817
+ const pause = yield* test.pauseNext(point);
818
+ yield* test.advance("1 minute").pipe(Effect.forkDetach);
819
+ yield* pause.reached;
820
+ return DateTime.toEpochMillis(yield* test.now);
821
+ })));
822
+ yield* environment.restart;
823
+ yield* Effect.promise(() => environment.run(Effect.gen(function* () {
824
+ const sql = yield* SqlClient.SqlClient;
825
+ const test = yield* ActorTest;
826
+ const row = sql `
827
+ SELECT attempts, due_at_ms::text AS due FROM actor_outbox
828
+ WHERE payload LIKE '%"shutdown"%'`;
829
+ const [released] = yield* row;
830
+ expect(released.attempts).toBe(1);
831
+ expect(Number(released.due) - claimedAt < 5000).toBe(true);
832
+ const wait = Number(released.due) - DateTime.toEpochMillis(yield* test.now);
833
+ yield* test.advance(Math.max(0, wait));
834
+ expect(fixture.taken.get("shutdown")).toBe(1);
835
+ expect(yield* row).toEqual([]);
836
+ })));
837
+ })),
838
+ });
839
+ /** Single-runner cases on the conformance environment, shared by PGlite and Postgres. */
840
+ export const relayConformance = [
841
+ releasesOnShutdown("afterClaim", "releases claimed but unstarted rows on graceful shutdown"),
842
+ releasesOnShutdown("beforeDelivery", "releases a claimed row whose delivery had not reached a receiver on graceful shutdown"),
843
+ {
844
+ name: "keeps scheduled_at_ms across claims while due_at_ms moves",
845
+ run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
846
+ yield* reset(fixture);
847
+ const sql = yield* SqlClient.SqlClient;
848
+ const test = yield* ActorTest;
849
+ const sender = yield* Relayer.get("scheduled");
850
+ const at = DateTime.toEpochMillis(yield* test.now) + 60_000;
851
+ yield* sender.Stage({ ids: ["scheduled"], atMs: at });
852
+ let defects = 1;
853
+ fixture.onTake = () => defects-- > 0 ? Effect.die(new Error("Receiver defect")) : Effect.void;
854
+ yield* test.advance("1 minute");
855
+ const rows = sql `
856
+ SELECT attempts, due_at_ms::text AS due, scheduled_at_ms::text AS scheduled
857
+ FROM actor_outbox WHERE payload LIKE '%"scheduled"%'`;
858
+ const [claimed] = yield* rows;
859
+ expect(claimed).toMatchObject({ attempts: 1, scheduled: String(at) });
860
+ expect(Number(claimed.due) > at).toBe(true);
861
+ yield* test.advance("1 second");
862
+ expect(fixture.taken.get("scheduled")).toBe(2);
863
+ expect(yield* rows).toEqual([]);
864
+ })),
865
+ },
866
+ {
867
+ name: "backs off a row whose settle dies by max(claim lease, backoff(attempts)) up to maxBackoff",
868
+ run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
869
+ yield* reset(fixture);
870
+ const sql = yield* SqlClient.SqlClient;
871
+ const test = yield* ActorTest;
872
+ const sender = yield* Relayer.get("backoff");
873
+ yield* sender.Stage({ ids: ["backoff"], afterMs: 60_000 });
874
+ const rows = Effect.gen(function* () {
875
+ const now = DateTime.toEpochMillis(yield* test.now);
876
+ return yield* sql `
877
+ SELECT attempts, (due_at_ms - ${now})::text AS wait
878
+ FROM actor_outbox WHERE payload LIKE '%"backoff"%'`;
879
+ });
880
+ yield* test.crashNext("beforeOutboxDelete");
881
+ yield* test.advance("1 minute");
882
+ const [first] = yield* rows;
883
+ expect(first.attempts).toBe(1);
884
+ expect(Number(first.wait) > 35_000 && Number(first.wait) <= 37_000).toBe(true);
885
+ yield* sql `UPDATE actor_outbox SET attempts = 12, due_at_ms = 0
886
+ WHERE payload LIKE '%"backoff"%'`;
887
+ yield* test.crashNext("beforeOutboxDelete");
888
+ yield* test.advance(0);
889
+ const [capped] = yield* rows;
890
+ expect(capped.attempts).toBe(13);
891
+ expect(Number(capped.wait) > 254_000 && Number(capped.wait) <= 256_000).toBe(true);
892
+ yield* test.advance("256 seconds");
893
+ expect(fixture.taken.get("backoff")).toBe(1);
894
+ expect(yield* rows).toEqual([]);
895
+ })),
896
+ },
897
+ {
898
+ name: "saturates intent backoff at the largest accepted maxBackoff",
899
+ run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
900
+ const sql = yield* SqlClient.SqlClient;
901
+ const maxBackoffMs = 2_147_483_647;
902
+ yield* sql `INSERT INTO actor_generations (routing_key, tenant_id, actor_type, actor_id)
903
+ VALUES (1, 'saturate', 'Ghost', 'saturate')`;
904
+ yield* sql `INSERT INTO actor_outbox (routing_key, intent_id, bucket, due_at_ms,
905
+ scheduled_at_ms, tenant_id, actor_type, actor_id, target_type, target_id, command,
906
+ payload, caller, kind, attempts)
907
+ VALUES (1, 'saturate', 0, 0, 0, 'saturate', 'Ghost', 'saturate', 'Ghost', 'saturate',
908
+ 'Haunt', '{}', '{}', 'intent', 30)`;
909
+ const claimed = yield* claimIntents({
910
+ sql,
911
+ now: 1,
912
+ limit: 1,
913
+ leaseMs: 37_000,
914
+ maxBackoffMs,
915
+ }).pipe(Effect.ensuring(Effect.gen(function* () {
916
+ yield* sql `DELETE FROM actor_outbox WHERE tenant_id = 'saturate'`;
917
+ yield* sql `DELETE FROM actor_generations WHERE tenant_id = 'saturate'`;
918
+ }).pipe(Effect.orDie)));
919
+ expect(claimed.map((row) => [row.attempts, Number(row.claimed_until)])).toEqual([
920
+ [31, 1 + maxBackoffMs],
921
+ ]);
922
+ })),
923
+ },
924
+ {
925
+ name: "claims later due intents while other transactions hold the earliest rows locked",
926
+ requiresFreshDatabase: true,
927
+ requiresIndependentConnections: true,
928
+ timeoutMs: 60_000,
929
+ run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 1, { relay: { deliveryConcurrency: 1, poll: "100 millis" } }, Effect.gen(function* () {
930
+ const ids = ["locked-1", "locked-2", "locked-3", "unlocked-4"];
931
+ for (const [index, id] of ids.entries())
932
+ yield* stage(0, [id], { afterMs: 60_000 + index * 1000 });
933
+ const locked = yield* Deferred.make();
934
+ const release = yield* Deferred.make();
935
+ const holder = yield* query(0, (sql) => sql.withTransaction(Effect.gen(function* () {
936
+ yield* sql `SELECT intent_id FROM actor_outbox WHERE kind = 'intent'
937
+ ORDER BY due_at_ms LIMIT 3 FOR UPDATE`;
938
+ yield* Deferred.succeed(locked, undefined);
939
+ yield* Deferred.await(release);
940
+ }))).pipe(Effect.forkChild);
941
+ yield* Deferred.await(locked);
942
+ yield* advance(0, "2 minutes");
943
+ yield* eventually(Effect.sync(() => fixture.taken.get("unlocked-4") === 1), "20 seconds", "the unlocked intent");
944
+ expect(ids.slice(0, 3).map((id) => fixture.taken.get(id))).toEqual([
945
+ undefined,
946
+ undefined,
947
+ undefined,
948
+ ]);
949
+ yield* Deferred.succeed(release, undefined);
950
+ yield* Fiber.join(holder);
951
+ yield* eventually(Effect.sync(() => takenOnce(fixture, ids)), "20 seconds", "every intent");
952
+ })),
953
+ },
954
+ {
955
+ name: "dead-letters an already exhausted row with its recorded outcome",
956
+ run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
957
+ yield* reset(fixture);
958
+ const sql = yield* SqlClient.SqlClient;
959
+ const test = yield* ActorTest;
960
+ fixture.provider = () => Effect.fail(ProviderDown.make({}));
961
+ const caller = yield* RelayCaller.get("exhausted");
962
+ yield* sql `CREATE OR REPLACE FUNCTION relay_refuse_dead_letter() RETURNS trigger
963
+ LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'dead letter refused'; END $$`;
964
+ yield* sql `CREATE TRIGGER relay_refuse_dead_letter BEFORE INSERT ON actor_dead_letters
965
+ FOR EACH ROW EXECUTE FUNCTION relay_refuse_dead_letter()`;
966
+ yield* caller
967
+ .Perform({ key: "exhausted", effect: "RelayCallOnce" })
968
+ .pipe(Effect.andThen(test.advance(0)), Effect.ensuring(sql `DROP TRIGGER relay_refuse_dead_letter ON actor_dead_letters`.pipe(Effect.orDie)));
969
+ const pending = sql `SELECT attempts, ambiguous FROM actor_outbox
970
+ WHERE actor_type = 'RelayCaller' AND actor_id = 'exhausted'`;
971
+ expect(yield* pending).toEqual([{ attempts: 1, ambiguous: false }]);
972
+ yield* test.advance("1 second");
973
+ expect(fixture.attempts.length).toBe(1);
974
+ expect(yield* pending).toEqual([]);
975
+ expect(yield* sql `SELECT attempts, ambiguous FROM actor_dead_letters
976
+ WHERE tenant_id = ${test.tenant} AND actor_id = 'exhausted'`).toEqual([{ attempts: 1, ambiguous: false }]);
977
+ expect(yield* test.receiptsFor(caller.ref, "OnceFailed")).toBe(1);
978
+ })),
979
+ },
980
+ {
981
+ name: "claims intents without reading due effect rows that no runner can execute",
982
+ timeoutMs: 60_000,
983
+ run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
984
+ const sql = yield* SqlClient.SqlClient;
985
+ const test = yield* ActorTest;
986
+ yield* test.advance(0);
987
+ const now = DateTime.toEpochMillis(yield* test.now);
988
+ yield* sql `INSERT INTO actor_generations (routing_key, tenant_id, actor_type, actor_id)
989
+ SELECT ((i % 256) - 128)::bigint << 56 | i, 'ghost', 'Ghost', i::text
990
+ FROM generate_series(1, 10000) AS i`;
991
+ yield* sql `INSERT INTO actor_outbox (routing_key, intent_id, bucket, due_at_ms,
992
+ scheduled_at_ms, tenant_id, actor_type, actor_id, target_type, target_id, command,
993
+ payload, caller, kind, ready_at_ms)
994
+ SELECT ((i % 256) - 128)::bigint << 56 | i, 'ghost-' || i, (i % 256) - 128, 0, 0,
995
+ 'ghost', 'Ghost', i::text, 'Ghost', i::text, 'Haunt', '{}', '{}', 'job', 0
996
+ FROM generate_series(1, 10000) AS i`;
997
+ yield* sql `ANALYZE actor_outbox`;
998
+ const [text, parameters] = claimIntents({
999
+ sql,
1000
+ now,
1001
+ limit: 16,
1002
+ leaseMs: 37_000,
1003
+ maxBackoffMs: 256_000,
1004
+ }).compile();
1005
+ class Explained extends Schema.TaggedError()("Explained", {
1006
+ plan: Schema.Unknown,
1007
+ }) {
1008
+ }
1009
+ const explained = yield* sql
1010
+ .withTransaction(sql
1011
+ .unsafe(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${text}`, parameters)
1012
+ .pipe(Effect.flatMap((rows) => Effect.fail(Explained.make({ plan: rows[0]["QUERY PLAN"] })))))
1013
+ .pipe(Effect.flip, Effect.ensuring(Effect.gen(function* () {
1014
+ yield* sql `DELETE FROM actor_outbox WHERE tenant_id = 'ghost'`;
1015
+ yield* sql `DELETE FROM actor_generations WHERE tenant_id = 'ghost'`;
1016
+ }).pipe(Effect.orDie)));
1017
+ expect(explained).toBeInstanceOf(Explained);
1018
+ const [output] = yield* Schema.decodeUnknownEffect(ExplainOutput)(Schema.is(Explained)(explained) ? explained.plan : undefined).pipe(Effect.orDie);
1019
+ const nodes = planNodes(output.Plan);
1020
+ const due = nodes.filter((node) => node["Index Name"] === "actor_outbox_due_kind");
1021
+ expect(nodes.some((node) => node["Node Type"] === "Seq Scan")).toBe(false);
1022
+ expect(due.length > 0).toBe(true);
1023
+ expect(due.every((node) => node["Actual Rows"] === 0)).toBe(true);
1024
+ expect(due.every((node) => (node["Rows Removed by Filter"] ?? 0) === 0)).toBe(true);
1025
+ })),
1026
+ },
1027
+ {
1028
+ name: "claims a runner's own effects past due effect rows it cannot execute in the same bucket",
1029
+ run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
1030
+ yield* reset(fixture);
1031
+ const sql = yield* SqlClient.SqlClient;
1032
+ const test = yield* ActorTest;
1033
+ const caller = yield* RelayCaller.get("starved");
1034
+ yield* caller.Perform({ key: "starved-1", effect: "RelayCall" });
1035
+ yield* test.advance(0);
1036
+ const [placed] = yield* sql `
1037
+ SELECT (routing_key >> 56)::int AS bucket FROM actor_generations
1038
+ WHERE tenant_id = ${test.tenant} AND actor_type = 'RelayCaller' AND actor_id = 'starved'`;
1039
+ yield* sql `INSERT INTO actor_generations (routing_key, tenant_id, actor_type, actor_id)
1040
+ SELECT (${placed.bucket}::bigint << 56) | i, 'orphan', 'Orphan', i::text
1041
+ FROM generate_series(1, 200) AS i`;
1042
+ yield* sql `INSERT INTO actor_outbox (routing_key, intent_id, bucket, due_at_ms,
1043
+ scheduled_at_ms, tenant_id, actor_type, actor_id, target_type, target_id, command,
1044
+ payload, caller, kind, ready_at_ms)
1045
+ SELECT (${placed.bucket}::bigint << 56) | i, 'orphan-' || i, ${placed.bucket}, 0, 0,
1046
+ 'orphan', 'Orphan', i::text, 'Orphan', i::text, 'Haunt', '{}', '{}', 'job', 0
1047
+ FROM generate_series(1, 200) AS i`;
1048
+ yield* Effect.gen(function* () {
1049
+ yield* caller.Perform({ key: "starved-2", effect: "RelayCall" });
1050
+ yield* test.advance(0);
1051
+ expect(fixture.attempts.map(({ key }) => key)).toEqual(["starved-1", "starved-2"]);
1052
+ expect(yield* test.receiptsFor(caller.ref, "Called")).toBe(2);
1053
+ }).pipe(Effect.ensuring(Effect.gen(function* () {
1054
+ yield* sql `DELETE FROM actor_outbox WHERE tenant_id = 'orphan'`;
1055
+ yield* sql `DELETE FROM actor_generations WHERE tenant_id = 'orphan'`;
1056
+ }).pipe(Effect.orDie)));
1057
+ })),
1058
+ },
1059
+ {
1060
+ name: "drains a due backlog far larger than the delivery slots in one advance",
1061
+ timeoutMs: 120_000,
1062
+ run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
1063
+ yield* reset(fixture);
1064
+ const test = yield* ActorTest;
1065
+ const ids = Array.from({ length: 2000 }, (_, index) => `backlog-${index}`);
1066
+ yield* Effect.forEach(Array.from({ length: 40 }, (_, index) => ids.slice(index * 50, (index + 1) * 50)), (chunk) => Relayer.get("backlog").pipe(Effect.flatMap((relayer) => relayer.Stage({ ids: chunk, afterMs: 60_000 }))), { discard: true });
1067
+ yield* test.advance("1 minute");
1068
+ expect(takenOnce(fixture, ids)).toBe(true);
1069
+ })),
1070
+ },
1071
+ {
1072
+ name: "rejects relay, executor, and per-effect timings out of range",
1073
+ run: ({ expect }) => Effect.runPromise(Effect.sync(() => {
1074
+ const rejects = (build) => {
1075
+ try {
1076
+ build();
1077
+ return undefined;
1078
+ }
1079
+ catch (error) {
1080
+ return String(error);
1081
+ }
1082
+ };
1083
+ expect(rejects(() => {
1084
+ runtimeLayer({ executors: { lease: "2 seconds" } });
1085
+ })).toContain("executors.lease must be at least 3 seconds");
1086
+ expect(rejects(() => {
1087
+ runtimeLayer({ relay: { deliveryConcurrency: 0 } });
1088
+ })).not.toBe(undefined);
1089
+ expect(rejects(() => {
1090
+ runtimeLayer({ executors: { lease: "3 seconds", concurrency: 1 } });
1091
+ })).toBe(undefined);
1092
+ const Probe = Actor.job("Probe", {});
1093
+ const make = (binding) => rejects(() => {
1094
+ Actor.make("TimingProbe", {
1095
+ key: Schema.String,
1096
+ jobs: { Probe: { job: Probe, ...binding } },
1097
+ api: {},
1098
+ });
1099
+ });
1100
+ expect(make({ timeout: "0 millis" })).toContain("jobs.Probe.timeout");
1101
+ expect(make({ timeout: Duration.millis(2 ** 31) })).toContain("jobs.Probe.timeout");
1102
+ expect(make({ retry: { times: 1, backoff: { base: "2 seconds", max: "1 second" } } })).toContain("must be at least its base");
1103
+ expect(make({ retry: { times: 1, backoff: { base: "0 millis", max: "1 second" } } })).toContain("jobs.Probe.retry.backoff.base");
1104
+ expect(make({
1105
+ timeout: "5 seconds",
1106
+ retry: { times: 2, backoff: { base: "10 millis", max: "1 minute" } },
1107
+ })).toBe(undefined);
1108
+ })),
1109
+ },
1110
+ ];
1111
+ /** Relay actors and the executors of runner 0. */
1112
+ export const relaySuite = {
1113
+ fixture: relayFixture,
1114
+ layer: (fixture) => Layer.merge(relayLayer(fixture), relayEffects(fixture)),
1115
+ };