@adonis-agora/durable 1.0.0-rc.2 → 1.0.0-rc.20

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 (398) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/dist/assets/spa/assets/index-BgoNV3TR.css +1 -0
  3. package/dist/assets/spa/assets/index-DzXygyDm.js +57 -0
  4. package/dist/assets/spa/assets/index-DzXygyDm.js.map +1 -0
  5. package/dist/assets/spa/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  6. package/dist/assets/spa/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  7. package/dist/assets/spa/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  8. package/dist/assets/spa/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  9. package/dist/assets/spa/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  10. package/dist/assets/spa/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  11. package/dist/assets/spa/assets/sora-latin-400-normal-CRt88UEn.woff2 +0 -0
  12. package/dist/assets/spa/assets/sora-latin-400-normal-OW7qkl5a.woff +0 -0
  13. package/dist/assets/spa/assets/sora-latin-500-normal-01eiPEn0.woff2 +0 -0
  14. package/dist/assets/spa/assets/sora-latin-500-normal-w58xtEt9.woff +0 -0
  15. package/dist/assets/spa/assets/sora-latin-600-normal-1_7fyUAY.woff +0 -0
  16. package/dist/assets/spa/assets/sora-latin-600-normal-Cdg4DaK0.woff2 +0 -0
  17. package/dist/assets/spa/assets/sora-latin-700-normal-9waGdLWo.woff2 +0 -0
  18. package/dist/assets/spa/assets/sora-latin-700-normal-BKPfQAnC.woff +0 -0
  19. package/dist/assets/spa/index.html +2 -8
  20. package/dist/commands/agora_doctor.d.ts +23 -0
  21. package/dist/commands/agora_doctor.d.ts.map +1 -0
  22. package/dist/commands/agora_doctor.js +110 -0
  23. package/dist/commands/agora_doctor.js.map +1 -0
  24. package/dist/commands/durable_doctor.d.ts +8 -0
  25. package/dist/commands/durable_doctor.d.ts.map +1 -0
  26. package/dist/commands/durable_doctor.js +8 -0
  27. package/dist/commands/durable_doctor.js.map +1 -0
  28. package/dist/commands/durable_worker.d.ts +3 -3
  29. package/dist/commands/durable_worker.js +7 -7
  30. package/dist/commands/durable_worker.js.map +1 -1
  31. package/dist/commands/health.d.ts +12 -0
  32. package/dist/commands/health.d.ts.map +1 -0
  33. package/dist/commands/health.js +34 -0
  34. package/dist/commands/health.js.map +1 -0
  35. package/dist/commands/lifecycle_republish.d.ts +8 -9
  36. package/dist/commands/lifecycle_republish.d.ts.map +1 -1
  37. package/dist/commands/lifecycle_republish.js +29 -16
  38. package/dist/commands/lifecycle_republish.js.map +1 -1
  39. package/dist/commands/main.d.ts +25 -5
  40. package/dist/commands/main.d.ts.map +1 -1
  41. package/dist/commands/main.js +54 -7
  42. package/dist/commands/main.js.map +1 -1
  43. package/dist/commands/runs.d.ts +9 -16
  44. package/dist/commands/runs.d.ts.map +1 -1
  45. package/dist/commands/runs.js +60 -47
  46. package/dist/commands/runs.js.map +1 -1
  47. package/dist/commands/runs_bulk.d.ts +17 -0
  48. package/dist/commands/runs_bulk.d.ts.map +1 -0
  49. package/dist/commands/runs_bulk.js +74 -0
  50. package/dist/commands/runs_bulk.js.map +1 -0
  51. package/dist/commands/runs_cancel.d.ts +15 -0
  52. package/dist/commands/runs_cancel.d.ts.map +1 -0
  53. package/dist/commands/runs_cancel.js +39 -0
  54. package/dist/commands/runs_cancel.js.map +1 -0
  55. package/dist/commands/runs_continue.d.ts +12 -0
  56. package/dist/commands/runs_continue.d.ts.map +1 -0
  57. package/dist/commands/runs_continue.js +28 -0
  58. package/dist/commands/runs_continue.js.map +1 -0
  59. package/dist/commands/runs_event.d.ts +13 -0
  60. package/dist/commands/runs_event.d.ts.map +1 -0
  61. package/dist/commands/runs_event.js +28 -0
  62. package/dist/commands/runs_event.js.map +1 -0
  63. package/dist/commands/runs_fan.d.ts +16 -0
  64. package/dist/commands/runs_fan.d.ts.map +1 -0
  65. package/dist/commands/runs_fan.js +58 -0
  66. package/dist/commands/runs_fan.js.map +1 -0
  67. package/dist/commands/runs_redispatch.d.ts +14 -0
  68. package/dist/commands/runs_redispatch.d.ts.map +1 -0
  69. package/dist/commands/runs_redispatch.js +35 -0
  70. package/dist/commands/runs_redispatch.js.map +1 -0
  71. package/dist/commands/runs_replay.d.ts +14 -0
  72. package/dist/commands/runs_replay.d.ts.map +1 -0
  73. package/dist/commands/runs_replay.js +38 -0
  74. package/dist/commands/runs_replay.js.map +1 -0
  75. package/dist/commands/runs_retry.d.ts +13 -0
  76. package/dist/commands/runs_retry.d.ts.map +1 -0
  77. package/dist/commands/runs_retry.js +29 -0
  78. package/dist/commands/runs_retry.js.map +1 -0
  79. package/dist/commands/runs_show.d.ts +14 -0
  80. package/dist/commands/runs_show.d.ts.map +1 -0
  81. package/dist/commands/runs_show.js +96 -0
  82. package/dist/commands/runs_show.js.map +1 -0
  83. package/dist/commands/runs_signal.d.ts +17 -0
  84. package/dist/commands/runs_signal.d.ts.map +1 -0
  85. package/dist/commands/runs_signal.js +49 -0
  86. package/dist/commands/runs_signal.js.map +1 -0
  87. package/dist/commands/runs_update.d.ts +15 -0
  88. package/dist/commands/runs_update.d.ts.map +1 -0
  89. package/dist/commands/runs_update.js +39 -0
  90. package/dist/commands/runs_update.js.map +1 -0
  91. package/dist/commands/runs_values.d.ts +15 -0
  92. package/dist/commands/runs_values.d.ts.map +1 -0
  93. package/dist/commands/runs_values.js +45 -0
  94. package/dist/commands/runs_values.js.map +1 -0
  95. package/dist/commands/schedules.d.ts +18 -0
  96. package/dist/commands/schedules.d.ts.map +1 -0
  97. package/dist/commands/schedules.js +70 -0
  98. package/dist/commands/schedules.js.map +1 -0
  99. package/dist/commands/schedules_pause.d.ts +16 -0
  100. package/dist/commands/schedules_pause.d.ts.map +1 -0
  101. package/dist/commands/schedules_pause.js +40 -0
  102. package/dist/commands/schedules_pause.js.map +1 -0
  103. package/dist/commands/schedules_resume.d.ts +8 -0
  104. package/dist/commands/schedules_resume.d.ts.map +1 -0
  105. package/dist/commands/schedules_resume.js +8 -0
  106. package/dist/commands/schedules_resume.js.map +1 -0
  107. package/dist/commands/schedules_trigger.d.ts +13 -0
  108. package/dist/commands/schedules_trigger.d.ts.map +1 -0
  109. package/dist/commands/schedules_trigger.js +32 -0
  110. package/dist/commands/schedules_trigger.js.map +1 -0
  111. package/dist/commands/tasks_complete.d.ts +15 -0
  112. package/dist/commands/tasks_complete.d.ts.map +1 -0
  113. package/dist/commands/tasks_complete.js +41 -0
  114. package/dist/commands/tasks_complete.js.map +1 -0
  115. package/dist/commands/tasks_fail.d.ts +16 -0
  116. package/dist/commands/tasks_fail.d.ts.map +1 -0
  117. package/dist/commands/tasks_fail.js +47 -0
  118. package/dist/commands/tasks_fail.js.map +1 -0
  119. package/dist/commands/workers.d.ts +11 -0
  120. package/dist/commands/workers.d.ts.map +1 -0
  121. package/dist/commands/workers.js +35 -0
  122. package/dist/commands/workers.js.map +1 -0
  123. package/dist/commands/workflows_lifecycle.d.ts +12 -0
  124. package/dist/commands/workflows_lifecycle.d.ts.map +1 -0
  125. package/dist/commands/workflows_lifecycle.js +37 -0
  126. package/dist/commands/workflows_lifecycle.js.map +1 -0
  127. package/dist/configure.d.ts +1 -1
  128. package/dist/configure.js +1 -1
  129. package/dist/providers/dashboard_provider.d.ts +57 -54
  130. package/dist/providers/dashboard_provider.d.ts.map +1 -1
  131. package/dist/providers/dashboard_provider.js +399 -288
  132. package/dist/providers/dashboard_provider.js.map +1 -1
  133. package/dist/providers/durable_provider.d.ts.map +1 -1
  134. package/dist/providers/durable_provider.js +67 -37
  135. package/dist/providers/durable_provider.js.map +1 -1
  136. package/dist/src/commands/console_command.d.ts +82 -0
  137. package/dist/src/commands/console_command.d.ts.map +1 -0
  138. package/dist/src/commands/console_command.js +238 -0
  139. package/dist/src/commands/console_command.js.map +1 -0
  140. package/dist/src/commands/runs.js +2 -2
  141. package/dist/src/commands/runs.js.map +1 -1
  142. package/dist/src/concurrency.d.ts.map +1 -1
  143. package/dist/src/concurrency.js +1 -0
  144. package/dist/src/concurrency.js.map +1 -1
  145. package/dist/src/config_types.d.ts +79 -26
  146. package/dist/src/config_types.d.ts.map +1 -1
  147. package/dist/src/dashboard/access_denied_page.d.ts +5 -7
  148. package/dist/src/dashboard/access_denied_page.d.ts.map +1 -1
  149. package/dist/src/dashboard/access_denied_page.js +2 -6
  150. package/dist/src/dashboard/access_denied_page.js.map +1 -1
  151. package/dist/src/dashboard/audit.d.ts +14 -0
  152. package/dist/src/dashboard/audit.d.ts.map +1 -0
  153. package/dist/src/dashboard/audit.js +36 -0
  154. package/dist/src/dashboard/audit.js.map +1 -0
  155. package/dist/src/dashboard/console_authorize.d.ts +98 -0
  156. package/dist/src/dashboard/console_authorize.d.ts.map +1 -0
  157. package/dist/src/dashboard/console_authorize.js +163 -0
  158. package/dist/src/dashboard/console_authorize.js.map +1 -0
  159. package/dist/src/dashboard/console_shell.d.ts +45 -0
  160. package/dist/src/dashboard/console_shell.d.ts.map +1 -0
  161. package/dist/src/dashboard/console_shell.js +125 -0
  162. package/dist/src/dashboard/console_shell.js.map +1 -0
  163. package/dist/src/dashboard/console_switcher.d.ts +132 -0
  164. package/dist/src/dashboard/console_switcher.d.ts.map +1 -0
  165. package/dist/src/dashboard/console_switcher.js +201 -0
  166. package/dist/src/dashboard/console_switcher.js.map +1 -0
  167. package/dist/src/dashboard/define_config.d.ts +68 -81
  168. package/dist/src/dashboard/define_config.d.ts.map +1 -1
  169. package/dist/src/dashboard/define_config.js +18 -112
  170. package/dist/src/dashboard/define_config.js.map +1 -1
  171. package/dist/src/dashboard/dev_tools.d.ts +102 -0
  172. package/dist/src/dashboard/dev_tools.d.ts.map +1 -0
  173. package/dist/src/dashboard/dev_tools.js +502 -0
  174. package/dist/src/dashboard/dev_tools.js.map +1 -0
  175. package/dist/src/dashboard/diagnostics-recorder.d.ts +1 -1
  176. package/dist/src/dashboard/diagnostics-recorder.js +1 -1
  177. package/dist/src/dashboard/gateway-adapter.d.ts +6 -6
  178. package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
  179. package/dist/src/dashboard/gateway-adapter.js +5 -5
  180. package/dist/src/dashboard/gateway-adapter.js.map +1 -1
  181. package/dist/src/dashboard/handlers.d.ts +48 -39
  182. package/dist/src/dashboard/handlers.d.ts.map +1 -1
  183. package/dist/src/dashboard/handlers.js +181 -83
  184. package/dist/src/dashboard/handlers.js.map +1 -1
  185. package/dist/src/dashboard/index.d.ts +12 -9
  186. package/dist/src/dashboard/index.d.ts.map +1 -1
  187. package/dist/src/dashboard/index.js +10 -7
  188. package/dist/src/dashboard/index.js.map +1 -1
  189. package/dist/src/dashboard/list-filters.d.ts +27 -0
  190. package/dist/src/dashboard/list-filters.d.ts.map +1 -0
  191. package/dist/src/dashboard/list-filters.js +69 -0
  192. package/dist/src/dashboard/list-filters.js.map +1 -0
  193. package/dist/src/dashboard/openapi.d.ts.map +1 -1
  194. package/dist/src/dashboard/openapi.js +127 -52
  195. package/dist/src/dashboard/openapi.js.map +1 -1
  196. package/dist/src/dashboard/run-filter.d.ts +18 -15
  197. package/dist/src/dashboard/run-filter.d.ts.map +1 -1
  198. package/dist/src/dashboard/run-filter.js +30 -22
  199. package/dist/src/dashboard/run-filter.js.map +1 -1
  200. package/dist/src/dashboard/spa.d.ts +26 -1
  201. package/dist/src/dashboard/spa.d.ts.map +1 -1
  202. package/dist/src/dashboard/spa.js +9 -2
  203. package/dist/src/dashboard/spa.js.map +1 -1
  204. package/dist/src/dashboard/tenant_scope.d.ts +38 -0
  205. package/dist/src/dashboard/tenant_scope.d.ts.map +1 -0
  206. package/dist/src/dashboard/tenant_scope.js +166 -0
  207. package/dist/src/dashboard/tenant_scope.js.map +1 -0
  208. package/dist/src/define_config.d.ts +27 -12
  209. package/dist/src/define_config.d.ts.map +1 -1
  210. package/dist/src/define_config.js +9 -4
  211. package/dist/src/define_config.js.map +1 -1
  212. package/dist/src/diagnostics/index.d.ts +11 -0
  213. package/dist/src/diagnostics/index.d.ts.map +1 -0
  214. package/dist/src/diagnostics/index.js +6 -0
  215. package/dist/src/diagnostics/index.js.map +1 -0
  216. package/dist/src/diagnostics/transport.d.ts +71 -0
  217. package/dist/src/diagnostics/transport.d.ts.map +1 -0
  218. package/dist/src/diagnostics/transport.js +188 -0
  219. package/dist/src/diagnostics/transport.js.map +1 -0
  220. package/dist/src/diagnostics-bridge.d.ts +2 -1
  221. package/dist/src/diagnostics-bridge.d.ts.map +1 -1
  222. package/dist/src/diagnostics-bridge.js +7 -1
  223. package/dist/src/diagnostics-bridge.js.map +1 -1
  224. package/dist/src/doctor/agora_doctor.d.ts +116 -0
  225. package/dist/src/doctor/agora_doctor.d.ts.map +1 -0
  226. package/dist/src/doctor/agora_doctor.js +685 -0
  227. package/dist/src/doctor/agora_doctor.js.map +1 -0
  228. package/dist/src/doctor/checks.d.ts +28 -0
  229. package/dist/src/doctor/checks.d.ts.map +1 -0
  230. package/dist/src/doctor/checks.js +288 -0
  231. package/dist/src/doctor/checks.js.map +1 -0
  232. package/dist/src/doctor/index.d.ts +9 -0
  233. package/dist/src/doctor/index.d.ts.map +1 -0
  234. package/dist/src/doctor/index.js +16 -0
  235. package/dist/src/doctor/index.js.map +1 -0
  236. package/dist/src/engine.d.ts +55 -9
  237. package/dist/src/engine.d.ts.map +1 -1
  238. package/dist/src/engine.js +333 -84
  239. package/dist/src/engine.js.map +1 -1
  240. package/dist/src/errors.d.ts +9 -0
  241. package/dist/src/errors.d.ts.map +1 -1
  242. package/dist/src/errors.js +13 -0
  243. package/dist/src/errors.js.map +1 -1
  244. package/dist/src/event-trigger-bridge.d.ts +5 -14
  245. package/dist/src/event-trigger-bridge.d.ts.map +1 -1
  246. package/dist/src/event-trigger-bridge.js +11 -99
  247. package/dist/src/event-trigger-bridge.js.map +1 -1
  248. package/dist/src/index.d.ts +8 -5
  249. package/dist/src/index.d.ts.map +1 -1
  250. package/dist/src/index.js +6 -4
  251. package/dist/src/index.js.map +1 -1
  252. package/dist/src/interfaces.d.ts +53 -9
  253. package/dist/src/interfaces.d.ts.map +1 -1
  254. package/dist/src/interfaces.js +8 -0
  255. package/dist/src/interfaces.js.map +1 -1
  256. package/dist/src/origin.js +3 -3
  257. package/dist/src/role_bindings.d.ts +8 -8
  258. package/dist/src/role_bindings.js +8 -8
  259. package/dist/src/run-gateway/interface.d.ts +4 -3
  260. package/dist/src/run-gateway/interface.d.ts.map +1 -1
  261. package/dist/src/run-gateway/proxy-run-gateway.d.ts +4 -2
  262. package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
  263. package/dist/src/run-gateway/proxy-run-gateway.js +21 -6
  264. package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
  265. package/dist/src/run-gateway/run-request-responder.d.ts +6 -1
  266. package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
  267. package/dist/src/run-gateway/run-request-responder.js +30 -3
  268. package/dist/src/run-gateway/run-request-responder.js.map +1 -1
  269. package/dist/src/run-gateway/store-run-gateway.d.ts +4 -4
  270. package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
  271. package/dist/src/run-gateway/store-run-gateway.js +4 -4
  272. package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
  273. package/dist/src/run-gateway/tenant-auth.d.ts +19 -1
  274. package/dist/src/run-gateway/tenant-auth.d.ts.map +1 -1
  275. package/dist/src/run-gateway/tenant-auth.js +133 -0
  276. package/dist/src/run-gateway/tenant-auth.js.map +1 -1
  277. package/dist/src/run-gateway/tenant-scoped-gateway.d.ts +10 -0
  278. package/dist/src/run-gateway/tenant-scoped-gateway.d.ts.map +1 -0
  279. package/dist/src/run-gateway/tenant-scoped-gateway.js +107 -0
  280. package/dist/src/run-gateway/tenant-scoped-gateway.js.map +1 -0
  281. package/dist/src/run-value-facets.d.ts +1 -1
  282. package/dist/src/run-value-facets.d.ts.map +1 -1
  283. package/dist/src/run-value-facets.js +5 -1
  284. package/dist/src/run-value-facets.js.map +1 -1
  285. package/dist/src/scheduler.d.ts.map +1 -1
  286. package/dist/src/scheduler.js +2 -1
  287. package/dist/src/scheduler.js.map +1 -1
  288. package/dist/src/schedules.d.ts +16 -1
  289. package/dist/src/schedules.d.ts.map +1 -1
  290. package/dist/src/schedules.js +29 -4
  291. package/dist/src/schedules.js.map +1 -1
  292. package/dist/src/services/main.d.ts +3 -3
  293. package/dist/src/services/main.js +6 -6
  294. package/dist/src/services/main.js.map +1 -1
  295. package/dist/src/singleton-admission.d.ts.map +1 -1
  296. package/dist/src/singleton-admission.js +2 -0
  297. package/dist/src/singleton-admission.js.map +1 -1
  298. package/dist/src/singleton-gate.d.ts +1 -1
  299. package/dist/src/singleton-gate.d.ts.map +1 -1
  300. package/dist/src/singleton-gate.js +4 -3
  301. package/dist/src/singleton-gate.js.map +1 -1
  302. package/dist/src/sql-schema.d.ts +54 -0
  303. package/dist/src/sql-schema.d.ts.map +1 -0
  304. package/dist/src/sql-schema.js +145 -0
  305. package/dist/src/sql-schema.js.map +1 -0
  306. package/dist/src/stores/factory.d.ts +6 -0
  307. package/dist/src/stores/factory.d.ts.map +1 -1
  308. package/dist/src/stores/factory.js +3 -0
  309. package/dist/src/stores/factory.js.map +1 -1
  310. package/dist/src/stores/lucid-mappers.d.ts +3 -0
  311. package/dist/src/stores/lucid-mappers.d.ts.map +1 -1
  312. package/dist/src/stores/lucid-mappers.js +12 -0
  313. package/dist/src/stores/lucid-mappers.js.map +1 -1
  314. package/dist/src/stores/lucid-pg-hot-path.d.ts +6 -2
  315. package/dist/src/stores/lucid-pg-hot-path.d.ts.map +1 -1
  316. package/dist/src/stores/lucid-pg-hot-path.js +7 -3
  317. package/dist/src/stores/lucid-pg-hot-path.js.map +1 -1
  318. package/dist/src/stores/lucid-schema.d.ts +18 -4
  319. package/dist/src/stores/lucid-schema.d.ts.map +1 -1
  320. package/dist/src/stores/lucid-schema.js +106 -61
  321. package/dist/src/stores/lucid-schema.js.map +1 -1
  322. package/dist/src/stores/lucid.d.ts +42 -2
  323. package/dist/src/stores/lucid.d.ts.map +1 -1
  324. package/dist/src/stores/lucid.js +450 -231
  325. package/dist/src/stores/lucid.js.map +1 -1
  326. package/dist/src/tenancy.d.ts +104 -0
  327. package/dist/src/tenancy.d.ts.map +1 -0
  328. package/dist/src/tenancy.js +91 -0
  329. package/dist/src/tenancy.js.map +1 -0
  330. package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
  331. package/dist/src/testing/in-memory-state-store.js +9 -3
  332. package/dist/src/testing/in-memory-state-store.js.map +1 -1
  333. package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
  334. package/dist/src/testing-kit/state-store-conformance.js +59 -0
  335. package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
  336. package/dist/src/testing-kit/transport-conformance.d.ts.map +1 -1
  337. package/dist/src/testing-kit/transport-conformance.js +5 -1
  338. package/dist/src/testing-kit/transport-conformance.js.map +1 -1
  339. package/dist/src/transports/bullmq/bullmq-transport.d.ts +1 -1
  340. package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
  341. package/dist/src/transports/bullmq/bullmq-transport.js +14 -3
  342. package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
  343. package/dist/src/transports/db-schema.d.ts +13 -5
  344. package/dist/src/transports/db-schema.d.ts.map +1 -1
  345. package/dist/src/transports/db-schema.js +52 -27
  346. package/dist/src/transports/db-schema.js.map +1 -1
  347. package/dist/src/transports/db.d.ts +6 -0
  348. package/dist/src/transports/db.d.ts.map +1 -1
  349. package/dist/src/transports/db.js +91 -42
  350. package/dist/src/transports/db.js.map +1 -1
  351. package/dist/src/transports/factory.d.ts +16 -0
  352. package/dist/src/transports/factory.d.ts.map +1 -1
  353. package/dist/src/transports/factory.js +6 -1
  354. package/dist/src/transports/factory.js.map +1 -1
  355. package/dist/src/transports/queue.d.ts +2 -2
  356. package/dist/src/transports/queue.d.ts.map +1 -1
  357. package/dist/src/transports/queue.js +49 -8
  358. package/dist/src/transports/queue.js.map +1 -1
  359. package/dist/src/transports/worker-keys-db.d.ts +5 -0
  360. package/dist/src/transports/worker-keys-db.d.ts.map +1 -1
  361. package/dist/src/transports/worker-keys-db.js +10 -8
  362. package/dist/src/transports/worker-keys-db.js.map +1 -1
  363. package/dist/src/workflow-discovery.d.ts +1 -1
  364. package/dist/src/workflow-discovery.d.ts.map +1 -1
  365. package/dist/src/workflow-discovery.js +4 -7
  366. package/dist/src/workflow-discovery.js.map +1 -1
  367. package/dist/src/workflow-events.d.ts +20 -51
  368. package/dist/src/workflow-events.d.ts.map +1 -1
  369. package/dist/src/workflow-events.js +12 -30
  370. package/dist/src/workflow-events.js.map +1 -1
  371. package/dist/src/workflow-ref.d.ts +1 -1
  372. package/dist/stubs/config/durable_dashboard.stub +9 -29
  373. package/dist/stubs/database/migrations/create_durable_tables.stub +12 -2
  374. package/dist/stubs/database/migrations/create_durable_transport_messages.stub +34 -5
  375. package/dist/stubs/database/migrations/create_durable_transport_tables.stub +30 -8
  376. package/dist/stubs/database/migrations/create_durable_transport_worker_keys.stub +24 -2
  377. package/package.json +25 -7
  378. package/skills/durable-cluster/SKILL.md +43 -23
  379. package/skills/durable-reliability/SKILL.md +13 -7
  380. package/dist/assets/spa/assets/index-70hGzOnT.css +0 -1
  381. package/dist/assets/spa/assets/index-CJjMAUau.js +0 -16
  382. package/dist/assets/spa/assets/index-CJjMAUau.js.map +0 -1
  383. package/dist/commands/retry.d.ts +0 -16
  384. package/dist/commands/retry.d.ts.map +0 -1
  385. package/dist/commands/retry.js +0 -34
  386. package/dist/commands/retry.js.map +0 -1
  387. package/dist/src/dashboard/auth.d.ts +0 -134
  388. package/dist/src/dashboard/auth.d.ts.map +0 -1
  389. package/dist/src/dashboard/auth.js +0 -156
  390. package/dist/src/dashboard/auth.js.map +0 -1
  391. package/dist/src/dashboard/login_page.d.ts +0 -36
  392. package/dist/src/dashboard/login_page.d.ts.map +0 -1
  393. package/dist/src/dashboard/login_page.js +0 -158
  394. package/dist/src/dashboard/login_page.js.map +0 -1
  395. package/dist/src/dashboard/session_cookie.d.ts +0 -45
  396. package/dist/src/dashboard/session_cookie.d.ts.map +0 -1
  397. package/dist/src/dashboard/session_cookie.js +0 -97
  398. package/dist/src/dashboard/session_cookie.js.map +0 -1
