pipework 0.252.0 → 0.252.1

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 (690) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/package.json +4 -3
  3. package/src/async/jobs/abortable-delay.ts +18 -0
  4. package/src/async/jobs/claims.ts +52 -0
  5. package/src/async/jobs/cron.ts +179 -0
  6. package/src/async/jobs/errors.ts +56 -0
  7. package/src/async/jobs/exclusion.ts +117 -0
  8. package/src/async/jobs/execute.ts +219 -0
  9. package/src/async/jobs/index.ts +10 -0
  10. package/src/async/jobs/listener.ts +135 -0
  11. package/src/async/jobs/namespace.ts +20 -0
  12. package/src/async/jobs/queue-door.ts +44 -0
  13. package/src/async/jobs/queue.ts +1012 -0
  14. package/src/async/jobs/run-script.ts +67 -0
  15. package/src/async/jobs/scheduler.ts +218 -0
  16. package/src/async/jobs/schedules-table.ts +37 -0
  17. package/src/async/jobs/serialize-error.ts +48 -0
  18. package/src/async/jobs/table.ts +191 -0
  19. package/src/async/pipeline/define.ts +53 -0
  20. package/src/async/pipeline/errors.ts +35 -0
  21. package/src/async/pipeline/execute.ts +53 -0
  22. package/src/async/pipeline/index.ts +13 -0
  23. package/src/async/pipeline/namespace.ts +12 -0
  24. package/src/async/pipeline/resume.ts +67 -0
  25. package/src/async/pipeline/run-steps.ts +98 -0
  26. package/src/async/pipeline/types.ts +57 -0
  27. package/src/async/state-machine/define.ts +64 -0
  28. package/src/async/state-machine/errors.ts +29 -0
  29. package/src/async/state-machine/index.ts +3 -0
  30. package/src/async/state-machine/namespace.ts +12 -0
  31. package/src/async/state-machine/transition.ts +90 -0
  32. package/src/async/state-machine/types.ts +37 -0
  33. package/src/async/steps/errors.ts +31 -0
  34. package/src/async/steps/graph.ts +1214 -0
  35. package/src/async/steps/hash.ts +40 -0
  36. package/src/async/steps/index.ts +7 -0
  37. package/src/async/steps/namespace.ts +7 -0
  38. package/src/async/steps/table.ts +97 -0
  39. package/src/async/steps/types.ts +318 -0
  40. package/src/audit/chain/index.ts +9 -0
  41. package/src/audit/chain/install.ts +113 -0
  42. package/src/audit/chain/retire.ts +52 -0
  43. package/src/audit/chain/verify.ts +257 -0
  44. package/src/audit/query/cursor.ts +37 -0
  45. package/src/audit/query/index.ts +9 -0
  46. package/src/audit/query/query.ts +223 -0
  47. package/src/audit/schema/audit-record-fields.ts +68 -0
  48. package/src/audit/schema/audit-record.ts +23 -0
  49. package/src/auth/auth/audit.ts +31 -0
  50. package/src/auth/auth/chain.ts +84 -0
  51. package/src/auth/auth/cookie.ts +136 -0
  52. package/src/auth/auth/email-links.ts +311 -0
  53. package/src/auth/auth/guard.ts +128 -0
  54. package/src/auth/auth/index.ts +18 -0
  55. package/src/auth/auth/multi-org.ts +337 -0
  56. package/src/auth/auth/namespace.ts +34 -0
  57. package/src/auth/auth/parse-expiry.ts +13 -0
  58. package/src/auth/auth/passwords.ts +110 -0
  59. package/src/auth/auth/schema.ts +110 -0
  60. package/src/auth/auth/sessions.ts +565 -0
  61. package/src/auth/auth/sign-in.ts +156 -0
  62. package/src/auth/auth/signing-keys.ts +69 -0
  63. package/src/auth/auth/types.ts +26 -0
  64. package/src/auth/auth/uuid.ts +10 -0
  65. package/src/auth/rbac/cache.ts +55 -0
  66. package/src/auth/rbac/check.ts +30 -0
  67. package/src/auth/rbac/create.ts +35 -0
  68. package/src/auth/rbac/index.ts +5 -0
  69. package/src/auth/rbac/middleware.ts +20 -0
  70. package/src/auth/rbac/namespace.ts +15 -0
  71. package/src/auth/rbac/resolve.ts +31 -0
  72. package/src/auth/rbac/schema.sql +31 -0
  73. package/src/auth/rbac/seed.ts +67 -0
  74. package/src/auth/rbac/tables.ts +32 -0
  75. package/src/auth/rbac/types.ts +65 -0
  76. package/src/auth/tenant/extract.ts +33 -0
  77. package/src/auth/tenant/in-tenant.ts +49 -0
  78. package/src/auth/tenant/index.ts +7 -0
  79. package/src/auth/tenant/namespace.ts +23 -0
  80. package/src/auth/tenant/propagate.ts +20 -0
  81. package/src/auth/tenant/rls.ts +120 -0
  82. package/src/auth/tenant/scope.ts +33 -0
  83. package/src/auth/tenant/scoped-db.ts +581 -0
  84. package/src/auth/tenant/types.ts +14 -0
  85. package/src/auth/tenant/validate.ts +27 -0
  86. package/src/cli/commands/add.ts +158 -0
  87. package/src/cli/commands/alerts.ts +98 -0
  88. package/src/cli/commands/check.ts +1418 -0
  89. package/src/cli/commands/codemod.ts +78 -0
  90. package/src/cli/commands/db.ts +356 -0
  91. package/src/cli/commands/dev.ts +101 -0
  92. package/src/cli/commands/generate.ts +75 -0
  93. package/src/cli/commands/init.ts +36 -0
  94. package/src/cli/commands/install.ts +28 -0
  95. package/src/cli/commands/lint.ts +62 -0
  96. package/src/cli/commands/migrate.ts +132 -0
  97. package/src/cli/commands/openapi.ts +63 -0
  98. package/src/cli/commands/remove.ts +72 -0
  99. package/src/cli/commands/run.ts +50 -0
  100. package/src/cli/commands/scope.ts +54 -0
  101. package/src/cli/commands/serve.ts +68 -0
  102. package/src/cli/commands/shard.ts +31 -0
  103. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  104. package/src/cli/commands/shared/find-config.ts +22 -0
  105. package/src/cli/commands/shared/load-config.ts +35 -0
  106. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  107. package/src/cli/commands/shared/report-removed.ts +8 -0
  108. package/src/cli/commands/sync.ts +32 -0
  109. package/src/cli/commands/test.ts +307 -0
  110. package/src/cli/commands/upgrade.ts +48 -0
  111. package/src/cli/dev-bootstrap.ts +24 -0
  112. package/src/cli/errors.ts +10 -0
  113. package/src/cli/index.ts +303 -0
  114. package/src/cli/output.ts +17 -0
  115. package/src/cli/port.ts +15 -0
  116. package/src/cli/surface-kinds.ts +20 -0
  117. package/src/client/index.ts +4 -0
  118. package/src/client/poll.ts +130 -0
  119. package/src/client/report-fault.ts +119 -0
  120. package/src/core/config/app-pool.ts +19 -0
  121. package/src/core/config/aws-env.ts +29 -0
  122. package/src/core/config/base-dir.ts +24 -0
  123. package/src/core/config/discover.ts +64 -0
  124. package/src/core/config/env-resolve.ts +87 -0
  125. package/src/core/config/env-types.ts +116 -0
  126. package/src/core/config/env.ts +55 -0
  127. package/src/core/config/errors.ts +9 -0
  128. package/src/core/config/import-config.ts +40 -0
  129. package/src/core/config/index.ts +8 -0
  130. package/src/core/config/load.ts +417 -0
  131. package/src/core/config/namespace.ts +14 -0
  132. package/src/core/config/resolve.ts +195 -0
  133. package/src/core/config/schema.ts +488 -0
  134. package/src/core/config/statement-observer.ts +48 -0
  135. package/src/core/config/ts-inline-hooks.ts +75 -0
  136. package/src/core/config/ts-register.ts +4 -0
  137. package/src/core/config/ts-resolve-hooks.ts +101 -0
  138. package/src/core/errors/base.ts +49 -0
  139. package/src/core/errors/http.ts +146 -0
  140. package/src/core/errors/index.ts +2 -0
  141. package/src/core/errors/namespace.ts +31 -0
  142. package/src/core/lifecycle/drain.ts +33 -0
  143. package/src/core/lifecycle/health.ts +72 -0
  144. package/src/core/lifecycle/index.ts +4 -0
  145. package/src/core/lifecycle/namespace.ts +7 -0
  146. package/src/core/lifecycle/startup.ts +102 -0
  147. package/src/core/lifecycle/types.ts +36 -0
  148. package/src/core/logging/context.ts +60 -0
  149. package/src/core/logging/error-chain.ts +68 -0
  150. package/src/core/logging/index.ts +5 -0
  151. package/src/core/logging/logger.ts +65 -0
  152. package/src/core/logging/namespace.ts +16 -0
  153. package/src/core/logging/proxy.ts +22 -0
  154. package/src/core/logging/redact-match.ts +24 -0
  155. package/src/core/logging/types.ts +47 -0
  156. package/src/core/pipework.ts +566 -0
  157. package/src/core/runtime/broker.ts +69 -0
  158. package/src/core/surface/http.ts +133 -0
  159. package/src/core/surface/index.ts +11 -0
  160. package/src/core/surface/job-timeout.ts +40 -0
  161. package/src/core/surface/namespace.ts +13 -0
  162. package/src/core/surface/script.ts +33 -0
  163. package/src/core/surface/start.ts +142 -0
  164. package/src/core/surface/types.ts +225 -0
  165. package/src/core/surface/validate.ts +88 -0
  166. package/src/core/surface/worker.ts +528 -0
  167. package/src/core/validation/connectivity.ts +49 -0
  168. package/src/core/validation/domain.ts +99 -0
  169. package/src/core/validation/index.ts +5 -0
  170. package/src/core/validation/queue-tables.ts +312 -0
  171. package/src/core/validation/rls-role.ts +138 -0
  172. package/src/core/validation/startup.ts +51 -0
  173. package/src/core/validation/surfaces.ts +65 -0
  174. package/src/core/validation/types.ts +25 -0
  175. package/src/data/db/advisory-lock.ts +55 -0
  176. package/src/data/db/aggregates.ts +172 -0
  177. package/src/data/db/array-elements.ts +111 -0
  178. package/src/data/db/bulk-set.ts +53 -0
  179. package/src/data/db/col/index.ts +14 -0
  180. package/src/data/db/composed.ts +53 -0
  181. package/src/data/db/db.ts +276 -0
  182. package/src/data/db/excluded.ts +16 -0
  183. package/src/data/db/expressions.ts +473 -0
  184. package/src/data/db/filter.ts +90 -0
  185. package/src/data/db/function-source.ts +114 -0
  186. package/src/data/db/identifiers.ts +16 -0
  187. package/src/data/db/idx/index.ts +9 -0
  188. package/src/data/db/in-list.ts +49 -0
  189. package/src/data/db/index.ts +9 -0
  190. package/src/data/db/insert-split.ts +137 -0
  191. package/src/data/db/instrumentation.ts +82 -0
  192. package/src/data/db/maintenance.ts +28 -0
  193. package/src/data/db/namespace.ts +107 -0
  194. package/src/data/db/owner-labels.ts +9 -0
  195. package/src/data/db/pool.ts +187 -0
  196. package/src/data/db/query-label.ts +13 -0
  197. package/src/data/db/query-watch.ts +116 -0
  198. package/src/data/db/raw-client-warning.ts +37 -0
  199. package/src/data/db/readonly.ts +23 -0
  200. package/src/data/db/reservation.ts +151 -0
  201. package/src/data/db/schema-features.ts +14 -0
  202. package/src/data/db/serializable.ts +83 -0
  203. package/src/data/db/sql.ts +1 -0
  204. package/src/data/db/timeouts.ts +76 -0
  205. package/src/data/db/types.ts +18 -0
  206. package/src/data/db/unbound.ts +33 -0
  207. package/src/data/db/values.ts +29 -0
  208. package/src/data/domain/brand.ts +15 -0
  209. package/src/data/domain/build-factory.ts +193 -0
  210. package/src/data/domain/build-schema.ts +161 -0
  211. package/src/data/domain/build-table.ts +355 -0
  212. package/src/data/domain/define.ts +239 -0
  213. package/src/data/domain/external-registry.ts +34 -0
  214. package/src/data/domain/external.ts +77 -0
  215. package/src/data/domain/field.ts +279 -0
  216. package/src/data/domain/index.ts +13 -0
  217. package/src/data/domain/naming.ts +6 -0
  218. package/src/data/domain/projections.ts +57 -0
  219. package/src/data/domain/trace-safe-projection.ts +31 -0
  220. package/src/data/domain/traits.ts +108 -0
  221. package/src/data/domain/types.ts +289 -0
  222. package/src/data/jsonb/composed.ts +18 -0
  223. package/src/data/jsonb/index.ts +1 -0
  224. package/src/data/jsonb/namespace.ts +30 -0
  225. package/src/data/jsonb/query.ts +86 -0
  226. package/src/data/migrate/append-only-generate.ts +47 -0
  227. package/src/data/migrate/applied.ts +47 -0
  228. package/src/data/migrate/apply.ts +309 -0
  229. package/src/data/migrate/check.ts +119 -0
  230. package/src/data/migrate/diff.ts +357 -0
  231. package/src/data/migrate/door-generate.ts +130 -0
  232. package/src/data/migrate/framework-rls.ts +58 -0
  233. package/src/data/migrate/generate.ts +201 -0
  234. package/src/data/migrate/history.ts +215 -0
  235. package/src/data/migrate/index.ts +9 -0
  236. package/src/data/migrate/internal-definitions.ts +175 -0
  237. package/src/data/migrate/post-process.ts +73 -0
  238. package/src/data/migrate/pragma.ts +73 -0
  239. package/src/data/migrate/provision-framework.ts +147 -0
  240. package/src/data/migrate/rls-generate.ts +61 -0
  241. package/src/data/migrate/run.ts +124 -0
  242. package/src/data/migrate/safety.ts +167 -0
  243. package/src/data/migrate/snapshot.ts +368 -0
  244. package/src/data/migrate/sql-emit.ts +392 -0
  245. package/src/data/migrate/statements.ts +312 -0
  246. package/src/data/query/alias.ts +126 -0
  247. package/src/data/query/batch.ts +8 -0
  248. package/src/data/query/cache/core/cache.ts +78 -0
  249. package/src/data/query/cache/core/index.ts +1 -0
  250. package/src/data/query/cache/core/types.ts +29 -0
  251. package/src/data/query/cache/readme.md +230 -0
  252. package/src/data/query/casing.ts +76 -0
  253. package/src/data/query/collation.ts +11 -0
  254. package/src/data/query/column-builder.ts +370 -0
  255. package/src/data/query/column.ts +150 -0
  256. package/src/data/query/entity.ts +42 -0
  257. package/src/data/query/errors.ts +33 -0
  258. package/src/data/query/index.ts +14 -0
  259. package/src/data/query/logger.ts +47 -0
  260. package/src/data/query/operations.ts +45 -0
  261. package/src/data/query/pg-core/alias.ts +12 -0
  262. package/src/data/query/pg-core/checks.ts +32 -0
  263. package/src/data/query/pg-core/columns/all.ts +71 -0
  264. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  265. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  266. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  267. package/src/data/query/pg-core/columns/char.ts +85 -0
  268. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  269. package/src/data/query/pg-core/columns/common.ts +366 -0
  270. package/src/data/query/pg-core/columns/custom.ts +232 -0
  271. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  272. package/src/data/query/pg-core/columns/date.ts +112 -0
  273. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  274. package/src/data/query/pg-core/columns/enum.ts +201 -0
  275. package/src/data/query/pg-core/columns/index.ts +35 -0
  276. package/src/data/query/pg-core/columns/inet.ts +43 -0
  277. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  278. package/src/data/query/pg-core/columns/integer.ts +53 -0
  279. package/src/data/query/pg-core/columns/interval.ts +83 -0
  280. package/src/data/query/pg-core/columns/json.ts +58 -0
  281. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  282. package/src/data/query/pg-core/columns/line.ts +117 -0
  283. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  284. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  285. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  286. package/src/data/query/pg-core/columns/point.ts +127 -0
  287. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  288. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  289. package/src/data/query/pg-core/columns/real.ts +58 -0
  290. package/src/data/query/pg-core/columns/serial.ts +55 -0
  291. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  292. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  293. package/src/data/query/pg-core/columns/text.ts +67 -0
  294. package/src/data/query/pg-core/columns/time.ts +72 -0
  295. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  296. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  297. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  298. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  299. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  300. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  301. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  302. package/src/data/query/pg-core/db.ts +674 -0
  303. package/src/data/query/pg-core/dialect.ts +1404 -0
  304. package/src/data/query/pg-core/expressions.ts +25 -0
  305. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  306. package/src/data/query/pg-core/index.ts +21 -0
  307. package/src/data/query/pg-core/indexes.ts +276 -0
  308. package/src/data/query/pg-core/policies.ts +56 -0
  309. package/src/data/query/pg-core/primary-keys.ts +62 -0
  310. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  311. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  312. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  313. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  314. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  315. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  316. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  317. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  318. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  319. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  320. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  321. package/src/data/query/pg-core/roles.ts +41 -0
  322. package/src/data/query/pg-core/schema.ts +72 -0
  323. package/src/data/query/pg-core/sequence.ts +41 -0
  324. package/src/data/query/pg-core/session.ts +292 -0
  325. package/src/data/query/pg-core/subquery.ts +46 -0
  326. package/src/data/query/pg-core/table.ts +252 -0
  327. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  328. package/src/data/query/pg-core/utils/array.ts +95 -0
  329. package/src/data/query/pg-core/utils/index.ts +1 -0
  330. package/src/data/query/pg-core/utils.ts +104 -0
  331. package/src/data/query/pg-core/view-base.ts +14 -0
  332. package/src/data/query/pg-core/view-common.ts +1 -0
  333. package/src/data/query/pg-core/view.ts +435 -0
  334. package/src/data/query/pg-core/with-builder.ts +41 -0
  335. package/src/data/query/postgres-js/README.md +46 -0
  336. package/src/data/query/postgres-js/array-parser.ts +45 -0
  337. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  338. package/src/data/query/postgres-js/driver.ts +163 -0
  339. package/src/data/query/postgres-js/index.ts +2 -0
  340. package/src/data/query/postgres-js/session.ts +234 -0
  341. package/src/data/query/primary-key.ts +11 -0
  342. package/src/data/query/query-builders/query-builder.ts +19 -0
  343. package/src/data/query/query-builders/select.types.ts +175 -0
  344. package/src/data/query/query-promise.ts +35 -0
  345. package/src/data/query/relations.ts +725 -0
  346. package/src/data/query/runnable-query.ts +12 -0
  347. package/src/data/query/selection-proxy.ts +121 -0
  348. package/src/data/query/session.ts +8 -0
  349. package/src/data/query/sql/expressions/conditions.ts +741 -0
  350. package/src/data/query/sql/expressions/index.ts +2 -0
  351. package/src/data/query/sql/expressions/select.ts +45 -0
  352. package/src/data/query/sql/functions/aggregate.ts +129 -0
  353. package/src/data/query/sql/functions/index.ts +2 -0
  354. package/src/data/query/sql/functions/vector.ts +178 -0
  355. package/src/data/query/sql/index.ts +3 -0
  356. package/src/data/query/sql/sql.ts +794 -0
  357. package/src/data/query/subquery.ts +53 -0
  358. package/src/data/query/table.ts +208 -0
  359. package/src/data/query/table.utils.ts +2 -0
  360. package/src/data/query/tracing-utils.ts +3 -0
  361. package/src/data/query/tracing.ts +11 -0
  362. package/src/data/query/utils.ts +357 -0
  363. package/src/data/query/version.ts +4 -0
  364. package/src/data/query/view-common.ts +1 -0
  365. package/src/data/schema/branded.ts +13 -0
  366. package/src/data/schema/generated/coerce.ts +5 -0
  367. package/src/data/schema/generated/combinator.ts +6 -0
  368. package/src/data/schema/generated/encoding.ts +5 -0
  369. package/src/data/schema/generated/error.ts +6 -0
  370. package/src/data/schema/generated/format.ts +5 -0
  371. package/src/data/schema/generated/type-constructor.ts +10 -0
  372. package/src/data/schema/generated/type.ts +6 -0
  373. package/src/data/schema/index.ts +6 -0
  374. package/src/data/schema/json-schema.ts +33 -0
  375. package/src/data/schema/namespace.ts +55 -0
  376. package/src/data/schema/parse.ts +39 -0
  377. package/src/data/schema/types.ts +39 -0
  378. package/src/data/temporal/append-only.ts +134 -0
  379. package/src/data/temporal/close.ts +27 -0
  380. package/src/data/temporal/columns.ts +10 -0
  381. package/src/data/temporal/definition-queries.ts +70 -0
  382. package/src/data/temporal/errors.ts +10 -0
  383. package/src/data/temporal/get-current.ts +22 -0
  384. package/src/data/temporal/index.ts +9 -0
  385. package/src/data/temporal/namespace.ts +36 -0
  386. package/src/data/temporal/query.ts +25 -0
  387. package/src/data/temporal/resolve.ts +17 -0
  388. package/src/data/temporal/revise.ts +67 -0
  389. package/src/data/temporal/types.ts +22 -0
  390. package/src/data/valve/artifact.ts +33 -0
  391. package/src/data/valve/contract-diff.ts +21 -0
  392. package/src/data/valve/cross.ts +57 -0
  393. package/src/data/valve/crossing-ledger.ts +50 -0
  394. package/src/data/valve/decision.ts +44 -0
  395. package/src/data/valve/durable-crossing.ts +52 -0
  396. package/src/data/valve/execute-crossing.ts +77 -0
  397. package/src/data/valve/faucet.ts +56 -0
  398. package/src/data/valve/gate.ts +114 -0
  399. package/src/data/valve/graph.ts +77 -0
  400. package/src/data/valve/index.ts +53 -0
  401. package/src/data/valve/namespace.ts +57 -0
  402. package/src/data/valve/registry.ts +50 -0
  403. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  404. package/src/data/valve/schema/valve-crossing.ts +9 -0
  405. package/src/data/valve/supply.ts +154 -0
  406. package/src/data/valve/types.ts +143 -0
  407. package/src/data/vector/columns.ts +27 -0
  408. package/src/data/vector/index.ts +4 -0
  409. package/src/data/vector/namespace.ts +33 -0
  410. package/src/data/vector/query.ts +85 -0
  411. package/src/data/vector/validate.ts +16 -0
  412. package/src/data/write/audit/actor.ts +15 -0
  413. package/src/data/write/audit/audit-columns.ts +18 -0
  414. package/src/data/write/audit/audit-select.ts +111 -0
  415. package/src/data/write/audit/bound-transaction.ts +14 -0
  416. package/src/data/write/audit/cte-compile.ts +32 -0
  417. package/src/data/write/audit/decode-returning.ts +47 -0
  418. package/src/data/write/audit/final-select.ts +43 -0
  419. package/src/data/write/audit/infra-audit.ts +36 -0
  420. package/src/data/write/audit/primary-key.ts +65 -0
  421. package/src/data/write/audit/projection.ts +41 -0
  422. package/src/data/write/audit/read-touches.ts +86 -0
  423. package/src/data/write/audit/touch-match.ts +78 -0
  424. package/src/data/write/audit/verb-delete.ts +62 -0
  425. package/src/data/write/audit/verb-insert.ts +36 -0
  426. package/src/data/write/audit/verb-select.ts +47 -0
  427. package/src/data/write/audit/verb-update.ts +60 -0
  428. package/src/data/write/audit/verb-upsert.ts +104 -0
  429. package/src/data/write/conflict.ts +43 -0
  430. package/src/data/write/delete.ts +38 -0
  431. package/src/data/write/execute.ts +243 -0
  432. package/src/data/write/index.ts +11 -0
  433. package/src/data/write/insert.ts +63 -0
  434. package/src/data/write/state.ts +43 -0
  435. package/src/data/write/update.ts +54 -0
  436. package/src/data/write/upsert.ts +58 -0
  437. package/src/data/write/values.ts +19 -0
  438. package/src/index.ts +230 -0
  439. package/src/infra/access-links/code.ts +70 -0
  440. package/src/infra/access-links/handle.ts +892 -0
  441. package/src/infra/access-links/index.ts +9 -0
  442. package/src/infra/access-links/namespace.ts +30 -0
  443. package/src/infra/access-links/person-check.ts +177 -0
  444. package/src/infra/access-links/schema.ts +71 -0
  445. package/src/infra/alerts/async-sources.ts +231 -0
  446. package/src/infra/alerts/audit.ts +36 -0
  447. package/src/infra/alerts/beat.ts +123 -0
  448. package/src/infra/alerts/client-faults.ts +111 -0
  449. package/src/infra/alerts/faults.ts +156 -0
  450. package/src/infra/alerts/handle.ts +778 -0
  451. package/src/infra/alerts/http.ts +192 -0
  452. package/src/infra/alerts/index.ts +12 -0
  453. package/src/infra/alerts/infra-sources.ts +180 -0
  454. package/src/infra/alerts/namespace.ts +81 -0
  455. package/src/infra/alerts/redact.ts +21 -0
  456. package/src/infra/alerts/request-faults.ts +41 -0
  457. package/src/infra/alerts/schema.ts +124 -0
  458. package/src/infra/alerts/sinks.ts +125 -0
  459. package/src/infra/alerts/source-options.ts +26 -0
  460. package/src/infra/alerts/sources.ts +503 -0
  461. package/src/infra/alerts/subsystem-sources.ts +137 -0
  462. package/src/infra/audit/emit.ts +87 -0
  463. package/src/infra/audit/index.ts +2 -0
  464. package/src/infra/audit/namespace.ts +22 -0
  465. package/src/infra/audit/schema.sql +35 -0
  466. package/src/infra/audit/types.ts +18 -0
  467. package/src/infra/aws/credentials.ts +147 -0
  468. package/src/infra/aws/sigv4.ts +140 -0
  469. package/src/infra/cache/create.ts +58 -0
  470. package/src/infra/cache/entry.ts +81 -0
  471. package/src/infra/cache/index.ts +4 -0
  472. package/src/infra/cache/namespace.ts +10 -0
  473. package/src/infra/cache/tenant.ts +93 -0
  474. package/src/infra/cache/types.ts +24 -0
  475. package/src/infra/crawl/audit.ts +35 -0
  476. package/src/infra/crawl/errors.ts +25 -0
  477. package/src/infra/crawl/handle.ts +693 -0
  478. package/src/infra/crawl/index.ts +9 -0
  479. package/src/infra/crawl/links.ts +160 -0
  480. package/src/infra/crawl/namespace.ts +36 -0
  481. package/src/infra/crawl/robots.ts +100 -0
  482. package/src/infra/crawl/schema.ts +89 -0
  483. package/src/infra/credit-line/handle.ts +883 -0
  484. package/src/infra/credit-line/index.ts +6 -0
  485. package/src/infra/credit-line/namespace.ts +7 -0
  486. package/src/infra/credit-line/schema.ts +96 -0
  487. package/src/infra/credits/handle.ts +706 -0
  488. package/src/infra/credits/index.ts +5 -0
  489. package/src/infra/credits/namespace.ts +7 -0
  490. package/src/infra/credits/schema.ts +83 -0
  491. package/src/infra/email/audit.ts +36 -0
  492. package/src/infra/email/inbound.ts +622 -0
  493. package/src/infra/email/index.ts +11 -0
  494. package/src/infra/email/mail.ts +517 -0
  495. package/src/infra/email/mime.ts +447 -0
  496. package/src/infra/email/namespace.ts +22 -0
  497. package/src/infra/email/providers.ts +111 -0
  498. package/src/infra/email/schema.ts +96 -0
  499. package/src/infra/email/sns.ts +200 -0
  500. package/src/infra/email/unsubscribe.ts +44 -0
  501. package/src/infra/fetch/address.ts +88 -0
  502. package/src/infra/fetch/connect-retry.ts +87 -0
  503. package/src/infra/fetch/handle.ts +329 -0
  504. package/src/infra/fetch/index.ts +7 -0
  505. package/src/infra/fetch/namespace.ts +70 -0
  506. package/src/infra/idempotency/index.ts +3 -0
  507. package/src/infra/idempotency/middleware.ts +87 -0
  508. package/src/infra/idempotency/store.ts +92 -0
  509. package/src/infra/idempotency/types.ts +30 -0
  510. package/src/infra/join-requests/handle.ts +312 -0
  511. package/src/infra/join-requests/index.ts +2 -0
  512. package/src/infra/join-requests/namespace.ts +7 -0
  513. package/src/infra/join-requests/schema.ts +33 -0
  514. package/src/infra/outbox/core.ts +165 -0
  515. package/src/infra/quota/errors.ts +32 -0
  516. package/src/infra/quota/handle.ts +215 -0
  517. package/src/infra/quota/index.ts +5 -0
  518. package/src/infra/quota/namespace.ts +7 -0
  519. package/src/infra/quota/schema.ts +15 -0
  520. package/src/infra/storage/backend.ts +34 -0
  521. package/src/infra/storage/handle.ts +513 -0
  522. package/src/infra/storage/index.ts +9 -0
  523. package/src/infra/storage/local.ts +104 -0
  524. package/src/infra/storage/namespace.ts +13 -0
  525. package/src/infra/storage/s3.ts +257 -0
  526. package/src/infra/storage/schema.ts +24 -0
  527. package/src/infra/support/handle.ts +854 -0
  528. package/src/infra/support/http.ts +229 -0
  529. package/src/infra/support/index.ts +7 -0
  530. package/src/infra/support/namespace.ts +12 -0
  531. package/src/infra/support/schema.ts +96 -0
  532. package/src/infra/webhook/inbound.ts +45 -0
  533. package/src/infra/webhook/index.ts +5 -0
  534. package/src/infra/webhook/namespace.ts +24 -0
  535. package/src/infra/webhook/outbound.ts +315 -0
  536. package/src/infra/webhook/sign.ts +32 -0
  537. package/src/infra/webhook/verify.ts +82 -0
  538. package/src/invariants/assert.ts +26 -0
  539. package/src/invariants/index.ts +1 -0
  540. package/src/lint/config.ts +56 -0
  541. package/src/lint/doctrine.ts +163 -0
  542. package/src/lint/include.ts +97 -0
  543. package/src/lint/index.ts +3 -0
  544. package/src/lint/preset.ts +10 -0
  545. package/src/lint/resolver.ts +218 -0
  546. package/src/lint/rules/no-reexports.ts +46 -0
  547. package/src/request/behavior/audited.ts +85 -0
  548. package/src/request/behavior/cached.ts +62 -0
  549. package/src/request/behavior/compose.ts +28 -0
  550. package/src/request/behavior/index.ts +5 -0
  551. package/src/request/behavior/namespace.ts +16 -0
  552. package/src/request/behavior/types.ts +26 -0
  553. package/src/request/behavior/versioned.ts +54 -0
  554. package/src/request/context/create.ts +103 -0
  555. package/src/request/context/guard.ts +49 -0
  556. package/src/request/context/index.ts +4 -0
  557. package/src/request/context/run-in-transaction.ts +200 -0
  558. package/src/request/context/store.ts +28 -0
  559. package/src/request/context/types.ts +50 -0
  560. package/src/request/di/builder.ts +535 -0
  561. package/src/request/di/index.ts +8 -0
  562. package/src/request/di/resolve.ts +271 -0
  563. package/src/request/di/types.ts +262 -0
  564. package/src/request/fitting/namespace.ts +7 -0
  565. package/src/request/fixture/batch.ts +71 -0
  566. package/src/request/fixture/builder.ts +26 -0
  567. package/src/request/fixture/errors.ts +10 -0
  568. package/src/request/fixture/index.ts +8 -0
  569. package/src/request/fixture/namespace.ts +23 -0
  570. package/src/request/fixture/paginate.ts +44 -0
  571. package/src/request/fixture/to-handlers.ts +260 -0
  572. package/src/request/fixture/types.ts +79 -0
  573. package/src/request/flow/act-as.ts +78 -0
  574. package/src/request/flow/namespace.ts +17 -0
  575. package/src/request/http/error-step.ts +42 -0
  576. package/src/request/http/file.ts +141 -0
  577. package/src/request/http/index.ts +4 -0
  578. package/src/request/http/middleware.ts +141 -0
  579. package/src/request/http/namespace.ts +5 -0
  580. package/src/request/http/rate-limit-classes.ts +170 -0
  581. package/src/request/http/response.ts +82 -0
  582. package/src/request/http/server.ts +916 -0
  583. package/src/request/http/sniff.ts +261 -0
  584. package/src/request/http/types.ts +250 -0
  585. package/src/request/http/upload.ts +284 -0
  586. package/src/request/internal/text.ts +6 -0
  587. package/src/request/openapi/generate.ts +205 -0
  588. package/src/request/openapi/index.ts +2 -0
  589. package/src/request/openapi/namespace.ts +7 -0
  590. package/src/test/auth.ts +13 -0
  591. package/src/test/auto-setup.ts +45 -0
  592. package/src/test/context.ts +91 -0
  593. package/src/test/crawl-fixture.ts +157 -0
  594. package/src/test/discover-fork.ts +17 -0
  595. package/src/test/fingerprint.ts +135 -0
  596. package/src/test/globals.d.ts +7 -0
  597. package/src/test/index.ts +19 -0
  598. package/src/test/namespace.ts +87 -0
  599. package/src/test/plugin.ts +189 -0
  600. package/src/test/progress.ts +185 -0
  601. package/src/test/reap-after-run.ts +28 -0
  602. package/src/test/reap.ts +286 -0
  603. package/src/test/reset.ts +253 -0
  604. package/src/test/savepoints.ts +172 -0
  605. package/src/test/setup.ts +1422 -0
  606. package/src/test/system-channel.ts +50 -0
  607. package/src/test/vitest-plugin.ts +142 -0
  608. package/src/test/vitest-workspace.ts +507 -0
  609. package/src/test/vitest.ts +333 -0
  610. package/src/test/watch.ts +105 -0
  611. package/src/trace/capture/bounded-serializer.ts +146 -0
  612. package/src/trace/cardinality/analyze.ts +173 -0
  613. package/src/trace/cardinality/branch-context.ts +214 -0
  614. package/src/trace/cardinality/check.ts +33 -0
  615. package/src/trace/cardinality/classify-edge.ts +64 -0
  616. package/src/trace/cardinality/structural-max.ts +107 -0
  617. package/src/trace/check/coverage-check.ts +188 -0
  618. package/src/trace/check/flow-check.ts +103 -0
  619. package/src/trace/check/flow-step-site.ts +234 -0
  620. package/src/trace/check/io-pattern.ts +116 -0
  621. package/src/trace/codemod/add-cardinality.ts +152 -0
  622. package/src/trace/codemod/run.ts +80 -0
  623. package/src/trace/entry/child-trace.ts +21 -0
  624. package/src/trace/entry/close-trace.ts +87 -0
  625. package/src/trace/entry/finalize-trace.ts +28 -0
  626. package/src/trace/entry/open-trace.ts +138 -0
  627. package/src/trace/entry/parent-trace.ts +20 -0
  628. package/src/trace/entry/parse-traceparent.ts +39 -0
  629. package/src/trace/entry/process-start.ts +38 -0
  630. package/src/trace/entry/trace-context.ts +26 -0
  631. package/src/trace/entry/tracing-enabled.ts +10 -0
  632. package/src/trace/external/record.ts +195 -0
  633. package/src/trace/external/report.ts +66 -0
  634. package/src/trace/namespace.ts +19 -0
  635. package/src/trace/partition/day.ts +47 -0
  636. package/src/trace/partition/index.ts +14 -0
  637. package/src/trace/partition/partition-name.ts +23 -0
  638. package/src/trace/partition/plan.ts +82 -0
  639. package/src/trace/partition/schedule.ts +26 -0
  640. package/src/trace/partition/startup-hook.ts +39 -0
  641. package/src/trace/partition/sync.ts +154 -0
  642. package/src/trace/partition/tables.ts +53 -0
  643. package/src/trace/read/tree.ts +83 -0
  644. package/src/trace/retain/copy-on-error.ts +76 -0
  645. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  646. package/src/trace/runtime/flow-step.ts +147 -0
  647. package/src/trace/runtime/flush-steps.ts +31 -0
  648. package/src/trace/runtime/pending-writes.ts +54 -0
  649. package/src/trace/runtime/step-buffer.ts +298 -0
  650. package/src/trace/runtime/step-error.ts +21 -0
  651. package/src/trace/runtime/step-meta.ts +55 -0
  652. package/src/trace/runtime/thenable.ts +12 -0
  653. package/src/trace/runtime/truncate-trace.ts +62 -0
  654. package/src/trace/schema/trace-fields.ts +52 -0
  655. package/src/trace/schema/trace-retained.ts +22 -0
  656. package/src/trace/schema/trace-step-fields.ts +44 -0
  657. package/src/trace/schema/trace-step-retained.ts +25 -0
  658. package/src/trace/schema/trace-step.ts +18 -0
  659. package/src/trace/schema/trace.ts +12 -0
  660. package/src/trace/transformer/apply.ts +79 -0
  661. package/src/trace/transformer/build-meta-literal.ts +30 -0
  662. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  663. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  664. package/src/trace/transformer/transform-error.ts +14 -0
  665. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  666. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  667. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  668. package/src/workspace/affected.ts +110 -0
  669. package/src/workspace/changed-files.ts +163 -0
  670. package/src/workspace/check-deps.ts +213 -0
  671. package/src/workspace/checksum.ts +20 -0
  672. package/src/workspace/dependency-graph.ts +67 -0
  673. package/src/workspace/detect-pipework-copies.ts +93 -0
  674. package/src/workspace/discover.ts +206 -0
  675. package/src/workspace/generate.ts +401 -0
  676. package/src/workspace/index.ts +7 -0
  677. package/src/workspace/module-imports-pipework.ts +39 -0
  678. package/src/workspace/module-package-name.ts +12 -0
  679. package/src/workspace/scrub-git-env.ts +23 -0
  680. package/src/workspace/source-paths.ts +36 -0
  681. package/src/workspace/sync.ts +182 -0
  682. package/dist/REFERENCE.md +0 -1477
  683. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  684. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  685. package/dist/infra/access-links/rate-limit.js +0 -28
  686. package/dist/infra/access-links/rate-limit.js.map +0 -1
  687. package/dist/infra/email/sigv4.d.ts +0 -24
  688. package/dist/infra/email/sigv4.d.ts.map +0 -1
  689. package/dist/infra/email/sigv4.js +0 -55
  690. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,1214 @@
