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,193 @@
1
+ /**
2
+ * Live connection to `GET /api/v1/events` (Spec §15, docs/api.md) with
3
+ * reconnect logic, independent of the DOM (the browser passes
4
+ * `url => new EventSource(url)`, tests pass a fake).
5
+ *
6
+ * - The browser's EventSource reconnects by itself after a dropped connection
7
+ * (`readyState` CONNECTING) and sends `Last-Event-ID`; the server replays
8
+ * missed persistent events.
9
+ * - If the EventSource gives up (`readyState` CLOSED, e.g. HTTP 503 during a
10
+ * restart), a new one is created after an exponential backoff, passing the
11
+ * last seen ID as `?lastEventId=`.
12
+ * - Non-persistent events (logs, actions, system status) are lost while
13
+ * disconnected: after every re-open (and on `stream.reset`) `onResync` is
14
+ * called so the current page reloads its state via REST.
15
+ * - Persistent events already delivered (ID ≤ last seen ID) are dropped.
16
+ */
17
+ /** EventSource `readyState` values. */
18
+ export const ES_CONNECTING = 0;
19
+ export const ES_OPEN = 1;
20
+ export const ES_CLOSED = 2;
21
+ /** All event types sent by the server (`event:` field). */
22
+ export const LIVE_EVENT_TYPES = [
23
+ 'stream.open',
24
+ 'stream.reset',
25
+ 'run.event',
26
+ 'run.output',
27
+ 'run.action',
28
+ 'block.created',
29
+ 'block.cleared',
30
+ 'system.status',
31
+ 'system.ready',
32
+ 'notification',
33
+ 'config.reloaded',
34
+ 'retention.finished',
35
+ ];
36
+ /** Parse one SSE message; `null` if the data is not a JSON object. */
37
+ export function parseLiveMessage(type, data) {
38
+ if (typeof data !== 'string')
39
+ return null;
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(data);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
48
+ return null;
49
+ const o = parsed;
50
+ // Only the `id` inside the data marks a persistent event. `MessageEvent.lastEventId` is NOT used: the
51
+ // browser carries the last seen ID over to later non-persistent messages (they would look like duplicates).
52
+ const id = typeof o.id === 'number' && Number.isInteger(o.id) && o.id >= 0 ? o.id : null;
53
+ const payload = o.payload && typeof o.payload === 'object' && !Array.isArray(o.payload) ? o.payload : {};
54
+ return { type: typeof o.type === 'string' ? o.type : type, time: typeof o.time === 'string' ? o.time : null, id, payload };
55
+ }
56
+ /** Reconnect delay for the n-th consecutive failed attempt (1-based). */
57
+ export function backoffDelay(attempt, initialMs = 1000, maxMs = 30_000) {
58
+ const n = Math.max(1, Math.floor(attempt));
59
+ return Math.min(maxMs, initialMs * 2 ** Math.min(n - 1, 20));
60
+ }
61
+ /** Build the stream URL with an optional `lastEventId`. */
62
+ export function streamUrl(base, lastEventId) {
63
+ if (lastEventId === null)
64
+ return base;
65
+ return `${base}${base.includes('?') ? '&' : '?'}lastEventId=${lastEventId}`;
66
+ }
67
+ /** See module comment. */
68
+ export class LiveConnection {
69
+ opts;
70
+ es = null;
71
+ timer = null;
72
+ attempts = 0;
73
+ opened = false;
74
+ stopped = true;
75
+ current = 'closed';
76
+ /** Highest persistent event ID seen. */
77
+ lastEventId = null;
78
+ timers;
79
+ constructor(opts) {
80
+ this.opts = opts;
81
+ this.timers = opts.timers ?? {
82
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
83
+ clearTimeout: (h) => clearTimeout(h),
84
+ };
85
+ }
86
+ get state() {
87
+ return this.current;
88
+ }
89
+ /** Connect (idempotent). */
90
+ start() {
91
+ if (!this.stopped)
92
+ return;
93
+ this.stopped = false;
94
+ this.connect();
95
+ }
96
+ /** Disconnect and stop reconnecting. */
97
+ stop() {
98
+ this.stopped = true;
99
+ this.clearTimer();
100
+ this.es?.close();
101
+ this.es = null;
102
+ this.setState('closed');
103
+ }
104
+ /** Reconnect now (e.g. browser came back online) if currently waiting for a retry. */
105
+ reconnectNow() {
106
+ if (this.stopped || this.timer === null)
107
+ return;
108
+ this.clearTimer();
109
+ this.connect();
110
+ }
111
+ connect() {
112
+ this.setState(this.opened ? 'reconnecting' : 'connecting');
113
+ const es = this.opts.create(streamUrl(this.opts.url, this.lastEventId));
114
+ this.es = es;
115
+ es.onopen = () => {
116
+ if (this.es !== es)
117
+ return;
118
+ this.attempts = 0;
119
+ };
120
+ es.onerror = () => {
121
+ if (this.es !== es || this.stopped)
122
+ return;
123
+ if (es.readyState === ES_CLOSED) {
124
+ es.close();
125
+ this.es = null;
126
+ this.scheduleReconnect();
127
+ }
128
+ else {
129
+ // The browser retries by itself (Last-Event-ID is sent automatically).
130
+ this.setState('reconnecting');
131
+ }
132
+ };
133
+ for (const type of LIVE_EVENT_TYPES) {
134
+ es.addEventListener(type, (ev) => {
135
+ if (this.es !== es || this.stopped)
136
+ return;
137
+ this.handle(type, ev);
138
+ });
139
+ }
140
+ }
141
+ handle(type, ev) {
142
+ const msg = parseLiveMessage(type, ev.data);
143
+ if (!msg)
144
+ return;
145
+ if (msg.type === 'stream.open') {
146
+ const reopened = this.opened;
147
+ this.opened = true;
148
+ this.attempts = 0;
149
+ if (msg.id !== null && (this.lastEventId === null || msg.id > this.lastEventId))
150
+ this.lastEventId = msg.id;
151
+ this.setState('open');
152
+ if (reopened)
153
+ this.opts.onResync('reconnected');
154
+ return;
155
+ }
156
+ if (msg.type === 'stream.reset') {
157
+ // The server's baseline wins (it may be lower, e.g. after a data-dir restore).
158
+ if (msg.id !== null)
159
+ this.lastEventId = msg.id;
160
+ this.opts.onResync('reset');
161
+ return;
162
+ }
163
+ if (msg.id !== null) {
164
+ if (this.lastEventId !== null && msg.id <= this.lastEventId)
165
+ return; // duplicate
166
+ this.lastEventId = msg.id;
167
+ }
168
+ if (this.current !== 'open')
169
+ this.setState('open');
170
+ this.opts.onMessage(msg);
171
+ }
172
+ scheduleReconnect() {
173
+ this.attempts++;
174
+ this.setState('reconnecting');
175
+ const delay = backoffDelay(this.attempts, this.opts.initialDelayMs ?? 1000, this.opts.maxDelayMs ?? 30_000);
176
+ this.timer = this.timers.setTimeout(() => {
177
+ this.timer = null;
178
+ if (!this.stopped)
179
+ this.connect();
180
+ }, delay);
181
+ }
182
+ clearTimer() {
183
+ if (this.timer !== null)
184
+ this.timers.clearTimeout(this.timer);
185
+ this.timer = null;
186
+ }
187
+ setState(state) {
188
+ if (this.current === state)
189
+ return;
190
+ this.current = state;
191
+ this.opts.onState?.(state);
192
+ }
193
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Coalescing refresher: many live events → at most one REST reload per
3
+ * interval, never two concurrent reloads, and always a trailing reload after
4
+ * the last trigger.
5
+ */
6
+ const defaultTimers = {
7
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
8
+ clearTimeout: (h) => clearTimeout(h),
9
+ };
10
+ /** Create a {@link Refresher} for `fn` with a minimal distance of `intervalMs`. */
11
+ export function createRefresher(fn, intervalMs, timers = defaultTimers) {
12
+ let timer = null;
13
+ let running = null;
14
+ let dirty = false;
15
+ let disposed = false;
16
+ const run = async () => {
17
+ if (disposed)
18
+ return;
19
+ if (running) {
20
+ dirty = true;
21
+ return running;
22
+ }
23
+ dirty = false;
24
+ running = (async () => {
25
+ try {
26
+ await fn();
27
+ }
28
+ catch {
29
+ // errors are reported by fn itself
30
+ }
31
+ })();
32
+ try {
33
+ await running;
34
+ }
35
+ finally {
36
+ running = null;
37
+ }
38
+ if (dirty && !disposed)
39
+ schedule();
40
+ };
41
+ const schedule = () => {
42
+ if (disposed || timer !== null)
43
+ return;
44
+ timer = timers.setTimeout(() => {
45
+ timer = null;
46
+ void run();
47
+ }, intervalMs);
48
+ };
49
+ return {
50
+ trigger: schedule,
51
+ now: () => {
52
+ if (timer !== null) {
53
+ timers.clearTimeout(timer);
54
+ timer = null;
55
+ }
56
+ return run();
57
+ },
58
+ dispose: () => {
59
+ disposed = true;
60
+ if (timer !== null)
61
+ timers.clearTimeout(timer);
62
+ timer = null;
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Component `views` – public API (WI-10): read-only views over one or more
3
+ * sources with run enrichment (Spec §17). Other components import from this
4
+ * module only.
5
+ */
6
+ export { ViewService, UnknownViewError, DEFAULT_VIEW_CACHE_TTL_MS, MAX_VIEW_ROWS, VIEW_RUN_FIELDS, runFields, safeLink, type ViewCell, type ViewColumn, type ViewItemsResult, type ViewRow, type ViewServiceOptions, type ViewSummary, } from './service.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Component `views` – public API (WI-10): read-only views over one or more
3
+ * sources with run enrichment (Spec §17). Other components import from this
4
+ * module only.
5
+ */
6
+ export { ViewService, UnknownViewError, DEFAULT_VIEW_CACHE_TTL_MS, MAX_VIEW_ROWS, VIEW_RUN_FIELDS, runFields, safeLink, } from './service.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/views/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,yBAAyB,EACzB,aAAa,EACb,eAAe,EACf,SAAS,EACT,QAAQ,GAOT,MAAM,cAAc,CAAC"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Read-only views (Spec §17): items from one or more sources of the same
3
+ * adapter type, filtered declaratively (no shell), enriched with internal run
4
+ * metadata (`run.last_*`) by the global item key, sorted, with link columns.
5
+ *
6
+ * - Data is loaded on open/refresh; results are cached briefly (default 30 s).
7
+ * - The view filter is evaluated locally on every item (adapters may receive
8
+ * the pushable part but are allowed to ignore it); only `true` matches –
9
+ * `unknown` items are not shown (counted in `filterUnknown`).
10
+ * - Unavailable columns stay recognizably empty (`value: null`, `known: false`).
11
+ * - A failing source does not hide the other sources' rows (`errors`).
12
+ */
13
+ import { OrchestratorError, VIEW_RUN_FIELDS, type Clock, type FieldType, type FieldValue, type Logger, type Redactor, type SourceAdapter } from '../core/index.js';
14
+ import type { ConfigManager, ViewConfig } from '../config/index.js';
15
+ import type { RunRecord, RunRepository } from '../persistence/index.js';
16
+ /** Default cache lifetime of view results. */
17
+ export declare const DEFAULT_VIEW_CACHE_TTL_MS = 30000;
18
+ /** Max rows returned per view (more are cut off, `truncated: true`). */
19
+ export declare const MAX_VIEW_ROWS = 2000;
20
+ /** Internal run fields available to views (defined in core so config validation can use them). */
21
+ export { VIEW_RUN_FIELDS };
22
+ /** A view column. */
23
+ export interface ViewColumn {
24
+ field: string;
25
+ label: string;
26
+ /** Field providing the link target (http(s) only). */
27
+ link: string | null;
28
+ /** Declared type, if known. */
29
+ type: FieldType | null;
30
+ }
31
+ /** One cell (same order as the columns). */
32
+ export interface ViewCell {
33
+ /** `null` when empty or unavailable. */
34
+ value: FieldValue | null;
35
+ /** False if the field could not be resolved (unknown field, missing permission, ...). */
36
+ known: boolean;
37
+ /** http(s) URL of the link column, if any. */
38
+ link: string | null;
39
+ }
40
+ /** One row (= one external item). */
41
+ export interface ViewRow {
42
+ /** Global item key `<sourceId>:<itemId>`. */
43
+ key: string;
44
+ sourceId: string;
45
+ itemId: string;
46
+ url: string | null;
47
+ /** Latest run of this item (for linking into the run detail), if any. */
48
+ lastRunId: string | null;
49
+ cells: ViewCell[];
50
+ }
51
+ /** Result of {@link ViewService.items}. */
52
+ export interface ViewItemsResult {
53
+ view: {
54
+ id: string;
55
+ title: string;
56
+ sources: string[];
57
+ };
58
+ columns: ViewColumn[];
59
+ rows: ViewRow[];
60
+ /** Per-source problems (other sources still contribute rows). */
61
+ errors: Array<{
62
+ sourceId: string;
63
+ message: string;
64
+ }>;
65
+ /** Items whose filter evaluated to `unknown` (not shown). */
66
+ filterUnknown: number;
67
+ truncated: boolean;
68
+ fetchedAt: string;
69
+ /** True if served from the cache. */
70
+ cached: boolean;
71
+ }
72
+ /** View definition as exposed by the API. */
73
+ export interface ViewSummary {
74
+ id: string;
75
+ title: string;
76
+ file: string;
77
+ sources: string[];
78
+ columns: ViewColumn[];
79
+ sort: ViewConfig['sort'];
80
+ filter: ViewConfig['filter'];
81
+ }
82
+ /** Options of {@link ViewService}. */
83
+ export interface ViewServiceOptions {
84
+ config: Pick<ConfigManager, 'current'>;
85
+ sources: (sourceId: string) => SourceAdapter | undefined;
86
+ runs: Pick<RunRepository, 'lastRunsForItems'>;
87
+ clock?: Clock;
88
+ logger?: Logger;
89
+ cacheTtlMs?: number;
90
+ /** Redaction of error texts (shared `SecretRedactor`). */
91
+ redact?: Redactor;
92
+ }
93
+ /** Error for unknown views. */
94
+ export declare class UnknownViewError extends OrchestratorError {
95
+ readonly viewId: string;
96
+ constructor(viewId: string);
97
+ }
98
+ /** See module docs. */
99
+ export declare class ViewService {
100
+ private readonly options;
101
+ private readonly clock;
102
+ private readonly logger;
103
+ private readonly ttl;
104
+ private readonly redact;
105
+ private readonly cache;
106
+ private readonly inflight;
107
+ constructor(options: ViewServiceOptions);
108
+ /** All view definitions, sorted by title. */
109
+ list(): ViewSummary[];
110
+ /** One view definition or `undefined`. */
111
+ get(viewId: string): ViewSummary | undefined;
112
+ /** Drop cached results (config reload). */
113
+ invalidate(): void;
114
+ /**
115
+ * Current items of a view. Cached for the TTL unless `refresh`; concurrent
116
+ * requests for the same view share one load.
117
+ * @throws UnknownViewError
118
+ */
119
+ items(viewId: string, options?: {
120
+ refresh?: boolean;
121
+ }): Promise<ViewItemsResult>;
122
+ private view;
123
+ private summary;
124
+ private definitionsFor;
125
+ private columns;
126
+ private load;
127
+ }
128
+ /** `run.last_*` values for an item (known empty if the item has no run). */
129
+ export declare function runFields(run: RunRecord | null): Record<string, FieldValue>;
130
+ /** Only absolute http(s) URLs are used as links (XSS, Spec §20.5). */
131
+ export declare function safeLink(value: unknown): string | null;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Read-only views (Spec §17): items from one or more sources of the same
3
+ * adapter type, filtered declaratively (no shell), enriched with internal run
4
+ * metadata (`run.last_*`) by the global item key, sorted, with link columns.
5
+ *
6
+ * - Data is loaded on open/refresh; results are cached briefly (default 30 s).
7
+ * - The view filter is evaluated locally on every item (adapters may receive
8
+ * the pushable part but are allowed to ignore it); only `true` matches –
9
+ * `unknown` items are not shown (counted in `filterUnknown`).
10
+ * - Unavailable columns stay recognizably empty (`value: null`, `known: false`).
11
+ * - A failing source does not hide the other sources' rows (`errors`).
12
+ */
13
+ import { errorMessage, noopLogger, OrchestratorError, systemClock, VIEW_RUN_FIELDS, } from '../core/index.js';
14
+ import { collectFields, createItemFieldSource, evaluateFilter, naturalCompare, splitForPushdown } from '../filters/index.js';
15
+ /** Default cache lifetime of view results. */
16
+ export const DEFAULT_VIEW_CACHE_TTL_MS = 30_000;
17
+ /** Max rows returned per view (more are cut off, `truncated: true`). */
18
+ export const MAX_VIEW_ROWS = 2_000;
19
+ /** Internal run fields available to views (defined in core so config validation can use them). */
20
+ export { VIEW_RUN_FIELDS };
21
+ /** Error for unknown views. */
22
+ export class UnknownViewError extends OrchestratorError {
23
+ viewId;
24
+ constructor(viewId) {
25
+ super('view_not_found', `Unknown view "${viewId}"`, { details: { viewId } });
26
+ this.viewId = viewId;
27
+ }
28
+ }
29
+ /** See module docs. */
30
+ export class ViewService {
31
+ options;
32
+ clock;
33
+ logger;
34
+ ttl;
35
+ redact;
36
+ cache = new Map();
37
+ inflight = new Map();
38
+ constructor(options) {
39
+ this.options = options;
40
+ this.clock = options.clock ?? systemClock;
41
+ this.logger = (options.logger ?? noopLogger).child({ component: 'views' });
42
+ this.ttl = options.cacheTtlMs ?? DEFAULT_VIEW_CACHE_TTL_MS;
43
+ this.redact = options.redact ?? ((t) => t);
44
+ }
45
+ /** All view definitions, sorted by title. */
46
+ list() {
47
+ return Object.values(this.options.config.current.views)
48
+ .map((v) => this.summary(v))
49
+ .sort((a, b) => naturalCompare(a.title, b.title) || naturalCompare(a.id, b.id));
50
+ }
51
+ /** One view definition or `undefined`. */
52
+ get(viewId) {
53
+ const view = this.view(viewId);
54
+ return view ? this.summary(view) : undefined;
55
+ }
56
+ /** Drop cached results (config reload). */
57
+ invalidate() {
58
+ this.cache.clear();
59
+ }
60
+ /**
61
+ * Current items of a view. Cached for the TTL unless `refresh`; concurrent
62
+ * requests for the same view share one load.
63
+ * @throws UnknownViewError
64
+ */
65
+ async items(viewId, options = {}) {
66
+ const view = this.view(viewId);
67
+ if (!view)
68
+ throw new UnknownViewError(viewId);
69
+ const version = this.options.config.current.version;
70
+ const now = this.clock.now().getTime();
71
+ const hit = this.cache.get(viewId);
72
+ if (!options.refresh && hit && hit.configVersion === version && now - hit.at < this.ttl) {
73
+ return { ...hit.result, cached: true };
74
+ }
75
+ const running = this.inflight.get(viewId);
76
+ if (running)
77
+ return running;
78
+ const load = this.load(view)
79
+ .then((result) => {
80
+ if (this.options.config.current.version === version)
81
+ this.cache.set(viewId, { at: now, configVersion: version, result });
82
+ return result;
83
+ })
84
+ .finally(() => this.inflight.delete(viewId));
85
+ this.inflight.set(viewId, load);
86
+ return load;
87
+ }
88
+ view(viewId) {
89
+ const views = this.options.config.current.views;
90
+ return Object.hasOwn(views, viewId) ? views[viewId] : undefined;
91
+ }
92
+ summary(view) {
93
+ return {
94
+ id: view.id,
95
+ title: view.title,
96
+ file: view.file,
97
+ sources: [...view.sources],
98
+ columns: this.columns(view, this.definitionsFor(view)),
99
+ sort: view.sort,
100
+ filter: view.filter,
101
+ };
102
+ }
103
+ definitionsFor(view) {
104
+ const defs = new Map();
105
+ for (const sourceId of view.sources) {
106
+ const adapter = this.options.sources(sourceId);
107
+ for (const d of adapter?.fieldDefinitions() ?? [])
108
+ if (!defs.has(d.name))
109
+ defs.set(d.name, d);
110
+ }
111
+ for (const d of VIEW_RUN_FIELDS)
112
+ defs.set(d.name, d);
113
+ return defs;
114
+ }
115
+ columns(view, defs) {
116
+ return view.fields.map((f) => {
117
+ const def = defs.get(f.field);
118
+ return { field: f.field, label: f.label, link: f.link ?? def?.link ?? null, type: def?.type ?? null };
119
+ });
120
+ }
121
+ async load(view) {
122
+ const fetchedAt = this.clock.now().toISOString();
123
+ const defs = this.definitionsFor(view);
124
+ const columns = this.columns(view, defs);
125
+ const errors = [];
126
+ const loaded = [];
127
+ await Promise.all(view.sources.map(async (sourceId) => {
128
+ const adapter = this.options.sources(sourceId);
129
+ if (!adapter) {
130
+ errors.push({ sourceId, message: `source '${sourceId}' is not available` });
131
+ return;
132
+ }
133
+ try {
134
+ const { pushdown } = splitForPushdown(view.filter, adapter.fieldDefinitions());
135
+ const items = await adapter.listCandidates(pushdown ? { filter: pushdown } : {});
136
+ for (const item of items)
137
+ loaded.push({ item, adapter });
138
+ }
139
+ catch (err) {
140
+ const message = this.redact(errorMessage(err));
141
+ this.logger.warn('view source failed', { view: view.id, sourceId, error: message });
142
+ errors.push({ sourceId, message });
143
+ }
144
+ }));
145
+ // Run enrichment (one query for all items).
146
+ const lastRuns = this.options.runs.lastRunsForItems(loaded.map((l) => l.item.key));
147
+ const wanted = [...new Set([...collectFields(view.filter), ...view.fields.flatMap((f) => [f.field, ...(f.link ? [f.link] : [])])])];
148
+ if (view.sort)
149
+ wanted.push(view.sort.field);
150
+ let filterUnknown = 0;
151
+ const seen = new Set();
152
+ const rows = [];
153
+ await Promise.all(loaded.map(async ({ item, adapter }) => {
154
+ if (seen.has(item.key))
155
+ return;
156
+ seen.add(item.key);
157
+ const run = lastRuns.get(item.key) ?? null;
158
+ const fields = createItemFieldSource(item, {
159
+ // Run fields are declared too, so that their types apply (dates compare as instants).
160
+ definitions: [...adapter.fieldDefinitions(), ...VIEW_RUN_FIELDS],
161
+ resolveFields: (i, f) => adapter.resolveFields(i, f),
162
+ wantedFields: wanted,
163
+ extraFields: runFields(run),
164
+ });
165
+ const verdict = (await evaluateFilter(view.filter, fields)).value;
166
+ if (verdict !== true) {
167
+ if (verdict === 'unknown')
168
+ filterUnknown++;
169
+ return;
170
+ }
171
+ const resolve = async (field) => {
172
+ try {
173
+ return await fields.resolve(field);
174
+ }
175
+ catch (err) {
176
+ return { known: false, reason: errorMessage(err) };
177
+ }
178
+ };
179
+ const cells = [];
180
+ for (const column of columns) {
181
+ const res = await resolve(column.field);
182
+ const linkRes = column.link ? await resolve(column.link) : null;
183
+ cells.push({
184
+ value: res.known ? (res.value ?? null) : null,
185
+ known: res.known,
186
+ link: linkRes?.known ? safeLink(linkRes.value) : null,
187
+ });
188
+ }
189
+ const sortRes = view.sort ? await resolve(view.sort.field) : null;
190
+ rows.push({
191
+ row: { key: item.key, sourceId: item.sourceId, itemId: item.itemId, url: safeLink(item.url), lastRunId: run?.id ?? null, cells },
192
+ sortValue: sortRes?.known ? (sortRes.value ?? null) : null,
193
+ });
194
+ }));
195
+ const sortType = view.sort ? (defs.get(view.sort.field)?.type ?? null) : null;
196
+ const direction = view.sort?.direction === 'desc' ? -1 : 1;
197
+ rows.sort((a, b) => {
198
+ if (view.sort) {
199
+ const c = compareValues(a.sortValue, b.sortValue, sortType, direction);
200
+ if (c !== 0)
201
+ return c;
202
+ }
203
+ return naturalCompare(a.row.key, b.row.key);
204
+ });
205
+ errors.sort((a, b) => naturalCompare(a.sourceId, b.sourceId));
206
+ return {
207
+ view: { id: view.id, title: view.title, sources: [...view.sources] },
208
+ columns,
209
+ rows: rows.slice(0, MAX_VIEW_ROWS).map((r) => r.row),
210
+ errors,
211
+ filterUnknown,
212
+ truncated: rows.length > MAX_VIEW_ROWS,
213
+ fetchedAt,
214
+ cached: false,
215
+ };
216
+ }
217
+ }
218
+ /** `run.last_*` values for an item (known empty if the item has no run). */
219
+ export function runFields(run) {
220
+ const result = run?.result;
221
+ return {
222
+ 'run.last_id': run?.id ?? null,
223
+ 'run.last_workflow': run?.workflowId ?? null,
224
+ 'run.last_status': run?.status ?? null,
225
+ 'run.last_result': run?.businessResult ?? null,
226
+ 'run.last_result.message': typeof result?.message === 'string' ? result.message : null,
227
+ 'run.last_status_message': run?.statusMessage ?? null,
228
+ 'run.last_started_at': run?.startedAt ?? null,
229
+ 'run.last_finished_at': run?.finishedAt ?? null,
230
+ };
231
+ }
232
+ /** Only absolute http(s) URLs are used as links (XSS, Spec §20.5). */
233
+ export function safeLink(value) {
234
+ if (typeof value !== 'string')
235
+ return null;
236
+ try {
237
+ const url = new URL(value);
238
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : null;
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
244
+ /** Compare two cell values; empty values always last, independent of the direction. */
245
+ function compareValues(a, b, type, direction) {
246
+ const emptyA = a === null || a === undefined;
247
+ const emptyB = b === null || b === undefined;
248
+ if (emptyA || emptyB)
249
+ return emptyA === emptyB ? 0 : emptyA ? 1 : -1;
250
+ if (type === 'date' && typeof a === 'string' && typeof b === 'string') {
251
+ const ta = Date.parse(a);
252
+ const tb = Date.parse(b);
253
+ if (!Number.isNaN(ta) && !Number.isNaN(tb))
254
+ return direction * Math.sign(ta - tb);
255
+ }
256
+ if (typeof a === 'number' && typeof b === 'number')
257
+ return direction * Math.sign(a - b);
258
+ if (typeof a === 'boolean' && typeof b === 'boolean')
259
+ return direction * (Number(a) - Number(b));
260
+ const sa = Array.isArray(a) ? a.join(', ') : String(a);
261
+ const sb = Array.isArray(b) ? b.join(', ') : String(b);
262
+ return direction * naturalCompare(sa, sb);
263
+ }
264
+ //# sourceMappingURL=service.js.map