@@ -1,13 +1,16 @@
1
- import { SETTLEABLE_RUN_STATUSES } from '../interfaces.js';
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { RunAlreadyExistsError } from '../errors.js';
3
+ import { RECOVERABLE_RUN_STATUSES, SETTLEABLE_RUN_STATUSES } from '../interfaces.js';
2
4
  import { namespaceFilterValues, normalizeNamespace } from '../namespaces.js';
3
5
  import { mergeRunFacetRows } from '../run-facets.js';
4
6
  import { runPageWindow } from '../run-pagination.js';
5
7
  import { axisIsRunColumn, mergeRunValueFacetRows, RUN_VALUE_FACET_LIMIT, RUN_VALUE_FACET_SCAN, runValueFacetsFromRuns, } from '../run-value-facets.js';
6
8
  import { attributeColumnFor, attributeOperand, attributeOperands, normalizeAttributeRows, sqlComparator, } from '../search-attributes.js';
7
9
  import { SINGLETON_ACTIVE_STATUSES, SINGLETON_ADMITTED_TAG, settlesTerminally, singletonAdmissionDenialPatch, singletonAdmissionTags, withoutAdmission, } from '../singleton-admission.js';
10
+ import { assertSchemaSupported, hasColumn, hasTable, qualify, qualifyAll, schemaExists as schemaExistsIn, validateSchemaName, } from '../sql-schema.js';
8
11
  import { CHECKPOINT_COLUMNS_BUT_INPUT, checkpointToRow, parseJsonColumn, rowToCheckpoint, rowToRun, rowToSignalWaiter, runPatchToRow, runToRow, SETTLED_CHECKPOINT_COLUMNS, settlementToRow, } from './lucid-mappers.js';
9
12
  import { LucidPgHotPath, quote } from './lucid-pg-hot-path.js';
10
- import { createDurableTables, DURABLE_TABLES } from './lucid-schema.js';
13
+ import { createDurableTables, DURABLE_SCHEMA_META_TABLE, DURABLE_TABLES } from './lucid-schema.js';
11
14
  /**
12
15
  * A production-grade, persistent `StateStore` backed by AdonisJS **Lucid** (Knex). Runs, checkpoints,
13
16
  * timers, signal waiters, run attributes and recovery leases live in SQL, so durable state survives
@@ -25,11 +28,22 @@ export class LucidStateStore {
25
28
  options;
26
29
  /** The namespace run reads are fenced to, or undefined for the operator view. */