1
+ // Step graphs over a jobs Queue. A graph is defined once; each request names the
2
+ // results it wants for an item, and only the steps whose result is not already
3
+ // recorded for their inputs run, each as an ordinary queued job.
4
+ //
5
+ // A result's identity is its content key: sha256 of (graph, step, version, the
6
+ // element, the hash of every need). A changed input changes the keys that depend
7
+ // on it and nothing else, so invalidation needs no bookkeeping.
8
+ //
9
+ // Run state changes only under a row lock on the run (`advance` and every writer
10
+ // take it before touching tasks), so steps finishing together serialize.
11
+
12
+ import { randomUUID } from 'node:crypto'
13
+ import { sql } from '../../data/db/sql.js'
14
+ import type { DB } from '../../data/db/index.js'
15
+ import { scopedExecutor } from '../../auth/tenant/scoped-db.js'
16
+ import { schema } from '../../data/schema/namespace.js'
17
+ import { fitting } from '../../request/di/index.js'
18
+ import type { Handler } from '../../request/di/types.js'
19
+ import { resolveActorId } from '../../data/write/audit/actor.js'
20
+ import { getContext, runInContext } from '../../request/context/store.js'
21
+ import { createJobContext } from '../../request/context/create.js'
22
+ import { bindTransaction } from '../../request/context/guard.js'
23
+ import { propagateLocals } from '../../request/context/run-in-transaction.js'
24
+ import { isTracingEnabled } from '../../trace/entry/tracing-enabled.js'
25
+ import { openTrace } from '../../trace/entry/open-trace.js'
26
+ import { finalizeTrace } from '../../trace/entry/finalize-trace.js'
27
+ import { __flowStep } from '../../trace/runtime/flow-step.js'
28
+ import { parentTraceRef } from '../../trace/entry/parent-trace.js'
29
+ import type { ParentTraceRef } from '../../trace/entry/open-trace.js'
30
+ import { serializeForTrace } from '../../trace/capture/bounded-serializer.js'
31
+ import { getBaseLogger } from '../../core/logging/index.js'
32
+ import { recordFaultFor, faultMessage } from '../../infra/alerts/faults.js'
33
+ import type { DeadLetterInfo } from '../jobs/queue.js'
34
+ import type { Manifold } from '../../core/pipework.js'
35
+ import { projectStepError } from '../../trace/runtime/step-error.js'
36
+ import { StepItemRunningError, StepTimeoutError } from './errors.js'
37
+ import { canonicalJson, inputHash, sha256Hex, valueHash } from './hash.js'
38
+ import { SINGLE, registerStepTables } from './table.js'
39
+ import type {
40
+ DecideOptions, EndedRun, EndedRunsOptions, GraphConfig, GraphFailure, GraphSelection, ScopeOptions, GraphForEachContext, GraphHost, GraphRunStatus, GraphStepContext, GraphStepDefinition,
41
+ PlanOptions, PlanResult, RemoveItemOptions, RemoveItemResult, RequestOptions, RequestResult, RunStatus, RunTask, StepCounts, StepGraph, StepPlan, StepResultRecord, TaskState,
42
+ } from './types.js'
43
+
44
+ const RUN = 'pipework_step_run'
45
+ const TASK = 'pipework_step_task'
46
+ const RESULT = 'pipework_step_result'
47
+ const NAME_RE = /^[A-Za-z][A-Za-z0-9_.-]*$/
48
+ const CLASS_RE = /^[A-Za-z][A-Za-z0-9_-]*$/
49
+ const DEFAULT_CLASS = 'default'
50
+ const MS_PER_DAY = 86_400_000
51
+
52
+ type TaskStatus = 'blocked' | 'queued' | 'waiting' | 'done' | 'reused' | 'failed' | 'cancelled'
53
+
54
+ interface Task {
55
+ readonly id: string
56
+ readonly step: string
57
+ readonly instance: number
58
+ status: TaskStatus
59
+ jobId: string | null
60
+ inputKey: string | null
61
+ resultId: string | null
62
+ readonly element: unknown
63
+ }
64
+
65
+ interface RunRow {
66
+ readonly id: string
67
+ readonly tenant: string
68
+ readonly item: string
69
+ readonly status: GraphRunStatus
70
+ readonly priority: number
71
+ readonly stepPriority: Readonly<Record<string, number>> | null
72
+ readonly targets: readonly string[]
73
+ readonly inputs: Readonly<Record<string, unknown>>
74
+ readonly parent: ParentTraceRef | null
75
+ readonly actor: string | null
76
+ }
77
+
78
+ interface ResultValue { readonly value: unknown; readonly hash: string }
79
+
80
+ interface Outcome { reused: number; queued: number; completed: boolean }
81
+
82
+ function fail(graph: string, message: string): never {
83
+ throw new Error(`[pipework] steps.define("${graph}"): ${message}\n`)
84
+ }
85
+
86
+ function toJson(value: unknown): unknown {
87
+ return value === undefined ? null : JSON.parse(JSON.stringify(value))
88
+ }
89
+
90
+ function stepDefinitionChecks(config: GraphConfig): { order: string[]; inputs: ReadonlySet<string> } {
91
+ const graph = config.name
92
+ if (typeof graph !== 'string' || !NAME_RE.test(graph)) {
93
+ throw new Error(`[pipework] steps.define: graph name "${String(graph)}" must start with a letter and use letters, digits, "_", "." or "-".\n`)
94
+ }
95
+ const inputs = new Set(config.inputs ?? [])
96
+ const names = Object.keys(config.steps)
97
+ if (names.length === 0) fail(graph, 'no steps.')
98
+ for (const name of names) {
99
+ const def = config.steps[name]!
100
+ if (!NAME_RE.test(name)) fail(graph, `step name "${name}" must start with a letter and use letters, digits, "_", "." or "-".`)
101
+ if (inputs.has(name)) fail(graph, `"${name}" is both a request input and a step.`)
102
+ if (!Number.isInteger(def.version)) fail(graph, `step "${name}" needs an integer version.`)
103
+ for (const need of def.needs ?? []) {
104
+ if (!inputs.has(need) && config.steps[need] === undefined) {
105
+ fail(graph, `step "${name}" needs "${need}", which is neither a step nor a declared input (steps: ${names.join(', ')}; inputs: ${[...inputs].join(', ') || '(none)'}).`)
106
+ }
107
+ }
108
+ if (def.workers !== undefined && (typeof def.workers !== 'string' || !CLASS_RE.test(def.workers))) {
109
+ fail(graph, `step "${name}" names worker class "${String(def.workers)}", which must start with a letter and use letters, digits, "_" or "-".`)
110
+ }
111
+ if (def.priority !== undefined && !Number.isInteger(def.priority)) {
112
+ fail(graph, `step "${name}" has priority ${String(def.priority)}, which must be an integer.`)
113
+ }
114
+ if (def.timeoutMs !== undefined) {
115
+ if (!Number.isInteger(def.timeoutMs) || def.timeoutMs <= 0) fail(graph, `step "${name}" has timeoutMs ${String(def.timeoutMs)}, which must be a positive integer (milliseconds).`)
116
+ if (def.person === true) fail(graph, `step "${name}" is a person step and cannot have timeoutMs: it holds no worker.`)
117
+ }
118
+ if (def.forEachOnly !== undefined) {
119
+ if (!Array.isArray(def.forEachOnly)) fail(graph, `step "${name}" has forEachOnly ${String(def.forEachOnly)}, which must be an array of need names.`)
120
+ for (const n of def.forEachOnly) {
121
+ if (!(def.needs ?? []).includes(n)) fail(graph, `step "${name}" lists "${String(n)}" in forEachOnly, which is not one of its needs (${(def.needs ?? []).join(', ') || '(none)'}).`)
122
+ }
123
+ if (def.forEachOnly.length > 0 && def.forEach === undefined) fail(graph, `step "${name}" has forEachOnly "${def.forEachOnly.join('", "')}" but no forEach.`)
124
+ }
125
+ if (def.byElement !== undefined) {
126
+ const by = def.byElement
127
+ if (!Array.isArray(by) || by.length === 0) fail(graph, `step "${name}" has byElement ${String(by)}, which must be a non-empty array of need names.`)
128
+ if (def.forEach !== undefined) fail(graph, `step "${name}" has both forEach and byElement; byElement takes its list from the steps it names.`)
129
+ if (def.forEachOnly !== undefined) fail(graph, `step "${name}" has forEachOnly but no forEach.`)
130
+ if (def.person === true) fail(graph, `step "${name}" is a person step and cannot have byElement.`)
131
+ for (const n of by) {
132
+ if (!(def.needs ?? []).includes(n)) fail(graph, `step "${name}" lists "${String(n)}" in byElement, which is not one of its needs (${(def.needs ?? []).join(', ') || '(none)'}).`)
133
+ if (inputs.has(n) || config.steps[n] === undefined) fail(graph, `step "${name}" lists "${n}" in byElement, but it is a request input, not a fan-out step.`)
134
+ if (config.steps[n]!.forEach === undefined && config.steps[n]!.byElement === undefined) {
135
+ fail(graph, `step "${name}" lists "${n}" in byElement, but "${n}" is not a fan-out step (it has neither forEach nor byElement).`)
136
+ }
137
+ }
138
+ }
139
+ if (def.person === true) {
140
+ if (def.forEach !== undefined) fail(graph, `step "${name}" is a person step and cannot have forEach.`)
141
+ if (def.run !== undefined) fail(graph, `step "${name}" is a person step and cannot have run: its value comes from decide() or autoAccept.`)
142
+ } else {
143
+ if (def.run === undefined) fail(graph, `step "${name}" has no run.`)
144
+ if (def.autoAccept !== undefined) fail(graph, `step "${name}" has autoAccept but is not a person step.`)
145
+ }
146
+ }
147
+ // Topological order; a back edge is a cycle.
148
+ const order: string[] = []
149
+ const state = new Map<string, 'visiting' | 'done'>()
150
+ const visit = (name: string, path: string[]): void => {
151
+ const seen = state.get(name)
152
+ if (seen === 'done') return
153
+ if (seen === 'visiting') fail(graph, `steps form a cycle: ${[...path.slice(path.indexOf(name)), name].join(' -> ')}.`)
154
+ state.set(name, 'visiting')
155
+ for (const need of config.steps[name]!.needs ?? []) {
156
+ if (config.steps[need] !== undefined) visit(need, [...path, name])
157
+ }
158
+ state.set(name, 'done')
159
+ order.push(name)
160
+ }
161
+ for (const name of names) visit(name, [])
162
+ // byElement is a subset of needs, so with no cycle every chain of byElement ends at a forEach step.
163
+ const rootOf = (name: string): string => {
164
+ const def = config.steps[name]!
165
+ return def.forEach !== undefined ? name : rootOf(def.byElement![0]!)
166
+ }
167
+ for (const name of names) {
168
+ const by = config.steps[name]!.byElement
169
+ if (by === undefined) continue
170
+ const roots = by.map(rootOf)
171
+ if (new Set(roots).size > 1) {
172
+ fail(graph, `step "${name}" lists byElement steps over different lists: ${by.map((n, i) => `"${n}" (list of "${roots[i]!}")`).join(', ')}.`)
173
+ }
174
+ }
175
+ return { order, inputs }
176
+ }
177
+
178
+ export function defineGraph(config: GraphConfig): StepGraph {
179
+ const { order, inputs: inputNames } = stepDefinitionChecks(config)
180
+ registerStepTables()
181
+
182
+ const graph = config.name
183
+ const jobType = `pipework.step.${graph}`
184
+ const steps = config.steps
185
+ const classOf = (name: string): string => steps[name]!.workers ?? DEFAULT_CLASS
186
+ const typeOfClass = (cls: string): string => cls === DEFAULT_CLASS ? jobType : `${jobType}:${cls}`
187
+ const classes = [...new Set(order.map(classOf))]
188
+ const jobTypes = classes.map(typeOfClass)
189
+ const queue = config.queue
190
+ const isFanOut = (name: string): boolean => steps[name]!.forEach !== undefined || steps[name]!.byElement !== undefined
191
+ const running = new Map<string, Set<AbortController>>()
192
+
193
+ /** Every step the targets need, transitively, in dependency order. */
194
+ function closureOf(targets: readonly string[]): string[] {
195
+ const wanted = new Set<string>()
196
+ const add = (name: string): void => {
197
+ if (wanted.has(name)) return
198
+ wanted.add(name)
199
+ for (const need of steps[name]!.needs ?? []) if (steps[need] !== undefined) add(need)
200
+ }
201
+ for (const t of targets) add(t)
202
+ return order.filter(n => wanted.has(n))
203
+ }
204
+
205
+ /** The steps that depend on `step`, directly or not. */
206
+ function dependentsOf(step: string): Set<string> {
207
+ const out = new Set<string>()
208
+ const grow = (name: string): void => {
209
+ for (const other of order) {
210
+ if (!out.has(other) && (steps[other]!.needs ?? []).includes(name)) { out.add(other); grow(other) }
211
+ }
212
+ }
213
+ grow(step)
214
+ return out
215
+ }
216
+
217
+ function checkRequest(opts: { item: string; targets: readonly string[]; inputs?: Readonly<Record<string, unknown>>; stepPriority?: Readonly<Record<string, number>> }, what: string): string[] {
218
+ if (typeof opts.item !== 'string' || opts.item === '') throw new Error(`[pipework] ${graph}.${what}: item must be a non-empty string.\n`)
219
+ if (!Array.isArray(opts.targets) || opts.targets.length === 0) throw new Error(`[pipework] ${graph}.${what}: targets must name at least one step.\n`)
220
+ for (const t of opts.targets) {
221
+ if (steps[t] === undefined) {
222
+ throw new Error(`[pipework] ${graph}.${what}: unknown target step "${t}" (steps: ${order.join(', ')}).\n`)
223
+ }
224
+ }
225
+ for (const name of Object.keys(opts.inputs ?? {})) {
226
+ if (!inputNames.has(name)) {
227
+ throw new Error(`[pipework] ${graph}.${what}: unknown input "${name}" (inputs: ${[...inputNames].join(', ') || '(none)'}).\n`)
228
+ }
229
+ }
230
+ if (opts.stepPriority !== undefined) {
231
+ for (const [name, p] of Object.entries(opts.stepPriority)) {
232
+ if (steps[name] === undefined) {
233
+ throw new Error(`[pipework] ${graph}.${what}: stepPriority names unknown step "${name}" (steps: ${order.join(', ')}).\n`)
234
+ }
235
+ if (!Number.isInteger(p)) throw new Error(`[pipework] ${graph}.${what}: stepPriority for step "${name}" is ${String(p)}, which must be an integer.\n`)
236
+ }
237
+ }
238
+ const closure = closureOf(opts.targets)
239
+ for (const name of closure) {
240
+ for (const need of steps[name]!.needs ?? []) {
241
+ if (inputNames.has(need) && (opts.inputs ?? {})[need] === undefined) {
242
+ throw new Error(`[pipework] ${graph}.${what}: step "${name}" needs input "${need}", which the request does not give.\n`)
243
+ }
244
+ }
245
+ }
246
+ return closure
247
+ }
248
+
249
+ function tenantOf(explicit: string | undefined, what: string): string {
250
+ const tenant = explicit ?? getContext()?.tenant ?? null
251
+ if (tenant === null || tenant === '') {
252
+ throw new Error(`[pipework] ${graph}.${what}: no tenant. Pass { tenant } or call inside a tenant Flow; results are recorded per tenant.\n`)
253
+ }
254
+ return tenant
255
+ }
256
+
257
+ /** The tenant of a tenant-scoped handle, which stands in when a call names none. */
258
+ const handleTenant = (db: DB): string | undefined => scopedExecutor(db, null, graph).tenant ?? undefined
259
+
260
+ let attached: Manifold | undefined
261
+
262
+ /** The session variable the tenant policies read: the graph's own, else the active or attached Manifold's, else the default. */
263
+ function sessionVarOf(instance?: Manifold): string {
264
+ if (config.sessionVar !== undefined) return config.sessionVar
265
+ const m = instance ?? getContext()?.instance ?? attached
266
+ if (m === undefined) return 'pipework.tenant_id'
267
+ return m.config.database(m.config.defaultDatabaseName()).rls?.sessionVar ?? 'pipework.tenant_id'
268
+ }
269
+
270
+ /** Sets the tenant session variable for the rest of the transaction `tx` is in, and puts back what was there. */
271
+ async function asTenant<T>(tx: DB, tenant: string | null, fn: () => Promise<T>, instance?: Manifold): Promise<T> {
272
+ if (tenant === null) return fn()
273
+ const sessionVar = sessionVarOf(instance)
274
+ const before = await tx.execute(sql`SELECT current_setting(${sessionVar}, true) AS v`)
275
+ await tx.execute(sql`SELECT set_config(${sessionVar}, ${tenant}, true)`)
276
+ const result = await fn()
277
+ await tx.execute(sql`SELECT set_config(${sessionVar}, ${(before[0]?.['v'] as string | null) ?? ''}, true)`)
278
+ return result
279
+ }
280
+
281
+ async function inTenantTx<T>(handle: DB, scope: string | null, fn: (tx: DB) => Promise<T>): Promise<T> {
282
+ const { db, tenant } = scopedExecutor(handle, scope, `${graph} step graph`)
283
+ return db.transaction(async (tx) => asTenant(tx, tenant, () => fn(tx)))
284
+ }
285
+
286
+ /** The tenant a call acts for: the one given, else the Flow's, else none (an owner connection needs none). */
287
+ const scopeOf = (explicit?: string): string | null => explicit ?? getContext()?.tenant ?? null
288
+
289
+ /** Cancels the jobs that are still pending in one statement, skipping any row another transaction holds: never waits on a job while the run lock is held. */
290
+ async function cancelPending(tx: DB, jobIds: readonly string[], why: { reason: string; message: string }): Promise<void> {
291
+ if (jobIds.length === 0) return
292
+ const table = quoted(queueTable())
293
+ await tx.execute(sql`
294
+ UPDATE ${sql.raw(table)} SET status = 'cancelled', completed_at = now(), claim_token = NULL,
295
+ error_payload = jsonb_build_object('reason', ${why.reason}::text, 'message', ${why.message}::text)
296
+ WHERE id IN (SELECT id FROM ${sql.raw(table)}
297
+ WHERE id ${sql.inList(jobIds, 'uuid')} AND status = 'pending'
298
+ FOR UPDATE SKIP LOCKED)`)
299
+ }
300
+
301
+ async function lockRun(tx: DB, runId: string): Promise<RunRow | null> {
302
+ const rows = await tx.execute(sql`
303
+ SELECT id::text, tenant_id::text AS tenant, item, status, priority, step_priority, targets, inputs,
304
+ trace_id::text AS trace_id, trace_root_id::text AS trace_root_id, trace_step_id::text AS trace_step_id, actor
305
+ FROM ${sql.raw(RUN)} WHERE id = ${runId}::uuid AND graph = ${graph} FOR UPDATE`)
306
+ const row = rows[0]
307
+ if (row === undefined) return null
308
+ const traceId = row['trace_id'] as string | null
309
+ const rootId = row['trace_root_id'] as string | null
310
+ return {
311
+ id: row['id'] as string,
312
+ tenant: row['tenant'] as string,
313
+ item: row['item'] as string,
314
+ status: row['status'] as GraphRunStatus,
315
+ priority: row['priority'] as number,
316
+ stepPriority: row['step_priority'] as Record<string, number> | null,
317
+ targets: row['targets'] as string[],
318
+ inputs: row['inputs'] as Record<string, unknown>,
319
+ parent: traceId !== null && rootId !== null ? { id: traceId, rootId, stepId: row['trace_step_id'] as string | null } : null,
320
+ actor: row['actor'] as string | null,
321
+ }
322
+ }
323
+
324
+ async function loadTasks(tx: DB, runId: string): Promise<Task[]> {
325
+ const rows = await tx.execute(sql`
326
+ SELECT id::text, step, instance, status, job_id::text AS job_id, input_key, result_id::text AS result_id, element
327
+ FROM ${sql.raw(TASK)} WHERE run_id = ${runId}::uuid ORDER BY step, instance`)
328
+ return rows.map(r => ({
329
+ id: r['id'] as string,
330
+ step: r['step'] as string,
331
+ instance: r['instance'] as number,
332
+ status: r['status'] as TaskStatus,
333
+ jobId: r['job_id'] as string | null,
334
+ inputKey: r['input_key'] as string | null,
335
+ resultId: r['result_id'] as string | null,
336
+ element: r['element'],
337
+ }))
338
+ }
339
+
340
+ async function loadResults(tx: DB, ids: readonly string[]): Promise<Map<string, ResultValue>> {
341
+ const out = new Map<string, ResultValue>()
342
+ if (ids.length === 0) return out
343
+ const rows = await tx.execute(sql`
344
+ SELECT id::text, value, value_hash FROM ${sql.raw(RESULT)}
345
+ WHERE id ${sql.inList(ids, 'uuid')}`)
346
+ for (const r of rows) out.set(r['id'] as string, { value: r['value'], hash: r['value_hash'] as string })
347
+ return out
348
+ }
349
+
350
+ async function findRecorded(
351
+ tx: DB, tenant: string, item: string, step: string, key: string, instance: number,
352
+ ): Promise<{ id: string; value: unknown; hash: string } | null> {
353
+ const rows = await tx.execute(sql`
354
+ SELECT id::text, value, value_hash FROM ${sql.raw(RESULT)}
355
+ WHERE tenant_id = ${tenant}::uuid AND graph = ${graph} AND item = ${item} AND step = ${step}
356
+ AND version = ${steps[step]!.version} AND input_key = ${key}
357
+ ORDER BY (instance = ${instance}) DESC, created_at ASC LIMIT 1`)
358
+ const row = rows[0]
359
+ return row === undefined ? null : { id: row['id'] as string, value: row['value'], hash: row['value_hash'] as string }
360
+ }
361
+
362
+ /** Records a result; when one already holds the key (a concurrent run got there) that one is returned. */
363
+ async function recordResult(
364
+ tx: DB, run: RunRow, step: string, key: string, instance: number, value: unknown, replace: boolean,
365
+ ): Promise<{ id: string; hash: string; value: unknown }> {
366
+ const json = JSON.stringify(value)
367
+ const hash = valueHash(value)
368
+ const conflict = replace
369
+ ? sql`ON CONFLICT (tenant_id, graph, item, step, version, input_key, instance)
370
+ DO UPDATE SET value = EXCLUDED.value, value_hash = EXCLUDED.value_hash, created_at = now()`
371
+ : sql`ON CONFLICT (tenant_id, graph, item, step, version, input_key, instance) DO NOTHING`
372
+ const inserted = await tx.execute(sql`
373
+ INSERT INTO ${sql.raw(RESULT)} (tenant_id, graph, item, step, version, input_key, instance, value, value_hash)
374
+ VALUES (${run.tenant}::uuid, ${graph}, ${run.item}, ${step}, ${steps[step]!.version}, ${key}, ${instance}, ${json}::jsonb, ${hash})
375
+ ${conflict} RETURNING id::text`)
376
+ if (inserted[0] !== undefined) return { id: inserted[0]['id'] as string, hash, value }
377
+ const rows = await tx.execute(sql`
378
+ SELECT id::text, value, value_hash FROM ${sql.raw(RESULT)}
379
+ WHERE tenant_id = ${run.tenant}::uuid AND graph = ${graph} AND item = ${run.item} AND step = ${step}
380
+ AND version = ${steps[step]!.version} AND input_key = ${key} AND instance = ${instance}`)
381
+ const row = rows[0]!
382
+ return { id: row['id'] as string, hash: row['value_hash'] as string, value: row['value'] }
383
+ }
384
+
385
+ function stageReused(step: string, instance: number, resultId: string): void {
386
+ const buffer = getContext()?.trace?.buffer
387
+ if (buffer === undefined) return
388
+ const now = new Date()
389
+ buffer.stageExternal({
390
+ id: randomUUID(),
391
+ parentStepId: null,
392
+ flowStepRef: 'external:pipework.step.reused',
393
+ status: 'ok',
394
+ startedAt: now,
395
+ completedAt: now,
396
+ durationMicros: 0n,
397
+ capturedInputs: [serializeForTrace({ step, instance: instance === SINGLE ? null : instance, resultId })],
398
+ error: null,
399
+ })
400
+ }
401
+
402
+ function stageTimeout(step: string, instance: number, timeoutMs: number, startedAt: Date, error: StepTimeoutError): void {
403
+ const buffer = getContext()?.trace?.buffer
404
+ if (buffer === undefined) return
405
+ const now = new Date()
406
+ buffer.stageExternal({
407
+ id: randomUUID(),
408
+ parentStepId: null,
409
+ flowStepRef: 'external:pipework.step.timeout',
410
+ status: 'error',
411
+ startedAt,
412
+ completedAt: now,
413
+ durationMicros: BigInt(Math.max(0, now.getTime() - startedAt.getTime())) * 1000n,
414
+ capturedInputs: [serializeForTrace({ step, instance: instance === SINGLE ? null : instance, timeoutMs })],
415
+ error: projectStepError(error),
416
+ })
417
+ }
418
+
419
+ /** The state of one run during an `advance`: its tasks and the results they hold. */
420
+ class RunState {
421
+ constructor(
422
+ readonly run: RunRow,
423
+ readonly tasks: Task[],
424
+ readonly results: Map<string, ResultValue>,
425
+ readonly inputHashes: Record<string, string>,
426
+ ) {}
427
+
428
+ of(step: string): Task[] { return this.tasks.filter(t => t.step === step) }
429
+ single(step: string): Task | undefined { return this.tasks.find(t => t.step === step && t.instance === SINGLE) }
430
+ instances(step: string): Task[] {
431
+ return this.tasks.filter(t => t.step === step && t.instance >= 0).sort((a, b) => a.instance - b.instance)
432
+ }
433
+ instanceOf(step: string, i: number): Task { return this.instances(step).find(t => t.instance === i)! }
434
+ good(t: Task): boolean { return t.status === 'done' || t.status === 'reused' }
435
+
436
+ complete(step: string): boolean {
437
+ const single = this.single(step)
438
+ if (single === undefined) return false
439
+ if (!isFanOut(step)) return this.good(single)
440
+ return single.status === 'done' && this.instances(step).every(t => this.good(t))
441
+ }
442
+
443
+ resultOf(t: Task): ResultValue {
444
+ const r = t.resultId === null ? undefined : this.results.get(t.resultId)
445
+ if (r === undefined) throw new Error(`[pipework] ${graph}: task ${t.id} (${t.step}) has no recorded result.\n`)
446
+ return r
447
+ }
448
+
449
+ /** What a step reads: the value and hash of each need, and the request inputs among them. */
450
+ gather(def: GraphStepDefinition, task?: Task): { values: Record<string, unknown>; hashes: Record<string, string | string[]>; inputs: Record<string, unknown> } {
451
+ const values: Record<string, unknown> = {}
452
+ const hashes: Record<string, string | string[]> = {}
453
+ const inputs: Record<string, unknown> = {}
454
+ for (const need of def.needs ?? []) {
455
+ if (inputNames.has(need)) {
456
+ values[need] = this.run.inputs[need]
457
+ inputs[need] = this.run.inputs[need]
458
+ hashes[need] = this.inputHashes[need]!
459
+ } else if (def.byElement?.includes(need) === true && task !== undefined) {
460
+ const r = this.resultOf(this.instanceOf(need, task.instance))
461
+ values[need] = r.value
462
+ hashes[need] = r.hash
463
+ } else if (isFanOut(need)) {
464
+ const rs = this.instances(need).map(t => this.resultOf(t))
465
+ values[need] = rs.map(r => r.value)
466
+ hashes[need] = rs.map(r => r.hash)
467
+ } else {
468
+ const r = this.resultOf(this.single(need)!)
469
+ values[need] = r.value
470
+ hashes[need] = r.hash
471
+ }
472
+ }
473
+ return { values, hashes, inputs }
474
+ }
475
+ }
476
+
477
+ function inputHashesOf(inputs: Readonly<Record<string, unknown>>): Record<string, string> {
478
+ return Object.fromEntries(Object.entries(inputs).map(([k, v]) => [k, inputHash(v)]))
479
+ }
480
+
481
+ /** A step's needs (values or hashes) without those that only choose its list: what its key and `run` cover. */
482
+ function without<T>(def: GraphStepDefinition, values: Record<string, T>): Record<string, T> {
483
+ const only = def.forEachOnly
484
+ if (only === undefined || only.length === 0) return values
485
+ return Object.fromEntries(Object.entries(values).filter(([n]) => !only.includes(n)))
486
+ }
487
+
488
+ function contentKey(step: string, element: unknown, fanOut: boolean, hashes: Record<string, string | string[]>): string {
489
+ return sha256Hex(canonicalJson({
490
+ graph, step, version: steps[step]!.version, element: fanOut ? valueHash(element) : null, needs: hashes,
491
+ }))
492
+ }
493
+
494
+ async function loadState(tx: DB, run: RunRow): Promise<RunState> {
495
+ const tasks = await loadTasks(tx, run.id)
496
+ const results = await loadResults(tx, tasks.flatMap(t => t.resultId === null ? [] : [t.resultId]))
497
+ return new RunState(run, tasks, results, inputHashesOf(run.inputs))
498
+ }
499
+
500
+ /** What a step's context carries besides its data: the database handles `run` is given. */
501
+ interface StepIo {
502
+ checkpoint<R>(fn: (tx: DB) => Promise<R>): Promise<R>
503
+ withResult(fn: (tx: DB) => Promise<void>): void
504
+ }
505
+
506
+ function stepContext(state: RunState, def: GraphStepDefinition, task: Task, fanOut: boolean, signal: AbortSignal, io: StepIo): Omit<GraphStepContext, 'attempt' | 'maxAttempts'> {
507
+ const g = state.gather(def, task)
508
+ return {
509
+ needs: without(def, g.values),
510
+ element: fanOut ? task.element : undefined,
511
+ index: fanOut ? task.instance : null,
512
+ item: state.run.item,
513
+ inputs: without(def, g.inputs),
514
+ runId: state.run.id,
515
+ signal,
516
+ checkpoint: io.checkpoint,
517
+ withResult: io.withResult,
518
+ }
519
+ }
520
+
521
+ /** The priority of a step's jobs: the request's `stepPriority` for it, else its definition's `priority`, else the run's. */
522
+ function stepJobPriority(run: RunRow, def: GraphStepDefinition, step: string): number {
523
+ return run.stepPriority?.[step] ?? def.priority ?? run.priority
524
+ }
525
+
526
+ async function setTask(tx: DB, t: Task, patch: { status: TaskStatus; jobId?: string; inputKey?: string; resultId?: string; error?: unknown }): Promise<void> {
527
+ await tx.execute(sql`
528
+ UPDATE ${sql.raw(TASK)} SET status = ${patch.status},
529
+ job_id = COALESCE(${patch.jobId ?? null}::uuid, job_id),
530
+ input_key = COALESCE(${patch.inputKey ?? null}, input_key),
531
+ result_id = COALESCE(${patch.resultId ?? null}::uuid, result_id),
532
+ error = COALESCE(${patch.error === undefined ? null : JSON.stringify(patch.error)}::jsonb, error)
533
+ WHERE id = ${t.id}::uuid`)
534
+ t.status = patch.status
535
+ if (patch.jobId !== undefined) t.jobId = patch.jobId
536
+ if (patch.inputKey !== undefined) t.inputKey = patch.inputKey
537
+ if (patch.resultId !== undefined) t.resultId = patch.resultId
538
+ }
539
+
540
+ /**
541
+ * Moves a run as far as its recorded results allow, in one pass over the steps in
542
+ * dependency order: expands fan-out steps whose list is now known, reuses recorded
543
+ * results, queues the rest, and marks the run done when nothing is left. The run
544
+ * row is locked first, so concurrent finishes advance it one at a time.
545
+ */
546
+ async function advance(tx: DB, runId: string): Promise<Outcome & { run: RunRow | null }> {
547
+ const outcome: Outcome = { reused: 0, queued: 0, completed: false }
548
+ const run = await lockRun(tx, runId)
549
+ if (run === null || run.status !== 'running') return { ...outcome, run }
550
+ const state = await loadState(tx, run)
551
+ const closure = closureOf(run.targets)
552
+ const signal = new AbortController().signal
553
+
554
+ const process = async (task: Task, def: GraphStepDefinition, fanOut: boolean): Promise<void> => {
555
+ const g = state.gather(def, task)
556
+ const key = contentKey(task.step, task.element, fanOut, without(def, g.hashes))
557
+ const recorded = await findRecorded(tx, run.tenant, run.item, task.step, key, task.instance)
558
+ if (recorded !== null) {
559
+ state.results.set(recorded.id, { value: recorded.value, hash: recorded.hash })
560
+ await setTask(tx, task, { status: 'reused', inputKey: key, resultId: recorded.id })
561
+ stageReused(task.step, task.instance, recorded.id)
562
+ outcome.reused++
563
+ return
564
+ }
565
+ if (def.person === true) {
566
+ const accepted = def.autoAccept === undefined ? undefined : await def.autoAccept(stepContext(state, def, task, fanOut, signal, {
567
+ checkpoint: (fn) => fn(tx),
568
+ withResult: () => {
569
+ throw new Error(`[pipework] ${graph}: step "${task.step}" is a person step; withResult is for steps that run (a person step records through decide).\n`)
570
+ },
571
+ }))
572
+ if (accepted !== undefined) {
573
+ const rec = await recordResult(tx, run, task.step, key, task.instance, toJson(accepted), false)
574
+ state.results.set(rec.id, { value: rec.value, hash: rec.hash })
575
+ await setTask(tx, task, { status: 'done', inputKey: key, resultId: rec.id })
576
+ } else {
577
+ await setTask(tx, task, { status: 'waiting', inputKey: key })
578
+ }
579
+ return
580
+ }
581
+ const jobId = await queue.enqueue(tx, {
582
+ type: typeOfClass(classOf(task.step)),
583
+ payload: { kind: 'step', runId: run.id, taskId: task.id },
584
+ priority: stepJobPriority(run, def, task.step),
585
+ ...(def.maxAttempts !== undefined ? { maxAttempts: def.maxAttempts } : {}),
586
+ tenant: run.tenant,
587
+ parent: run.parent,
588
+ actor: run.actor,
589
+ })
590
+ await setTask(tx, task, { status: 'queued', inputKey: key, jobId })
591
+ outcome.queued++
592
+ }
593
+
594
+ for (const name of closure) {
595
+ if (state.complete(name)) continue
596
+ const def = steps[name]!
597
+ const by = def.byElement
598
+ const ready = (def.needs ?? []).every(n => steps[n] === undefined
599
+ || (by?.includes(n) === true ? state.single(n)?.status === 'done' : state.complete(n)))
600
+ if (!ready) continue
601
+ const expander = state.single(name)
602
+ if (expander === undefined) continue
603
+
604
+ if (!isFanOut(name)) {
605
+ if (expander.status === 'blocked') await process(expander, def, false)
606
+ continue
607
+ }
608
+
609
+ const expand = async (elements: readonly unknown[]): Promise<void> => {
610
+ if (elements.length > 0) {
611
+ const rows = await tx.execute(sql`
612
+ INSERT INTO ${sql.raw(TASK)} (tenant_id, run_id, step, instance, element, status)
613
+ VALUES ${sql.join(elements.map((el, i) =>
614
+ sql`(${run.tenant}::uuid, ${run.id}::uuid, ${name}, ${i}, ${JSON.stringify(el)}::jsonb, 'blocked')`), sql`, `)}
615
+ RETURNING id::text, instance`)
616
+ for (const r of rows) {
617
+ const i = r['instance'] as number
618
+ state.tasks.push({
619
+ id: r['id'] as string, step: name, instance: i, status: 'blocked', jobId: null, inputKey: null, resultId: null, element: elements[i],
620
+ })
621
+ }
622
+ }
623
+ await setTask(tx, expander, { status: 'done' })
624
+ }
625
+
626
+ if (expander.status === 'blocked') {
627
+ if (by !== undefined) {
628
+ await expand(state.instances(by[0]!).map(t => t.element))
629
+ } else {
630
+ const g = state.gather(def)
631
+ const forEachCtx: GraphForEachContext = { needs: g.values, item: run.item, inputs: g.inputs, runId: run.id }
632
+ const list = await def.forEach!(forEachCtx)
633
+ if (!Array.isArray(list)) {
634
+ throw new Error(`[pipework] ${graph}: forEach of step "${name}" must return an array, got ${typeof list}.\n`)
635
+ }
636
+ await expand(list.map(toJson))
637
+ }
638
+ }
639
+ for (const t of state.instances(name)) {
640
+ if (t.status !== 'blocked') continue
641
+ if (by !== undefined && !by.every(n => state.good(state.instanceOf(n, t.instance)))) continue
642
+ await process(t, def, true)
643
+ }
644
+ }
645
+
646
+ if (closure.every(n => state.complete(n))) {
647
+ const done = await tx.execute(sql`
648
+ UPDATE ${sql.raw(RUN)} SET status = 'done', finished_at = now(), notified = ${config.onDone === undefined}
649
+ WHERE id = ${run.id}::uuid AND status = 'running' RETURNING id`)
650
+ if (done.length > 0) {
651
+ outcome.completed = true
652
+ await tx.execute(sql`SELECT pg_notify('pipework_step_run', ${run.id})`)
653
+ }
654
+ }
655
+ return { ...outcome, run }
656
+ }
657
+
658
+ /** Marks the run's done callback delivered and calls it; false when it already was. */
659
+ async function deliverDone(tx: DB, runId: string): Promise<void> {
660
+ const rows = await tx.execute(sql`
661
+ UPDATE ${sql.raw(RUN)} SET notified = true
662
+ WHERE id = ${runId}::uuid AND status = 'done' AND notified = false
663
+ RETURNING item, tenant_id::text AS tenant`)
664
+ const row = rows[0]
665
+ if (row === undefined || config.onDone === undefined) return
666
+ await config.onDone({ runId, item: row['item'] as string, tenant: row['tenant'] as string })
667
+ }
668
+
669
+ /** After an advance outside a step job: a finished run's callback is delivered by a job, which has a Flow to run it in. */
670
+ async function scheduleDone(tx: DB, run: RunRow | null, outcome: Outcome): Promise<void> {
671
+ if (run === null || !outcome.completed || config.onDone === undefined) return
672
+ await queue.enqueue(tx, {
673
+ type: typeOfClass(classOf(run.targets[0]!)), payload: { kind: 'finish', runId: run.id, taskId: run.id }, priority: run.priority, tenant: run.tenant, parent: run.parent, actor: run.actor,
674
+ })
675
+ }
676
+
677
+ async function request(db: DB, opts: RequestOptions): Promise<RequestResult> {
678
+ const closure = checkRequest(opts, 'request')
679
+ const tenant = tenantOf(opts.tenant ?? handleTenant(db), 'request')
680
+ const priority = opts.priority ?? config.priority ?? 0
681
+ const stepPriority = opts.stepPriority === undefined || Object.keys(opts.stepPriority).length === 0 ? null : opts.stepPriority
682
+ const parent = parentTraceRef()
683
+ const flow = getContext()
684
+ const actor = opts.carryActor === false || flow === undefined ? null : resolveActorId(flow)
685
+ const inputs = toJson(opts.inputs ?? {})
686
+
687
+ return inTenantTx(db, tenant, async (tx) => {
688
+ const created = await tx.execute(sql`
689
+ INSERT INTO ${sql.raw(RUN)} (tenant_id, graph, item, targets, priority, step_priority, inputs, trace_id, trace_root_id, trace_step_id, actor)
690
+ VALUES (${tenant}::uuid, ${graph}, ${opts.item}, ${JSON.stringify(opts.targets)}::jsonb, ${priority}, ${stepPriority === null ? null : JSON.stringify(stepPriority)}::jsonb, ${JSON.stringify(inputs)}::jsonb,
691
+ ${parent?.id ?? null}::uuid, ${parent?.rootId ?? null}::uuid, ${parent?.stepId ?? null}::uuid, ${actor}::text)
692
+ RETURNING id::text`)
693
+ const runId = created[0]!['id'] as string
694
+ await tx.execute(sql`
695
+ INSERT INTO ${sql.raw(TASK)} (tenant_id, run_id, step, instance, status)
696
+ VALUES ${sql.join(closure.map(step => sql`(${tenant}::uuid, ${runId}::uuid, ${step}, ${SINGLE}, 'blocked')`), sql`, `)}`)
697
+ const outcome = await advance(tx, runId)
698
+ await scheduleDone(tx, outcome.run, outcome)
699
+ return { runId, reused: outcome.reused, queued: outcome.queued }
700
+ })
701
+ }
702
+
703
+ async function plan(db: DB, opts: PlanOptions): Promise<PlanResult> {
704
+ const closure = checkRequest(opts, 'plan')
705
+ const tenant = tenantOf(opts.tenant ?? handleTenant(db), 'plan')
706
+ const inputs = toJson(opts.inputs ?? {}) as Record<string, unknown>
707
+ const hashes = inputHashesOf(inputs)
708
+
709
+ return inTenantTx(db, tenant, async (tx) => {
710
+ // What each step would hold if the run were made now: its list and, per instance, the recorded hash and value (null where none is).
711
+ interface Known { elements: unknown[] | null; hashes: Array<string | null>; values: unknown[] }
712
+ const known = new Map<string, Known>()
713
+ const full = (k: Known | undefined): k is Known & { elements: unknown[] } =>
714
+ k !== undefined && k.elements !== null && k.hashes.every(h => h !== null)
715
+ const result: Record<string, StepPlan> = {}
716
+ for (const name of closure) {
717
+ const def = steps[name]!
718
+ const fanOut = isFanOut(name)
719
+ const needs = def.needs ?? []
720
+ const by = def.byElement
721
+ const unknownList = (): void => { result[name] = { reusable: 0, toRun: fanOut ? null : 1 } }
722
+ const stepNeeds = needs.filter(n => steps[n] !== undefined && by?.includes(n) !== true)
723
+ if (!stepNeeds.every(n => full(known.get(n)))) { unknownList(); continue }
724
+ if (by !== undefined && known.get(by[0]!)?.elements == null) { unknownList(); continue }
725
+
726
+ const needValues: Record<string, unknown> = {}
727
+ const needHashes: Record<string, string | string[]> = {}
728
+ const given: Record<string, unknown> = {}
729
+ for (const n of needs) {
730
+ if (inputNames.has(n)) { needValues[n] = inputs[n]; given[n] = inputs[n]; needHashes[n] = hashes[n]! } else if (by?.includes(n) !== true) {
731
+ const k = known.get(n)!
732
+ needValues[n] = isFanOut(n) ? k.values : k.values[0]
733
+ needHashes[n] = isFanOut(n) ? k.hashes as string[] : k.hashes[0]!
734
+ }
735
+ }
736
+ const elements: unknown[] = by !== undefined
737
+ ? known.get(by[0]!)!.elements!
738
+ : fanOut ? (await def.forEach!({ needs: needValues, item: opts.item, inputs: given, runId: '' })).map(toJson) : [undefined]
739
+ let reusable = 0
740
+ const values: unknown[] = []
741
+ const valueHashes: Array<string | null> = []
742
+ for (const [i, element] of elements.entries()) {
743
+ let own = needHashes
744
+ let checkable = true
745
+ if (by !== undefined) {
746
+ own = { ...needHashes }
747
+ for (const n of by) {
748
+ const h = known.get(n)?.hashes[i] ?? null
749
+ if (h === null) checkable = false
750
+ else own[n] = h
751
+ }
752
+ }
753
+ const rec = checkable
754
+ ? await findRecorded(tx, tenant, opts.item, name, contentKey(name, element, fanOut, without(def, own)), fanOut ? i : SINGLE)
755
+ : null
756
+ if (rec !== null) reusable++
757
+ values.push(rec?.value)
758
+ valueHashes.push(rec?.hash ?? null)
759
+ }
760
+ result[name] = { reusable, toRun: elements.length - reusable }
761
+ known.set(name, { elements, hashes: valueHashes, values })
762
+ }
763
+ return { steps: result }
764
+ })
765
+ }
766
+
767
+ async function status(db: DB, runId: string, opts?: ScopeOptions): Promise<RunStatus> {
768
+ return inTenantTx(db, scopeOf(opts?.tenant), tx => statusIn(tx, runId))
769
+ }
770
+
771
+ function stateOf(status: TaskStatus, jobStatus: string | null): TaskState {
772
+ if (status === 'done' || status === 'reused' || status === 'failed' || status === 'cancelled') return status
773
+ return status === 'queued' && (jobStatus === 'claimed' || jobStatus === 'running') ? 'running' : 'waiting'
774
+ }
775
+
776
+ async function statusIn(db: DB, runId: string): Promise<RunStatus> {
777
+ const runs = await db.execute(sql`SELECT status, inputs, finished_at FROM ${sql.raw(RUN)} WHERE id = ${runId}::uuid AND graph = ${graph}`)
778
+ if (runs[0] === undefined) throw new Error(`[pipework] ${graph}.status: no run "${runId}".\n`)
779
+ const tasks = await db.execute(sql`
780
+ SELECT t.step, t.instance, t.status, t.element, j.status AS job_status
781
+ FROM ${sql.raw(TASK)} t LEFT JOIN ${sql.raw(quoted(queueTable()))} j ON j.id = t.job_id
782
+ WHERE t.run_id = ${runId}::uuid`)
783
+ const zero = (): { total: number; done: number; reused: number; running: number; waiting: number; failed: number; cancelled: number } =>
784
+ ({ total: 0, done: 0, reused: 0, running: 0, waiting: 0, failed: 0, cancelled: 0 })
785
+ const counts = new Map<string, ReturnType<typeof zero>>()
786
+ for (const t of tasks) {
787
+ const step = t['step'] as string
788
+ const fanOut = steps[step] !== undefined && isFanOut(step)
789
+ const c = counts.get(step) ?? zero()
790
+ counts.set(step, c)
791
+ if (fanOut && (t['instance'] as number) < 0) continue
792
+ c.total++
793
+ c[stateOf(t['status'] as TaskStatus, t['job_status'] as string | null)]++
794
+ }
795
+ const listDone = new Set<string>()
796
+ for (const t of tasks) if ((t['instance'] as number) < 0 && t['status'] === 'done') listDone.add(t['step'] as string)
797
+ const out: Record<string, StepCounts> = {}
798
+ const list: RunTask[] = []
799
+ for (const name of order) {
800
+ const c = counts.get(name)
801
+ if (c === undefined) continue
802
+ out[name] = steps[name] !== undefined && isFanOut(name) ? { ...c, listKnown: listDone.has(name) } : c
803
+ const mine = tasks.filter(t => t['step'] === name).sort((a, b) => (a['instance'] as number) - (b['instance'] as number))
804
+ for (const t of mine) {
805
+ list.push({
806
+ step: name, instance: t['instance'] as number, element: t['element'] ?? null,
807
+ state: stateOf(t['status'] as TaskStatus, t['job_status'] as string | null),
808
+ })
809
+ }
810
+ }
811
+ const finished = runs[0]['finished_at'] as string | Date | null
812
+ const raw = runs[0]['inputs']
813
+ const inputs = (typeof raw === 'string' ? JSON.parse(raw) : raw ?? {}) as Record<string, unknown>
814
+ return {
815
+ status: runs[0]['status'] as GraphRunStatus, steps: out, tasks: list, inputs,
816
+ finishedAt: finished === null ? null : new Date(finished),
817
+ }
818
+ }
819
+
820
+ async function ended(db: DB, opts?: EndedRunsOptions): Promise<readonly EndedRun[]> {
821
+ const tenant = scopedExecutor(db, scopeOf(opts?.tenant), `${graph}.ended`).tenant
822
+ const limit = opts?.limit ?? 1000
823
+ if (!Number.isInteger(limit) || limit < 1) throw new Error(`[pipework] ${graph}.ended: limit must be a positive integer.\n`)
824
+ const only = tenant === null ? sql`` : sql` AND tenant_id = ${tenant}::uuid`
825
+ const byIds = opts?.ids === undefined ? sql`` : opts.ids.length === 0 ? sql` AND false`
826
+ : sql` AND id ${sql.inList(opts.ids, 'uuid')}`
827
+ const before = opts?.finishedBefore === undefined ? sql`` : sql` AND finished_at < ${opts.finishedBefore.toISOString()}::timestamptz`
828
+ const rows = await inTenantTx(db, tenant, tx => tx.execute(sql`
829
+ SELECT id::text, tenant_id::text, item, status, finished_at, notified FROM ${sql.raw(RUN)}
830
+ WHERE graph = ${graph} AND status <> 'running' AND finished_at IS NOT NULL${only}${byIds}${before}
831
+ ORDER BY finished_at, id LIMIT ${limit}`))
832
+ return rows.map(r => ({
833
+ id: r['id'] as string,
834
+ tenant: r['tenant_id'] as string,
835
+ item: r['item'] as string,
836
+ status: r['status'] as EndedRun['status'],
837
+ finishedAt: new Date(r['finished_at'] as string | Date),
838
+ notified: r['notified'] as boolean,
839
+ }))
840
+ }
841
+
842
+ /** The queue's table name, read from its definition. */
843
+ function queueTable(): string {
844
+ return (queue.definition as unknown as { $name: string }).$name
845
+ }
846
+ const quoted = (name: string): string => `"${name.replace(/"/g, '""')}"`
847
+
848
+ async function cancelRun(tx: DB, runId: string): Promise<void> {
849
+ const run = await lockRun(tx, runId)
850
+ if (run === null || run.status !== 'running') return
851
+ await tx.execute(sql`UPDATE ${sql.raw(RUN)} SET status = 'cancelled', finished_at = now() WHERE id = ${runId}::uuid`)
852
+ const live = await tx.execute(sql`
853
+ UPDATE ${sql.raw(TASK)} SET status = 'cancelled'
854
+ WHERE run_id = ${runId}::uuid AND status IN ('blocked', 'queued', 'waiting') RETURNING job_id::text`)
855
+ await cancelPending(tx, live.flatMap(r => r['job_id'] === null ? [] : [r['job_id'] as string]),
856
+ { reason: 'run-cancelled', message: `${graph} run ${runId} was cancelled` })
857
+ await tx.execute(sql`SELECT pg_notify('pipework_step_run', ${runId})`)
858
+ for (const c of running.get(runId) ?? []) c.abort(new Error(`[pipework] ${graph} run ${runId} was cancelled.`))
859
+ }
860
+
861
+ async function cancel(db: DB, runId: string, opts?: ScopeOptions): Promise<void> {
862
+ await inTenantTx(db, scopeOf(opts?.tenant), async (tx) => { await cancelRun(tx, runId) })
863
+ }
864
+
865
+ async function decide(db: DB, opts: DecideOptions): Promise<void> {
866
+ const def = steps[opts.step]
867
+ if (def === undefined || def.person !== true) {
868
+ throw new Error(`[pipework] ${graph}.decide: "${opts.step}" is not a person step.\n`)
869
+ }
870
+ const instance = opts.index ?? SINGLE
871
+ const value = toJson(opts.value)
872
+ await inTenantTx(db, scopeOf(opts.tenant), async (tx) => {
873
+ const run = await lockRun(tx, opts.runId)
874
+ if (run === null) throw new Error(`[pipework] ${graph}.decide: no run "${opts.runId}".\n`)
875
+ const rows = await tx.execute(sql`
876
+ SELECT id::text, status, input_key, result_id::text AS result_id FROM ${sql.raw(TASK)}
877
+ WHERE run_id = ${run.id}::uuid AND step = ${opts.step} AND instance = ${instance} FOR UPDATE`)
878
+ const task = rows[0]
879
+ if (task === undefined) {
880
+ throw new Error(`[pipework] ${graph}.decide: run ${opts.runId} has no task for step "${opts.step}"${opts.index === undefined ? '' : ` instance ${String(opts.index)}`}.\n`)
881
+ }
882
+ const taskStatus = task['status'] as TaskStatus
883
+ if (taskStatus === 'waiting') {
884
+ const rec = await recordResult(tx, run, opts.step, task['input_key'] as string, instance, value, true)
885
+ await tx.execute(sql`UPDATE ${sql.raw(TASK)} SET status = 'done', result_id = ${rec.id}::uuid WHERE id = ${task['id'] as string}::uuid`)
886
+ const outcome = await advance(tx, run.id)
887
+ await scheduleDone(tx, outcome.run, outcome)
888
+ return
889
+ }
890
+ if (taskStatus === 'done' || taskStatus === 'reused') {
891
+ await tx.execute(sql`
892
+ UPDATE ${sql.raw(RESULT)} SET value = ${JSON.stringify(value)}::jsonb, value_hash = ${valueHash(value)}, created_at = now()
893
+ WHERE id = ${task['result_id'] as string}::uuid`)
894
+ return
895
+ }
896
+ throw new Error(`[pipework] ${graph}.decide: step "${opts.step}" of run ${opts.runId} is ${taskStatus}, not waiting for a decision.\n`)
897
+ })
898
+ }
899
+
900
+ async function wait(db: DB, runId: string, opts: { readonly timeoutMs: number; readonly tenant?: string }): Promise<GraphRunStatus | 'timeout'> {
901
+ const deadline = Date.now() + opts.timeoutMs
902
+ for (;;) {
903
+ const rows = await inTenantTx(db, scopeOf(opts.tenant), tx =>
904
+ tx.execute(sql`SELECT status FROM ${sql.raw(RUN)} WHERE id = ${runId}::uuid AND graph = ${graph}`))
905
+ if (rows[0] === undefined) throw new Error(`[pipework] ${graph}.wait: no run "${runId}".\n`)
906
+ const s = rows[0]['status'] as GraphRunStatus
907
+ if (s !== 'running') return s
908
+ if (Date.now() >= deadline) return 'timeout'
909
+ await new Promise(r => setTimeout(r, Math.min(50, Math.max(1, deadline - Date.now()))))
910
+ }
911
+ }
912
+
913
+ async function sweep(db: DB, opts?: ScopeOptions): Promise<number> {
914
+ if (config.resultRetentionDays === undefined) return 0
915
+ const cutoff = new Date(Date.now() - config.resultRetentionDays * MS_PER_DAY).toISOString()
916
+ const tenant = scopedExecutor(db, scopeOf(opts?.tenant), `${graph}.sweep`).tenant
917
+ const only = tenant === null ? sql`` : sql` AND tenant_id = ${tenant}::uuid`
918
+ return inTenantTx(db, tenant, async (tx) => {
919
+ const old = await tx.execute(sql`
920
+ SELECT id::text FROM ${sql.raw(RUN)}
921
+ WHERE graph = ${graph} AND status <> 'running' AND finished_at < ${cutoff}::timestamptz${only}`)
922
+ if (old.length > 0) {
923
+ const ids = sql.inList(old.map(r => r['id'] as string), 'uuid')
924
+ await tx.execute(sql`DELETE FROM ${sql.raw(TASK)} WHERE run_id ${ids}`)
925
+ await tx.execute(sql`DELETE FROM ${sql.raw(RUN)} WHERE id ${ids}`)
926
+ }
927
+ const rows = await tx.execute(sql`
928
+ DELETE FROM ${sql.raw(RESULT)} WHERE graph = ${graph} AND created_at < ${cutoff}::timestamptz${only} RETURNING id`)
929
+ return rows.length
930
+ })
931
+ }
932
+
933
+ async function removeTenant(db: DB, tenantId: string): Promise<void> {
934
+ if (typeof tenantId !== 'string' || tenantId.trim() === '') {
935
+ throw new Error(`[pipework] ${graph}.removeTenant: tenantId must be a non-empty string.\n`)
936
+ }
937
+ await inTenantTx(db, tenantId, async (tx) => {
938
+ await tx.execute(sql`DELETE FROM ${sql.raw(TASK)} WHERE tenant_id = ${tenantId}::uuid`)
939
+ await tx.execute(sql`DELETE FROM ${sql.raw(RUN)} WHERE tenant_id = ${tenantId}::uuid`)
940
+ await tx.execute(sql`DELETE FROM ${sql.raw(RESULT)} WHERE tenant_id = ${tenantId}::uuid`)
941
+ })
942
+ }
943
+
944
+ function itemScope(method: string, item: string, explicit?: string): string {
945
+ if (typeof item !== 'string' || item.trim() === '') {
946
+ throw new Error(`[pipework] ${graph}.${method}: item must be a non-empty string.\n`)
947
+ }
948
+ const tenant = scopeOf(explicit)
949
+ if (tenant === null) {
950
+ throw new Error(`[pipework] ${graph}.${method}: no tenant — pass options.tenant or call inside a Flow with one.\n`)
951
+ }
952
+ return tenant
953
+ }
954
+
955
+ async function results(db: DB, item: string, opts?: ScopeOptions): Promise<readonly StepResultRecord[]> {
956
+ const tenant = itemScope('results', item, opts?.tenant ?? handleTenant(db))
957
+ return inTenantTx(db, tenant, async (tx) => {
958
+ const rows = await tx.execute(sql`
959
+ SELECT id::text, step, version, instance, value, created_at FROM ${sql.raw(RESULT)}
960
+ WHERE graph = ${graph} AND item = ${item} AND tenant_id = ${tenant}::uuid
961
+ ORDER BY step, instance, created_at`)
962
+ return rows.map(r => ({
963
+ id: r['id'] as string,
964
+ step: r['step'] as string,
965
+ version: Number(r['version']),
966
+ instance: Number(r['instance']),
967
+ value: r['value'],
968
+ createdAt: new Date(r['created_at'] as string | Date),
969
+ }))
970
+ })
971
+ }
972
+
973
+ async function removeItem(db: DB, item: string, opts?: RemoveItemOptions): Promise<RemoveItemResult> {
974
+ const tenant = itemScope('removeItem', item, opts?.tenant ?? handleTenant(db))
975
+ return inTenantTx(db, tenant, async (tx) => {
976
+ const runs = await tx.execute(sql`
977
+ SELECT id::text, status FROM ${sql.raw(RUN)}
978
+ WHERE graph = ${graph} AND item = ${item} AND tenant_id = ${tenant}::uuid ORDER BY id FOR UPDATE`)
979
+ const live = runs.filter(r => r['status'] === 'running').map(r => r['id'] as string)
980
+ if (live.length > 0) {
981
+ if (opts?.running !== 'cancel') throw new StepItemRunningError(graph, item, live)
982
+ for (const id of live) await cancelRun(tx, id)
983
+ }
984
+ let tasks = 0
985
+ if (runs.length > 0) {
986
+ const ids = sql.inList(runs.map(r => r['id'] as string), 'uuid')
987
+ tasks = (await tx.execute(sql`DELETE FROM ${sql.raw(TASK)} WHERE run_id ${ids} RETURNING id`)).length
988
+ await tx.execute(sql`DELETE FROM ${sql.raw(RUN)} WHERE id ${ids}`)
989
+ }
990
+ const gone = await tx.execute(sql`
991
+ DELETE FROM ${sql.raw(RESULT)} WHERE graph = ${graph} AND item = ${item} AND tenant_id = ${tenant}::uuid RETURNING id`)
992
+ return { runs: runs.length, tasks, results: gone.length }
993
+ })
994
+ }
995
+
996
+ type Checkpoint = <R>(fn: (tx: DB) => Promise<R>) => Promise<R>
997
+
998
+ /**
999
+ * The step job's attempt and limit, from the Flow the worker surface built; a job run
1000
+ * outside it has neither, so they are read from the job's row.
1001
+ */
1002
+ async function triesOf(checkpoint: Checkpoint, taskId: string): Promise<{ attempt: number; maxAttempts: number }> {
1003
+ const flow = getContext()
1004
+ if (flow?.jobAttempt != null && flow.jobMaxAttempts != null) return { attempt: flow.jobAttempt, maxAttempts: flow.jobMaxAttempts }
1005
+ return checkpoint(async (tx) => {
1006
+ const rows = await tx.execute(sql`
1007
+ SELECT j.attempt_count, j.max_attempts FROM ${sql.raw(TASK)} t JOIN ${sql.raw(quoted(queueTable()))} j ON j.id = t.job_id
1008
+ WHERE t.id = ${taskId}::uuid`)
1009
+ const row = rows[0]
1010
+ return { attempt: ((row?.['attempt_count'] as number | undefined) ?? 0) + 1, maxAttempts: (row?.['max_attempts'] as number | undefined) ?? 1 }
1011
+ })
1012
+ }
1013
+
1014
+ /**
1015
+ * The step job. The job holds no transaction while `run` waits: one short checkpoint
1016
+ * checks the task and reads the run's state, `run` executes outside any transaction,
1017
+ * and a second checkpoint records the result (with whatever `withResult` callbacks
1018
+ * asked to write) and advances the run.
1019
+ */
1020
+ async function handleJob(checkpoint: Checkpoint, payload: { kind: string; runId: string; taskId: string }): Promise<unknown> {
1021
+ if (payload.kind === 'finish') {
1022
+ await checkpoint((tx) => deliverDone(tx, payload.runId))
1023
+ return { finished: payload.runId }
1024
+ }
1025
+
1026
+ const loaded = await checkpoint(async (tx) => {
1027
+ const live = await tx.execute(sql`
1028
+ SELECT 1 FROM ${sql.raw(TASK)} t JOIN ${sql.raw(RUN)} r ON r.id = t.run_id
1029
+ WHERE t.id = ${payload.taskId}::uuid AND t.run_id = ${payload.runId}::uuid AND t.status = 'queued' AND r.status = 'running'`)
1030
+ if (live.length === 0) return null
1031
+
1032
+ // Read the run's state without the lock: the step may take a long time, and other steps of the run must proceed meanwhile.
1033
+ const runRows = await tx.execute(sql`
1034
+ SELECT id::text, tenant_id::text AS tenant, item, status, priority, step_priority, targets, inputs, actor FROM ${sql.raw(RUN)} WHERE id = ${payload.runId}::uuid`)
1035
+ const r = runRows[0]!
1036
+ const run: RunRow = {
1037
+ id: r['id'] as string, tenant: r['tenant'] as string, item: r['item'] as string, status: r['status'] as GraphRunStatus,
1038
+ priority: r['priority'] as number, stepPriority: r['step_priority'] as Record<string, number> | null, targets: r['targets'] as string[], inputs: r['inputs'] as Record<string, unknown>, parent: null, actor: r['actor'] as string | null,
1039
+ }
1040
+ return { run, state: await loadState(tx, run) }
1041
+ })
1042
+ if (loaded === null) return { skipped: payload.taskId }
1043
+ const { run, state } = loaded
1044
+ const task = state.tasks.find(t => t.id === payload.taskId)!
1045
+ const def = steps[task.step]!
1046
+ const fanOut = isFanOut(task.step)
1047
+
1048
+ const tries = await triesOf(checkpoint, payload.taskId)
1049
+ const resultWrites: Array<(tx: DB) => Promise<void>> = []
1050
+ const controller = new AbortController()
1051
+ let controllers = running.get(run.id)
1052
+ if (controllers === undefined) { controllers = new Set(); running.set(run.id, controllers) }
1053
+ controllers.add(controller)
1054
+ let value: unknown
1055
+ let timer: NodeJS.Timeout | undefined
1056
+ const startedAt = new Date()
1057
+ try {
1058
+ const work = Promise.resolve(def.run!({
1059
+ ...stepContext(state, def, task, fanOut, controller.signal, {
1060
+ checkpoint,
1061
+ withResult: (fn) => { resultWrites.push(fn) },
1062
+ }),
1063
+ ...tries,
1064
+ }))
1065
+ if (def.timeoutMs === undefined) {
1066
+ value = toJson(await work)
1067
+ } else {
1068
+ const limit = def.timeoutMs
1069
+ const expired = new Promise<never>((_, reject) => {
1070
+ timer = setTimeout(() => {
1071
+ const timeout = new StepTimeoutError(graph, task.step, limit)
1072
+ controller.abort(timeout)
1073
+ reject(timeout)
1074
+ }, limit)
1075
+ timer.unref()
1076
+ })
1077
+ work.catch(() => {})
1078
+ try {
1079
+ value = toJson(await Promise.race([work, expired]))
1080
+ } catch (err) {
1081
+ if (err instanceof StepTimeoutError) stageTimeout(task.step, task.instance, limit, startedAt, err)
1082
+ throw err
1083
+ }
1084
+ }
1085
+ } finally {
1086
+ if (timer !== undefined) clearTimeout(timer)
1087
+ controllers.delete(controller)
1088
+ if (controllers.size === 0) running.delete(run.id)
1089
+ }
1090
+
1091
+ return checkpoint(async (tx) => {
1092
+ const locked = await lockRun(tx, run.id)
1093
+ if (locked === null) return { skipped: payload.taskId }
1094
+ const current = await tx.execute(sql`SELECT status FROM ${sql.raw(TASK)} WHERE id = ${task.id}::uuid FOR UPDATE`)
1095
+ for (const write of resultWrites) await write(tx)
1096
+ const rec = await recordResult(tx, locked, task.step, task.inputKey!, task.instance, value, false)
1097
+ if (current[0]?.['status'] !== 'queued' || locked.status !== 'running') {
1098
+ // Cancelled or failed while it ran: the result is kept for later requests, the run does not move.
1099
+ return { recorded: rec.id }
1100
+ }
1101
+ await tx.execute(sql`UPDATE ${sql.raw(TASK)} SET status = 'done', result_id = ${rec.id}::uuid WHERE id = ${task.id}::uuid`)
1102
+ const outcome = await advance(tx, run.id)
1103
+ if (outcome.completed) await deliverDone(tx, run.id)
1104
+ return { step: task.step, resultId: rec.id }
1105
+ })
1106
+ }
1107
+
1108
+ function handlerFor(type: string, database: string | undefined): Handler<Record<string, unknown>, unknown> {
1109
+ const builder = database === undefined ? fitting.use() : fitting.use(database)
1110
+ const key = database ?? 'db'
1111
+ return builder.job(type)
1112
+ .transaction('manual')
1113
+ .input(schema.check.object({ kind: schema.check.string(), runId: schema.check.string(), taskId: schema.check.string() }))
1114
+ .fit(async ({ input, checkpoint }: {
1115
+ input: { kind: string; runId: string; taskId: string }
1116
+ checkpoint: <R>(fn: (dbs: Record<string, DB>) => Promise<R>) => Promise<R>
1117
+ }) => {
1118
+ const tenant = getContext()?.tenant ?? null
1119
+ const atTenant: Checkpoint = (fn) => checkpoint((dbs) => {
1120
+ const tx = dbs[key]!
1121
+ return asTenant(tx, tenant, () => fn(tx))
1122
+ })
1123
+ return handleJob(atTenant, input)
1124
+ }) as unknown as Handler<Record<string, unknown>, unknown>
1125
+ }
1126
+
1127
+ function handlers(database: string | undefined, selected?: readonly string[]): Handler<Record<string, unknown>, unknown>[] {
1128
+ return (selected ?? classes).map(cls => handlerFor(typeOfClass(cls), database))
1129
+ }
1130
+
1131
+ function workers(...wanted: string[]): GraphSelection {
1132
+ if (wanted.length === 0) fail(graph, 'workers() needs at least one class.')
1133
+ for (const cls of wanted) {
1134
+ if (typeof cls !== 'string' || !CLASS_RE.test(cls)) {
1135
+ fail(graph, `worker class "${String(cls)}" must start with a letter and use letters, digits, "_" or "-".`)
1136
+ }
1137
+ if (!classes.includes(cls)) fail(graph, `no step uses worker class "${cls}" (classes: ${classes.join(', ')}).`)
1138
+ }
1139
+ return { graph: self, classes: [...new Set(wanted)] }
1140
+ }
1141
+
1142
+ /** A step job of this graph dead-lettered: fail the task, what depends on it and the run, cancel the rest, tell the app once. */
1143
+ async function onDeadLetter(tx: DB, job: DeadLetterInfo, host: GraphHost): Promise<void> {
1144
+ if (!jobTypes.includes(job.type)) return
1145
+ const payload = job.payload as { kind?: string; runId?: string; taskId?: string }
1146
+ if (payload.kind !== 'step' || payload.runId === undefined || payload.taskId === undefined) return
1147
+ attached = host.instance
1148
+ await asTenant(tx, job.tenant, () => failRun(tx, job, payload.runId!, payload.taskId!, host), host.instance)
1149
+ }
1150
+
1151
+ /** The graph's `onFailed` inside its trace step; the step captures the failure as its input. */
1152
+ const onFailedStep = __flowStep({ ref: 'pipework/steps:on-failed', capture: [(failure) => failure] }, async (failure: GraphFailure) => { await config.onFailed!(failure) })
1153
+
1154
+ async function failRun(tx: DB, job: DeadLetterInfo, runId: string, taskId: string, host: GraphHost): Promise<void> {
1155
+ const run = await lockRun(tx, runId)
1156
+ if (run === null || run.status !== 'running') return
1157
+ const tasks = await loadTasks(tx, run.id)
1158
+ const dead = tasks.find(t => t.id === taskId)
1159
+ if (dead === undefined || dead.status !== 'queued') return
1160
+
1161
+ const dependents = dependentsOf(dead.step)
1162
+ await setTask(tx, dead, { status: 'failed', error: job.error })
1163
+ const cancelJobs: string[] = []
1164
+ for (const t of tasks) {
1165
+ if (t === dead || (t.status !== 'blocked' && t.status !== 'queued' && t.status !== 'waiting')) continue
1166
+ if (t.jobId !== null) cancelJobs.push(t.jobId)
1167
+ await setTask(tx, t, { status: dependents.has(t.step) ? 'failed' : 'cancelled' })
1168
+ }
1169
+ await cancelPending(tx, cancelJobs, { reason: 'run-failed', message: `${graph} run ${runId} failed at step ${dead.step}` })
1170
+ await tx.execute(sql`
1171
+ UPDATE ${sql.raw(RUN)} SET status = 'failed', finished_at = now(), notified = true,
1172
+ error = ${JSON.stringify({ step: dead.step, error: job.error })}::jsonb
1173
+ WHERE id = ${run.id}::uuid`)
1174
+ await tx.execute(sql`SELECT pg_notify('pipework_step_run', ${run.id})`)
1175
+
1176
+ if (config.onFailed === undefined) return
1177
+ const failure = { runId: run.id, item: run.item, tenant: run.tenant, step: dead.step, error: job.error }
1178
+ try {
1179
+ await tx.transaction(async (savepoint) => {
1180
+ const dbName = host.database ?? host.instance.config.defaultDatabaseName()
1181
+ const trace = isTracingEnabled(host.instance) ? openTrace({
1182
+ instance: host.instance,
1183
+ entryKind: 'job',
1184
+ parent: job.flow.traceId !== null && job.flow.rootId !== null
1185
+ ? { id: job.flow.traceId, rootId: job.flow.rootId, stepId: job.flow.stepId }
1186
+ : null,
1187
+ tenant: run.tenant,
1188
+ auth: run.actor !== null ? { userId: run.actor } : null,
1189
+ requestId: job.id,
1190
+ sessionId: null,
1191
+ }) : null
1192
+ const base = createJobContext({
1193
+ instance: host.instance, jobId: job.id, jobType: job.type, tenant: run.tenant, auth: run.actor !== null ? { userId: run.actor } : null, ...(trace !== null ? { trace } : {}),
1194
+ })
1195
+ const ctx = { ...base, transactions: new Map([[dbName, bindTransaction(base, dbName, savepoint)]]) }
1196
+ await propagateLocals(savepoint, ctx, null)
1197
+ await finalizeTrace(host.instance, trace, runInContext(ctx, async () => { await onFailedStep(failure) }))
1198
+ })
1199
+ } catch (err) {
1200
+ getBaseLogger().error({ err, runId: run.id, graph }, 'Step graph onFailed callback failed; the run is already marked failed')
1201
+ void recordFaultFor(host.instance, 'step-graph-on-failed', faultMessage(err), { runId: run.id, graph })
1202
+ }
1203
+ }
1204
+
1205
+ const self: StepGraph = {
1206
+ name: graph,
1207
+ jobType,
1208
+ jobTypes,
1209
+ queue,
1210
+ request, plan, status, ended, cancel, decide, wait, sweep, removeTenant, results, removeItem, workers,
1211
+ worker: { handlers, onDeadLetter },
1212
+ }
1213
+ return self
1214
+ }