adispatchd 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/api/errors.d.ts +27 -0
  4. package/dist/api/errors.js +80 -0
  5. package/dist/api/errors.js.map +1 -0
  6. package/dist/api/index.d.ts +10 -0
  7. package/dist/api/index.js +11 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/logs.d.ts +30 -0
  10. package/dist/api/logs.js +88 -0
  11. package/dist/api/logs.js.map +1 -0
  12. package/dist/api/serialize.d.ts +65 -0
  13. package/dist/api/serialize.js +135 -0
  14. package/dist/api/serialize.js.map +1 -0
  15. package/dist/api/server.d.ts +89 -0
  16. package/dist/api/server.js +751 -0
  17. package/dist/api/server.js.map +1 -0
  18. package/dist/api/sse.d.ts +90 -0
  19. package/dist/api/sse.js +225 -0
  20. package/dist/api/sse.js.map +1 -0
  21. package/dist/app.d.ts +173 -0
  22. package/dist/app.js +488 -0
  23. package/dist/app.js.map +1 -0
  24. package/dist/cli/config-validate.d.ts +6 -0
  25. package/dist/cli/config-validate.js +39 -0
  26. package/dist/cli/config-validate.js.map +1 -0
  27. package/dist/cli/control.d.ts +31 -0
  28. package/dist/cli/control.js +186 -0
  29. package/dist/cli/control.js.map +1 -0
  30. package/dist/cli/http-client.d.ts +57 -0
  31. package/dist/cli/http-client.js +174 -0
  32. package/dist/cli/http-client.js.map +1 -0
  33. package/dist/cli/index.d.ts +14 -0
  34. package/dist/cli/index.js +14 -0
  35. package/dist/cli/index.js.map +1 -0
  36. package/dist/cli/init-templates.d.ts +20 -0
  37. package/dist/cli/init-templates.js +275 -0
  38. package/dist/cli/init-templates.js.map +1 -0
  39. package/dist/cli/init.d.ts +16 -0
  40. package/dist/cli/init.js +107 -0
  41. package/dist/cli/init.js.map +1 -0
  42. package/dist/cli/main.d.ts +2 -0
  43. package/dist/cli/main.js +25 -0
  44. package/dist/cli/main.js.map +1 -0
  45. package/dist/cli/program.d.ts +102 -0
  46. package/dist/cli/program.js +133 -0
  47. package/dist/cli/program.js.map +1 -0
  48. package/dist/cli/service.d.ts +27 -0
  49. package/dist/cli/service.js +252 -0
  50. package/dist/cli/service.js.map +1 -0
  51. package/dist/cli/start.d.ts +22 -0
  52. package/dist/cli/start.js +133 -0
  53. package/dist/cli/start.js.map +1 -0
  54. package/dist/cli/workflow-dry-run.d.ts +13 -0
  55. package/dist/cli/workflow-dry-run.js +144 -0
  56. package/dist/cli/workflow-dry-run.js.map +1 -0
  57. package/dist/config/field-catalog.d.ts +33 -0
  58. package/dist/config/field-catalog.js +172 -0
  59. package/dist/config/field-catalog.js.map +1 -0
  60. package/dist/config/filter-schema.d.ts +15 -0
  61. package/dist/config/filter-schema.js +110 -0
  62. package/dist/config/filter-schema.js.map +1 -0
  63. package/dist/config/index.d.ts +15 -0
  64. package/dist/config/index.js +16 -0
  65. package/dist/config/index.js.map +1 -0
  66. package/dist/config/issues.d.ts +27 -0
  67. package/dist/config/issues.js +37 -0
  68. package/dist/config/issues.js.map +1 -0
  69. package/dist/config/loader.d.ts +32 -0
  70. package/dist/config/loader.js +250 -0
  71. package/dist/config/loader.js.map +1 -0
  72. package/dist/config/manager.d.ts +153 -0
  73. package/dist/config/manager.js +276 -0
  74. package/dist/config/manager.js.map +1 -0
  75. package/dist/config/model.d.ts +334 -0
  76. package/dist/config/model.js +2 -0
  77. package/dist/config/model.js.map +1 -0
  78. package/dist/config/normalize.d.ts +20 -0
  79. package/dist/config/normalize.js +200 -0
  80. package/dist/config/normalize.js.map +1 -0
  81. package/dist/config/parameters.d.ts +37 -0
  82. package/dist/config/parameters.js +71 -0
  83. package/dist/config/parameters.js.map +1 -0
  84. package/dist/config/public.d.ts +6 -0
  85. package/dist/config/public.js +18 -0
  86. package/dist/config/public.js.map +1 -0
  87. package/dist/config/schemas.d.ts +538 -0
  88. package/dist/config/schemas.js +321 -0
  89. package/dist/config/schemas.js.map +1 -0
  90. package/dist/config/snapshot.d.ts +38 -0
  91. package/dist/config/snapshot.js +57 -0
  92. package/dist/config/snapshot.js.map +1 -0
  93. package/dist/config/validate.d.ts +33 -0
  94. package/dist/config/validate.js +346 -0
  95. package/dist/config/validate.js.map +1 -0
  96. package/dist/core/clock.d.ts +59 -0
  97. package/dist/core/clock.js +133 -0
  98. package/dist/core/clock.js.map +1 -0
  99. package/dist/core/duration.d.ts +19 -0
  100. package/dist/core/duration.js +93 -0
  101. package/dist/core/duration.js.map +1 -0
  102. package/dist/core/errors.d.ts +62 -0
  103. package/dist/core/errors.js +72 -0
  104. package/dist/core/errors.js.map +1 -0
  105. package/dist/core/ids.d.ts +57 -0
  106. package/dist/core/ids.js +88 -0
  107. package/dist/core/ids.js.map +1 -0
  108. package/dist/core/index.d.ts +13 -0
  109. package/dist/core/index.js +14 -0
  110. package/dist/core/index.js.map +1 -0
  111. package/dist/core/logger.d.ts +100 -0
  112. package/dist/core/logger.js +207 -0
  113. package/dist/core/logger.js.map +1 -0
  114. package/dist/core/paths.d.ts +84 -0
  115. package/dist/core/paths.js +138 -0
  116. package/dist/core/paths.js.map +1 -0
  117. package/dist/core/template.d.ts +136 -0
  118. package/dist/core/template.js +261 -0
  119. package/dist/core/template.js.map +1 -0
  120. package/dist/core/types.d.ts +544 -0
  121. package/dist/core/types.js +172 -0
  122. package/dist/core/types.js.map +1 -0
  123. package/dist/core/worker-protocol.d.ts +102 -0
  124. package/dist/core/worker-protocol.js +75 -0
  125. package/dist/core/worker-protocol.js.map +1 -0
  126. package/dist/executors/classify.d.ts +25 -0
  127. package/dist/executors/classify.js +54 -0
  128. package/dist/executors/classify.js.map +1 -0
  129. package/dist/executors/dispatcher.d.ts +35 -0
  130. package/dist/executors/dispatcher.js +68 -0
  131. package/dist/executors/dispatcher.js.map +1 -0
  132. package/dist/executors/env.d.ts +43 -0
  133. package/dist/executors/env.js +66 -0
  134. package/dist/executors/env.js.map +1 -0
  135. package/dist/executors/event-parser.d.ts +41 -0
  136. package/dist/executors/event-parser.js +106 -0
  137. package/dist/executors/event-parser.js.map +1 -0
  138. package/dist/executors/index.d.ts +12 -0
  139. package/dist/executors/index.js +13 -0
  140. package/dist/executors/index.js.map +1 -0
  141. package/dist/executors/log-capture.d.ts +57 -0
  142. package/dist/executors/log-capture.js +155 -0
  143. package/dist/executors/log-capture.js.map +1 -0
  144. package/dist/executors/process-identity.d.ts +124 -0
  145. package/dist/executors/process-identity.js +283 -0
  146. package/dist/executors/process-identity.js.map +1 -0
  147. package/dist/executors/shell-executor.d.ts +35 -0
  148. package/dist/executors/shell-executor.js +478 -0
  149. package/dist/executors/shell-executor.js.map +1 -0
  150. package/dist/executors/stream-redactor.d.ts +40 -0
  151. package/dist/executors/stream-redactor.js +89 -0
  152. package/dist/executors/stream-redactor.js.map +1 -0
  153. package/dist/executors/workspace.d.ts +10 -0
  154. package/dist/executors/workspace.js +47 -0
  155. package/dist/executors/workspace.js.map +1 -0
  156. package/dist/filters/evaluate.d.ts +77 -0
  157. package/dist/filters/evaluate.js +187 -0
  158. package/dist/filters/evaluate.js.map +1 -0
  159. package/dist/filters/fields.d.ts +35 -0
  160. package/dist/filters/fields.js +75 -0
  161. package/dist/filters/fields.js.map +1 -0
  162. package/dist/filters/index.d.ts +11 -0
  163. package/dist/filters/index.js +12 -0
  164. package/dist/filters/index.js.map +1 -0
  165. package/dist/filters/iso-date.d.ts +10 -0
  166. package/dist/filters/iso-date.js +42 -0
  167. package/dist/filters/iso-date.js.map +1 -0
  168. package/dist/filters/predicate.d.ts +24 -0
  169. package/dist/filters/predicate.js +178 -0
  170. package/dist/filters/predicate.js.map +1 -0
  171. package/dist/filters/priority.d.ts +51 -0
  172. package/dist/filters/priority.js +91 -0
  173. package/dist/filters/priority.js.map +1 -0
  174. package/dist/filters/pushdown.d.ts +12 -0
  175. package/dist/filters/pushdown.js +52 -0
  176. package/dist/filters/pushdown.js.map +1 -0
  177. package/dist/filters/shell-check.d.ts +110 -0
  178. package/dist/filters/shell-check.js +304 -0
  179. package/dist/filters/shell-check.js.map +1 -0
  180. package/dist/identities/github-app.d.ts +55 -0
  181. package/dist/identities/github-app.js +160 -0
  182. package/dist/identities/github-app.js.map +1 -0
  183. package/dist/identities/index.d.ts +6 -0
  184. package/dist/identities/index.js +7 -0
  185. package/dist/identities/index.js.map +1 -0
  186. package/dist/identities/jwt.d.ts +24 -0
  187. package/dist/identities/jwt.js +56 -0
  188. package/dist/identities/jwt.js.map +1 -0
  189. package/dist/lifecycle/context.d.ts +106 -0
  190. package/dist/lifecycle/context.js +170 -0
  191. package/dist/lifecycle/context.js.map +1 -0
  192. package/dist/lifecycle/engine.d.ts +185 -0
  193. package/dist/lifecycle/engine.js +551 -0
  194. package/dist/lifecycle/engine.js.map +1 -0
  195. package/dist/lifecycle/errors.d.ts +48 -0
  196. package/dist/lifecycle/errors.js +71 -0
  197. package/dist/lifecycle/errors.js.map +1 -0
  198. package/dist/lifecycle/index.d.ts +9 -0
  199. package/dist/lifecycle/index.js +9 -0
  200. package/dist/lifecycle/index.js.map +1 -0
  201. package/dist/lifecycle/performers.d.ts +66 -0
  202. package/dist/lifecycle/performers.js +179 -0
  203. package/dist/lifecycle/performers.js.map +1 -0
  204. package/dist/notifications/index.d.ts +6 -0
  205. package/dist/notifications/index.js +7 -0
  206. package/dist/notifications/index.js.map +1 -0
  207. package/dist/notifications/service.d.ts +115 -0
  208. package/dist/notifications/service.js +364 -0
  209. package/dist/notifications/service.js.map +1 -0
  210. package/dist/persistence/actions.d.ts +101 -0
  211. package/dist/persistence/actions.js +196 -0
  212. package/dist/persistence/actions.js.map +1 -0
  213. package/dist/persistence/blocks.d.ts +85 -0
  214. package/dist/persistence/blocks.js +140 -0
  215. package/dist/persistence/blocks.js.map +1 -0
  216. package/dist/persistence/cron-firings.d.ts +55 -0
  217. package/dist/persistence/cron-firings.js +70 -0
  218. package/dist/persistence/cron-firings.js.map +1 -0
  219. package/dist/persistence/database.d.ts +60 -0
  220. package/dist/persistence/database.js +124 -0
  221. package/dist/persistence/database.js.map +1 -0
  222. package/dist/persistence/errors.d.ts +39 -0
  223. package/dist/persistence/errors.js +67 -0
  224. package/dist/persistence/errors.js.map +1 -0
  225. package/dist/persistence/events.d.ts +72 -0
  226. package/dist/persistence/events.js +83 -0
  227. package/dist/persistence/events.js.map +1 -0
  228. package/dist/persistence/index.d.ts +18 -0
  229. package/dist/persistence/index.js +19 -0
  230. package/dist/persistence/index.js.map +1 -0
  231. package/dist/persistence/instance-lock.d.ts +26 -0
  232. package/dist/persistence/instance-lock.js +117 -0
  233. package/dist/persistence/instance-lock.js.map +1 -0
  234. package/dist/persistence/lock-keys.d.ts +16 -0
  235. package/dist/persistence/lock-keys.js +41 -0
  236. package/dist/persistence/lock-keys.js.map +1 -0
  237. package/dist/persistence/notifications.d.ts +113 -0
  238. package/dist/persistence/notifications.js +213 -0
  239. package/dist/persistence/notifications.js.map +1 -0
  240. package/dist/persistence/persistence.d.ts +46 -0
  241. package/dist/persistence/persistence.js +42 -0
  242. package/dist/persistence/persistence.js.map +1 -0
  243. package/dist/persistence/retention.d.ts +46 -0
  244. package/dist/persistence/retention.js +68 -0
  245. package/dist/persistence/retention.js.map +1 -0
  246. package/dist/persistence/runs.d.ts +215 -0
  247. package/dist/persistence/runs.js +417 -0
  248. package/dist/persistence/runs.js.map +1 -0
  249. package/dist/persistence/schema.d.ts +16 -0
  250. package/dist/persistence/schema.js +183 -0
  251. package/dist/persistence/schema.js.map +1 -0
  252. package/dist/persistence/sqlite.d.ts +49 -0
  253. package/dist/persistence/sqlite.js +131 -0
  254. package/dist/persistence/sqlite.js.map +1 -0
  255. package/dist/runs/controller.d.ts +307 -0
  256. package/dist/runs/controller.js +1078 -0
  257. package/dist/runs/controller.js.map +1 -0
  258. package/dist/runs/errors.d.ts +20 -0
  259. package/dist/runs/errors.js +26 -0
  260. package/dist/runs/errors.js.map +1 -0
  261. package/dist/runs/event-bus.d.ts +106 -0
  262. package/dist/runs/event-bus.js +105 -0
  263. package/dist/runs/event-bus.js.map +1 -0
  264. package/dist/runs/execution.d.ts +39 -0
  265. package/dist/runs/execution.js +146 -0
  266. package/dist/runs/execution.js.map +1 -0
  267. package/dist/runs/index.d.ts +12 -0
  268. package/dist/runs/index.js +13 -0
  269. package/dist/runs/index.js.map +1 -0
  270. package/dist/runs/retention.d.ts +64 -0
  271. package/dist/runs/retention.js +232 -0
  272. package/dist/runs/retention.js.map +1 -0
  273. package/dist/runs/workspace.d.ts +20 -0
  274. package/dist/runs/workspace.js +51 -0
  275. package/dist/runs/workspace.js.map +1 -0
  276. package/dist/scheduler/checks.d.ts +41 -0
  277. package/dist/scheduler/checks.js +111 -0
  278. package/dist/scheduler/checks.js.map +1 -0
  279. package/dist/scheduler/cron.d.ts +32 -0
  280. package/dist/scheduler/cron.js +83 -0
  281. package/dist/scheduler/cron.js.map +1 -0
  282. package/dist/scheduler/dry-run.d.ts +42 -0
  283. package/dist/scheduler/dry-run.js +167 -0
  284. package/dist/scheduler/dry-run.js.map +1 -0
  285. package/dist/scheduler/errors.d.ts +22 -0
  286. package/dist/scheduler/errors.js +22 -0
  287. package/dist/scheduler/errors.js.map +1 -0
  288. package/dist/scheduler/format.d.ts +3 -0
  289. package/dist/scheduler/format.js +73 -0
  290. package/dist/scheduler/format.js.map +1 -0
  291. package/dist/scheduler/index.d.ts +13 -0
  292. package/dist/scheduler/index.js +14 -0
  293. package/dist/scheduler/index.js.map +1 -0
  294. package/dist/scheduler/planner.d.ts +88 -0
  295. package/dist/scheduler/planner.js +255 -0
  296. package/dist/scheduler/planner.js.map +1 -0
  297. package/dist/scheduler/scheduler.d.ts +160 -0
  298. package/dist/scheduler/scheduler.js +680 -0
  299. package/dist/scheduler/scheduler.js.map +1 -0
  300. package/dist/scheduler/state.d.ts +26 -0
  301. package/dist/scheduler/state.js +25 -0
  302. package/dist/scheduler/state.js.map +1 -0
  303. package/dist/scheduler/types.d.ts +197 -0
  304. package/dist/scheduler/types.js +3 -0
  305. package/dist/scheduler/types.js.map +1 -0
  306. package/dist/service/index.d.ts +6 -0
  307. package/dist/service/index.js +7 -0
  308. package/dist/service/index.js.map +1 -0
  309. package/dist/service/install.d.ts +93 -0
  310. package/dist/service/install.js +250 -0
  311. package/dist/service/install.js.map +1 -0
  312. package/dist/service/unit.d.ts +50 -0
  313. package/dist/service/unit.js +171 -0
  314. package/dist/service/unit.js.map +1 -0
  315. package/dist/sources/github/adapter.d.ts +99 -0
  316. package/dist/sources/github/adapter.js +336 -0
  317. package/dist/sources/github/adapter.js.map +1 -0
  318. package/dist/sources/github/fields.d.ts +8 -0
  319. package/dist/sources/github/fields.js +72 -0
  320. package/dist/sources/github/fields.js.map +1 -0
  321. package/dist/sources/github/http.d.ts +100 -0
  322. package/dist/sources/github/http.js +288 -0
  323. package/dist/sources/github/http.js.map +1 -0
  324. package/dist/sources/github/index.d.ts +6 -0
  325. package/dist/sources/github/index.js +7 -0
  326. package/dist/sources/github/index.js.map +1 -0
  327. package/dist/sources/github/pushdown.d.ts +14 -0
  328. package/dist/sources/github/pushdown.js +47 -0
  329. package/dist/sources/github/pushdown.js.map +1 -0
  330. package/dist/sources/github/refs.d.ts +17 -0
  331. package/dist/sources/github/refs.js +61 -0
  332. package/dist/sources/github/refs.js.map +1 -0
  333. package/dist/sources/index.d.ts +6 -0
  334. package/dist/sources/index.js +7 -0
  335. package/dist/sources/index.js.map +1 -0
  336. package/dist/ui/index.d.ts +12 -0
  337. package/dist/ui/index.js +28 -0
  338. package/dist/ui/index.js.map +1 -0
  339. package/dist/ui/public/app.css +239 -0
  340. package/dist/ui/public/favicon.svg +1 -0
  341. package/dist/ui/public/index.html +27 -0
  342. package/dist/ui/public/js/client/api.js +68 -0
  343. package/dist/ui/public/js/client/app.js +146 -0
  344. package/dist/ui/public/js/client/dom.js +34 -0
  345. package/dist/ui/public/js/client/page.js +9 -0
  346. package/dist/ui/public/js/client/pages/actions.js +35 -0
  347. package/dist/ui/public/js/client/pages/blocked.js +58 -0
  348. package/dist/ui/public/js/client/pages/config.js +126 -0
  349. package/dist/ui/public/js/client/pages/dashboard.js +175 -0
  350. package/dist/ui/public/js/client/pages/run.js +367 -0
  351. package/dist/ui/public/js/client/pages/runs.js +72 -0
  352. package/dist/ui/public/js/client/pages/views.js +80 -0
  353. package/dist/ui/public/js/client/pages/workflows.js +291 -0
  354. package/dist/ui/public/js/client/types.js +1 -0
  355. package/dist/ui/public/js/client/ui.js +152 -0
  356. package/dist/ui/public/js/shared/dom.js +77 -0
  357. package/dist/ui/public/js/shared/events.js +88 -0
  358. package/dist/ui/public/js/shared/format.js +213 -0
  359. package/dist/ui/public/js/shared/logs.js +136 -0
  360. package/dist/ui/public/js/shared/params.js +90 -0
  361. package/dist/ui/public/js/shared/route.js +36 -0
  362. package/dist/ui/public/js/shared/sort.js +70 -0
  363. package/dist/ui/public/js/shared/sse.js +193 -0
  364. package/dist/ui/public/js/shared/throttle.js +65 -0
  365. package/dist/views/index.d.ts +6 -0
  366. package/dist/views/index.js +7 -0
  367. package/dist/views/index.js.map +1 -0
  368. package/dist/views/service.d.ts +131 -0
  369. package/dist/views/service.js +264 -0
  370. package/dist/views/service.js.map +1 -0
  371. package/docs/api.md +239 -0
  372. package/docs/configuration-reference.md +1785 -0
  373. package/docs/cron-semantics.md +96 -0
  374. package/docs/manual-tests/github-fixture/config.yaml +30 -0
  375. package/docs/manual-tests/github-fixture/helpers.sh +93 -0
  376. package/docs/manual-tests/github-fixture/identities.yaml +18 -0
  377. package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
  378. package/docs/manual-tests/github-fixture/sources.yaml +13 -0
  379. package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
  380. package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
  381. package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
  382. package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
  383. package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
  384. package/docs/manual-tests/github.md +1308 -0
  385. package/docs/manual-tests/process-supervision.md +64 -0
  386. package/docs/manual-tests/systemd.md +68 -0
  387. package/docs/operations.md +271 -0
  388. package/docs/worker-protocol.md +145 -0
  389. package/examples/config/config.yaml +35 -0
  390. package/examples/config/identities.yaml +18 -0
  391. package/examples/config/sources.yaml +13 -0
  392. package/examples/config/views/needs-maintainer.yaml +15 -0
  393. package/examples/config/workflows/coder-project-a.yaml +86 -0
  394. package/examples/config/workflows/deploy.yaml +15 -0
  395. package/examples/config/workflows/nightly-maintenance.yaml +13 -0
  396. package/package.json +75 -4
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Opening the orchestrator database and applying versioned migrations with a
3
+ * pre-migration backup (Spec §4, §12, §21).
4
+ */
5
+ import { existsSync, mkdirSync, statSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { DatabaseSync } from 'node:sqlite';
8
+ import { dataPaths, formatCompactTimestamp, noopLogger, systemClock } from '../core/index.js';
9
+ import { MigrationError } from './errors.js';
10
+ import { MIGRATIONS } from './schema.js';
11
+ import { all, get, run, transaction } from './sqlite.js';
12
+ /** Default SQLite busy timeout (ms). */
13
+ export const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
14
+ /**
15
+ * Open (creating if needed) `<dataDir>/adispatchd.sqlite`, configure it
16
+ * (foreign keys, WAL, busy timeout) and apply pending migrations. If the
17
+ * database already existed and migrations are pending, it is first backed up
18
+ * to `<dataDir>/backups/adispatchd-<version>-<ts>.sqlite`.
19
+ *
20
+ * The caller should hold the data-dir instance lock ({@link acquireInstanceLock}) first.
21
+ */
22
+ export function openDatabase(dataDir, options = {}) {
23
+ const paths = dataPaths(dataDir);
24
+ mkdirSync(paths.dataDir, { recursive: true });
25
+ const existed = existsSync(paths.database) && statSync(paths.database).size > 0;
26
+ const db = new DatabaseSync(paths.database);
27
+ try {
28
+ configureConnection(db, options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS);
29
+ const migration = migrate(db, {
30
+ migrations: options.migrations ?? MIGRATIONS,
31
+ backupDir: existed ? paths.backupsDir : null,
32
+ clock: options.clock ?? systemClock,
33
+ logger: options.logger ?? noopLogger,
34
+ });
35
+ return { db, path: paths.database, migration, close: () => db.close() };
36
+ }
37
+ catch (err) {
38
+ db.close();
39
+ throw err;
40
+ }
41
+ }
42
+ /** Apply connection pragmas. */
43
+ export function configureConnection(db, busyTimeoutMs = DEFAULT_BUSY_TIMEOUT_MS) {
44
+ db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.trunc(busyTimeoutMs))}`);
45
+ db.exec('PRAGMA foreign_keys = ON');
46
+ db.exec('PRAGMA journal_mode = WAL');
47
+ db.exec('PRAGMA synchronous = NORMAL');
48
+ }
49
+ /** Current schema version (0 if `schema_migrations` does not exist yet). */
50
+ export function schemaVersion(db) {
51
+ const table = get(db, `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'`);
52
+ if (!table)
53
+ return 0;
54
+ const row = get(db, 'SELECT MAX(version) AS v FROM schema_migrations');
55
+ return Number(row?.v ?? 0);
56
+ }
57
+ /** Apply all pending migrations, each in its own transaction. Idempotent. */
58
+ export function migrate(db, options) {
59
+ validateMigrations(options.migrations);
60
+ db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
61
+ version INTEGER PRIMARY KEY,
62
+ name TEXT NOT NULL,
63
+ applied_at TEXT NOT NULL
64
+ )`);
65
+ const applied = new Set(all(db, 'SELECT version FROM schema_migrations').map((r) => Number(r.version)));
66
+ const fromVersion = applied.size === 0 ? 0 : Math.max(...applied);
67
+ const known = options.migrations.length === 0 ? 0 : Math.max(...options.migrations.map((m) => m.version));
68
+ if (fromVersion > known) {
69
+ throw new MigrationError(`Database schema version ${fromVersion} is newer than this build supports (${known}); refusing to start`, { fromVersion, known });
70
+ }
71
+ const pending = options.migrations.filter((m) => !applied.has(m.version));
72
+ let backupPath = null;
73
+ if (pending.length > 0 && options.backupDir !== null) {
74
+ backupPath = backupDatabase(db, options.backupDir, fromVersion, options.clock);
75
+ options.logger.info('database backup written before migration', { backupPath, fromVersion });
76
+ }
77
+ for (const migration of pending) {
78
+ try {
79
+ transaction(db, () => {
80
+ migration.up(db);
81
+ run(db, 'INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)', [
82
+ migration.version,
83
+ migration.name,
84
+ options.clock.now().toISOString(),
85
+ ]);
86
+ });
87
+ }
88
+ catch (err) {
89
+ throw new MigrationError(`Migration ${migration.version} (${migration.name}) failed`, { version: migration.version, backupPath }, err);
90
+ }
91
+ options.logger.info('database migration applied', { version: migration.version, name: migration.name });
92
+ }
93
+ return {
94
+ fromVersion,
95
+ toVersion: schemaVersion(db),
96
+ applied: pending.map((m) => m.version),
97
+ backupPath,
98
+ };
99
+ }
100
+ /**
101
+ * Write a consistent copy of the database (`VACUUM INTO`, safe in WAL mode)
102
+ * to `<backupDir>/adispatchd-<version>-<ts>.sqlite`. Returns its path.
103
+ */
104
+ export function backupDatabase(db, backupDir, version, clock) {
105
+ mkdirSync(backupDir, { recursive: true });
106
+ const base = `adispatchd-${version}-${formatCompactTimestamp(clock.now(), true)}`;
107
+ let target = path.join(backupDir, `${base}.sqlite`);
108
+ for (let i = 1; existsSync(target); i++)
109
+ target = path.join(backupDir, `${base}-${i}.sqlite`);
110
+ db.prepare('VACUUM INTO ?').run(target);
111
+ return target;
112
+ }
113
+ function validateMigrations(migrations) {
114
+ let previous = 0;
115
+ for (const m of migrations) {
116
+ if (!Number.isInteger(m.version) || m.version <= previous) {
117
+ throw new MigrationError('Migrations must have strictly increasing positive integer versions', {
118
+ version: m.version,
119
+ });
120
+ }
121
+ previous = m.version;
122
+ }
123
+ }
124
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/persistence/database.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,UAAU,EAAE,WAAW,EAA2B,MAAM,kBAAkB,CAAC;AACvH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAkB,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEzD,wCAAwC;AACxC,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAgC7C;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,OAAO,GAAwB,EAAE;IAC7E,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;IAChF,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC;QACH,mBAAmB,CAAC,EAAE,EAAE,OAAO,CAAC,aAAa,IAAI,uBAAuB,CAAC,CAAC;QAC1E,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,EAAE;YAC5B,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,UAAU;YAC5C,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI;YAC5C,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,WAAW;YACnC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,UAAU;SACrC,CAAC,CAAC;QACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;IAC1E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,EAAE,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,mBAAmB,CAAC,EAAgB,EAAE,aAAa,GAAW,uBAAuB;IACnG,EAAE,CAAC,IAAI,CAAC,yBAAyB,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC;IAC3E,EAAE,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IACpC,EAAE,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IACrC,EAAE,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;AACzC,CAAC;AAWD,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,EAAgB;IAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,EAAE,EAAE,oFAAoF,CAAC,CAAC;IAC5G,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC;IACrB,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE,EAAE,iDAAiD,CAAC,CAAC;IACvE,OAAO,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,OAAO,CAAC,EAAgB,EAAE,OAAuB;IAC/D,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACvC,EAAE,CAAC,IAAI,CAAC;;;;IAIN,CAAC,CAAC;IACJ,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,uCAAuC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACxG,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC;IAClE,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1G,IAAI,WAAW,GAAG,KAAK,EAAE,CAAC;QACxB,MAAM,IAAI,cAAc,CACtB,2BAA2B,WAAW,uCAAuC,KAAK,sBAAsB,EACxG,EAAE,WAAW,EAAE,KAAK,EAAE,CACvB,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1E,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QACrD,UAAU,GAAG,cAAc,CAAC,EAAE,EAAE,OAAO,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QAC/E,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,0CAA0C,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC;IAC/F,CAAC;IACD,KAAK,MAAM,SAAS,IAAI,OAAO,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,WAAW,CAAC,EAAE,EAAE,GAAG,EAAE;gBACnB,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;gBACjB,GAAG,CAAC,EAAE,EAAE,4EAA4E,EAAE;oBACpF,SAAS,CAAC,OAAO;oBACjB,SAAS,CAAC,IAAI;oBACd,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;iBAClC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,cAAc,CACtB,aAAa,SAAS,CAAC,OAAO,KAAK,SAAS,CAAC,IAAI,UAAU,EAC3D,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,EAC1C,GAAG,CACJ,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,4BAA4B,EAAE,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1G,CAAC;IACD,OAAO;QACL,WAAW;QACX,SAAS,EAAE,aAAa,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QACtC,UAAU;KACX,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,EAAgB,EAAE,SAAiB,EAAE,OAAe,EAAE,KAAY;IAC/F,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,cAAc,OAAO,IAAI,sBAAsB,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;IAClF,IAAI,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,SAAS,CAAC,CAAC;IACpD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE;QAAE,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9F,EAAE,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,kBAAkB,CAAC,UAAgC;IAC1D,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC1D,MAAM,IAAI,cAAc,CAAC,oEAAoE,EAAE;gBAC7F,OAAO,EAAE,CAAC,CAAC,OAAO;aACnB,CAAC,CAAC;QACL,CAAC;QACD,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC;IACvB,CAAC;AACH,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Persistence-specific errors. All extend {@link OrchestratorError} so callers
3
+ * (API: 409/404 mapping) can react on the stable `code`.
4
+ */
5
+ import { OrchestratorError } from '../core/index.js';
6
+ /** An active lock for `(workflowId, lockKey)` already exists (HTTP 409). */
7
+ export declare class LockConflictError extends OrchestratorError {
8
+ readonly workflowId: string;
9
+ readonly lockKey: string;
10
+ /** Run currently holding the lock. */
11
+ readonly holderRunId: string;
12
+ constructor(workflowId: string, lockKey: string, holderRunId: string);
13
+ }
14
+ /** Reservation refused because an active retry block exists (only with `failIfBlocked`). */
15
+ export declare class BlockedError extends OrchestratorError {
16
+ readonly blockId: number;
17
+ constructor(workflowId: string, itemKey: string | null, blockId: number);
18
+ }
19
+ /** An active block already exists for the workflow/scope. */
20
+ export declare class BlockExistsError extends OrchestratorError {
21
+ readonly blockId: number;
22
+ constructor(blockId: number);
23
+ }
24
+ /** A run status transition that the state machine (core `canTransition`) does not allow. */
25
+ export declare class InvalidTransitionError extends OrchestratorError {
26
+ constructor(message: string, details: Record<string, unknown>);
27
+ }
28
+ /** A referenced row does not exist. */
29
+ export declare class NotFoundError extends OrchestratorError {
30
+ constructor(entity: string, id: string | number);
31
+ }
32
+ /** Another orchestrator instance holds the data-dir instance lock (Spec §21). */
33
+ export declare class InstanceLockedError extends OrchestratorError {
34
+ constructor(lockPath: string, holder: Record<string, unknown> | null, cause?: unknown);
35
+ }
36
+ /** Schema migration failed or the database schema is newer than this build. */
37
+ export declare class MigrationError extends OrchestratorError {
38
+ constructor(message: string, details?: Record<string, unknown>, cause?: unknown);
39
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Persistence-specific errors. All extend {@link OrchestratorError} so callers
3
+ * (API: 409/404 mapping) can react on the stable `code`.
4
+ */
5
+ import { OrchestratorError } from '../core/index.js';
6
+ /** An active lock for `(workflowId, lockKey)` already exists (HTTP 409). */
7
+ export class LockConflictError extends OrchestratorError {
8
+ workflowId;
9
+ lockKey;
10
+ /** Run currently holding the lock. */
11
+ holderRunId;
12
+ constructor(workflowId, lockKey, holderRunId) {
13
+ super('lock_conflict', `Workflow '${workflowId}' already has an active run for '${lockKey}' (${holderRunId})`, {
14
+ details: { workflowId, lockKey, holderRunId },
15
+ });
16
+ this.workflowId = workflowId;
17
+ this.lockKey = lockKey;
18
+ this.holderRunId = holderRunId;
19
+ }
20
+ }
21
+ /** Reservation refused because an active retry block exists (only with `failIfBlocked`). */
22
+ export class BlockedError extends OrchestratorError {
23
+ blockId;
24
+ constructor(workflowId, itemKey, blockId) {
25
+ super('blocked', `Workflow '${workflowId}' is blocked for '${itemKey ?? '<workflow>'}' (block ${blockId})`, {
26
+ details: { workflowId, itemKey, blockId },
27
+ });
28
+ this.blockId = blockId;
29
+ }
30
+ }
31
+ /** An active block already exists for the workflow/scope. */
32
+ export class BlockExistsError extends OrchestratorError {
33
+ blockId;
34
+ constructor(blockId) {
35
+ super('block_exists', `An active block already exists (block ${blockId})`, { details: { blockId } });
36
+ this.blockId = blockId;
37
+ }
38
+ }
39
+ /** A run status transition that the state machine (core `canTransition`) does not allow. */
40
+ export class InvalidTransitionError extends OrchestratorError {
41
+ constructor(message, details) {
42
+ super('invalid_transition', message, { details });
43
+ }
44
+ }
45
+ /** A referenced row does not exist. */
46
+ export class NotFoundError extends OrchestratorError {
47
+ constructor(entity, id) {
48
+ super('not_found', `${entity} '${String(id)}' not found`, { details: { entity, id } });
49
+ }
50
+ }
51
+ /** Another orchestrator instance holds the data-dir instance lock (Spec §21). */
52
+ export class InstanceLockedError extends OrchestratorError {
53
+ constructor(lockPath, holder, cause) {
54
+ const who = holder && typeof holder.pid === 'number' ? ` (pid ${holder.pid})` : '';
55
+ super('instance_locked', `Another orchestrator instance is running on this data directory${who}: ${lockPath}`, {
56
+ details: { lockPath, holder },
57
+ cause,
58
+ });
59
+ }
60
+ }
61
+ /** Schema migration failed or the database schema is newer than this build. */
62
+ export class MigrationError extends OrchestratorError {
63
+ constructor(message, details, cause) {
64
+ super('migration_error', message, { ...(details ? { details } : {}), cause });
65
+ }
66
+ }
67
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/persistence/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,4EAA4E;AAC5E,MAAM,OAAO,iBAAkB,SAAQ,iBAAiB;IAC7C,UAAU,CAAS;IACnB,OAAO,CAAS;IACzB,sCAAsC;IAC7B,WAAW,CAAS;IAE7B,YAAY,UAAkB,EAAE,OAAe,EAAE,WAAmB;QAClE,KAAK,CAAC,eAAe,EAAE,aAAa,UAAU,oCAAoC,OAAO,MAAM,WAAW,GAAG,EAAE;YAC7G,OAAO,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE;SAC9C,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;CACF;AAED,4FAA4F;AAC5F,MAAM,OAAO,YAAa,SAAQ,iBAAiB;IACxC,OAAO,CAAS;IAEzB,YAAY,UAAkB,EAAE,OAAsB,EAAE,OAAe;QACrE,KAAK,CAAC,SAAS,EAAE,aAAa,UAAU,qBAAqB,OAAO,IAAI,YAAY,YAAY,OAAO,GAAG,EAAE;YAC1G,OAAO,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE;SAC1C,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED,6DAA6D;AAC7D,MAAM,OAAO,gBAAiB,SAAQ,iBAAiB;IAC5C,OAAO,CAAS;IAEzB,YAAY,OAAe;QACzB,KAAK,CAAC,cAAc,EAAE,yCAAyC,OAAO,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;QACrG,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED,4FAA4F;AAC5F,MAAM,OAAO,sBAAuB,SAAQ,iBAAiB;IAC3D,YAAY,OAAe,EAAE,OAAgC;QAC3D,KAAK,CAAC,oBAAoB,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IACpD,CAAC;CACF;AAED,uCAAuC;AACvC,MAAM,OAAO,aAAc,SAAQ,iBAAiB;IAClD,YAAY,MAAc,EAAE,EAAmB;QAC7C,KAAK,CAAC,WAAW,EAAE,GAAG,MAAM,KAAK,MAAM,CAAC,EAAE,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IACzF,CAAC;CACF;AAED,iFAAiF;AACjF,MAAM,OAAO,mBAAoB,SAAQ,iBAAiB;IACxD,YAAY,QAAgB,EAAE,MAAsC,EAAE,KAAe;QACnF,MAAM,GAAG,GAAG,MAAM,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACnF,KAAK,CAAC,iBAAiB,EAAE,kEAAkE,GAAG,KAAK,QAAQ,EAAE,EAAE;YAC7G,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;YAC7B,KAAK;SACN,CAAC,CAAC;IACL,CAAC;CACF;AAED,+EAA+E;AAC/E,MAAM,OAAO,cAAe,SAAQ,iBAAiB;IACnD,YAAY,OAAe,EAAE,OAAiC,EAAE,KAAe;QAC7E,KAAK,CAAC,iBAAiB,EAAE,OAAO,EAAE,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAChF,CAAC;CACF"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Structured run events (Spec §9.3, §12, §15 SSE). Every event has a
3
+ * globally monotonic `id` (SQLite AUTOINCREMENT: never reused after deletion
4
+ * of committed rows – usable as SSE `Last-Event-ID`) and a per-run `sequence`.
5
+ *
6
+ * Caveat: the ID of an insert that is rolled back may be handed out again.
7
+ * Therefore an event (and its ID) must only be published to live consumers
8
+ * (event bus / SSE) after the surrounding transaction committed – use
9
+ * `Repositories.afterCommit` / `afterCommit(db, cb)`. Persisted IDs are then
10
+ * strictly increasing in commit order (single writer connection).
11
+ */
12
+ import type { DatabaseSync } from 'node:sqlite';
13
+ import type { Clock, ExecutorDiagnostic, WorkerEvent } from '../core/index.js';
14
+ /**
15
+ * Who produced the event: `worker` = `@@ADISPATCH_EVENT` line (untrusted, informative),
16
+ * `orchestrator` = status transitions and executor diagnostics.
17
+ */
18
+ export type EventOrigin = 'worker' | 'orchestrator';
19
+ /** Event type of orchestrator-side run status transitions. Payload: {@link RunStatusEventPayload}. */
20
+ export declare const RUN_STATUS_EVENT = "run.status";
21
+ /** Prefix of executor diagnostic event types, e.g. `diagnostic.log_truncated` (Spec §12 note on log/DB inconsistencies). */
22
+ export declare const DIAGNOSTIC_EVENT_PREFIX = "diagnostic.";
23
+ /** Payload of a {@link RUN_STATUS_EVENT}. */
24
+ export interface RunStatusEventPayload {
25
+ from: string | null;
26
+ to: string;
27
+ [key: string]: unknown;
28
+ }
29
+ /** A persisted run event. */
30
+ export interface RunEventRecord {
31
+ /** Global, strictly increasing event ID. */
32
+ id: number;
33
+ runId: string;
34
+ /** 1-based, strictly increasing per run. */
35
+ sequence: number;
36
+ timestamp: string;
37
+ origin: EventOrigin;
38
+ type: string;
39
+ payload: Record<string, unknown>;
40
+ }
41
+ /** Options for {@link EventRepository.append}. */
42
+ export interface AppendEventOptions {
43
+ origin?: EventOrigin;
44
+ at?: Date;
45
+ }
46
+ /** Repository for `run_events`. */
47
+ export declare class EventRepository {
48
+ private readonly db;
49
+ private readonly clock;
50
+ constructor(db: DatabaseSync, clock: Clock);
51
+ /**
52
+ * Append an event. Worker `status` events with a string `message` also
53
+ * update `runs.status_message` (same transaction).
54
+ */
55
+ append(runId: string, type: string, payload: Record<string, unknown>, options?: AppendEventOptions): RunEventRecord;
56
+ /** Persist a structured worker event (origin `worker`). */
57
+ appendWorkerEvent(runId: string, event: WorkerEvent): RunEventRecord;
58
+ /** Persist an executor diagnostic as `diagnostic.<type>` (origin `orchestrator`). */
59
+ appendDiagnostic(runId: string, diagnostic: ExecutorDiagnostic): RunEventRecord;
60
+ /** Events with `id > afterId` in ID order (SSE replay). Optionally restricted to one run. */
61
+ listSince(afterId: number, options?: {
62
+ runId?: string;
63
+ limit?: number;
64
+ }): RunEventRecord[];
65
+ /** Events of one run in sequence order. */
66
+ listForRun(runId: string, options?: {
67
+ afterSequence?: number;
68
+ limit?: number;
69
+ }): RunEventRecord[];
70
+ /** Highest event ID ever assigned (0 if none); does not decrease when events are deleted. */
71
+ latestId(): number;
72
+ }
@@ -0,0 +1,83 @@
1
+ import { NotFoundError } from './errors.js';
2
+ import { all, get, iso, run, transaction } from './sqlite.js';
3
+ /** Event type of orchestrator-side run status transitions. Payload: {@link RunStatusEventPayload}. */
4
+ export const RUN_STATUS_EVENT = 'run.status';
5
+ /** Prefix of executor diagnostic event types, e.g. `diagnostic.log_truncated` (Spec §12 note on log/DB inconsistencies). */
6
+ export const DIAGNOSTIC_EVENT_PREFIX = 'diagnostic.';
7
+ /** Repository for `run_events`. */
8
+ export class EventRepository {
9
+ db;
10
+ clock;
11
+ constructor(db, clock) {
12
+ this.db = db;
13
+ this.clock = clock;
14
+ }
15
+ /**
16
+ * Append an event. Worker `status` events with a string `message` also
17
+ * update `runs.status_message` (same transaction).
18
+ */
19
+ append(runId, type, payload, options = {}) {
20
+ const origin = options.origin ?? 'orchestrator';
21
+ const at = iso(options.at ?? this.clock.now());
22
+ return transaction(this.db, () => {
23
+ if (!get(this.db, 'SELECT 1 FROM runs WHERE id = ?', [runId]))
24
+ throw new NotFoundError('run', runId);
25
+ const seqRow = get(this.db, 'SELECT COALESCE(MAX(sequence), 0) + 1 AS s FROM run_events WHERE run_id = ?', [runId]);
26
+ const sequence = Number(seqRow?.s ?? 1);
27
+ const res = run(this.db, 'INSERT INTO run_events (run_id, sequence, timestamp, origin, type, payload_json) VALUES (?, ?, ?, ?, ?, ?)', [runId, sequence, at, origin, type, JSON.stringify(payload)]);
28
+ if (origin === 'worker' && type === 'status' && typeof payload.message === 'string') {
29
+ run(this.db, 'UPDATE runs SET status_message = ?, updated_at = ? WHERE id = ?', [payload.message, at, runId]);
30
+ }
31
+ return { id: res.lastInsertRowid, runId, sequence, timestamp: at, origin, type, payload };
32
+ });
33
+ }
34
+ /** Persist a structured worker event (origin `worker`). */
35
+ appendWorkerEvent(runId, event) {
36
+ return this.append(runId, event.type, event.data, { origin: 'worker', at: event.at });
37
+ }
38
+ /** Persist an executor diagnostic as `diagnostic.<type>` (origin `orchestrator`). */
39
+ appendDiagnostic(runId, diagnostic) {
40
+ const payload = { message: diagnostic.message };
41
+ if (diagnostic.stream)
42
+ payload.stream = diagnostic.stream;
43
+ if (diagnostic.details)
44
+ payload.details = diagnostic.details;
45
+ return this.append(runId, `${DIAGNOSTIC_EVENT_PREFIX}${diagnostic.type}`, payload, { at: diagnostic.at });
46
+ }
47
+ /** Events with `id > afterId` in ID order (SSE replay). Optionally restricted to one run. */
48
+ listSince(afterId, options = {}) {
49
+ const params = [afterId];
50
+ let sql = 'SELECT * FROM run_events WHERE id > ?';
51
+ if (options.runId !== undefined) {
52
+ sql += ' AND run_id = ?';
53
+ params.push(options.runId);
54
+ }
55
+ sql += ' ORDER BY id ASC LIMIT ?';
56
+ params.push(options.limit ?? 1000);
57
+ return all(this.db, sql, params).map(toEvent);
58
+ }
59
+ /** Events of one run in sequence order. */
60
+ listForRun(runId, options = {}) {
61
+ return all(this.db, 'SELECT * FROM run_events WHERE run_id = ? AND sequence > ? ORDER BY sequence ASC LIMIT ?', [
62
+ runId,
63
+ options.afterSequence ?? 0,
64
+ options.limit ?? 10_000,
65
+ ]).map(toEvent);
66
+ }
67
+ /** Highest event ID ever assigned (0 if none); does not decrease when events are deleted. */
68
+ latestId() {
69
+ return Number(get(this.db, `SELECT seq FROM sqlite_sequence WHERE name = 'run_events'`)?.seq ?? 0);
70
+ }
71
+ }
72
+ function toEvent(row) {
73
+ return {
74
+ id: Number(row.id),
75
+ runId: String(row.run_id),
76
+ sequence: Number(row.sequence),
77
+ timestamp: String(row.timestamp),
78
+ origin: row.origin,
79
+ type: String(row.type),
80
+ payload: JSON.parse(String(row.payload_json)),
81
+ };
82
+ }
83
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/persistence/events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Component `persistence` – public API (WI-03).
3
+ * Other components import from this module only.
4
+ */
5
+ export { openPersistence, createRepositories, type Persistence, type Repositories } from './persistence.js';
6
+ export { openDatabase, migrate, schemaVersion, backupDatabase, configureConnection, DEFAULT_BUSY_TIMEOUT_MS, type OpenDatabaseOptions, type OpenedDatabase, type MigrationResult, type MigrateOptions, } from './database.js';
7
+ export { MIGRATIONS, type Migration } from './schema.js';
8
+ export { acquireInstanceLock, INSTANCE_LOCK_FILES, type InstanceLock, type InstanceLockHolder } from './instance-lock.js';
9
+ export { lockKeyFor, blockScopeKey, type LockKeyInput } from './lock-keys.js';
10
+ export { RunRepository, finalizationTarget, type RunRecord, type ReserveRunInput, type RunUpdate, type BlockPolicy, type BeginFinalizationOptions, type FinalizationResult, type RunListQuery, type RunPage, type LockRecord, } from './runs.js';
11
+ export { EventRepository, RUN_STATUS_EVENT, DIAGNOSTIC_EVENT_PREFIX, type EventOrigin, type RunEventRecord, type RunStatusEventPayload, type AppendEventOptions, } from './events.js';
12
+ export { ActionRepository, ACTION_STATUS_TRANSITIONS, type ActionJournalEntry, type RunActionRecord, type ActionAttemptRecord, type FailedActionRecord, } from './actions.js';
13
+ export { BlockRepository, type BlockRecord, type CreateBlockInput, type FailureCounterState, } from './blocks.js';
14
+ export { NotificationRepository, type NotificationRecord, type NotificationDeliveryRecord, type DeliveryAttemptOutcome, type DeliveryState, } from './notifications.js';
15
+ export { CronFiringRepository, type CronFiringRecord, type CronDecision, type RecordCronFiringInput, } from './cron-firings.js';
16
+ export { RetentionRepository, type RunRetentionCategory, type RunRetentionCandidate } from './retention.js';
17
+ export { LockConflictError, BlockedError, BlockExistsError, InvalidTransitionError, NotFoundError, InstanceLockedError, MigrationError, } from './errors.js';
18
+ export { transaction, afterCommit } from './sqlite.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Component `persistence` – public API (WI-03).
3
+ * Other components import from this module only.
4
+ */
5
+ export { openPersistence, createRepositories } from './persistence.js';
6
+ export { openDatabase, migrate, schemaVersion, backupDatabase, configureConnection, DEFAULT_BUSY_TIMEOUT_MS, } from './database.js';
7
+ export { MIGRATIONS } from './schema.js';
8
+ export { acquireInstanceLock, INSTANCE_LOCK_FILES } from './instance-lock.js';
9
+ export { lockKeyFor, blockScopeKey } from './lock-keys.js';
10
+ export { RunRepository, finalizationTarget, } from './runs.js';
11
+ export { EventRepository, RUN_STATUS_EVENT, DIAGNOSTIC_EVENT_PREFIX, } from './events.js';
12
+ export { ActionRepository, ACTION_STATUS_TRANSITIONS, } from './actions.js';
13
+ export { BlockRepository, } from './blocks.js';
14
+ export { NotificationRepository, } from './notifications.js';
15
+ export { CronFiringRepository, } from './cron-firings.js';
16
+ export { RetentionRepository } from './retention.js';
17
+ export { LockConflictError, BlockedError, BlockExistsError, InvalidTransitionError, NotFoundError, InstanceLockedError, MigrationError, } from './errors.js';
18
+ export { transaction, afterCommit } from './sqlite.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/persistence/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAuC,MAAM,kBAAkB,CAAC;AAC5G,OAAO,EACL,YAAY,EACZ,OAAO,EACP,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,uBAAuB,GAKxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,UAAU,EAAkB,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAA8C,MAAM,oBAAoB,CAAC;AAC1H,OAAO,EAAE,UAAU,EAAE,aAAa,EAAqB,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EACL,aAAa,EACb,kBAAkB,GAUnB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,uBAAuB,GAKxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,yBAAyB,GAK1B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,eAAe,GAIhB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,GAKvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,GAIrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAyD,MAAM,gBAAgB,CAAC;AAC5G,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,mBAAmB,EACnB,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { type Clock } from '../core/index.js';
2
+ /** File names inside the data dir that copy/backup/cleanup routines must never open. */
3
+ export declare const INSTANCE_LOCK_FILES: readonly ['adispatchd.lock', 'adispatchd.lock.json'];
4
+ /** A held instance lock. */
5
+ export interface InstanceLock {
6
+ /** Path of the lock file. */
7
+ readonly path: string;
8
+ /** True until {@link release} was called. */
9
+ readonly held: boolean;
10
+ /** Release the lock (idempotent). Also released automatically on process exit. */
11
+ release(): void;
12
+ }
13
+ /** Diagnostic holder information. */
14
+ export interface InstanceLockHolder {
15
+ pid: number;
16
+ hostname: string;
17
+ acquiredAt: string;
18
+ }
19
+ /**
20
+ * Acquire the exclusive instance lock for `dataDir` (creates the directory if
21
+ * needed). Throws {@link InstanceLockedError} immediately if another live
22
+ * instance – in this or another process – holds it.
23
+ */
24
+ export declare function acquireInstanceLock(dataDir: string, options?: {
25
+ clock?: Clock;
26
+ }): InstanceLock;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Data-dir instance lock (Spec §21): only one orchestrator per data directory.
3
+ *
4
+ * Approach: an OS-level advisory lock held by SQLite. `adispatchd.lock` is a
5
+ * tiny SQLite database opened with `locking_mode=EXCLUSIVE`; after the first
6
+ * write transaction SQLite keeps the EXCLUSIVE file lock (POSIX `fcntl` on
7
+ * Unix, `LockFileEx` on Windows) until the connection closes. The operating
8
+ * system drops such locks automatically when the process exits – also on
9
+ * SIGKILL/crash – so there is no "stale lock" that needs heuristic detection:
10
+ * a lock left by a dead process is simply free again, and a live holder can
11
+ * never be overridden (no PID/start-time guessing that could admit two
12
+ * instances). SQLite also tracks locks per inode inside one process, so a
13
+ * second acquisition from the same process is refused as well.
14
+ *
15
+ * Holder information (pid, host, start time) is written into the lock
16
+ * database and mirrored to `adispatchd.lock.json` for diagnostics only; it
17
+ * never decides who owns the lock.
18
+ *
19
+ * The connection is kept strongly reachable in a module-level set until
20
+ * `release()`: otherwise dropping the returned handle would let the garbage
21
+ * collector close the connection and silently free the OS lock.
22
+ *
23
+ * Limitations / rules:
24
+ * - Advisory `fcntl` locks are unreliable on some network file systems (NFS
25
+ * without lock daemon) – the data dir must be on a local file system.
26
+ * - POSIX `fcntl` locks belong to the process and are dropped when the process
27
+ * closes ANY file descriptor of that file. Therefore the holding process must
28
+ * never open `adispatchd.lock` otherwise (no `readFileSync`, `fs.cp`,
29
+ * copying the data dir, ...). Backup/copy/cleanup routines must exclude
30
+ * `adispatchd.lock` (and `adispatchd.lock.json`); `backupDatabase` only
31
+ * reads `adispatchd.sqlite` via SQLite. Opening it through `node:sqlite`
32
+ * in the same process is safe (SQLite shares lock state per inode).
33
+ */
34
+ import { readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
35
+ import os from 'node:os';
36
+ import { DatabaseSync } from 'node:sqlite';
37
+ import { dataPaths, systemClock } from '../core/index.js';
38
+ import { InstanceLockedError } from './errors.js';
39
+ /** Connections holding a lock; strong references prevent GC from closing them. */
40
+ const HELD = new Set();
41
+ /** File names inside the data dir that copy/backup/cleanup routines must never open. */
42
+ export const INSTANCE_LOCK_FILES = ['adispatchd.lock', 'adispatchd.lock.json'];
43
+ /**
44
+ * Acquire the exclusive instance lock for `dataDir` (creates the directory if
45
+ * needed). Throws {@link InstanceLockedError} immediately if another live
46
+ * instance – in this or another process – holds it.
47
+ */
48
+ export function acquireInstanceLock(dataDir, options = {}) {
49
+ const paths = dataPaths(dataDir);
50
+ mkdirSync(paths.dataDir, { recursive: true });
51
+ const lockPath = paths.instanceLock;
52
+ const infoPath = `${lockPath}.json`;
53
+ const db = new DatabaseSync(lockPath);
54
+ const holder = {
55
+ pid: process.pid,
56
+ hostname: os.hostname(),
57
+ acquiredAt: (options.clock ?? systemClock).now().toISOString(),
58
+ };
59
+ try {
60
+ db.exec('PRAGMA busy_timeout = 0');
61
+ db.exec('PRAGMA locking_mode = EXCLUSIVE');
62
+ db.exec('BEGIN EXCLUSIVE');
63
+ db.exec('CREATE TABLE IF NOT EXISTS holder (pid INTEGER NOT NULL, hostname TEXT NOT NULL, acquired_at TEXT NOT NULL)');
64
+ db.exec('DELETE FROM holder');
65
+ db.prepare('INSERT INTO holder (pid, hostname, acquired_at) VALUES (?, ?, ?)').run(holder.pid, holder.hostname, holder.acquiredAt);
66
+ db.exec('COMMIT');
67
+ }
68
+ catch (err) {
69
+ try {
70
+ db.close();
71
+ }
72
+ catch {
73
+ // ignore
74
+ }
75
+ if (err instanceof Error && /locked|busy/i.test(err.message)) {
76
+ throw new InstanceLockedError(lockPath, readHolderInfo(infoPath), err);
77
+ }
78
+ throw err;
79
+ }
80
+ try {
81
+ writeFileSync(infoPath, `${JSON.stringify(holder)}\n`, { mode: 0o600 });
82
+ }
83
+ catch {
84
+ // diagnostics only
85
+ }
86
+ HELD.add(db);
87
+ let held = true;
88
+ return {
89
+ path: lockPath,
90
+ get held() {
91
+ return held;
92
+ },
93
+ release() {
94
+ if (!held)
95
+ return;
96
+ held = false;
97
+ try {
98
+ rmSync(infoPath, { force: true });
99
+ }
100
+ catch {
101
+ // diagnostics only
102
+ }
103
+ HELD.delete(db);
104
+ db.close();
105
+ },
106
+ };
107
+ }
108
+ function readHolderInfo(infoPath) {
109
+ try {
110
+ const parsed = JSON.parse(readFileSync(infoPath, 'utf8'));
111
+ return parsed && typeof parsed === 'object' ? parsed : null;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ //# sourceMappingURL=instance-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-lock.js","sourceRoot":"","sources":["../../src/persistence/instance-lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { TriggerType } from '../core/index.js';
2
+ /** Inputs determining a run's lock key. */
3
+ export interface LockKeyInput {
4
+ triggerType: TriggerType;
5
+ runId: string;
6
+ sourceItemKey?: string | null;
7
+ /** Cron slot; required for `schedule` runs without item. */
8
+ scheduledFor?: Date | string | null;
9
+ }
10
+ /** Compute the lock key for a run (see module docs). */
11
+ export declare function lockKeyFor(input: LockKeyInput): string;
12
+ /**
13
+ * Internal non-null scope key for failure counters/blocks: the item key, or
14
+ * `''` for workflow scope (sourceless runs).
15
+ */
16
+ export declare function blockScopeKey(itemKey: string | null | undefined): string;