27
30
  scopeNamespace;
31
+ /** The schema the tables live in, or undefined for the connection's current one. */
32
+ schema;
33
+ /** The durable table names, qualified with {@link schema}, for the query builder. */
34
+ t;
35
+ /** The Postgres hot path's statements over {@link t}. */
36
+ sql;
28
37
  constructor(db, options = {}) {
29
38
  this.db = db;
30
39
  this.connectionName = options.connectionName;
31
40
  this.options = options;
32
41
  this.scopeNamespace = options.scope?.namespace;
42
+ this.schema = validateSchemaName(options.schema, 'stores.lucid');
43
+ if (this.schema !== undefined)
44
+ assertSchemaSupported(this.client(), this.schema, 'stores.lucid');
45
+ this.t = qualifyAll(DURABLE_TABLES, this.schema);
46
+ this.sql = hotStatements(this.schema);
33
47
  }
34
48
  /**
35
49
  * A store over the same `Database` whose run reads are confined to `scope.namespace` (see
@@ -51,19 +65,108 @@ export class LucidStateStore {
51
65
  }
52
66
  /**
53
67
  * The Postgres hot path (see `lucid-pg-hot-path.ts`): the statements a turn and a step result run
54
- * on every step, as constant SQL. `undefined` on any other dialect, or with Lucid's `debug` on.
55
- * Resolved per call, like {@link client}: under a global transaction it is the transaction's.
68
+ * on every step, as constant SQL. `undefined` on any other dialect, with Lucid's `debug` on, or
69
+ * under a (global) transaction. Resolved per call, like {@link client}.
56
70
  */
57
71
  hot() {
72
+ // Not on a transaction (an app's global one): see `LucidPgHotPath.for`.
58
73
  return LucidPgHotPath.for(this.client(), {
59
74
  preparedStatements: this.options.preparedStatements,
60
75
  });
61
76
  }
77
+ /**
78
+ * The hot reads with explicit column lists: each table's base columns plus the later-wave ones it
79
+ * has, probed once (an older schema without them keeps working) and again after this store's own
80
+ * {@link ensureSchema}. A column another process adds later is not read until the next probe, and a
81
+ * statement already prepared keeps its shape.
82
+ */
83
+ hotReadStatements;
84
+ hotReads(hot) {
85
+ this.hotReadStatements ??= hot
86
+ .rows(COLUMNS_PRESENT_SQL, [
87
+ this.schema ?? null,
88
+ [DURABLE_TABLES.runs, DURABLE_TABLES.checkpoints],
89
+ [...RUN_OPTIONAL_COLUMNS, ...CHECKPOINT_OPTIONAL_COLUMNS],
90
+ ])
91
+ .then((rows) => {
92
+ const present = new Set(rows.map((r) => `${String(r.table_name)}.${String(r.column_name)}`));
93
+ const has = (table) => (c) => present.has(`${table}.${c}`);
94
+ const runCols = [
95
+ ...RUN_BASE_COLUMNS,
96
+ ...RUN_OPTIONAL_COLUMNS.filter(has(DURABLE_TABLES.runs)),
97
+ ];
98
+ const checkpointCols = [
99
+ ...CHECKPOINT_BASE_COLUMNS,
100
+ ...CHECKPOINT_OPTIONAL_COLUMNS.filter(has(DURABLE_TABLES.checkpoints)),
101
+ ];
102
+ return this.sql.reads(runCols.map(quote).join(', '), checkpointCols.map(quote).join(', '));
103
+ })
104
+ .catch((error) => {
105
+ this.hotReadStatements = undefined; // a failed probe is retried, never cached
106
+ throw error;
107
+ });
108
+ return this.hotReadStatements;
109
+ }
62
110
  /** Idempotently provision the durable tables (called on boot when `autoSchema` is on). */
63
111
  async ensureSchema() {
64
- await createDurableTables(this.db, this.connectionName, this.options.collation !== undefined ? { collation: this.options.collation } : {});
112
+ await createDurableTables(this.db, this.connectionName, {
113
+ ...(this.options.collation !== undefined ? { collation: this.options.collation } : {}),
114
+ ...(this.schema !== undefined ? { schema: this.schema } : {}),
115
+ });
116
+ // The tables may have gained columns: the next hot read probes them again.
117
+ this.hotReadStatements = undefined;
118
+ }
119
+ /** The database schema the store's tables live in, or `undefined` for the connection's current one. */
120
+ get schemaName() {
121
+ return this.schema;
122
+ }
123
+ /**
124
+ * What `agora:doctor` reports for a store with a `schema`: whether the schema exists, which durable
125
+ * tables are missing from it, which of `optionalTables` (tables other pieces create when they first
126
+ * run, like the db transport's) are in it, and whether the connection can write there (a probe row
127
+ * inserted into the schema marker table, in a transaction that is rolled back).
128
+ */
129
+ async inspectSchema(optionalTables = []) {
130
+ const client = this.client();
131
+ const schema = this.schema;
132
+ const schemaExists = schema === undefined || (await schemaExistsIn(client, schema));
133
+ const missingTables = [];
134
+ for (const table of [...Object.values(DURABLE_TABLES), DURABLE_SCHEMA_META_TABLE]) {
135
+ if (!schemaExists || !(await hasTable(client, schema, table)))
136
+ missingTables.push(table);
137
+ }
138
+ const optionalPresent = [];
139
+ if (schemaExists) {
140
+ for (const table of optionalTables) {
141
+ if (await hasTable(client, schema, table))
142
+ optionalPresent.push(table);
143
+ }
144
+ }
145
+ let writeError;
146
+ if (!missingTables.includes(DURABLE_SCHEMA_META_TABLE)) {
147
+ const rollback = new Error('durable doctor probe rollback');
148
+ try {
149
+ await client.transaction(async (trx) => {
150
+ await trx
151
+ .insertQuery()
152
+ .table(qualify(schema, DURABLE_SCHEMA_META_TABLE))
153
+ .insert({ id: '__doctor_probe__', fingerprint: 'probe', applied_at: Date.now() });
154
+ throw rollback;
155
+ });
156
+ }
157
+ catch (error) {
158
+ if (error !== rollback)
159
+ writeError = error instanceof Error ? error.message : String(error);
160
+ }
161
+ }
162
+ return { schema, schemaExists, missingTables, optionalPresent, writeError };
65
163
  }
66
164
  // --- runs ---------------------------------------------------------------
165
+ /**
166
+ * Insert the run, or reject with {@link RunAlreadyExistsError} when its id is taken. The insert
167
+ * writes nothing on a conflict instead of failing, so a run that already exists never aborts a
168
+ * surrounding transaction (an app's test `wrapInGlobalTransaction`, on Postgres).
169
+ */
67
170
  async createRun(run) {
68
171
  if (normalizeAttributeRows(run.id, run.searchAttributes).length === 0) {
69
172
  // Nothing to index: the insert, then clear any attribute rows a deleted run of this id left,
@@ -72,17 +175,23 @@ export class LucidStateStore {
72
175
  if (hot) {
73
176
  const row = runToRow(run);
74
177
  const keys = Object.keys(row);
75
- await hot.run(insertSql(DURABLE_TABLES.runs, keys), keys.map((k) => row[k]));
76
- await hot.run(DELETE_ATTRIBUTES_SQL, [run.id]);
178
+ const inserted = await hot.rows(this.sql.insertRunIfAbsent(keys), keys.map((k) => row[k]));
179
+ if (inserted.length === 0)
180
+ throw new RunAlreadyExistsError(run.id);
181
+ await hot.run(this.sql.deleteAttributes, [run.id]);
77
182
  return;
78
183
  }
79
184
  const client = this.client();
80
- await client.table(DURABLE_TABLES.runs).insert(runToRow(run));
81
- await client.from(DURABLE_TABLES.attributes).where('run_id', run.id).delete();
185
+ if (!(await insertIfAbsent(client, this.t.runs, runToRow(run), ['id']))) {
186
+ throw new RunAlreadyExistsError(run.id);
187
+ }
188
+ await client.from(this.t.attributes).where('run_id', run.id).delete();
82
189
  return;
83
190
  }
84
191
  await this.client().transaction(async (trx) => {
85
- await trx.table(DURABLE_TABLES.runs).insert(runToRow(run));
192
+ if (!(await insertIfAbsent(trx, this.t.runs, runToRow(run), ['id']))) {
193
+ throw new RunAlreadyExistsError(run.id);
194
+ }
86
195
  await this.reindexAttributes(trx, run.id, run.searchAttributes);
87
196
  });
88
197
  }
@@ -102,7 +211,7 @@ export class LucidStateStore {
102
211
  const row = await this.releaseSlot(this.client(), runId, patch);
103
212
  const keys = Object.keys(row);
104
213
  if (keys.length) {
105
- await hot.run(updateRunSql(keys, false), [...keys.map((k) => row[k]), runId]);
214
+ await hot.run(this.sql.updateRun(keys, false), [...keys.map((k) => row[k]), runId]);
106
215
  }
107
216
  return;
108
217
  }
@@ -110,7 +219,7 @@ export class LucidStateStore {
110
219
  const row = await this.releaseSlot(trx, runId, patch);
111
220
  // Knex throws on an empty `.update({})`; skip the UPDATE when no mapped column changed.
112
221
  if (Object.keys(row).length) {
113
- await trx.from(DURABLE_TABLES.runs).where('id', runId).update(row);
222
+ await trx.from(this.t.runs).where('id', runId).update(row);
114
223
  }
115
224
  if ('searchAttributes' in patch) {
116
225
  await this.reindexAttributes(trx, runId, patch.searchAttributes);
@@ -123,10 +232,10 @@ export class LucidStateStore {
123
232
  const row = await this.releaseSlot(this.client(), runId, patch);
124
233
  const keys = Object.keys(row);
125
234
  if (keys.length === 0) {
126
- const found = await hot.rows(RUN_IN_STATUSES_SQL, [runId, expectedStatuses]);
235
+ const found = await hot.rows(this.sql.runInStatuses, [runId, expectedStatuses]);
127
236
  return found.length > 0;
128
237
  }
129
- const { count } = await hot.run(updateRunSql(keys, true), [
238
+ const { count } = await hot.run(this.sql.updateRun(keys, true), [
130
239
  ...keys.map((k) => row[k]),
131
240
  runId,
132
241
  expectedStatuses,
@@ -141,7 +250,7 @@ export class LucidStateStore {
141
250
  // structure — the database arbitrates, not a prior JS read. Two racing writers can never both
142
251
  // report `true` for the same row.
143
252
  const affected = await trx
144
- .from(DURABLE_TABLES.runs)
253
+ .from(this.t.runs)
145
254
  .where('id', runId)
146
255
  .whereIn('status', expectedStatuses)
147
256
  .update(row);
@@ -151,7 +260,7 @@ export class LucidStateStore {
151
260
  // Knex throws on an empty `.update({})`. Nothing to write, but the predicate's outcome is
152
261
  // still observable: report whether it would have matched, without mutating anything.
153
262
  const current = await trx
154
- .from(DURABLE_TABLES.runs)
263
+ .from(this.t.runs)
155
264
  .where('id', runId)
156
265
  .whereIn('status', expectedStatuses)
157
266
  .first();
@@ -178,7 +287,7 @@ export class LucidStateStore {
178
287
  // Sorted, so two batches sharing runs take their row locks in the same order.
179
288
  const ids = [...new Set(runIds)].sort();
180
289
  await this.client()
181
- .from(DURABLE_TABLES.runs)
290
+ .from(this.t.runs)
182
291
  .whereIn('id', ids)
183
292
  .where('status', 'suspended')
184
293
  .andWhere((w) => w.whereNull('wake_at').orWhere('wake_at', '>', wakeAt))
@@ -188,9 +297,7 @@ export class LucidStateStore {
188
297
  decisionMarkerColumn;
189
298
  hasDecisionMarkerColumn(trx) {
190
299
  // Probed on the caller's transaction: a single-connection pool (SQLite) has no other to use.
191
- this.decisionMarkerColumn ??= trx.schema
192
- .hasColumn(DURABLE_TABLES.runs, 'awaiting_decision_task_id')
193
- .catch(() => {
300
+ this.decisionMarkerColumn ??= hasColumn(trx, this.schema, DURABLE_TABLES.runs, 'awaiting_decision_task_id').catch(() => {
194
301
  this.decisionMarkerColumn = undefined; // a failed probe is retried, never cached
195
302
  return true;
196
303
  });
@@ -225,7 +332,7 @@ export class LucidStateStore {
225
332
  tags = patch.tags;
226
333
  }
227
334
  else {
228
- const q = trx.from(DURABLE_TABLES.runs).where('id', runId).select('tags');
335
+ const q = trx.from(this.t.runs).where('id', runId).select('tags');
229
336
  if (locksRows(trx))
230
337
  q.forUpdate();
231
338
  const current = (await q.first());
@@ -239,7 +346,7 @@ export class LucidStateStore {
239
346
  // Every active run sharing the key. The tag LIKE may over-match (it is only a lock-scope
240
347
  // narrowing); the decision re-filters exactly.
241
348
  const sharing = () => trx
242
- .from(DURABLE_TABLES.runs)
349
+ .from(this.t.runs)
243
350
  .where('workflow', workflow)
244
351
  .whereIn('status', SINGLETON_ACTIVE_STATUSES)
245
352
  .whereRaw(`tags like ? escape '!'`, [`%"${escapeLike(tag)}"%`]);
@@ -263,13 +370,13 @@ export class LucidStateStore {
263
370
  if (!tags) {
264
371
  const parked = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
265
372
  if (parked) {
266
- await trx.from(DURABLE_TABLES.runs).where('id', runId).update(runPatchToRow(parked));
373
+ await trx.from(this.t.runs).where('id', runId).update(runPatchToRow(parked));
267
374
  }
268
375
  return false;
269
376
  }
270
377
  // Already a holder (a resumed turn): nothing to write.
271
378
  if (!runs.find((r) => r.id === runId)?.tags?.includes(SINGLETON_ADMITTED_TAG)) {
272
- await trx.from(DURABLE_TABLES.runs).where('id', runId).update(runPatchToRow({ tags }));
379
+ await trx.from(this.t.runs).where('id', runId).update(runPatchToRow({ tags }));
273
380
  }
274
381
  return true;
275
382
  });
@@ -277,19 +384,20 @@ export class LucidStateStore {
277
384
  async getRun(runId) {
278
385
  const hot = this.hot();
279
386
  if (hot) {
387
+ const sql = await this.hotReads(hot);
280
388
  const rows = this.scopeNamespace === undefined
281
- ? await hot.rows(GET_RUN_SQL, [runId])
282
- : await hot.rows(GET_RUN_SCOPED_SQL, [runId, namespaceFilterValues(this.scopeNamespace)]);
389
+ ? await hot.rows(sql.get, [runId])
390
+ : await hot.rows(sql.getScoped, [runId, namespaceFilterValues(this.scopeNamespace)]);
283
391
  return rows[0] ? rowToRun(rows[0]) : null;
284
392
  }
285
- const row = await this.fenced(this.client().from(DURABLE_TABLES.runs).where('id', runId)).first();
393
+ const row = await this.fenced(this.client().from(this.t.runs).where('id', runId)).first();
286
394
  return row ? rowToRun(row) : null;
287
395
  }
288
396
  async getRuns(runIds) {
289
397
  const ids = [...new Set(runIds)];
290
398
  if (ids.length === 0)
291
399
  return [];
292
- const rows = await this.fenced(this.client().from(DURABLE_TABLES.runs).whereIn('id', ids));
400
+ const rows = await this.fenced(this.client().from(this.t.runs).whereIn('id', ids));
293
401
  return rows.map(rowToRun);
294
402
  }
295
403
  async deleteRun(runId) {
@@ -297,10 +405,10 @@ export class LucidStateStore {
297
405
  // transaction so a crash can't leave a half-deleted run. (Buffered signals are token-keyed,
298
406
  // not run-scoped, so they are not swept here.)
299
407
  await this.client().transaction(async (trx) => {
300
- await trx.from(DURABLE_TABLES.checkpoints).where('run_id', runId).delete();
301
- await trx.from(DURABLE_TABLES.signalWaiters).where('run_id', runId).delete();
302
- await trx.from(DURABLE_TABLES.attributes).where('run_id', runId).delete();
303
- await trx.from(DURABLE_TABLES.runs).where('id', runId).delete();
408
+ await trx.from(this.t.checkpoints).where('run_id', runId).delete();
409
+ await trx.from(this.t.signalWaiters).where('run_id', runId).delete();
410
+ await trx.from(this.t.attributes).where('run_id', runId).delete();
411
+ await trx.from(this.t.runs).where('id', runId).delete();
304
412
  });
305
413
  }
306
414
  async deleteRuns(runIds) {
@@ -312,10 +420,10 @@ export class LucidStateStore {
312
420
  await this.client().transaction(async (trx) => {
313
421
  for (let at = 0; at < ids.length; at += DELETE_RUNS_CHUNK) {
314
422
  const chunk = ids.slice(at, at + DELETE_RUNS_CHUNK);
315
- await trx.from(DURABLE_TABLES.checkpoints).whereIn('run_id', chunk).delete();
316
- await trx.from(DURABLE_TABLES.signalWaiters).whereIn('run_id', chunk).delete();
317
- await trx.from(DURABLE_TABLES.attributes).whereIn('run_id', chunk).delete();
318
- await trx.from(DURABLE_TABLES.runs).whereIn('id', chunk).delete();
423
+ await trx.from(this.t.checkpoints).whereIn('run_id', chunk).delete();
424
+ await trx.from(this.t.signalWaiters).whereIn('run_id', chunk).delete();
425
+ await trx.from(this.t.attributes).whereIn('run_id', chunk).delete();
426
+ await trx.from(this.t.runs).whereIn('id', chunk).delete();
319
427
  }
320
428
  });
321
429
  }
@@ -323,11 +431,11 @@ export class LucidStateStore {
323
431
  async getCheckpoint(runId, seq) {
324
432
  const hot = this.hot();
325
433
  if (hot) {
326
- const rows = await hot.rows(GET_CHECKPOINT_SQL, [runId, seq]);
434
+ const rows = await hot.rows((await this.hotReads(hot)).getCheckpoint, [runId, seq]);
327
435
  return rows[0] ? rowToCheckpoint(rows[0]) : null;
328
436
  }
329
437
  const row = await this.client()
330
- .from(DURABLE_TABLES.checkpoints)
438
+ .from(this.t.checkpoints)
331
439
  .where('run_id', runId)
332
440
  .andWhere('seq', seq)
333
441
  .first();
@@ -338,7 +446,7 @@ export class LucidStateStore {
338
446
  const out = [];
339
447
  for (let at = 0; at < unique.length; at += CHECKPOINT_BATCH) {
340
448
  const rows = (await this.client()
341
- .from(DURABLE_TABLES.checkpoints)
449
+ .from(this.t.checkpoints)
342
450
  .where('run_id', runId)
343
451
  .whereIn('seq', unique.slice(at, at + CHECKPOINT_BATCH)));
344
452
  out.push(...rows.map(rowToCheckpoint));
@@ -349,7 +457,7 @@ export class LucidStateStore {
349
457
  const unique = [...new Set(seqs)];
350
458
  const hot = this.hot();
351
459
  if (hot && unique.length <= CHECKPOINT_BATCH) {
352
- const rows = await hot.rows(CHECKPOINTS_FOR_SETTLE_SQL, [runId, unique]);
460
+ const rows = await hot.rows(this.sql.checkpointsForSettle, [runId, unique]);
353
461
  return rows
354
462
  .map(rowToCheckpoint)
355
463
  .sort((a, b) => a.seq - b.seq);
@@ -357,7 +465,7 @@ export class LucidStateStore {
357
465
  const out = [];
358
466
  for (let at = 0; at < unique.length; at += CHECKPOINT_BATCH) {
359
467
  const rows = (await this.client()
360
- .from(DURABLE_TABLES.checkpoints)
468
+ .from(this.t.checkpoints)
361
469
  .where('run_id', runId)
362
470
  .whereIn('seq', unique.slice(at, at + CHECKPOINT_BATCH))
363
471
  .select(...CHECKPOINT_COLUMNS_BUT_INPUT));
@@ -374,18 +482,18 @@ export class LucidStateStore {
374
482
  // One multi-row upsert per chunk, in one transaction: a rejection writes nothing.
375
483
  await this.client().transaction(async (trx) => {
376
484
  for (let at = 0; at < checkpoints.length; at += CHECKPOINT_UPSERT_BATCH) {
377
- await upsertCheckpointRows(trx, checkpoints.slice(at, at + CHECKPOINT_UPSERT_BATCH));
485
+ await upsertCheckpointRows(trx, this.t.checkpoints, checkpoints.slice(at, at + CHECKPOINT_UPSERT_BATCH));
378
486
  }
379
487
  });
380
488
  }
381
489
  async saveCheckpointIfPending(checkpoint) {
382
- return updateCheckpointIfPending(this.client(), checkpoint);
490
+ return updateCheckpointIfPending(this.client(), this.t.checkpoints, checkpoint);
383
491
  }
384
492
  async renewCheckpointLease(runId, seq, expectedWakeAt, wakeAt) {
385
493
  // One guarded UPDATE: the row-level compare-and-set is atomic on every dialect, so a result or a
386
494
  // re-dispatch committed since the caller's read makes this match nothing.
387
495
  const query = this.client()
388
- .from(DURABLE_TABLES.checkpoints)
496
+ .from(this.t.checkpoints)
389
497
  .where('run_id', runId)
390
498
  .andWhere('seq', seq)
391
499
  .andWhere('status', 'pending');
@@ -403,7 +511,7 @@ export class LucidStateStore {
403
511
  // for a row is compared against the first one's write), then write every winner with one
404
512
  // multi-row upsert per chunk.
405
513
  return this.client().transaction(async (trx) => {
406
- const pending = await lockPendingCheckpoints(trx, checkpoints, ['seq']);
514
+ const pending = await lockPendingCheckpoints(trx, this.t.checkpoints, checkpoints, ['seq']);
407
515
  const wrote = [];
408
516
  const writes = new Map();
409
517
  for (const cp of checkpoints) {
@@ -419,7 +527,7 @@ export class LucidStateStore {
419
527
  }
420
528
  const rows = [...writes.values()];
421
529
  for (let at = 0; at < rows.length; at += CHECKPOINT_UPSERT_BATCH) {
422
- await upsertCheckpointRows(trx, rows.slice(at, at + CHECKPOINT_UPSERT_BATCH));
530
+ await upsertCheckpointRows(trx, this.t.checkpoints, rows.slice(at, at + CHECKPOINT_UPSERT_BATCH));
423
531
  }
424
532
  return wrote;
425
533
  });
@@ -430,7 +538,7 @@ export class LucidStateStore {
430
538
  return [];
431
539
  if (settlements.length === 1) {
432
540
  const affected = await this.client()
433
- .from(DURABLE_TABLES.checkpoints)
541
+ .from(this.t.checkpoints)
434
542
  .where('run_id', only.runId)
435
543
  .andWhere('seq', only.seq)
436
544
  .andWhere('status', 'pending')
@@ -443,7 +551,7 @@ export class LucidStateStore {
443
551
  // locked, so the upsert never inserts: its row carries just the columns an insert would require,
444
552
  // and `input` is neither sent nor rewritten.
445
553
  return this.client().transaction(async (trx) => {
446
- const pending = await lockPendingCheckpoints(trx, settlements, [
554
+ const pending = await lockPendingCheckpoints(trx, this.t.checkpoints, settlements, [
447
555
  'seq',
448
556
  'name',
449
557
  'kind',
@@ -472,7 +580,7 @@ export class LucidStateStore {
472
580
  for (let at = 0; at < rows.length; at += CHECKPOINT_UPSERT_BATCH) {
473
581
  await trx
474
582
  .insertQuery()
475
- .table(DURABLE_TABLES.checkpoints)
583
+ .table(this.t.checkpoints)
476
584
  .knexQuery.insert(rows.slice(at, at + CHECKPOINT_UPSERT_BATCH))
477
585
  .onConflict(['run_id', 'seq'])
478
586
  .merge([...SETTLED_CHECKPOINT_COLUMNS]);
@@ -495,7 +603,7 @@ export class LucidStateStore {
495
603
  const live = results.filter((_, i) => first[i]);
496
604
  const client = this.client();
497
605
  const settled = client.dialect.name === 'postgres'
498
- ? await settleResultsReturningPg(client, this.hot(), live)
606
+ ? await settleResultsReturningPg(client, this.hot(), this.schema, live)
499
607
  : await this.settleResultsPlain(live);
500
608
  const out = [];
501
609
  let k = 0;
@@ -520,7 +628,7 @@ export class LucidStateStore {
520
628
  const out = new Map();
521
629
  const client = this.client();
522
630
  const runRows = (await client
523
- .from(DURABLE_TABLES.runs)
631
+ .from(this.t.runs)
524
632
  .whereIn('id', [...new Set(results.map((r) => r.runId))])
525
633
  .select('id', 'status', 'wake_at'));
526
634
  const runs = new Map(runRows.map((row) => [row.id, row]));
@@ -585,36 +693,16 @@ export class LucidStateStore {
585
693
  if (hot) {
586
694
  const keys = Object.keys(row);
587
695
  const values = keys.map((k) => row[k] ?? null);
588
- return (await hot.rows(insertCheckpointIfAbsentSql(keys), values)).length > 0;
589
- }
590
- if (client.dialect.name === 'postgres') {
591
- const inserted = (await client
592
- .insertQuery()
593
- .table(DURABLE_TABLES.checkpoints)
594
- .knexQuery.insert(row)
595
- .onConflict(['run_id', 'seq'])
596
- .ignore()
597
- .returning('seq'));
598
- return inserted.length > 0;
599
- }
600
- // MySQL and SQLite: a plain INSERT, whose duplicate-key error is the "already there" answer (an
601
- // `INSERT IGNORE` would also swallow every other error, and reports no row count through Knex).
602
- try {
603
- await client.table(DURABLE_TABLES.checkpoints).insert(row);
604
- return true;
605
- }
606
- catch (err) {
607
- if (isDuplicateKey(err))
608
- return false;
609
- throw err;
696
+ return (await hot.rows(this.sql.insertCheckpointIfAbsent(keys), values)).length > 0;
610
697
  }
698
+ return insertIfAbsent(client, this.t.checkpoints, row, ['run_id', 'seq']);
611
699
  }
612
700
  /** Persist the latest worker heartbeat on the step's checkpoint row (see the interface doc).
613
701
  * A targeted two-column UPDATE — never an upsert: a beat racing the checkpoint's own insert (or
614
702
  * outliving a deleted run) matches zero rows and is a silent no-op, by design. */
615
703
  async recordStepHeartbeat(runId, seq, at, progress) {
616
704
  await this.client()
617
- .from(DURABLE_TABLES.checkpoints)
705
+ .from(this.t.checkpoints)
618
706
  .where('run_id', runId)
619
707
  .andWhere('seq', seq)
620
708
  .update({
@@ -632,14 +720,16 @@ export class LucidStateStore {
632
720
  const row = checkpointToRow(checkpoint);
633
721
  await client
634
722
  .insertQuery()
635
- .table(DURABLE_TABLES.checkpoints)
723
+ .table(this.t.checkpoints)
636
724
  .knexQuery.insert(row)
637
725
  .onConflict(['run_id', 'seq'])
638
726
  .merge();
639
727
  }
640
728
  // --- recovery / dispatch queries ----------------------------------------
641
729
  async listIncompleteRuns(namespace) {
642
- const q = this.fenced(this.client().from(DURABLE_TABLES.runs).where('status', 'running'));
730
+ const q = this.fenced(this.client()
731
+ .from(this.t.runs)
732
+ .whereIn('status', [...RECOVERABLE_RUN_STATUSES]));
643
733
  if (namespace !== undefined)
644
734
  q.whereIn('namespace', namespaceFilterValues(namespace));
645
735
  const rows = await q;
@@ -651,8 +741,8 @@ export class LucidStateStore {
651
741
  // running run — and issues ~zero lock-probe UPDATEs. `owner` adds the runs still leased to that
652
742
  // instance id (a rebooted instance's own leftovers; the engine filters what it is executing).
653
743
  const q = this.fenced(this.client()
654
- .from(DURABLE_TABLES.runs)
655
- .where('status', 'running')
744
+ .from(this.t.runs)
745
+ .whereIn('status', [...RECOVERABLE_RUN_STATUSES])
656
746
  .andWhere((sub) => {
657
747
  sub.whereNull('locked_until').orWhere('locked_until', '<=', nowMs);
658
748
  if (owner !== undefined)
@@ -664,7 +754,7 @@ export class LucidStateStore {
664
754
  return rows.map(rowToRun);
665
755
  }
666
756
  async listPendingRuns(limit, namespace) {
667
- const q = this.fenced(this.client().from(DURABLE_TABLES.runs).where('status', 'pending'));
757
+ const q = this.fenced(this.client().from(this.t.runs).where('status', 'pending'));
668
758
  if (namespace !== undefined)
669
759
  q.whereIn('namespace', namespaceFilterValues(namespace));
670
760
  const rows = await q
@@ -691,9 +781,9 @@ export class LucidStateStore {
691
781
  if (dialect === 'postgres') {
692
782
  // One statement: pick the oldest free pending rows, skipping any another claimer holds locked
693
783
  // right now, and lease them. Several pollers at once each get a different slice.
694
- const pick = oldest(client.from(DURABLE_TABLES.runs)).forUpdate().skipLocked();
784
+ const pick = oldest(client.from(this.t.runs)).forUpdate().skipLocked();
695
785
  rows = (await client
696
- .from(DURABLE_TABLES.runs)
786
+ .from(this.t.runs)
697
787
  .whereIn('id', pick)
698
788
  .update({ locked_by: owner, locked_until: leaseUntilMs })
699
789
  .returning('*'));
@@ -702,20 +792,20 @@ export class LucidStateStore {
702
792
  // No UPDATE … RETURNING: lock the batch (skipping rows another claimer holds), lease it and read
703
793
  // it back, in one transaction.
704
794
  rows = await client.transaction(async (trx) => {
705
- const ids = (await oldest(trx.from(DURABLE_TABLES.runs)).forUpdate().skipLocked()).map((r) => r.id);
795
+ const ids = (await oldest(trx.from(this.t.runs)).forUpdate().skipLocked()).map((r) => r.id);
706
796
  if (ids.length === 0)
707
797
  return [];
708
798
  await trx
709
- .from(DURABLE_TABLES.runs)
799
+ .from(this.t.runs)
710
800
  .whereIn('id', ids)
711
801
  .update({ locked_by: owner, locked_until: leaseUntilMs });
712
- return (await trx.from(DURABLE_TABLES.runs).whereIn('id', ids));
802
+ return (await trx.from(this.t.runs).whereIn('id', ids));
713
803
  });
714
804
  }
715
805
  else {
716
806
  // SQLite has no row locks to skip: each run is taken with the lease's own compare-and-set.
717
807
  const claimed = [];
718
- const candidates = (await free(client.from(DURABLE_TABLES.runs))
808
+ const candidates = (await free(client.from(this.t.runs))
719
809
  .orderBy('created_at', 'asc')
720
810
  .orderBy('id', 'asc')
721
811
  .limit(limit));
@@ -733,7 +823,7 @@ export class LucidStateStore {
733
823
  async listDueTimers(nowMs, namespace, limit) {
734
824
  const marker = await this.hasWakeMarkerColumn();
735
825
  const q = this.fenced(this.client()
736
- .from(DURABLE_TABLES.runs)
826
+ .from(this.t.runs)
737
827
  .where('status', 'suspended')
738
828
  .where((w) => {
739
829
  w.where((d) => d.whereNotNull('wake_at').andWhere('wake_at', '<=', nowMs));
@@ -756,7 +846,7 @@ export class LucidStateStore {
756
846
  // Atomic compare-and-set: acquire ONLY if currently unlocked or the lease has expired. The single
757
847
  // conditional UPDATE is the concurrency guard — two instances racing can never both report `true`.
758
848
  const affected = await this.client()
759
- .from(DURABLE_TABLES.runs)
849
+ .from(this.t.runs)
760
850
  .where('id', runId)
761
851
  .andWhere((q) => {
762
852
  q.whereNull('locked_until').orWhere('locked_until', '<=', nowMs);
@@ -767,9 +857,10 @@ export class LucidStateStore {
767
857
  async tryLockRunAndGet(runId, owner, leaseUntilMs, nowMs) {
768
858
  const hot = this.hot();
769
859
  if (hot) {
860
+ const sql = await this.hotReads(hot);
770
861
  const rows = this.scopeNamespace === undefined
771
- ? await hot.rows(LOCK_AND_GET_SQL, [owner, leaseUntilMs, runId, nowMs])
772
- : await hot.rows(LOCK_AND_GET_SCOPED_SQL, [
862
+ ? await hot.rows(sql.lockAndGet, [owner, leaseUntilMs, runId, nowMs])
863
+ : await hot.rows(sql.lockAndGetScoped, [
773
864
  owner,
774
865
  leaseUntilMs,
775
866
  runId,
@@ -783,7 +874,7 @@ export class LucidStateStore {
783
874
  // One statement: the lease's compare-and-set and the read of the row it locked. The scope
784
875
  // fence sits in the WHERE clause, so a run outside it is neither locked nor read.
785
876
  const rows = (await this.fenced(client
786
- .from(DURABLE_TABLES.runs)
877
+ .from(this.t.runs)
787
878
  .where('id', runId)
788
879
  .andWhere((q) => {
789
880
  q.whereNull('locked_until').orWhere('locked_until', '<=', nowMs);
@@ -804,15 +895,15 @@ export class LucidStateStore {
804
895
  const hot = this.hot();
805
896
  if (hot) {
806
897
  if (owner === undefined)
807
- await hot.run(RELEASE_LOCK_SQL, [runId]);
898
+ await hot.run(this.sql.releaseLock, [runId]);
808
899
  else
809
- await hot.run(RELEASE_OWN_LOCK_SQL, [runId, owner]);
900
+ await hot.run(this.sql.releaseOwnLock, [runId, owner]);
810
901
  return;
811
902
  }
812
903
  // Owner-scoped release (when `owner` is given): the `locked_by` predicate lives in the UPDATE's
813
904
  // WHERE clause, so a stale executor whose lease was taken over matches zero rows and cannot
814
905
  // wipe the new owner's lease — same conditional-write shape as renewRunLock.
815
- const q = this.client().from(DURABLE_TABLES.runs).where('id', runId);
906
+ const q = this.client().from(this.t.runs).where('id', runId);
816
907
  if (owner !== undefined)
817
908
  q.andWhere('locked_by', owner);
818
909
  await q.update({ locked_by: null, locked_until: null });
@@ -823,19 +914,13 @@ export class LucidStateStore {
823
914
  const marker = await this.hasWakeMarkerColumn();
824
915
  const hot = this.hot();
825
916
  if (hot) {
826
- const released = await hot.run(marker ? RELEASE_UNLESS_WAKE_SQL : RELEASE_OWN_LOCK_SQL, [
827
- runId,
828
- owner,
829
- ]);
917
+ const released = await hot.run(marker ? this.sql.releaseUnlessWake : this.sql.releaseOwnLock, [runId, owner]);
830
918
  if (released.count === 1)
831
919
  return true;
832
- const rows = await hot.rows(LOCKED_BY_SQL, [runId]);
920
+ const rows = await hot.rows(this.sql.lockedBy, [runId]);
833
921
  return rows[0]?.locked_by !== owner;
834
922
  }
835
- const q = this.client()
836
- .from(DURABLE_TABLES.runs)
837
- .where('id', runId)
838
- .andWhere('locked_by', owner);
923
+ const q = this.client().from(this.t.runs).where('id', runId).andWhere('locked_by', owner);
839
924
  if (marker)
840
925
  q.whereNull('wake_requested_at');
841
926
  const affected = await q.update({ locked_by: null, locked_until: null });
@@ -843,7 +928,7 @@ export class LucidStateStore {
843
928
  return true;
844
929
  // Nothing matched: the lease is not ours (any more), or a turn was asked for.
845
930
  const row = (await this.client()
846
- .from(DURABLE_TABLES.runs)
931
+ .from(this.t.runs)
847
932
  .where('id', runId)
848
933
  .select('locked_by')
849
934
  .first());
@@ -851,9 +936,7 @@ export class LucidStateStore {
851
936
  }
852
937
  /** {@link hasWakeMarkerColumn} on the caller's transaction (a single-connection pool has no other). */
853
938
  hasWakeMarkerColumnIn(trx) {
854
- this.wakeMarkerColumn ??= trx.schema
855
- .hasColumn(DURABLE_TABLES.runs, 'wake_requested_at')
856
- .catch(() => {
939
+ this.wakeMarkerColumn ??= hasColumn(trx, this.schema, DURABLE_TABLES.runs, 'wake_requested_at').catch(() => {
857
940
  this.wakeMarkerColumn = undefined;
858
941
  return false;
859
942
  });
@@ -862,9 +945,7 @@ export class LucidStateStore {
862
945
  /** Whether the runs table has `wake_requested_at`, probed once per store and cached. */
863
946
  wakeMarkerColumn;
864
947
  hasWakeMarkerColumn() {
865
- this.wakeMarkerColumn ??= this.client()
866
- .schema.hasColumn(DURABLE_TABLES.runs, 'wake_requested_at')
867
- .catch(() => {
948
+ this.wakeMarkerColumn ??= hasColumn(this.client(), this.schema, DURABLE_TABLES.runs, 'wake_requested_at').catch(() => {
868
949
  this.wakeMarkerColumn = undefined;
869
950
  return false;
870
951
  });
@@ -873,10 +954,10 @@ export class LucidStateStore {
873
954
  async renewRunLock(runId, owner, leaseUntilMs) {
874
955
  const hot = this.hot();
875
956
  if (hot)
876
- return (await hot.run(RENEW_LOCK_SQL, [leaseUntilMs, runId, owner])).count === 1;
957
+ return (await hot.run(this.sql.renewLock, [leaseUntilMs, runId, owner])).count === 1;
877
958
  // Extend ONLY if `owner` still holds the lease — a dead worker's lease still expires & is reclaimed.
878
959
  const affected = await this.client()
879
- .from(DURABLE_TABLES.runs)
960
+ .from(this.t.runs)
880
961
  .where('id', runId)
881
962
  .andWhere('locked_by', owner)
882
963
  .update({ locked_until: leaseUntilMs });
@@ -888,7 +969,7 @@ export class LucidStateStore {
888
969
  // runs once per wait registration, i.e. on every signal/child/event suspension).
889
970
  await this.client()
890
971
  .insertQuery()
891
- .table(DURABLE_TABLES.signalWaiters)
972
+ .table(this.t.signalWaiters)
892
973
  .knexQuery.insert({
893
974
  token: waiter.token,
894
975
  run_id: waiter.runId,
@@ -903,10 +984,10 @@ export class LucidStateStore {
903
984
  // the same waiter to two callers. The DELETE arbitrates, so no transaction is needed around it,
904
985
  // and a token with no waiter (the common case) costs one read.
905
986
  const client = this.client();
906
- const row = await client.from(DURABLE_TABLES.signalWaiters).where('token', token).first();
987
+ const row = await client.from(this.t.signalWaiters).where('token', token).first();
907
988
  if (!row)
908
989
  return null;
909
- const deleted = await client.from(DURABLE_TABLES.signalWaiters).where('token', token).delete();
990
+ const deleted = await client.from(this.t.signalWaiters).where('token', token).delete();
910
991
  if (rowsAffected(deleted) !== 1)
911
992
  return null;
912
993
  return rowToSignalWaiter(row);
@@ -915,14 +996,14 @@ export class LucidStateStore {
915
996
  // The prefix embeds caller-supplied names (event names via eventPrefix) — escape their LIKE
916
997
  // metacharacters so a name containing `%`/`_` can't scan (or match) other tokens.
917
998
  const rows = await this.client()
918
- .from(DURABLE_TABLES.signalWaiters)
999
+ .from(this.t.signalWaiters)
919
1000
  .whereRaw(`token like ? escape '!'`, [`${escapeLike(prefix)}%`]);
920
1001
  return rows.map(rowToSignalWaiter);
921
1002
  }
922
1003
  async listSignalWaitersByRunIds(runIds) {
923
1004
  if (runIds.length === 0)
924
1005
  return [];
925
- const rows = await this.client().from(DURABLE_TABLES.signalWaiters).whereIn('run_id', runIds);
1006
+ const rows = await this.client().from(this.t.signalWaiters).whereIn('run_id', runIds);
926
1007
  return rows.map(rowToSignalWaiter);
927
1008
  }
928
1009
  async hasSignalWaiter(runId, tokenPrefix) {
@@ -930,7 +1011,7 @@ export class LucidStateStore {
930
1011
  // MySQL's default collations ignore case), so a hit is confirmed in JS. Only a match that is not
931
1012
  // a real prefix match pays for reading the run's whole matching set.
932
1013
  const matching = () => this.client()
933
- .from(DURABLE_TABLES.signalWaiters)
1014
+ .from(this.t.signalWaiters)
934
1015
  .select('token')
935
1016
  .where('run_id', runId)
936
1017
  .andWhereRaw(`token like ? escape '!'`, [`${escapeLike(tokenPrefix)}%`]);
@@ -953,7 +1034,7 @@ export class LucidStateStore {
953
1034
  const chunk = entries.slice(at, at + CHILD_WAIT_ROWS);
954
1035
  await trx
955
1036
  .insertQuery()
956
- .table(DURABLE_TABLES.signalWaiters)
1037
+ .table(this.t.signalWaiters)
957
1038
  .knexQuery.insert(chunk.map(({ waiter }) => ({
958
1039
  token: waiter.token,
959
1040
  run_id: waiter.runId,
@@ -967,7 +1048,7 @@ export class LucidStateStore {
967
1048
  const chunk = entries.slice(at, at + CHILD_WAIT_ROWS);
968
1049
  await trx
969
1050
  .insertQuery()
970
- .table(DURABLE_TABLES.checkpoints)
1051
+ .table(this.t.checkpoints)
971
1052
  .knexQuery.insert(chunk.map(({ checkpoint }) => checkpointToRow(checkpoint)))
972
1053
  .onConflict(['run_id', 'seq'])
973
1054
  .merge();
@@ -977,7 +1058,7 @@ export class LucidStateStore {
977
1058
  async compactCheckpoints(runId, fromSeq, toSeq, into) {
978
1059
  await this.client().transaction(async (trx) => {
979
1060
  await trx
980
- .from(DURABLE_TABLES.checkpoints)
1061
+ .from(this.t.checkpoints)
981
1062
  .where('run_id', runId)
982
1063
  .andWhere('seq', '>=', fromSeq)
983
1064
  .andWhere('seq', '<=', toSeq)
@@ -990,7 +1071,7 @@ export class LucidStateStore {
990
1071
  // primary key, so a later putSignalWaiter for the same token may have replaced the row with a
991
1072
  // different run's waiter — the run_id/seq predicate leaves that one untouched. No-op if absent.
992
1073
  await this.client()
993
- .from(DURABLE_TABLES.signalWaiters)
1074
+ .from(this.t.signalWaiters)
994
1075
  .where('token', waiter.token)
995
1076
  .andWhere('run_id', waiter.runId)
996
1077
  .andWhere('seq', waiter.seq)
@@ -999,12 +1080,12 @@ export class LucidStateStore {
999
1080
  // --- buffered signals (FIFO per token) ----------------------------------
1000
1081
  async bufferSignal(token, payload) {
1001
1082
  await this.client()
1002
- .table(DURABLE_TABLES.bufferedSignals)
1083
+ .table(this.t.bufferedSignals)
1003
1084
  .insert({ token, payload: payload === undefined ? null : JSON.stringify(payload) });
1004
1085
  }
1005
1086
  async listBufferedSignalTokens(prefix, limit) {
1006
1087
  const rows = (await this.client()
1007
- .from(DURABLE_TABLES.bufferedSignals)
1088
+ .from(this.t.bufferedSignals)
1008
1089
  .whereRaw(`token like ? escape '!'`, [`${escapeLike(prefix)}%`])
1009
1090
  .distinct('token')
1010
1091
  .orderBy('token', 'asc')
@@ -1013,7 +1094,7 @@ export class LucidStateStore {
1013
1094
  }
1014
1095
  async listBufferedSignals(tokenPrefix, afterId, limit) {
1015
1096
  const q = this.client()
1016
- .from(DURABLE_TABLES.bufferedSignals)
1097
+ .from(this.t.bufferedSignals)
1017
1098
  .whereRaw(`token like ? escape '!'`, [`${escapeLike(tokenPrefix)}%`]);
1018
1099
  if (afterId !== undefined)
1019
1100
  q.where('id', '>', Number(afterId));
@@ -1029,7 +1110,7 @@ export class LucidStateStore {
1029
1110
  async deleteBufferedSignals(ids) {
1030
1111
  for (let at = 0; at < ids.length; at += 500) {
1031
1112
  await this.client()
1032
- .from(DURABLE_TABLES.bufferedSignals)
1113
+ .from(this.t.bufferedSignals)
1033
1114
  .whereIn('id', ids.slice(at, at + 500).map((id) => Number(id)))
1034
1115
  .delete();
1035
1116
  }
@@ -1040,9 +1121,9 @@ export class LucidStateStore {
1040
1121
  const client = this.client();
1041
1122
  const hot = this.hot();
1042
1123
  const row = hot
1043
- ? (await hot.rows(OLDEST_BUFFERED_SIGNAL_SQL, [token]))[0]
1124
+ ? (await hot.rows(this.sql.oldestBufferedSignal, [token]))[0]
1044
1125
  : await client
1045
- .from(DURABLE_TABLES.bufferedSignals)
1126
+ .from(this.t.bufferedSignals)
1046
1127
  .where('token', token)
1047
1128
  .orderBy('id', 'asc')
1048
1129
  .first();
@@ -1050,8 +1131,8 @@ export class LucidStateStore {
1050
1131
  return null;
1051
1132
  const r = row;
1052
1133
  const deleted = hot
1053
- ? (await hot.run(DELETE_BUFFERED_SIGNAL_SQL, [r.id])).count
1054
- : rowsAffected(await client.from(DURABLE_TABLES.bufferedSignals).where('id', r.id).delete());
1134
+ ? (await hot.run(this.sql.deleteBufferedSignal, [r.id])).count
1135
+ : rowsAffected(await client.from(this.t.bufferedSignals).where('id', r.id).delete());
1055
1136
  if (deleted !== 1)
1056
1137
  return null;
1057
1138
  return {
@@ -1061,7 +1142,7 @@ export class LucidStateStore {
1061
1142
  // --- buffered events (name-keyed, match-based consumption) ---------------
1062
1143
  async bufferEvent(input) {
1063
1144
  await this.client()
1064
- .table(DURABLE_TABLES.bufferedEvents)
1145
+ .table(this.t.bufferedEvents)
1065
1146
  .insert({
1066
1147
  id: input.id,
1067
1148
  name: input.name,
@@ -1072,9 +1153,9 @@ export class LucidStateStore {
1072
1153
  async listBufferedEvents(name, limit) {
1073
1154
  const hot = this.hot();
1074
1155
  const rows = hot
1075
- ? await hot.rows(BUFFERED_EVENTS_SQL, [name, limit])
1156
+ ? await hot.rows(this.sql.bufferedEvents, [name, limit])
1076
1157
  : await this.client()
1077
- .from(DURABLE_TABLES.bufferedEvents)
1158
+ .from(this.t.bufferedEvents)
1078
1159
  .where('name', name)
1079
1160
  .orderBy('published_at', 'asc') // oldest first
1080
1161
  .limit(limit);
@@ -1091,7 +1172,7 @@ export class LucidStateStore {
1091
1172
  // IN subquery. The age predicate is repeated on the DELETE so a row re-published under the same
1092
1173
  // id meanwhile is left alone.
1093
1174
  const rows = (await this.client()
1094
- .from(DURABLE_TABLES.bufferedEvents)
1175
+ .from(this.t.bufferedEvents)
1095
1176
  .select('id')
1096
1177
  .where('published_at', '<', publishedBefore)
1097
1178
  .orderBy('published_at', 'asc')
@@ -1099,7 +1180,7 @@ export class LucidStateStore {
1099
1180
  if (rows.length === 0)
1100
1181
  return 0;
1101
1182
  const deleted = await this.client()
1102
- .from(DURABLE_TABLES.bufferedEvents)
1183
+ .from(this.t.bufferedEvents)
1103
1184
  .whereIn('id', rows.map((r) => r.id))
1104
1185
  .andWhere('published_at', '<', publishedBefore)
1105
1186
  .delete();
@@ -1108,10 +1189,7 @@ export class LucidStateStore {
1108
1189
  async removeBufferedEvent(id) {
1109
1190
  // Atomic claim: whoever's delete affects the row (returns 1) is the one that gets to deliver;
1110
1191
  // a concurrent claimant sees 0 and backs off. `id` is the primary key.
1111
- const deleted = await this.client()
1112
- .from(DURABLE_TABLES.bufferedEvents)
1113
- .where('id', id)
1114
- .delete();
1192
+ const deleted = await this.client().from(this.t.bufferedEvents).where('id', id).delete();
1115
1193
  return rowsAffected(deleted) === 1;
1116
1194
  }
1117
1195
  // --- transaction (exactly-once business write + checkpoint) -------------
@@ -1143,13 +1221,13 @@ export class LucidStateStore {
1143
1221
  const row = scheduleToRow(record);
1144
1222
  await this.client()
1145
1223
  .insertQuery()
1146
- .table(DURABLE_TABLES.schedules)
1224
+ .table(this.t.schedules)
1147
1225
  .knexQuery.insert(row)
1148
1226
  .onConflict('id')
1149
1227
  .merge();
1150
1228
  }
1151
1229
  async getSchedule(id) {
1152
- const row = await this.client().from(DURABLE_TABLES.schedules).where('id', id).first();
1230
+ const row = await this.client().from(this.t.schedules).where('id', id).first();
1153
1231
  return row ? rowToSchedule(row) : null;
1154
1232
  }
1155
1233
  /** One conditional UPDATE: the compare-and-set on `next_fire_at` lives in its WHERE clause. */
@@ -1173,7 +1251,7 @@ export class LucidStateStore {
1173
1251
  set.updated_at = patch.updatedAt.getTime();
1174
1252
  if (!Object.keys(set).length)
1175
1253
  return (await this.getSchedule(id)) !== null;
1176
- const q = this.client().from(DURABLE_TABLES.schedules).where('id', id);
1254
+ const q = this.client().from(this.t.schedules).where('id', id);
1177
1255
  if (expectedNextFireAt === null)
1178
1256
  q.whereNull('next_fire_at');
1179
1257
  else if (expectedNextFireAt !== undefined)
@@ -1181,11 +1259,11 @@ export class LucidStateStore {
1181
1259
  return rowsAffected(await q.update(set)) > 0;
1182
1260
  }
1183
1261
  async deleteSchedule(id) {
1184
- const affected = await this.client().from(DURABLE_TABLES.schedules).where('id', id).delete();
1262
+ const affected = await this.client().from(this.t.schedules).where('id', id).delete();
1185
1263
  return rowsAffected(affected) > 0;
1186
1264
  }
1187
1265
  async listSchedules(query) {
1188
- const q = this.client().from(DURABLE_TABLES.schedules);
1266
+ const q = this.client().from(this.t.schedules);
1189
1267
  if (query.namespace !== undefined) {
1190
1268
  q.whereIn('namespace', namespaceFilterValues(query.namespace));
1191
1269
  }
@@ -1246,7 +1324,7 @@ export class LucidStateStore {
1246
1324
  q.limit(scan);
1247
1325
  return runValueFacetsFromRuns((await q).map(rowToRun), axis, opts);
1248
1326
  }
1249
- const column = axis.field;
1327
+ const column = axis.field === 'tenant' ? 'tenant_id' : axis.field;
1250
1328
  const needle = opts?.search?.trim().toLowerCase();
1251
1329
  // One pass over the matching runs (the shape `runFacets` uses), not a self semi-join on `id`.
1252
1330
  const q = this.scopedRuns(query)
@@ -1268,7 +1346,7 @@ export class LucidStateStore {
1268
1346
  * exactly the set the list would show.
1269
1347
  */
1270
1348
  scopedRuns(query) {
1271
- const q = this.fenced(this.client().from(DURABLE_TABLES.runs));
1349
+ const q = this.fenced(this.client().from(this.t.runs));
1272
1350
  if (query.workflow)
1273
1351
  q.where('workflow', query.workflow);
1274
1352
  if (query.workflows) {
@@ -1292,6 +1370,23 @@ export class LucidStateStore {
1292
1370
  }
1293
1371
  if (query.origin)
1294
1372
  q.where('origin', query.origin);
1373
+ if (query.tenantId !== undefined)
1374
+ q.where('tenant_id', query.tenantId);
1375
+ if (query.tenantIds) {
1376
+ // `tenant_id IN (...)`, with `null` standing for "no tenant"; an empty set matches nothing.
1377
+ const ids = query.tenantIds.filter((id) => id !== null);
1378
+ const withNull = query.tenantIds.includes(null);
1379
+ if (ids.length === 0 && !withNull)
1380
+ q.whereRaw('1 = 0');
1381
+ else {
1382
+ q.andWhere((or) => {
1383
+ if (ids.length)
1384
+ or.orWhereIn('tenant_id', ids);
1385
+ if (withNull)
1386
+ or.orWhereNull('tenant_id');
1387
+ });
1388
+ }
1389
+ }
1295
1390
  if (query.status)
1296
1391
  q.where('status', query.status);
1297
1392
  if (query.statuses) {
@@ -1355,10 +1450,10 @@ export class LucidStateStore {
1355
1450
  }
1356
1451
  q.whereExists((sub) => {
1357
1452
  sub
1358
- .from(DURABLE_TABLES.attributes)
1359
- .whereRaw(`${DURABLE_TABLES.attributes}.run_id = ${DURABLE_TABLES.runs}.id`)
1360
- .andWhere(`${DURABLE_TABLES.attributes}.key`, f.key)
1361
- .whereIn(`${DURABLE_TABLES.attributes}.${col}`, operands);
1453
+ .from(this.t.attributes)
1454
+ .whereRaw('?? = ??', [`${this.t.attributes}.run_id`, `${this.t.runs}.id`])
1455
+ .andWhere(`${this.t.attributes}.key`, f.key)
1456
+ .whereIn(`${this.t.attributes}.${col}`, operands);
1362
1457
  });
1363
1458
  return;
1364
1459
  }
@@ -1366,15 +1461,15 @@ export class LucidStateStore {
1366
1461
  const operand = attributeOperand(f);
1367
1462
  q.whereExists((sub) => {
1368
1463
  sub
1369
- .from(DURABLE_TABLES.attributes)
1370
- .whereRaw(`${DURABLE_TABLES.attributes}.run_id = ${DURABLE_TABLES.runs}.id`)
1371
- .andWhere(`${DURABLE_TABLES.attributes}.key`, f.key)
1372
- .andWhere(`${DURABLE_TABLES.attributes}.${col}`, cmp, operand);
1464
+ .from(this.t.attributes)
1465
+ .whereRaw('?? = ??', [`${this.t.attributes}.run_id`, `${this.t.runs}.id`])
1466
+ .andWhere(`${this.t.attributes}.key`, f.key)
1467
+ .andWhere(`${this.t.attributes}.${col}`, cmp, operand);
1373
1468
  });
1374
1469
  }
1375
1470
  async listCheckpoints(runId) {
1376
1471
  const rows = await this.client()
1377
- .from(DURABLE_TABLES.checkpoints)
1472
+ .from(this.t.checkpoints)
1378
1473
  .where('run_id', runId)
1379
1474
  .orderBy('seq', 'asc');
1380
1475
  return rows.map(rowToCheckpoint);
@@ -1382,9 +1477,9 @@ export class LucidStateStore {
1382
1477
  async listCheckpointsForReplay(runId) {
1383
1478
  const hot = this.hot();
1384
1479
  const rows = (hot
1385
- ? await hot.rows(REPLAY_CHECKPOINTS_SQL, [runId])
1480
+ ? await hot.rows(this.sql.replayCheckpoints, [runId])
1386
1481
  : await this.client()
1387
- .from(DURABLE_TABLES.checkpoints)
1482
+ .from(this.t.checkpoints)
1388
1483
  .where('run_id', runId)
1389
1484
  .orderBy('seq', 'asc')
1390
1485
  .select(...CHECKPOINT_COLUMNS_BUT_INPUT));
@@ -1397,7 +1492,7 @@ export class LucidStateStore {
1397
1492
  const inputs = new Map();
1398
1493
  for (let at = 0; at < unsettled.length; at += CHECKPOINT_BATCH) {
1399
1494
  const found = (await this.client()
1400
- .from(DURABLE_TABLES.checkpoints)
1495
+ .from(this.t.checkpoints)
1401
1496
  .where('run_id', runId)
1402
1497
  .whereIn('seq', unsettled.slice(at, at + CHECKPOINT_BATCH))
1403
1498
  .select('seq', 'input'));
@@ -1413,7 +1508,7 @@ export class LucidStateStore {
1413
1508
  }
1414
1509
  async getLatestCheckpointByName(runId, name) {
1415
1510
  const row = await this.client()
1416
- .from(DURABLE_TABLES.checkpoints)
1511
+ .from(this.t.checkpoints)
1417
1512
  .where('run_id', runId)
1418
1513
  .andWhere('name', name)
1419
1514
  .orderBy('seq', 'desc')
@@ -1424,7 +1519,7 @@ export class LucidStateStore {
1424
1519
  if (prefixes.length === 0)
1425
1520
  return [];
1426
1521
  const rows = await this.client()
1427
- .from(DURABLE_TABLES.checkpoints)
1522
+ .from(this.t.checkpoints)
1428
1523
  .where('run_id', runId)
1429
1524
  .andWhere((q) => {
1430
1525
  for (const p of prefixes)
@@ -1438,11 +1533,11 @@ export class LucidStateStore {
1438
1533
  return 0;
1439
1534
  const hot = this.hot();
1440
1535
  if (hot) {
1441
- const rows = await hot.rows(COUNT_CHECKPOINTS_SQL, [runId, fromSeq, toSeq, statuses]);
1536
+ const rows = await hot.rows(this.sql.countCheckpoints, [runId, fromSeq, toSeq, statuses]);
1442
1537
  return Number(rows[0]?.count ?? 0);
1443
1538
  }
1444
1539
  const rows = await this.client()
1445
- .from(DURABLE_TABLES.checkpoints)
1540
+ .from(this.t.checkpoints)
1446
1541
  .where('run_id', runId)
1447
1542
  .andWhere('seq', '>=', fromSeq)
1448
1543
  .andWhere('seq', '<=', toSeq)
@@ -1455,7 +1550,7 @@ export class LucidStateStore {
1455
1550
  if (statuses.length === 0)
1456
1551
  return false;
1457
1552
  const row = await this.client()
1458
- .from(DURABLE_TABLES.checkpoints)
1553
+ .from(this.t.checkpoints)
1459
1554
  .select('seq')
1460
1555
  .where('run_id', runId)
1461
1556
  .andWhere('seq', '>=', fromSeq)
@@ -1475,7 +1570,7 @@ export class LucidStateStore {
1475
1570
  const client = this.client();
1476
1571
  const kind = nameKindSql(client.dialect.name);
1477
1572
  const q = client
1478
- .from(DURABLE_TABLES.checkpoints)
1573
+ .from(this.t.checkpoints)
1479
1574
  .select('parallel_group as grp', 'status')
1480
1575
  .select(client.raw(`${kind} as name_kind`))
1481
1576
  .count('* as n')
@@ -1521,7 +1616,7 @@ export class LucidStateStore {
1521
1616
  if (query.statuses?.length === 0 || query.namePrefixes?.length === 0)
1522
1617
  return [];
1523
1618
  const client = this.client();
1524
- const q = client.from(DURABLE_TABLES.checkpoints).where('run_id', runId);
1619
+ const q = client.from(this.t.checkpoints).where('run_id', runId);
1525
1620
  if (query.group !== undefined)
1526
1621
  q.andWhere('parallel_group', query.group);
1527
1622
  if (query.excludeGroups?.length) {
@@ -1556,10 +1651,10 @@ export class LucidStateStore {
1556
1651
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
1557
1652
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
1558
1653
  async reindexAttributes(client, runId, attributes) {
1559
- await client.from(DURABLE_TABLES.attributes).where('run_id', runId).delete();
1654
+ await client.from(this.t.attributes).where('run_id', runId).delete();
1560
1655
  const rows = normalizeAttributeRows(runId, attributes);
1561
1656
  if (rows.length) {
1562
- await client.table(DURABLE_TABLES.attributes).multiInsert(rows.map((r) => ({
1657
+ await client.table(this.t.attributes).multiInsert(rows.map((r) => ({
1563
1658
  run_id: r.runId,
1564
1659
  key: r.key,
1565
1660
  str_value: r.strValue,
@@ -1568,6 +1663,43 @@ export class LucidStateStore {
1568
1663
  }
1569
1664
  }
1570
1665
  }
1666
+ /**
1667
+ * Serialize the store's work on a transaction client. A transaction is ONE connection: two store
1668
+ * calls in flight at once (a background run pickup next to a request's own write, under an app's test
1669
+ * `wrapInGlobalTransaction`) would send overlapping queries down it, which `pg` deprecates (and 9.0
1670
+ * removes), and a statement could land inside a savepoint the other call is rolling back. So while
1671
+ * the store's client is a transaction, each call waits for the previous one on that transaction to
1672
+ * finish. Re-entrant: a store method calling another one in the same call does not wait for itself.
1673
+ * A store on a plain connection is untouched (its pool hands each query its own connection).
1674
+ */
1675
+ const transactionTails = new WeakMap();
1676
+ const holdingTransaction = new AsyncLocalStorage();
1677
+ function serializeOnTransactions(proto) {
1678
+ const AsyncFunction = (async () => { }).constructor;
1679
+ for (const name of Object.getOwnPropertyNames(proto)) {
1680
+ if (name === 'constructor')
1681
+ continue;
1682
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
1683
+ const original = descriptor?.value;
1684
+ if (typeof original !== 'function' || !(original instanceof AsyncFunction))
1685
+ continue;
1686
+ const method = original;
1687
+ Object.defineProperty(proto, name, {
1688
+ ...descriptor,
1689
+ value: function (...args) {
1690
+ const client = this.client();
1691
+ if (!client.isTransaction || holdingTransaction.getStore() === client) {
1692
+ return method.apply(this, args);
1693
+ }
1694
+ const previous = transactionTails.get(client) ?? Promise.resolve();
1695
+ const run = previous.then(() => holdingTransaction.run(client, () => method.apply(this, args)), () => holdingTransaction.run(client, () => method.apply(this, args)));
1696
+ transactionTails.set(client, run.catch(() => undefined));
1697
+ return run;
1698
+ },
1699
+ });
1700
+ }
1701
+ }
1702
+ serializeOnTransactions(LucidStateStore.prototype);
1571
1703
  /**
1572
1704
  * Escape LIKE metacharacters (`%`, `_`) — and the escape character itself — using `!` as the ESCAPE
1573
1705
  * char. `!` because a backslash inside a SQL string literal is dialect-divergent (MySQL treats it as
@@ -1618,20 +1750,20 @@ const CHECKPOINT_BATCH = 500;
1618
1750
  /** Most checkpoint rows one multi-row upsert writes (kept under every dialect's bind limit). */
1619
1751
  const CHECKPOINT_UPSERT_BATCH = 200;
1620
1752
  /** One multi-row `INSERT … ON CONFLICT (run_id, seq) DO UPDATE` (MySQL: `ON DUPLICATE KEY`). */
1621
- async function upsertCheckpointRows(client, checkpoints) {
1753
+ async function upsertCheckpointRows(client, checkpointsTable, checkpoints) {
1622
1754
  if (checkpoints.length === 0)
1623
1755
  return;
1624
1756
  await client
1625
1757
  .insertQuery()
1626
- .table(DURABLE_TABLES.checkpoints)
1758
+ .table(checkpointsTable)
1627
1759
  .knexQuery.insert(checkpoints.map(checkpointToRow))
1628
1760
  .onConflict(['run_id', 'seq'])
1629
1761
  .merge();
1630
1762
  }
1631
- async function updateCheckpointIfPending(client, checkpoint) {
1763
+ async function updateCheckpointIfPending(client, checkpointsTable, checkpoint) {
1632
1764
  const { run_id: _runId, seq: _seq, ...row } = checkpointToRow(checkpoint);
1633
1765
  const affected = await client
1634
- .from(DURABLE_TABLES.checkpoints)
1766
+ .from(checkpointsTable)
1635
1767
  .where('run_id', checkpoint.runId)
1636
1768
  .andWhere('seq', checkpoint.seq)
1637
1769
  .andWhere('status', 'pending')
@@ -1661,55 +1793,114 @@ function escapeLike(value) {
1661
1793
  return value.replace(/[!%_]/g, (c) => `!${c}`);
1662
1794
  }
1663
1795
  // --- the Postgres hot path's statements (see `lucid-pg-hot-path.ts`) --------------------------------
1664
- const RUNS_T = quote(DURABLE_TABLES.runs);
1665
- const CHECKPOINTS_T = quote(DURABLE_TABLES.checkpoints);
1666
1796
  const CHECKPOINT_LIST = CHECKPOINT_COLUMNS_BUT_INPUT.map(quote).join(', ');
1667
- const GET_RUN_SQL = `select * from ${RUNS_T} where "id" = ? limit 1`;
1668
- const GET_RUN_SCOPED_SQL = `select * from ${RUNS_T} where "id" = ? and "namespace" = any(?) limit 1`;
1669
- const RUN_IN_STATUSES_SQL = `select 1 from ${RUNS_T} where "id" = ? and "status" = any(?) limit 1`;
1670
- const LOCK_AND_GET_SQL = `update ${RUNS_T} set "locked_by" = ?, "locked_until" = ? where "id" = ? and ("locked_until" is null or "locked_until" <= ?) returning *`;
1671
- const LOCK_AND_GET_SCOPED_SQL = `update ${RUNS_T} set "locked_by" = ?, "locked_until" = ? where "id" = ? and ("locked_until" is null or "locked_until" <= ?) and "namespace" = any(?) returning *`;
1672
- const RELEASE_LOCK_SQL = `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ?`;
1673
- const RELEASE_OWN_LOCK_SQL = `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ? and "locked_by" = ?`;
1674
- const RELEASE_UNLESS_WAKE_SQL = `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ? and "locked_by" = ? and "wake_requested_at" is null`;
1675
- const LOCKED_BY_SQL = `select "locked_by" from ${RUNS_T} where "id" = ? limit 1`;
1676
- const RENEW_LOCK_SQL = `update ${RUNS_T} set "locked_until" = ? where "id" = ? and "locked_by" = ?`;
1677
- const DELETE_ATTRIBUTES_SQL = `delete from ${quote(DURABLE_TABLES.attributes)} where "run_id" = ?`;
1678
- const GET_CHECKPOINT_SQL = `select * from ${CHECKPOINTS_T} where "run_id" = ? and "seq" = ? limit 1`;
1679
- const CHECKPOINTS_FOR_SETTLE_SQL = `select ${CHECKPOINT_LIST} from ${CHECKPOINTS_T} where "run_id" = ? and "seq" = any(?)`;
1680
- const REPLAY_CHECKPOINTS_SQL = `select ${CHECKPOINT_LIST} from ${CHECKPOINTS_T} where "run_id" = ? order by "seq" asc`;
1681
- const COUNT_CHECKPOINTS_SQL = `select count(*) as "count" from ${CHECKPOINTS_T} where "run_id" = ? and "seq" >= ? and "seq" <= ? and "status" = any(?)`;
1682
- const OLDEST_BUFFERED_SIGNAL_SQL = `select * from ${quote(DURABLE_TABLES.bufferedSignals)} where "token" = ? order by "id" asc limit 1`;
1683
- const DELETE_BUFFERED_SIGNAL_SQL = `delete from ${quote(DURABLE_TABLES.bufferedSignals)} where "id" = ?`;
1684
- const BUFFERED_EVENTS_SQL = `select * from ${quote(DURABLE_TABLES.bufferedEvents)} where "name" = ? order by "published_at" asc limit ?`;
1685
- /** Statements whose column list is the row's own keys, built once per key set. */
1686
- const shaped = new Map();
1687
- function shapedSql(key, build) {
1688
- let text = shaped.get(key);
1689
- if (text === undefined) {
1690
- text = build();
1691
- shaped.set(key, text);
1692
- }
1693
- return text;
1694
- }
1695
- /** `INSERT` of one row of `table` with the columns `keys`. */
1696
- function insertSql(table, keys) {
1697
- return shapedSql(`insert:${table}:${keys.join(',')}`, () => {
1698
- const cols = keys.map(quote).join(', ');
1699
- return `insert into ${quote(table)} (${cols}) values (${keys.map(() => '?').join(', ')})`;
1700
- });
1701
- }
1702
- /** {@link insertSql} of a checkpoint that writes nothing when its position is taken. */
1703
- function insertCheckpointIfAbsentSql(keys) {
1704
- return shapedSql(`insert-if-absent:${keys.join(',')}`, () => `${insertSql(DURABLE_TABLES.checkpoints, keys)} on conflict ("run_id", "seq") do nothing returning "seq"`);
1797
+ /**
1798
+ * The runs columns every schema has. The hot path names its columns instead of `*`: a named prepared
1799
+ * statement's result type is fixed when it is parsed, so a `*` statement fails ("cached plan must
1800
+ * not change result type") once a column is added under it — another pod's `ensureSchema` during a
1801
+ * rolling upgrade.
1802
+ */
1803
+ const RUN_BASE_COLUMNS = [
1804
+ 'id',
1805
+ 'workflow',
1806
+ 'workflow_version',
1807
+ 'status',
1808
+ 'namespace',
1809
+ 'input',
1810
+ 'output',
1811
+ 'error',
1812
+ 'wake_at',
1813
+ 'locked_by',
1814
+ 'locked_until',
1815
+ 'recovery_attempts',
1816
+ 'tags',
1817
+ 'search_attributes',
1818
+ 'priority',
1819
+ 'created_at',
1820
+ 'updated_at',
1821
+ ];
1822
+ /** Runs columns that arrived in later schema waves: read only when the table has them. */
1823
+ const RUN_OPTIONAL_COLUMNS = [
1824
+ 'origin',
1825
+ 'awaiting_decision_task_id',
1826
+ 'wake_requested_at',
1827
+ 'tenant_id',
1828
+ 'tenant_kind',
1829
+ ];
1830
+ /** Checkpoint columns that arrived in later schema waves: read only when the table has them. */
1831
+ const CHECKPOINT_OPTIONAL_COLUMNS = [
1832
+ 'queue',
1833
+ 'parallel_group',
1834
+ 'last_heartbeat_at',
1835
+ 'heartbeat_progress',
1836
+ ];
1837
+ const CHECKPOINT_BASE_COLUMNS = [
1838
+ ...CHECKPOINT_COLUMNS_BUT_INPUT.filter((c) => !CHECKPOINT_OPTIONAL_COLUMNS.includes(c)),
1839
+ 'input',
1840
+ ];
1841
+ /** The later-wave columns present, in the configured schema (`?` first) or the current one. */
1842
+ const COLUMNS_PRESENT_SQL = `select "table_name", "column_name" from information_schema.columns where "table_schema" = coalesce(?, current_schema()) and "table_name" = any(?) and "column_name" = any(?)`;
1843
+ const BUFFERED_SIGNAL_LIST = ['id', 'token', 'payload'].map(quote).join(', ');
1844
+ const BUFFERED_EVENT_LIST = ['id', 'name', 'payload', 'published_at'].map(quote).join(', ');
1845
+ /** A quoted, schema-qualified table name for raw SQL: `"schema"."table"`, or `"table"`. */
1846
+ function quotedTable(schema, table) {
1847
+ return schema ? `${quote(schema)}.${quote(table)}` : quote(table);
1705
1848
  }
1706
- /** `UPDATE` of one run's columns `keys`, with a status predicate when `conditional`. */
1707
- function updateRunSql(keys, conditional) {
1708
- return shapedSql(`update-run:${conditional}:${keys.join(',')}`, () => {
1709
- const set = keys.map((k) => `${quote(k)} = ?`).join(', ');
1710
- const where = conditional ? `"id" = ? and "status" = any(?)` : `"id" = ?`;
1711
- return `update ${RUNS_T} set ${set} where ${where}`;
1712
- });
1849
+ const hotStatementsBySchema = new Map();
1850
+ /** {@link HotStatements} for `schema` (the connection's current schema when unset). */
1851
+ function hotStatements(schema) {
1852
+ const cacheKey = schema ?? '';
1853
+ const cached = hotStatementsBySchema.get(cacheKey);
1854
+ if (cached)
1855
+ return cached;
1856
+ const RUNS_T = quotedTable(schema, DURABLE_TABLES.runs);
1857
+ const CHECKPOINTS_T = quotedTable(schema, DURABLE_TABLES.checkpoints);
1858
+ const ATTRIBUTES_T = quotedTable(schema, DURABLE_TABLES.attributes);
1859
+ const SIGNALS_T = quotedTable(schema, DURABLE_TABLES.bufferedSignals);
1860
+ const EVENTS_T = quotedTable(schema, DURABLE_TABLES.bufferedEvents);
1861
+ /** Statements whose column list is the row's own keys, built once per key set. */
1862
+ const shaped = new Map();
1863
+ const shapedSql = (key, build) => {
1864
+ let text = shaped.get(key);
1865
+ if (text === undefined) {
1866
+ text = build();
1867
+ shaped.set(key, text);
1868
+ }
1869
+ return text;
1870
+ };
1871
+ /** `INSERT` of one row of `table` (quoted) with the columns `keys`. */
1872
+ const insertSql = (table, keys) => `insert into ${table} (${keys.map(quote).join(', ')}) values (${keys.map(() => '?').join(', ')})`;
1873
+ const statements = {
1874
+ reads: (cols, checkpointCols) => ({
1875
+ getCheckpoint: shapedSql(`checkpoint-get:${checkpointCols}`, () => `select ${checkpointCols} from ${CHECKPOINTS_T} where "run_id" = ? and "seq" = ? limit 1`),
1876
+ get: shapedSql(`run-get:${cols}`, () => `select ${cols} from ${RUNS_T} where "id" = ? limit 1`),
1877
+ getScoped: shapedSql(`run-get-scoped:${cols}`, () => `select ${cols} from ${RUNS_T} where "id" = ? and "namespace" = any(?) limit 1`),
1878
+ lockAndGet: shapedSql(`run-lock:${cols}`, () => `update ${RUNS_T} set "locked_by" = ?, "locked_until" = ? where "id" = ? and ("locked_until" is null or "locked_until" <= ?) returning ${cols}`),
1879
+ lockAndGetScoped: shapedSql(`run-lock-scoped:${cols}`, () => `update ${RUNS_T} set "locked_by" = ?, "locked_until" = ? where "id" = ? and ("locked_until" is null or "locked_until" <= ?) and "namespace" = any(?) returning ${cols}`),
1880
+ }),
1881
+ runInStatuses: `select 1 from ${RUNS_T} where "id" = ? and "status" = any(?) limit 1`,
1882
+ releaseLock: `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ?`,
1883
+ releaseOwnLock: `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ? and "locked_by" = ?`,
1884
+ releaseUnlessWake: `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ? and "locked_by" = ? and "wake_requested_at" is null`,
1885
+ lockedBy: `select "locked_by" from ${RUNS_T} where "id" = ? limit 1`,
1886
+ renewLock: `update ${RUNS_T} set "locked_until" = ? where "id" = ? and "locked_by" = ?`,
1887
+ deleteAttributes: `delete from ${ATTRIBUTES_T} where "run_id" = ?`,
1888
+ checkpointsForSettle: `select ${CHECKPOINT_LIST} from ${CHECKPOINTS_T} where "run_id" = ? and "seq" = any(?)`,
1889
+ replayCheckpoints: `select ${CHECKPOINT_LIST} from ${CHECKPOINTS_T} where "run_id" = ? order by "seq" asc`,
1890
+ countCheckpoints: `select count(*) as "count" from ${CHECKPOINTS_T} where "run_id" = ? and "seq" >= ? and "seq" <= ? and "status" = any(?)`,
1891
+ oldestBufferedSignal: `select ${BUFFERED_SIGNAL_LIST} from ${SIGNALS_T} where "token" = ? order by "id" asc limit 1`,
1892
+ deleteBufferedSignal: `delete from ${SIGNALS_T} where "id" = ?`,
1893
+ bufferedEvents: `select ${BUFFERED_EVENT_LIST} from ${EVENTS_T} where "name" = ? order by "published_at" asc limit ?`,
1894
+ insertRunIfAbsent: (keys) => shapedSql(`insert-run-if-absent:${keys.join(',')}`, () => `${insertSql(RUNS_T, keys)} on conflict ("id") do nothing returning "id"`),
1895
+ insertCheckpointIfAbsent: (keys) => shapedSql(`insert-if-absent:${keys.join(',')}`, () => `${insertSql(CHECKPOINTS_T, keys)} on conflict ("run_id", "seq") do nothing returning "seq"`),
1896
+ updateRun: (keys, conditional) => shapedSql(`update-run:${conditional}:${keys.join(',')}`, () => {
1897
+ const set = keys.map((k) => `${quote(k)} = ?`).join(', ');
1898
+ const where = conditional ? `"id" = ? and "status" = any(?)` : `"id" = ?`;
1899
+ return `update ${RUNS_T} set ${set} where ${where}`;
1900
+ }),
1901
+ };
1902
+ hotStatementsBySchema.set(cacheKey, statements);
1903
+ return statements;
1713
1904
  }
1714
1905
  /**
1715
1906
  * {@link LucidStateStore.settleResultsIfPending} on Postgres: one `UPDATE … FROM (VALUES …) …
@@ -1717,7 +1908,7 @@ function updateRunSql(keys, conditional) {
1717
1908
  * in the same statement, and read back with its `wake_at`. What a result does not carry keeps its
1718
1909
  * stored value: `output` unless it completed, `events` and `started_at` unless present.
1719
1910
  */
1720
- async function settleResultsReturningPg(client, hot, results) {
1911
+ async function settleResultsReturningPg(client, hot, schema, results) {
1721
1912
  const values = [];
1722
1913
  const bindings = [];
1723
1914
  for (const r of results) {
@@ -1731,7 +1922,7 @@ async function settleResultsReturningPg(client, hot, results) {
1731
1922
  const cols = CHECKPOINT_COLUMNS_BUT_INPUT.map((c) => `c.${c}`).join(', ');
1732
1923
  const statuses = SETTLEABLE_RUN_STATUSES.map(() => '?').join(', ');
1733
1924
  bindings.push(...SETTLEABLE_RUN_STATUSES);
1734
- const sql = `update ${DURABLE_TABLES.checkpoints} as c set
1925
+ const sql = `update ${quotedTable(schema, DURABLE_TABLES.checkpoints)} as c set
1735
1926
  status = v.status,
1736
1927
  output = case when v.status = 'completed' then v.output else c.output end,
1737
1928
  error = v.error,
@@ -1740,7 +1931,7 @@ async function settleResultsReturningPg(client, hot, results) {
1740
1931
  finished_at = v.finished_at
1741
1932
  from (values ${values.join(', ')})
1742
1933
  as v(run_id, seq, status, output, error, has_events, events, started_at, finished_at),
1743
- ${DURABLE_TABLES.runs} as r
1934
+ ${quotedTable(schema, DURABLE_TABLES.runs)} as r
1744
1935
  where c.run_id = v.run_id and c.seq = v.seq and c.status = 'pending'
1745
1936
  and r.id = c.run_id and r.status in (${statuses})
1746
1937
  returning ${cols}, r.status as run_status, r.wake_at as run_wake_at`;
@@ -1758,14 +1949,36 @@ async function settleResultsReturningPg(client, hot, results) {
1758
1949
  }
1759
1950
  return out;
1760
1951
  }
1761
- /** A duplicate-key violation, as MySQL and SQLite report it through Knex. */
1762
- function isDuplicateKey(err) {
1763
- const e = err;
1764
- return (e?.code === 'ER_DUP_ENTRY' ||
1765
- e?.errno === 1062 ||
1766
- e?.code === 'SQLITE_CONSTRAINT_PRIMARYKEY' ||
1767
- e?.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
1768
- e?.code === '23505');
1952
+ /**
1953
+ * Insert `row` into `table` unless a row with the same `key` exists, as a statement that never
1954
+ * fails on the conflict, and say whether it wrote. A failing statement would abort a surrounding
1955
+ * Postgres transaction, so the conflict is never detected by catching a duplicate-key error.
1956
+ *
1957
+ * - Postgres and SQLite: `INSERT … ON CONFLICT (key) DO NOTHING RETURNING key`, a row back when it wrote.
1958
+ * - MySQL: `INSERT … ON DUPLICATE KEY UPDATE key = IF(LAST_INSERT_ID(1), key, key)`, which leaves an
1959
+ * existing row as it is and reports `insertId` 1 for it, 0 for a row it wrote (`table` has no
1960
+ * AUTO_INCREMENT). The affected-row count cannot tell them apart (mysql2 counts found rows), and
1961
+ * `INSERT IGNORE` would turn a value the column cannot hold into a silent truncation.
1962
+ */
1963
+ async function insertIfAbsent(client, table, row, key) {
1964
+ if (client.dialect.name === 'mysql') {
1965
+ const first = key[0];
1966
+ const knex = client.getWriteClient();
1967
+ const { sql, bindings } = client
1968
+ .insertQuery()
1969
+ .table(table)
1970
+ .knexQuery.insert(row)
1971
+ .onConflict(key)
1972
+ .merge({ [first]: knex.raw('if(last_insert_id(1), ??, ??)', [first, first]) })
1973
+ .toSQL()
1974
+ .toNative();
1975
+ const result = (await client.rawQuery(sql, bindings));
1976
+ const header = (Array.isArray(result) ? result[0] : result);
1977
+ return Number(header?.insertId ?? 0) === 0;
1978
+ }
1979
+ const insert = client.insertQuery().table(table).knexQuery.insert(row).onConflict(key);
1980
+ const inserted = (await insert.ignore().returning(key[0]));
1981
+ return inserted.length > 0;
1769
1982
  }
1770
1983
  function checkpointKey(runId, seq) {
1771
1984
  return `${runId}\u0000${seq}`;
@@ -1776,11 +1989,11 @@ function checkpointKey(runId, seq) {
1776
1989
  * so the database write lock is taken FIRST with a no-op write, and no other connection or process
1777
1990
  * settles a row between the read and the caller's writes.
1778
1991
  */
1779
- async function lockPendingCheckpoints(trx, entries, columns) {
1992
+ async function lockPendingCheckpoints(trx, checkpointsTable, entries, columns) {
1780
1993
  const first = entries[0];
1781
1994
  if (first && !locksRows(trx)) {
1782
1995
  await trx
1783
- .from(DURABLE_TABLES.checkpoints)
1996
+ .from(checkpointsTable)
1784
1997
  .where('run_id', first.runId)
1785
1998
  .andWhere('seq', first.seq)
1786
1999
  .andWhere('status', 'pending')
@@ -1797,7 +2010,7 @@ async function lockPendingCheckpoints(trx, entries, columns) {
1797
2010
  const seqs = [...seqSet];
1798
2011
  for (let at = 0; at < seqs.length; at += CHECKPOINT_BATCH) {
1799
2012
  const q = trx
1800
- .from(DURABLE_TABLES.checkpoints)
2013
+ .from(checkpointsTable)
1801
2014
  .where('run_id', runId)
1802
2015
  .whereIn('seq', seqs.slice(at, at + CHECKPOINT_BATCH))
1803
2016
  .andWhere('status', 'pending')
@@ -1834,6 +2047,10 @@ function scheduleToRow(r) {
1834
2047
  tags: r.tags?.length ? JSON.stringify(r.tags) : null,
1835
2048
  spec: JSON.stringify(r.spec),
1836
2049
  state: JSON.stringify(r.state),
2050
+ // Only when set: a schedule with no tenant saves on a schema without the columns.
2051
+ ...(r.tenantId !== undefined
2052
+ ? { tenant_id: r.tenantId, tenant_kind: r.tenantKind ?? null }
2053
+ : {}),
1837
2054
  created_at: r.createdAt.getTime(),
1838
2055
  updated_at: r.updatedAt.getTime(),
1839
2056
  };
@@ -1850,6 +2067,8 @@ function rowToSchedule(row) {
1850
2067
  paused: row.paused === true || row.paused === 1 || row.paused === '1' || row.paused === 'true',
1851
2068
  nextFireAt: row.next_fire_at == null ? null : Number(row.next_fire_at),
1852
2069
  ...(tags?.length ? { tags } : {}),
2070
+ ...(row.tenant_id != null ? { tenantId: row.tenant_id } : {}),
2071
+ ...(row.tenant_kind != null ? { tenantKind: row.tenant_kind } : {}),
1853
2072
  spec: parseJsonColumn(row.spec, `${DURABLE_TABLES.schedules}.spec`, row.id) ?? {},
1854
2073
  state: parseJsonColumn(row.state, `${DURABLE_TABLES.schedules}.state`, row.id) ?? {},
1855
2074
  createdAt: new Date(Number(row.created_at)),