pipework 0.252.0 → 0.253.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (695) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/infra/support/handle.d.ts +8 -1
  3. package/dist/infra/support/handle.d.ts.map +1 -1
  4. package/dist/infra/support/handle.js +9 -2
  5. package/dist/infra/support/handle.js.map +1 -1
  6. package/dist/infra/support/namespace.d.ts.map +1 -1
  7. package/package.json +4 -3
  8. package/src/async/jobs/abortable-delay.ts +18 -0
  9. package/src/async/jobs/claims.ts +52 -0
  10. package/src/async/jobs/cron.ts +179 -0
  11. package/src/async/jobs/errors.ts +56 -0
  12. package/src/async/jobs/exclusion.ts +117 -0
  13. package/src/async/jobs/execute.ts +219 -0
  14. package/src/async/jobs/index.ts +10 -0
  15. package/src/async/jobs/listener.ts +135 -0
  16. package/src/async/jobs/namespace.ts +20 -0
  17. package/src/async/jobs/queue-door.ts +44 -0
  18. package/src/async/jobs/queue.ts +1012 -0
  19. package/src/async/jobs/run-script.ts +67 -0
  20. package/src/async/jobs/scheduler.ts +218 -0
  21. package/src/async/jobs/schedules-table.ts +37 -0
  22. package/src/async/jobs/serialize-error.ts +48 -0
  23. package/src/async/jobs/table.ts +191 -0
  24. package/src/async/pipeline/define.ts +53 -0
  25. package/src/async/pipeline/errors.ts +35 -0
  26. package/src/async/pipeline/execute.ts +53 -0
  27. package/src/async/pipeline/index.ts +13 -0
  28. package/src/async/pipeline/namespace.ts +12 -0
  29. package/src/async/pipeline/resume.ts +67 -0
  30. package/src/async/pipeline/run-steps.ts +98 -0
  31. package/src/async/pipeline/types.ts +57 -0
  32. package/src/async/state-machine/define.ts +64 -0
  33. package/src/async/state-machine/errors.ts +29 -0
  34. package/src/async/state-machine/index.ts +3 -0
  35. package/src/async/state-machine/namespace.ts +12 -0
  36. package/src/async/state-machine/transition.ts +90 -0
  37. package/src/async/state-machine/types.ts +37 -0
  38. package/src/async/steps/errors.ts +31 -0
  39. package/src/async/steps/graph.ts +1214 -0
  40. package/src/async/steps/hash.ts +40 -0
  41. package/src/async/steps/index.ts +7 -0
  42. package/src/async/steps/namespace.ts +7 -0
  43. package/src/async/steps/table.ts +97 -0
  44. package/src/async/steps/types.ts +318 -0
  45. package/src/audit/chain/index.ts +9 -0
  46. package/src/audit/chain/install.ts +113 -0
  47. package/src/audit/chain/retire.ts +52 -0
  48. package/src/audit/chain/verify.ts +257 -0
  49. package/src/audit/query/cursor.ts +37 -0
  50. package/src/audit/query/index.ts +9 -0
  51. package/src/audit/query/query.ts +223 -0
  52. package/src/audit/schema/audit-record-fields.ts +68 -0
  53. package/src/audit/schema/audit-record.ts +23 -0
  54. package/src/auth/auth/audit.ts +31 -0
  55. package/src/auth/auth/chain.ts +84 -0
  56. package/src/auth/auth/cookie.ts +136 -0
  57. package/src/auth/auth/email-links.ts +311 -0
  58. package/src/auth/auth/guard.ts +128 -0
  59. package/src/auth/auth/index.ts +18 -0
  60. package/src/auth/auth/multi-org.ts +337 -0
  61. package/src/auth/auth/namespace.ts +34 -0
  62. package/src/auth/auth/parse-expiry.ts +13 -0
  63. package/src/auth/auth/passwords.ts +110 -0
  64. package/src/auth/auth/schema.ts +110 -0
  65. package/src/auth/auth/sessions.ts +565 -0
  66. package/src/auth/auth/sign-in.ts +156 -0
  67. package/src/auth/auth/signing-keys.ts +69 -0
  68. package/src/auth/auth/types.ts +26 -0
  69. package/src/auth/auth/uuid.ts +10 -0
  70. package/src/auth/rbac/cache.ts +55 -0
  71. package/src/auth/rbac/check.ts +30 -0
  72. package/src/auth/rbac/create.ts +35 -0
  73. package/src/auth/rbac/index.ts +5 -0
  74. package/src/auth/rbac/middleware.ts +20 -0
  75. package/src/auth/rbac/namespace.ts +15 -0
  76. package/src/auth/rbac/resolve.ts +31 -0
  77. package/src/auth/rbac/schema.sql +31 -0
  78. package/src/auth/rbac/seed.ts +67 -0
  79. package/src/auth/rbac/tables.ts +32 -0
  80. package/src/auth/rbac/types.ts +65 -0
  81. package/src/auth/tenant/extract.ts +33 -0
  82. package/src/auth/tenant/in-tenant.ts +49 -0
  83. package/src/auth/tenant/index.ts +7 -0
  84. package/src/auth/tenant/namespace.ts +23 -0
  85. package/src/auth/tenant/propagate.ts +20 -0
  86. package/src/auth/tenant/rls.ts +120 -0
  87. package/src/auth/tenant/scope.ts +33 -0
  88. package/src/auth/tenant/scoped-db.ts +581 -0
  89. package/src/auth/tenant/types.ts +14 -0
  90. package/src/auth/tenant/validate.ts +27 -0
  91. package/src/cli/commands/add.ts +158 -0
  92. package/src/cli/commands/alerts.ts +98 -0
  93. package/src/cli/commands/check.ts +1418 -0
  94. package/src/cli/commands/codemod.ts +78 -0
  95. package/src/cli/commands/db.ts +356 -0
  96. package/src/cli/commands/dev.ts +101 -0
  97. package/src/cli/commands/generate.ts +75 -0
  98. package/src/cli/commands/init.ts +36 -0
  99. package/src/cli/commands/install.ts +28 -0
  100. package/src/cli/commands/lint.ts +62 -0
  101. package/src/cli/commands/migrate.ts +132 -0
  102. package/src/cli/commands/openapi.ts +63 -0
  103. package/src/cli/commands/remove.ts +72 -0
  104. package/src/cli/commands/run.ts +50 -0
  105. package/src/cli/commands/scope.ts +54 -0
  106. package/src/cli/commands/serve.ts +68 -0
  107. package/src/cli/commands/shard.ts +31 -0
  108. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  109. package/src/cli/commands/shared/find-config.ts +22 -0
  110. package/src/cli/commands/shared/load-config.ts +35 -0
  111. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  112. package/src/cli/commands/shared/report-removed.ts +8 -0
  113. package/src/cli/commands/sync.ts +32 -0
  114. package/src/cli/commands/test.ts +307 -0
  115. package/src/cli/commands/upgrade.ts +48 -0
  116. package/src/cli/dev-bootstrap.ts +24 -0
  117. package/src/cli/errors.ts +10 -0
  118. package/src/cli/index.ts +303 -0
  119. package/src/cli/output.ts +17 -0
  120. package/src/cli/port.ts +15 -0
  121. package/src/cli/surface-kinds.ts +20 -0
  122. package/src/client/index.ts +4 -0
  123. package/src/client/poll.ts +130 -0
  124. package/src/client/report-fault.ts +119 -0
  125. package/src/core/config/app-pool.ts +19 -0
  126. package/src/core/config/aws-env.ts +29 -0
  127. package/src/core/config/base-dir.ts +24 -0
  128. package/src/core/config/discover.ts +64 -0
  129. package/src/core/config/env-resolve.ts +87 -0
  130. package/src/core/config/env-types.ts +116 -0
  131. package/src/core/config/env.ts +55 -0
  132. package/src/core/config/errors.ts +9 -0
  133. package/src/core/config/import-config.ts +40 -0
  134. package/src/core/config/index.ts +8 -0
  135. package/src/core/config/load.ts +417 -0
  136. package/src/core/config/namespace.ts +14 -0
  137. package/src/core/config/resolve.ts +195 -0
  138. package/src/core/config/schema.ts +488 -0
  139. package/src/core/config/statement-observer.ts +48 -0
  140. package/src/core/config/ts-inline-hooks.ts +75 -0
  141. package/src/core/config/ts-register.ts +4 -0
  142. package/src/core/config/ts-resolve-hooks.ts +101 -0
  143. package/src/core/errors/base.ts +49 -0
  144. package/src/core/errors/http.ts +146 -0
  145. package/src/core/errors/index.ts +2 -0
  146. package/src/core/errors/namespace.ts +31 -0
  147. package/src/core/lifecycle/drain.ts +33 -0
  148. package/src/core/lifecycle/health.ts +72 -0
  149. package/src/core/lifecycle/index.ts +4 -0
  150. package/src/core/lifecycle/namespace.ts +7 -0
  151. package/src/core/lifecycle/startup.ts +102 -0
  152. package/src/core/lifecycle/types.ts +36 -0
  153. package/src/core/logging/context.ts +60 -0
  154. package/src/core/logging/error-chain.ts +68 -0
  155. package/src/core/logging/index.ts +5 -0
  156. package/src/core/logging/logger.ts +65 -0
  157. package/src/core/logging/namespace.ts +16 -0
  158. package/src/core/logging/proxy.ts +22 -0
  159. package/src/core/logging/redact-match.ts +24 -0
  160. package/src/core/logging/types.ts +47 -0
  161. package/src/core/pipework.ts +566 -0
  162. package/src/core/runtime/broker.ts +69 -0
  163. package/src/core/surface/http.ts +133 -0
  164. package/src/core/surface/index.ts +11 -0
  165. package/src/core/surface/job-timeout.ts +40 -0
  166. package/src/core/surface/namespace.ts +13 -0
  167. package/src/core/surface/script.ts +33 -0
  168. package/src/core/surface/start.ts +142 -0
  169. package/src/core/surface/types.ts +225 -0
  170. package/src/core/surface/validate.ts +88 -0
  171. package/src/core/surface/worker.ts +528 -0
  172. package/src/core/validation/connectivity.ts +49 -0
  173. package/src/core/validation/domain.ts +99 -0
  174. package/src/core/validation/index.ts +5 -0
  175. package/src/core/validation/queue-tables.ts +312 -0
  176. package/src/core/validation/rls-role.ts +138 -0
  177. package/src/core/validation/startup.ts +51 -0
  178. package/src/core/validation/surfaces.ts +65 -0
  179. package/src/core/validation/types.ts +25 -0
  180. package/src/data/db/advisory-lock.ts +55 -0
  181. package/src/data/db/aggregates.ts +172 -0
  182. package/src/data/db/array-elements.ts +111 -0
  183. package/src/data/db/bulk-set.ts +53 -0
  184. package/src/data/db/col/index.ts +14 -0
  185. package/src/data/db/composed.ts +53 -0
  186. package/src/data/db/db.ts +276 -0
  187. package/src/data/db/excluded.ts +16 -0
  188. package/src/data/db/expressions.ts +473 -0
  189. package/src/data/db/filter.ts +90 -0
  190. package/src/data/db/function-source.ts +114 -0
  191. package/src/data/db/identifiers.ts +16 -0
  192. package/src/data/db/idx/index.ts +9 -0
  193. package/src/data/db/in-list.ts +49 -0
  194. package/src/data/db/index.ts +9 -0
  195. package/src/data/db/insert-split.ts +137 -0
  196. package/src/data/db/instrumentation.ts +82 -0
  197. package/src/data/db/maintenance.ts +28 -0
  198. package/src/data/db/namespace.ts +107 -0
  199. package/src/data/db/owner-labels.ts +9 -0
  200. package/src/data/db/pool.ts +187 -0
  201. package/src/data/db/query-label.ts +13 -0
  202. package/src/data/db/query-watch.ts +116 -0
  203. package/src/data/db/raw-client-warning.ts +37 -0
  204. package/src/data/db/readonly.ts +23 -0
  205. package/src/data/db/reservation.ts +151 -0
  206. package/src/data/db/schema-features.ts +14 -0
  207. package/src/data/db/serializable.ts +83 -0
  208. package/src/data/db/sql.ts +1 -0
  209. package/src/data/db/timeouts.ts +76 -0
  210. package/src/data/db/types.ts +18 -0
  211. package/src/data/db/unbound.ts +33 -0
  212. package/src/data/db/values.ts +29 -0
  213. package/src/data/domain/brand.ts +15 -0
  214. package/src/data/domain/build-factory.ts +193 -0
  215. package/src/data/domain/build-schema.ts +161 -0
  216. package/src/data/domain/build-table.ts +355 -0
  217. package/src/data/domain/define.ts +239 -0
  218. package/src/data/domain/external-registry.ts +34 -0
  219. package/src/data/domain/external.ts +77 -0
  220. package/src/data/domain/field.ts +279 -0
  221. package/src/data/domain/index.ts +13 -0
  222. package/src/data/domain/naming.ts +6 -0
  223. package/src/data/domain/projections.ts +57 -0
  224. package/src/data/domain/trace-safe-projection.ts +31 -0
  225. package/src/data/domain/traits.ts +108 -0
  226. package/src/data/domain/types.ts +289 -0
  227. package/src/data/jsonb/composed.ts +18 -0
  228. package/src/data/jsonb/index.ts +1 -0
  229. package/src/data/jsonb/namespace.ts +30 -0
  230. package/src/data/jsonb/query.ts +86 -0
  231. package/src/data/migrate/append-only-generate.ts +47 -0
  232. package/src/data/migrate/applied.ts +47 -0
  233. package/src/data/migrate/apply.ts +309 -0
  234. package/src/data/migrate/check.ts +119 -0
  235. package/src/data/migrate/diff.ts +357 -0
  236. package/src/data/migrate/door-generate.ts +130 -0
  237. package/src/data/migrate/framework-rls.ts +58 -0
  238. package/src/data/migrate/generate.ts +201 -0
  239. package/src/data/migrate/history.ts +215 -0
  240. package/src/data/migrate/index.ts +9 -0
  241. package/src/data/migrate/internal-definitions.ts +175 -0
  242. package/src/data/migrate/post-process.ts +73 -0
  243. package/src/data/migrate/pragma.ts +73 -0
  244. package/src/data/migrate/provision-framework.ts +147 -0
  245. package/src/data/migrate/rls-generate.ts +61 -0
  246. package/src/data/migrate/run.ts +124 -0
  247. package/src/data/migrate/safety.ts +167 -0
  248. package/src/data/migrate/snapshot.ts +368 -0
  249. package/src/data/migrate/sql-emit.ts +392 -0
  250. package/src/data/migrate/statements.ts +312 -0
  251. package/src/data/query/alias.ts +126 -0
  252. package/src/data/query/batch.ts +8 -0
  253. package/src/data/query/cache/core/cache.ts +78 -0
  254. package/src/data/query/cache/core/index.ts +1 -0
  255. package/src/data/query/cache/core/types.ts +29 -0
  256. package/src/data/query/cache/readme.md +230 -0
  257. package/src/data/query/casing.ts +76 -0
  258. package/src/data/query/collation.ts +11 -0
  259. package/src/data/query/column-builder.ts +370 -0
  260. package/src/data/query/column.ts +150 -0
  261. package/src/data/query/entity.ts +42 -0
  262. package/src/data/query/errors.ts +33 -0
  263. package/src/data/query/index.ts +14 -0
  264. package/src/data/query/logger.ts +47 -0
  265. package/src/data/query/operations.ts +45 -0
  266. package/src/data/query/pg-core/alias.ts +12 -0
  267. package/src/data/query/pg-core/checks.ts +32 -0
  268. package/src/data/query/pg-core/columns/all.ts +71 -0
  269. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  270. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  271. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  272. package/src/data/query/pg-core/columns/char.ts +85 -0
  273. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  274. package/src/data/query/pg-core/columns/common.ts +366 -0
  275. package/src/data/query/pg-core/columns/custom.ts +232 -0
  276. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  277. package/src/data/query/pg-core/columns/date.ts +112 -0
  278. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  279. package/src/data/query/pg-core/columns/enum.ts +201 -0
  280. package/src/data/query/pg-core/columns/index.ts +35 -0
  281. package/src/data/query/pg-core/columns/inet.ts +43 -0
  282. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  283. package/src/data/query/pg-core/columns/integer.ts +53 -0
  284. package/src/data/query/pg-core/columns/interval.ts +83 -0
  285. package/src/data/query/pg-core/columns/json.ts +58 -0
  286. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  287. package/src/data/query/pg-core/columns/line.ts +117 -0
  288. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  289. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  290. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  291. package/src/data/query/pg-core/columns/point.ts +127 -0
  292. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  293. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  294. package/src/data/query/pg-core/columns/real.ts +58 -0
  295. package/src/data/query/pg-core/columns/serial.ts +55 -0
  296. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  297. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  298. package/src/data/query/pg-core/columns/text.ts +67 -0
  299. package/src/data/query/pg-core/columns/time.ts +72 -0
  300. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  301. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  302. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  303. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  304. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  305. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  306. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  307. package/src/data/query/pg-core/db.ts +674 -0
  308. package/src/data/query/pg-core/dialect.ts +1404 -0
  309. package/src/data/query/pg-core/expressions.ts +25 -0
  310. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  311. package/src/data/query/pg-core/index.ts +21 -0
  312. package/src/data/query/pg-core/indexes.ts +276 -0
  313. package/src/data/query/pg-core/policies.ts +56 -0
  314. package/src/data/query/pg-core/primary-keys.ts +62 -0
  315. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  316. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  317. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  318. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  319. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  320. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  321. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  322. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  323. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  324. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  325. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  326. package/src/data/query/pg-core/roles.ts +41 -0
  327. package/src/data/query/pg-core/schema.ts +72 -0
  328. package/src/data/query/pg-core/sequence.ts +41 -0
  329. package/src/data/query/pg-core/session.ts +292 -0
  330. package/src/data/query/pg-core/subquery.ts +46 -0
  331. package/src/data/query/pg-core/table.ts +252 -0
  332. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  333. package/src/data/query/pg-core/utils/array.ts +95 -0
  334. package/src/data/query/pg-core/utils/index.ts +1 -0
  335. package/src/data/query/pg-core/utils.ts +104 -0
  336. package/src/data/query/pg-core/view-base.ts +14 -0
  337. package/src/data/query/pg-core/view-common.ts +1 -0
  338. package/src/data/query/pg-core/view.ts +435 -0
  339. package/src/data/query/pg-core/with-builder.ts +41 -0
  340. package/src/data/query/postgres-js/README.md +46 -0
  341. package/src/data/query/postgres-js/array-parser.ts +45 -0
  342. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  343. package/src/data/query/postgres-js/driver.ts +163 -0
  344. package/src/data/query/postgres-js/index.ts +2 -0
  345. package/src/data/query/postgres-js/session.ts +234 -0
  346. package/src/data/query/primary-key.ts +11 -0
  347. package/src/data/query/query-builders/query-builder.ts +19 -0
  348. package/src/data/query/query-builders/select.types.ts +175 -0
  349. package/src/data/query/query-promise.ts +35 -0
  350. package/src/data/query/relations.ts +725 -0
  351. package/src/data/query/runnable-query.ts +12 -0
  352. package/src/data/query/selection-proxy.ts +121 -0
  353. package/src/data/query/session.ts +8 -0
  354. package/src/data/query/sql/expressions/conditions.ts +741 -0
  355. package/src/data/query/sql/expressions/index.ts +2 -0
  356. package/src/data/query/sql/expressions/select.ts +45 -0
  357. package/src/data/query/sql/functions/aggregate.ts +129 -0
  358. package/src/data/query/sql/functions/index.ts +2 -0
  359. package/src/data/query/sql/functions/vector.ts +178 -0
  360. package/src/data/query/sql/index.ts +3 -0
  361. package/src/data/query/sql/sql.ts +794 -0
  362. package/src/data/query/subquery.ts +53 -0
  363. package/src/data/query/table.ts +208 -0
  364. package/src/data/query/table.utils.ts +2 -0
  365. package/src/data/query/tracing-utils.ts +3 -0
  366. package/src/data/query/tracing.ts +11 -0
  367. package/src/data/query/utils.ts +357 -0
  368. package/src/data/query/version.ts +4 -0
  369. package/src/data/query/view-common.ts +1 -0
  370. package/src/data/schema/branded.ts +13 -0
  371. package/src/data/schema/generated/coerce.ts +5 -0
  372. package/src/data/schema/generated/combinator.ts +6 -0
  373. package/src/data/schema/generated/encoding.ts +5 -0
  374. package/src/data/schema/generated/error.ts +6 -0
  375. package/src/data/schema/generated/format.ts +5 -0
  376. package/src/data/schema/generated/type-constructor.ts +10 -0
  377. package/src/data/schema/generated/type.ts +6 -0
  378. package/src/data/schema/index.ts +6 -0
  379. package/src/data/schema/json-schema.ts +33 -0
  380. package/src/data/schema/namespace.ts +55 -0
  381. package/src/data/schema/parse.ts +39 -0
  382. package/src/data/schema/types.ts +39 -0
  383. package/src/data/temporal/append-only.ts +134 -0
  384. package/src/data/temporal/close.ts +27 -0
  385. package/src/data/temporal/columns.ts +10 -0
  386. package/src/data/temporal/definition-queries.ts +70 -0
  387. package/src/data/temporal/errors.ts +10 -0
  388. package/src/data/temporal/get-current.ts +22 -0
  389. package/src/data/temporal/index.ts +9 -0
  390. package/src/data/temporal/namespace.ts +36 -0
  391. package/src/data/temporal/query.ts +25 -0
  392. package/src/data/temporal/resolve.ts +17 -0
  393. package/src/data/temporal/revise.ts +67 -0
  394. package/src/data/temporal/types.ts +22 -0
  395. package/src/data/valve/artifact.ts +33 -0
  396. package/src/data/valve/contract-diff.ts +21 -0
  397. package/src/data/valve/cross.ts +57 -0
  398. package/src/data/valve/crossing-ledger.ts +50 -0
  399. package/src/data/valve/decision.ts +44 -0
  400. package/src/data/valve/durable-crossing.ts +52 -0
  401. package/src/data/valve/execute-crossing.ts +77 -0
  402. package/src/data/valve/faucet.ts +56 -0
  403. package/src/data/valve/gate.ts +114 -0
  404. package/src/data/valve/graph.ts +77 -0
  405. package/src/data/valve/index.ts +53 -0
  406. package/src/data/valve/namespace.ts +57 -0
  407. package/src/data/valve/registry.ts +50 -0
  408. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  409. package/src/data/valve/schema/valve-crossing.ts +9 -0
  410. package/src/data/valve/supply.ts +154 -0
  411. package/src/data/valve/types.ts +143 -0
  412. package/src/data/vector/columns.ts +27 -0
  413. package/src/data/vector/index.ts +4 -0
  414. package/src/data/vector/namespace.ts +33 -0
  415. package/src/data/vector/query.ts +85 -0
  416. package/src/data/vector/validate.ts +16 -0
  417. package/src/data/write/audit/actor.ts +15 -0
  418. package/src/data/write/audit/audit-columns.ts +18 -0
  419. package/src/data/write/audit/audit-select.ts +111 -0
  420. package/src/data/write/audit/bound-transaction.ts +14 -0
  421. package/src/data/write/audit/cte-compile.ts +32 -0
  422. package/src/data/write/audit/decode-returning.ts +47 -0
  423. package/src/data/write/audit/final-select.ts +43 -0
  424. package/src/data/write/audit/infra-audit.ts +36 -0
  425. package/src/data/write/audit/primary-key.ts +65 -0
  426. package/src/data/write/audit/projection.ts +41 -0
  427. package/src/data/write/audit/read-touches.ts +86 -0
  428. package/src/data/write/audit/touch-match.ts +78 -0
  429. package/src/data/write/audit/verb-delete.ts +62 -0
  430. package/src/data/write/audit/verb-insert.ts +36 -0
  431. package/src/data/write/audit/verb-select.ts +47 -0
  432. package/src/data/write/audit/verb-update.ts +60 -0
  433. package/src/data/write/audit/verb-upsert.ts +104 -0
  434. package/src/data/write/conflict.ts +43 -0
  435. package/src/data/write/delete.ts +38 -0
  436. package/src/data/write/execute.ts +243 -0
  437. package/src/data/write/index.ts +11 -0
  438. package/src/data/write/insert.ts +63 -0
  439. package/src/data/write/state.ts +43 -0
  440. package/src/data/write/update.ts +54 -0
  441. package/src/data/write/upsert.ts +58 -0
  442. package/src/data/write/values.ts +19 -0
  443. package/src/index.ts +230 -0
  444. package/src/infra/access-links/code.ts +70 -0
  445. package/src/infra/access-links/handle.ts +892 -0
  446. package/src/infra/access-links/index.ts +9 -0
  447. package/src/infra/access-links/namespace.ts +30 -0
  448. package/src/infra/access-links/person-check.ts +177 -0
  449. package/src/infra/access-links/schema.ts +71 -0
  450. package/src/infra/alerts/async-sources.ts +231 -0
  451. package/src/infra/alerts/audit.ts +36 -0
  452. package/src/infra/alerts/beat.ts +123 -0
  453. package/src/infra/alerts/client-faults.ts +111 -0
  454. package/src/infra/alerts/faults.ts +156 -0
  455. package/src/infra/alerts/handle.ts +778 -0
  456. package/src/infra/alerts/http.ts +192 -0
  457. package/src/infra/alerts/index.ts +12 -0
  458. package/src/infra/alerts/infra-sources.ts +180 -0
  459. package/src/infra/alerts/namespace.ts +81 -0
  460. package/src/infra/alerts/redact.ts +21 -0
  461. package/src/infra/alerts/request-faults.ts +41 -0
  462. package/src/infra/alerts/schema.ts +124 -0
  463. package/src/infra/alerts/sinks.ts +125 -0
  464. package/src/infra/alerts/source-options.ts +26 -0
  465. package/src/infra/alerts/sources.ts +503 -0
  466. package/src/infra/alerts/subsystem-sources.ts +137 -0
  467. package/src/infra/audit/emit.ts +87 -0
  468. package/src/infra/audit/index.ts +2 -0
  469. package/src/infra/audit/namespace.ts +22 -0
  470. package/src/infra/audit/schema.sql +35 -0
  471. package/src/infra/audit/types.ts +18 -0
  472. package/src/infra/aws/credentials.ts +147 -0
  473. package/src/infra/aws/sigv4.ts +140 -0
  474. package/src/infra/cache/create.ts +58 -0
  475. package/src/infra/cache/entry.ts +81 -0
  476. package/src/infra/cache/index.ts +4 -0
  477. package/src/infra/cache/namespace.ts +10 -0
  478. package/src/infra/cache/tenant.ts +93 -0
  479. package/src/infra/cache/types.ts +24 -0
  480. package/src/infra/crawl/audit.ts +35 -0
  481. package/src/infra/crawl/errors.ts +25 -0
  482. package/src/infra/crawl/handle.ts +693 -0
  483. package/src/infra/crawl/index.ts +9 -0
  484. package/src/infra/crawl/links.ts +160 -0
  485. package/src/infra/crawl/namespace.ts +36 -0
  486. package/src/infra/crawl/robots.ts +100 -0
  487. package/src/infra/crawl/schema.ts +89 -0
  488. package/src/infra/credit-line/handle.ts +883 -0
  489. package/src/infra/credit-line/index.ts +6 -0
  490. package/src/infra/credit-line/namespace.ts +7 -0
  491. package/src/infra/credit-line/schema.ts +96 -0
  492. package/src/infra/credits/handle.ts +706 -0
  493. package/src/infra/credits/index.ts +5 -0
  494. package/src/infra/credits/namespace.ts +7 -0
  495. package/src/infra/credits/schema.ts +83 -0
  496. package/src/infra/email/audit.ts +36 -0
  497. package/src/infra/email/inbound.ts +622 -0
  498. package/src/infra/email/index.ts +11 -0
  499. package/src/infra/email/mail.ts +517 -0
  500. package/src/infra/email/mime.ts +447 -0
  501. package/src/infra/email/namespace.ts +22 -0
  502. package/src/infra/email/providers.ts +111 -0
  503. package/src/infra/email/schema.ts +96 -0
  504. package/src/infra/email/sns.ts +200 -0
  505. package/src/infra/email/unsubscribe.ts +44 -0
  506. package/src/infra/fetch/address.ts +88 -0
  507. package/src/infra/fetch/connect-retry.ts +87 -0
  508. package/src/infra/fetch/handle.ts +329 -0
  509. package/src/infra/fetch/index.ts +7 -0
  510. package/src/infra/fetch/namespace.ts +70 -0
  511. package/src/infra/idempotency/index.ts +3 -0
  512. package/src/infra/idempotency/middleware.ts +87 -0
  513. package/src/infra/idempotency/store.ts +92 -0
  514. package/src/infra/idempotency/types.ts +30 -0
  515. package/src/infra/join-requests/handle.ts +312 -0
  516. package/src/infra/join-requests/index.ts +2 -0
  517. package/src/infra/join-requests/namespace.ts +7 -0
  518. package/src/infra/join-requests/schema.ts +33 -0
  519. package/src/infra/outbox/core.ts +165 -0
  520. package/src/infra/quota/errors.ts +32 -0
  521. package/src/infra/quota/handle.ts +215 -0
  522. package/src/infra/quota/index.ts +5 -0
  523. package/src/infra/quota/namespace.ts +7 -0
  524. package/src/infra/quota/schema.ts +15 -0
  525. package/src/infra/storage/backend.ts +34 -0
  526. package/src/infra/storage/handle.ts +513 -0
  527. package/src/infra/storage/index.ts +9 -0
  528. package/src/infra/storage/local.ts +104 -0
  529. package/src/infra/storage/namespace.ts +13 -0
  530. package/src/infra/storage/s3.ts +257 -0
  531. package/src/infra/storage/schema.ts +24 -0
  532. package/src/infra/support/handle.ts +869 -0
  533. package/src/infra/support/http.ts +229 -0
  534. package/src/infra/support/index.ts +7 -0
  535. package/src/infra/support/namespace.ts +12 -0
  536. package/src/infra/support/schema.ts +96 -0
  537. package/src/infra/webhook/inbound.ts +45 -0
  538. package/src/infra/webhook/index.ts +5 -0
  539. package/src/infra/webhook/namespace.ts +24 -0
  540. package/src/infra/webhook/outbound.ts +315 -0
  541. package/src/infra/webhook/sign.ts +32 -0
  542. package/src/infra/webhook/verify.ts +82 -0
  543. package/src/invariants/assert.ts +26 -0
  544. package/src/invariants/index.ts +1 -0
  545. package/src/lint/config.ts +56 -0
  546. package/src/lint/doctrine.ts +163 -0
  547. package/src/lint/include.ts +97 -0
  548. package/src/lint/index.ts +3 -0
  549. package/src/lint/preset.ts +10 -0
  550. package/src/lint/resolver.ts +218 -0
  551. package/src/lint/rules/no-reexports.ts +46 -0
  552. package/src/request/behavior/audited.ts +85 -0
  553. package/src/request/behavior/cached.ts +62 -0
  554. package/src/request/behavior/compose.ts +28 -0
  555. package/src/request/behavior/index.ts +5 -0
  556. package/src/request/behavior/namespace.ts +16 -0
  557. package/src/request/behavior/types.ts +26 -0
  558. package/src/request/behavior/versioned.ts +54 -0
  559. package/src/request/context/create.ts +103 -0
  560. package/src/request/context/guard.ts +49 -0
  561. package/src/request/context/index.ts +4 -0
  562. package/src/request/context/run-in-transaction.ts +200 -0
  563. package/src/request/context/store.ts +28 -0
  564. package/src/request/context/types.ts +50 -0
  565. package/src/request/di/builder.ts +535 -0
  566. package/src/request/di/index.ts +8 -0
  567. package/src/request/di/resolve.ts +271 -0
  568. package/src/request/di/types.ts +262 -0
  569. package/src/request/fitting/namespace.ts +7 -0
  570. package/src/request/fixture/batch.ts +71 -0
  571. package/src/request/fixture/builder.ts +26 -0
  572. package/src/request/fixture/errors.ts +10 -0
  573. package/src/request/fixture/index.ts +8 -0
  574. package/src/request/fixture/namespace.ts +23 -0
  575. package/src/request/fixture/paginate.ts +44 -0
  576. package/src/request/fixture/to-handlers.ts +260 -0
  577. package/src/request/fixture/types.ts +79 -0
  578. package/src/request/flow/act-as.ts +78 -0
  579. package/src/request/flow/namespace.ts +17 -0
  580. package/src/request/http/error-step.ts +42 -0
  581. package/src/request/http/file.ts +141 -0
  582. package/src/request/http/index.ts +4 -0
  583. package/src/request/http/middleware.ts +141 -0
  584. package/src/request/http/namespace.ts +5 -0
  585. package/src/request/http/rate-limit-classes.ts +170 -0
  586. package/src/request/http/response.ts +82 -0
  587. package/src/request/http/server.ts +916 -0
  588. package/src/request/http/sniff.ts +261 -0
  589. package/src/request/http/types.ts +250 -0
  590. package/src/request/http/upload.ts +284 -0
  591. package/src/request/internal/text.ts +6 -0
  592. package/src/request/openapi/generate.ts +205 -0
  593. package/src/request/openapi/index.ts +2 -0
  594. package/src/request/openapi/namespace.ts +7 -0
  595. package/src/test/auth.ts +13 -0
  596. package/src/test/auto-setup.ts +45 -0
  597. package/src/test/context.ts +91 -0
  598. package/src/test/crawl-fixture.ts +157 -0
  599. package/src/test/discover-fork.ts +17 -0
  600. package/src/test/fingerprint.ts +135 -0
  601. package/src/test/globals.d.ts +7 -0
  602. package/src/test/index.ts +19 -0
  603. package/src/test/namespace.ts +87 -0
  604. package/src/test/plugin.ts +189 -0
  605. package/src/test/progress.ts +185 -0
  606. package/src/test/reap-after-run.ts +28 -0
  607. package/src/test/reap.ts +286 -0
  608. package/src/test/reset.ts +253 -0
  609. package/src/test/savepoints.ts +172 -0
  610. package/src/test/setup.ts +1422 -0
  611. package/src/test/system-channel.ts +50 -0
  612. package/src/test/vitest-plugin.ts +142 -0
  613. package/src/test/vitest-workspace.ts +507 -0
  614. package/src/test/vitest.ts +333 -0
  615. package/src/test/watch.ts +105 -0
  616. package/src/trace/capture/bounded-serializer.ts +146 -0
  617. package/src/trace/cardinality/analyze.ts +173 -0
  618. package/src/trace/cardinality/branch-context.ts +214 -0
  619. package/src/trace/cardinality/check.ts +33 -0
  620. package/src/trace/cardinality/classify-edge.ts +64 -0
  621. package/src/trace/cardinality/structural-max.ts +107 -0
  622. package/src/trace/check/coverage-check.ts +188 -0
  623. package/src/trace/check/flow-check.ts +103 -0
  624. package/src/trace/check/flow-step-site.ts +234 -0
  625. package/src/trace/check/io-pattern.ts +116 -0
  626. package/src/trace/codemod/add-cardinality.ts +152 -0
  627. package/src/trace/codemod/run.ts +80 -0
  628. package/src/trace/entry/child-trace.ts +21 -0
  629. package/src/trace/entry/close-trace.ts +87 -0
  630. package/src/trace/entry/finalize-trace.ts +28 -0
  631. package/src/trace/entry/open-trace.ts +138 -0
  632. package/src/trace/entry/parent-trace.ts +20 -0
  633. package/src/trace/entry/parse-traceparent.ts +39 -0
  634. package/src/trace/entry/process-start.ts +38 -0
  635. package/src/trace/entry/trace-context.ts +26 -0
  636. package/src/trace/entry/tracing-enabled.ts +10 -0
  637. package/src/trace/external/record.ts +195 -0
  638. package/src/trace/external/report.ts +66 -0
  639. package/src/trace/namespace.ts +19 -0
  640. package/src/trace/partition/day.ts +47 -0
  641. package/src/trace/partition/index.ts +14 -0
  642. package/src/trace/partition/partition-name.ts +23 -0
  643. package/src/trace/partition/plan.ts +82 -0
  644. package/src/trace/partition/schedule.ts +26 -0
  645. package/src/trace/partition/startup-hook.ts +39 -0
  646. package/src/trace/partition/sync.ts +154 -0
  647. package/src/trace/partition/tables.ts +53 -0
  648. package/src/trace/read/tree.ts +83 -0
  649. package/src/trace/retain/copy-on-error.ts +76 -0
  650. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  651. package/src/trace/runtime/flow-step.ts +147 -0
  652. package/src/trace/runtime/flush-steps.ts +31 -0
  653. package/src/trace/runtime/pending-writes.ts +54 -0
  654. package/src/trace/runtime/step-buffer.ts +298 -0
  655. package/src/trace/runtime/step-error.ts +21 -0
  656. package/src/trace/runtime/step-meta.ts +55 -0
  657. package/src/trace/runtime/thenable.ts +12 -0
  658. package/src/trace/runtime/truncate-trace.ts +62 -0
  659. package/src/trace/schema/trace-fields.ts +52 -0
  660. package/src/trace/schema/trace-retained.ts +22 -0
  661. package/src/trace/schema/trace-step-fields.ts +44 -0
  662. package/src/trace/schema/trace-step-retained.ts +25 -0
  663. package/src/trace/schema/trace-step.ts +18 -0
  664. package/src/trace/schema/trace.ts +12 -0
  665. package/src/trace/transformer/apply.ts +79 -0
  666. package/src/trace/transformer/build-meta-literal.ts +30 -0
  667. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  668. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  669. package/src/trace/transformer/transform-error.ts +14 -0
  670. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  671. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  672. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  673. package/src/workspace/affected.ts +110 -0
  674. package/src/workspace/changed-files.ts +163 -0
  675. package/src/workspace/check-deps.ts +213 -0
  676. package/src/workspace/checksum.ts +20 -0
  677. package/src/workspace/dependency-graph.ts +67 -0
  678. package/src/workspace/detect-pipework-copies.ts +93 -0
  679. package/src/workspace/discover.ts +206 -0
  680. package/src/workspace/generate.ts +401 -0
  681. package/src/workspace/index.ts +7 -0
  682. package/src/workspace/module-imports-pipework.ts +39 -0
  683. package/src/workspace/module-package-name.ts +12 -0
  684. package/src/workspace/scrub-git-env.ts +23 -0
  685. package/src/workspace/source-paths.ts +36 -0
  686. package/src/workspace/sync.ts +182 -0
  687. package/dist/REFERENCE.md +0 -1477
  688. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  689. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  690. package/dist/infra/access-links/rate-limit.js +0 -28
  691. package/dist/infra/access-links/rate-limit.js.map +0 -1
  692. package/dist/infra/email/sigv4.d.ts +0 -24
  693. package/dist/infra/email/sigv4.d.ts.map +0 -1
  694. package/dist/infra/email/sigv4.js +0 -55
  695. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,1012 @@
1
+ import { sql } from '../../data/db/sql.js'
2
+ import type { DB, SQL } from '../../data/db/index.js'
3
+ import { assertValidIdentifier, quoteIdentifier } from '../../data/db/identifiers.js'
4
+ import { serializable, isTransientConcurrencyError } from '../../data/db/serializable.js'
5
+ import type { DefinedTable } from '../../data/domain/types.js'
6
+ import { abortableDelay } from './abortable-delay.js'
7
+ import { JobListener } from './listener.js'
8
+ import type { ListenClient, WaitOptions, WaitResult } from './listener.js'
9
+ import { createScheduler } from './scheduler.js'
10
+ import type { ScheduleSpec, ScheduleInfo, SchedulerOptions } from './scheduler.js'
11
+ import { resolveExclusion, exclusionClaimClause } from './exclusion.js'
12
+ import type { ExclusionConfig } from './exclusion.js'
13
+ import type { JobLaterError } from './errors.js'
14
+ import { openClaim, closeClaim, parseClaims } from './claims.js'
15
+ import type { JobClaim, JobClaimReason } from './claims.js'
16
+ import { serializeError } from './serialize-error.js'
17
+ import { queueDefinition, registerProgressColumn, liveKeyIndexName, LIVE_KEY_TENANT_EXPR, LIVE_KEY_PREDICATE, pendingKeyIndexName, PENDING_KEY_TENANT_EXPR, PENDING_KEY_PREDICATE } from './table.js'
18
+ import { queueDoorName } from './queue-door.js'
19
+ import { unscopedExecutor, requireUnscopedExecutor } from '../../auth/tenant/scoped-db.js'
20
+ import { getContext } from '../../request/context/store.js'
21
+ import { resolveActorId } from '../../data/write/audit/actor.js'
22
+ import { getBaseLogger } from '../../core/logging/index.js'
23
+ import { recordFault, faultMessage } from '../../infra/alerts/faults.js'
24
+ import type { ParentTraceRef } from '../../trace/entry/open-trace.js'
25
+
26
+ export interface BackoffConfig {
27
+ readonly type: 'exponential' | 'fixed'
28
+ readonly delayMs: number
29
+ readonly maxDelayMs?: number
30
+ }
31
+
32
+ /** Queue configuration — table name, stale timeout, max attempts, backoff strategy, cross-job exclusion. */
33
+ export interface QueueConfig {
34
+ readonly table?: string
35
+ readonly staleTimeoutMs?: number
36
+ readonly notifyChannel?: string
37
+ readonly claimFilter?: (candidateAlias: string) => string
38
+ readonly maxAttempts?: number
39
+ readonly backoff?: BackoffConfig
40
+ /** An app-added column on the queue table whose value is recorded as each claim's `progressMark` when the claim is released. */
41
+ readonly progressColumn?: string
42
+ /** Cross-job exclusion gate — overlap/blocks-all/tenant columns checked against claimed/running jobs at claim time. */
43
+ readonly exclusion?: ExclusionConfig
44
+ }
45
+
46
+ /** Exclusion values for one job — mapped onto the configured exclusion columns. */
47
+ export interface EnqueueExclusion {
48
+ /** Conflict set for the configured arrayColumn; null/omitted = non-participating. */
49
+ readonly array?: readonly string[] | null
50
+ /** TRUE makes this job exclusive against every participating job in its scope. */
51
+ readonly blocksAll?: boolean
52
+ /** Tenant value for the configured tenantColumn — required when one is configured. */
53
+ readonly tenant?: string
54
+ }
55
+
56
+ /** Options for enqueuing a job — type, payload, priority, max attempts. */
57
+ export interface EnqueueOptions {
58
+ readonly type: string
59
+ readonly priority?: number
60
+ readonly payload: unknown
61
+ readonly maxAttempts?: number
62
+ readonly exclusion?: EnqueueExclusion
63
+ /**
64
+ * Record the enqueuing Flow's trace, open step and tenant so the worker runs
65
+ * the job as a child of that trace under that tenant. Default true; false
66
+ * records none of them (the job runs as a root trace with no tenant).
67
+ */
68
+ readonly continueFlow?: boolean
69
+ /** Run the job under this tenant instead of the Flow's; `null` runs it without a tenant. */
70
+ readonly tenant?: string | null
71
+ /** Also record the Flow's actor (`auth.userId`); the job's `auth` is `{ userId }` when set. Default false. */
72
+ readonly carryActor?: boolean
73
+ /** Record this actor instead of the Flow's (`null`: none); overrides `carryActor`. Ignored with `continueFlow: false`. */
74
+ readonly actor?: string | null
75
+ /**
76
+ * The trace (and open step) the job continues, in place of the Flow's own.
77
+ * `null` records no trace parent at all. Tenant and actor still follow `tenant` and `carryActor`.
78
+ */
79
+ readonly parent?: ParentTraceRef | null
80
+ /**
81
+ * While a job on this queue with the same key and the same tenant is live (pending, claimed or running),
82
+ * enqueue returns that job's id and inserts nothing — the payload and other options of this call are
83
+ * ignored. Once that job completes, fails for good or is cancelled, the next enqueue inserts a new job.
84
+ * A non-empty string.
85
+ */
86
+ readonly liveKey?: string
87
+ /**
88
+ * While a job on this queue with the same key and the same tenant is pending (not yet claimed), enqueue
89
+ * replaces that job's payload with this call's and returns its id; every other option of this call is
90
+ * ignored and the job keeps its place in line. Once the job is claimed, the next enqueue inserts a new job.
91
+ * A non-empty string; not combinable with `liveKey`.
92
+ */
93
+ readonly pendingKey?: string
94
+ }
95
+
96
+ /** A job that has just been dead-lettered — handed to every `onDeadLetter` listener inside the transaction that did it. */
97
+ export interface DeadLetterInfo {
98
+ readonly id: string
99
+ readonly type: string
100
+ readonly tenant: string | null
101
+ readonly payload: unknown
102
+ /** The serialized error of the last attempt. */
103
+ readonly error: unknown
104
+ /** The Flow that enqueued the job. */
105
+ readonly flow: ClaimedFlow
106
+ }
107
+
108
+ /** The enqueuing Flow a claimed job continues — every field null when none was recorded. */
109
+ export interface ClaimedFlow {
110
+ readonly traceId: string | null
111
+ readonly rootId: string | null
112
+ readonly stepId: string | null
113
+ readonly tenant: string | null
114
+ readonly actor: string | null
115
+ }
116
+
117
+ /** The exclusion values a claimed job carries — present when the queue configures exclusion. */
118
+ export interface ClaimedExclusion {
119
+ readonly array: readonly string[] | null
120
+ readonly blocksAll: boolean
121
+ readonly tenant: string | null
122
+ }
123
+
124
+ export type { JobClaim, JobClaimReason }
125
+
126
+ /** A job claimed by a worker — includes ID, type, payload, and created LSN. */
127
+ export interface ClaimedJob {
128
+ readonly id: string
129
+ readonly type: string
130
+ readonly priority: number
131
+ readonly payload: unknown
132
+ readonly createdLsn: string
133
+ /** Minted by this claim's own UPDATE; nulled when the claim ends. Compare it exactly to fence writes made under this claim. */
134
+ readonly claimToken: string
135
+ /** 1-based number of the attempt this claim starts. */
136
+ readonly attempt: number
137
+ readonly maxAttempts: number
138
+ /** Why the previous claim ended without a failure of its own — a reap or a hand-back — and the worker that held it; null otherwise. */
139
+ readonly previous: { readonly reason: 'reaped' | 'handed-back' | 'later'; readonly workerId: string | null } | null
140
+ readonly exclusion?: ClaimedExclusion
141
+ /** The Flow that enqueued this job; the worker surface restores its trace, tenant and actor. */
142
+ readonly flow: ClaimedFlow
143
+ /** The `liveKey` the job was enqueued with; null when none. */
144
+ readonly liveKey: string | null
145
+ /** The `pendingKey` the job was enqueued with; null when none. */
146
+ readonly pendingKey: string | null
147
+ /** The claims before this one, oldest first, each closed with its release time, reason and progress mark. */
148
+ readonly claims: readonly JobClaim[]
149
+ }
150
+
151
+ /** One job `reap` took from a lost worker. `attempt` is the 1-based number of the attempt that was lost. */
152
+ export interface ReapedJob {
153
+ readonly jobId: string
154
+ readonly type: string
155
+ readonly attempt: number
156
+ readonly outcome: 'retry' | 'dead_letter' | 'superseded'
157
+ readonly maxAttempts: number
158
+ readonly flow: ClaimedFlow
159
+ }
160
+
161
+ /** What `failAttempt` did: `outcome` and the error as stored in the row's `error_payload`. */
162
+ export interface FailedAttempt {
163
+ readonly outcome: 'retry' | 'dead_letter' | 'superseded'
164
+ readonly error: unknown
165
+ }
166
+
167
+ export interface JobResult {
168
+ readonly status: 'completed' | 'failed' | 'cancelled' | 'timeout' | 'dead_letter'
169
+ readonly output?: unknown
170
+ readonly error?: unknown
171
+ }
172
+
173
+ /** Jobs ready to claim, jobs claimed or running, and how long the oldest ready job has waited since it was queued (null when none is ready). */
174
+ export interface QueueTypeTotals {
175
+ readonly ready: number
176
+ readonly running: number
177
+ readonly oldestReadyWaitMs: number | null
178
+ }
179
+
180
+ /** The whole queue's totals; `byType` omits a type with nothing ready and nothing running. */
181
+ export interface QueueTotals {
182
+ readonly overall: QueueTypeTotals
183
+ readonly byType: Readonly<Record<string, QueueTypeTotals>>
184
+ }
185
+
186
+ /** Job queue instance with claim/complete/fail lifecycle, LISTEN/NOTIFY, and cron scheduling. */
187
+ export interface Queue {
188
+ /** The queue table as a pipe.define() definition — `pipework generate` emits its DDL. */
189
+ readonly definition: DefinedTable
190
+ /**
191
+ * The framework door (`pipework.queue.<table>`) that claim, reap, heartbeat and complete need on a platform-mode
192
+ * database; null when the queue has no `exclusion.tenantColumn`, whose table is not row-secured by tenant.
193
+ */
194
+ readonly door: string | null
195
+ enqueue(db: DB, opts: EnqueueOptions): Promise<string>
196
+ /** Claims the next eligible job (of one of `types`, when given), retrying transient conflicts until clean. The signal ends the retry loop on shutdown — claim resolves null promptly. */
197
+ claim(db: DB, workerId: string, signal?: AbortSignal, types?: readonly string[], release?: string | null): Promise<ClaimedJob | null>
198
+ /**
199
+ * Each of heartbeat, complete and fail takes the claim's token and applies only to that claim:
200
+ * once the row was reaped, handed back or claimed again they change nothing and return false.
201
+ * Without a token they match on the id alone.
202
+ */
203
+ heartbeat(db: DB, jobId: string, claimToken?: string): Promise<boolean>
204
+ complete(db: DB, jobId: string, output?: unknown, claimToken?: string): Promise<boolean>
205
+ /** Counts the attempt as failed: retried with backoff while attempts remain, dead-lettered after. One transaction, row locked. */
206
+ fail(db: DB, jobId: string, error: unknown, claimToken?: string, opts?: { permanent?: boolean }): Promise<boolean>
207
+ /** `fail` that says what it did: null when the claim no longer holds the row, else the outcome and the stored error. */
208
+ failAttempt(db: DB, jobId: string, error: unknown, claimToken?: string, opts?: { permanent?: boolean }): Promise<FailedAttempt | null>
209
+ /** Returns a claimed job to pending after the error's `delayMs` (else the backoff, at least 1 s) without counting the attempt. False when the claim no longer holds the row. */
210
+ later(db: DB, jobId: string, error: JobLaterError, claimToken: string): Promise<boolean>
211
+ /** Returns a claimed job to pending without counting the attempt. False when the claim no longer holds the row. */
212
+ handBack(db: DB, jobId: string, claimToken: string): Promise<boolean>
213
+ /**
214
+ * Cancels a pending or claimed job; no-op on any other status. `error_payload` becomes exactly
215
+ * `{ reason, message }`, replacing what an earlier attempt left, and a held claim is closed 'cancelled'
216
+ * with the same object as its `cancel`. An empty `reason.reason` throws.
217
+ */
218
+ cancel(db: DB, jobId: string, reason: { reason: string; message: string }): Promise<void>
219
+ /** For tests and support tools: makes pending jobs waiting on a retry time due now — the one job, or every one. Returns how many it changed. */
220
+ retryNow(db: DB, jobId?: string): Promise<number>
221
+ /** For tests and support tools: the Flow a job was enqueued under, by its id; null when no such job. */
222
+ flowOf(db: DB, jobId: string): Promise<ClaimedFlow | null>
223
+ /** Ready (pending and not waiting on a retry time), running (claimed or running) and the oldest ready job's wait, per job type and overall. One query. */
224
+ totals(db: DB): Promise<QueueTotals>
225
+ /** Fails every stale claimed job through the same retry/dead-letter rule as `fail`, and reports each. */
226
+ reap(db: DB): Promise<ReapedJob[]>
227
+ /**
228
+ * Registers a listener called, inside the transaction that dead-letters a job (by `fail` or `reap`), after the
229
+ * row is updated. A listener's throw is logged and rolled back to a savepoint; it never fails the dead-lettering.
230
+ */
231
+ onDeadLetter(listener: (tx: DB, job: DeadLetterInfo) => Promise<void>): () => void
232
+ requeueDeadLetter(db: DB, jobId: string): Promise<void>
233
+ startListener(client: ListenClient): Promise<void>
234
+ stopListener(): Promise<void>
235
+ waitFor(db: DB, jobId: string, opts: WaitOptions): Promise<WaitResult>
236
+ enqueueAndWait(db: DB, opts: EnqueueOptions, waitOpts: WaitOptions): Promise<WaitResult>
237
+ schedule(db: DB, spec: ScheduleSpec): Promise<void>
238
+ /** Upserts every declared schedule and removes schedules named in `owned` that are not declared. Returns the removed names. */
239
+ syncSchedules(db: DB, declared: readonly ScheduleSpec[], owned: readonly string[]): Promise<string[]>
240
+ /** Test hook: enqueues the named schedule's job now, as a scheduler tick would, without moving its next run. Returns the job id. */
241
+ fireSchedule(db: DB, name: string): Promise<string>
242
+ /** Fires every due schedule once (one transaction each, so processes sharing a database fire it once between them). Returns the count fired. */
243
+ runDueSchedules(db: DB, names?: readonly string[]): Promise<number>
244
+ unschedule(db: DB, name: string): Promise<boolean>
245
+ listSchedules(db: DB): Promise<ScheduleInfo[]>
246
+ startScheduler(db: DB, opts?: SchedulerOptions): void
247
+ stopScheduler(): void
248
+ }
249
+
250
+ /** Whether the error (or a cause in its chain) is the unique violation of the queue's live-key index. */
251
+ function isLiveKeyViolation(err: unknown, constraint: string): boolean {
252
+ let current: unknown = err
253
+ for (let depth = 0; depth < 4 && typeof current === 'object' && current !== null; depth++) {
254
+ const e = current as { code?: unknown; constraint_name?: unknown }
255
+ if (e.code === '23505' && e.constraint_name === constraint) return true
256
+ current = (current as { cause?: unknown }).cause
257
+ }
258
+ return false
259
+ }
260
+
261
+ /** Serializes values into a Postgres array literal (`{"a","b"}`) — sent as one text param and cast to the element type. */
262
+ function pgArrayLiteral(values: readonly string[]): string {
263
+ const escaped = values.map(v => `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`)
264
+ return `{${escaped.join(',')}}`
265
+ }
266
+
267
+ /** The fields of a locked job row that the retry/dead-letter rule needs. */
268
+ interface JobRow {
269
+ readonly jobId: string
270
+ readonly type: string
271
+ readonly payload: unknown
272
+ readonly tenant: string | null
273
+ readonly attemptCount: number
274
+ readonly maxAttempts: number
275
+ readonly flow: ClaimedFlow
276
+ }
277
+
278
+ function flowOf(row: Record<string, unknown>): ClaimedFlow {
279
+ return {
280
+ traceId: (row['flow_trace_id'] as string | null) ?? null,
281
+ rootId: (row['flow_root_id'] as string | null) ?? null,
282
+ stepId: (row['flow_step_id'] as string | null) ?? null,
283
+ tenant: (row['flow_tenant'] as string | null) ?? null,
284
+ actor: (row['flow_actor'] as string | null) ?? null,
285
+ }
286
+ }
287
+
288
+ export function createQueue(config: QueueConfig = {}): Queue {
289
+ const table = config.table ?? 'pipework_jobs'
290
+ const staleTimeoutMs = config.staleTimeoutMs ?? 120_000
291
+ const channel = config.notifyChannel ?? 'pipework_job_complete'
292
+ const maxAttempts = config.maxAttempts ?? 1
293
+ const backoff = config.backoff
294
+
295
+ assertValidIdentifier(table, 'queue table')
296
+ assertValidIdentifier(channel, 'notify channel')
297
+ if (config.progressColumn !== undefined) assertValidIdentifier(config.progressColumn, 'queue progress column')
298
+ const progressQ = config.progressColumn === undefined ? null : quoteIdentifier(config.progressColumn, 'queue progress column')
299
+ registerProgressColumn(table, config.progressColumn)
300
+
301
+ const tableQ = quoteIdentifier(table, 'queue table')
302
+ const listener = new JobListener(channel, tableQ)
303
+
304
+ const deadLetterListeners = new Set<(tx: DB, job: DeadLetterInfo) => Promise<void>>()
305
+
306
+ const exclusion = resolveExclusion(config.exclusion)
307
+ const definition = queueDefinition(table, exclusion)
308
+
309
+ /** What the active Flow contributes to a job: its trace, open step, tenant and (on request) actor. */
310
+ function enqueuingFlow(opts: EnqueueOptions): ClaimedFlow {
311
+ const none: ClaimedFlow = { traceId: null, rootId: null, stepId: null, tenant: null, actor: null }
312
+ if (opts.continueFlow === false) return none
313
+ const flow = getContext()
314
+ if (flow === undefined) {
315
+ const parent = opts.parent ?? null
316
+ return {
317
+ ...none,
318
+ traceId: parent?.id ?? null,
319
+ rootId: parent?.rootId ?? null,
320
+ stepId: parent?.stepId ?? null,
321
+ ...(opts.tenant !== undefined ? { tenant: opts.tenant } : {}),
322
+ actor: opts.actor ?? null,
323
+ }
324
+ }
325
+
326
+ const trace = flow.trace !== null && !flow.trace.buffer.truncated ? flow.trace : null
327
+ const parent = opts.parent !== undefined ? opts.parent : null
328
+ return {
329
+ traceId: opts.parent !== undefined ? (parent?.id ?? null) : (trace?.id ?? null),
330
+ rootId: opts.parent !== undefined ? (parent?.rootId ?? null) : (trace?.rootId ?? null),
331
+ stepId: opts.parent !== undefined ? (parent?.stepId ?? null) : (trace?.buffer.openStepId ?? null),
332
+ tenant: opts.tenant !== undefined ? opts.tenant : flow.tenant,
333
+ actor: opts.actor !== undefined ? opts.actor : opts.carryActor === true ? resolveActorId(flow) : null,
334
+ }
335
+ }
336
+
337
+ /** `AND flow_tenant = T` for a tenant-scoped caller, nothing otherwise. */
338
+ function tenantAnd(tenant: string | null): SQL {
339
+ return tenant === null ? sql`` : sql` AND flow_tenant = ${tenant}::text`
340
+ }
341
+
342
+ /** Worker and cross-tenant ops refuse a tenant-scoped handle up front and run on the handle they were given. */
343
+ function crossTenant(db: DB, op: string): DB {
344
+ return requireUnscopedExecutor(db, `queue ${op}()`, `${op} acts across tenants on queue "${table}"`)
345
+ }
346
+
347
+ function foreignTenant(op: string, scoped: string, named: string, what: string): never {
348
+ throw new Error(
349
+ `[pipework] ${op}() on queue "${table}" was given a handle scoped to tenant "${scoped}" but ${what} "${named}".\n\n` +
350
+ ' A tenant-scoped connection enqueues only its own tenant\'s jobs. Enqueue for another tenant\n' +
351
+ ' from an unscoped handle (pipe.system(), or pool.getOrCreate(...).drizzle).\n',
352
+ )
353
+ }
354
+
355
+ async function enqueue(handle: DB, opts: EnqueueOptions): Promise<string> {
356
+ return (await enqueueJob(handle, opts)).id
357
+ }
358
+
359
+ /** The tries an enqueue with a liveKey makes while the live job it conflicts with keeps finishing before it can be read. */
360
+ const LIVE_KEY_TRIES = 3
361
+
362
+ /** Enqueues, or with a liveKey returns the live job holding the key (`created: false`). */
363
+ async function enqueueJob(handle: DB, opts: EnqueueOptions): Promise<{ id: string; created: boolean }> {
364
+ if (opts.liveKey !== undefined && opts.liveKey === '') {
365
+ throw new Error(
366
+ `[pipework] enqueue() on queue "${table}" received an empty liveKey.\n\n` +
367
+ ' An empty key would make every such job share one slot. Pass a non-empty string, or omit liveKey.\n',
368
+ )
369
+ }
370
+ if (opts.pendingKey !== undefined && opts.pendingKey === '') {
371
+ throw new Error(
372
+ `[pipework] enqueue() on queue "${table}" received an empty pendingKey.\n\n` +
373
+ ' An empty key would make every such job share one slot. Pass a non-empty string, or omit pendingKey.\n',
374
+ )
375
+ }
376
+ if (opts.pendingKey !== undefined && opts.liveKey !== undefined) {
377
+ throw new Error(
378
+ `[pipework] enqueue() on queue "${table}" received both liveKey and pendingKey.\n\n` +
379
+ ' liveKey returns the live job and keeps its payload; pendingKey replaces the pending job\'s payload. Pass one.\n',
380
+ )
381
+ }
382
+ const { db, tenant: scopedTenant } = unscopedExecutor(handle)
383
+ const priority = opts.priority ?? 0
384
+ const payload = JSON.stringify(opts.payload)
385
+ const attempts = opts.maxAttempts ?? maxAttempts
386
+
387
+ const columns = ['job_type', 'priority', 'payload', 'max_attempts']
388
+ const values: SQL[] = [sql`${opts.type}`, sql`${priority}`, sql`${payload}::jsonb`, sql`${attempts}`]
389
+
390
+ const flow: { -readonly [K in keyof ClaimedFlow]: ClaimedFlow[K] } = { ...enqueuingFlow(opts) }
391
+ if (scopedTenant !== null) {
392
+ if (flow.tenant !== null && flow.tenant !== scopedTenant) {
393
+ foreignTenant('enqueue', scopedTenant, flow.tenant, opts.tenant !== undefined ? 'opts.tenant names' : 'the flow carries tenant')
394
+ }
395
+ flow.tenant = scopedTenant
396
+ }
397
+ columns.push('flow_trace_id', 'flow_root_id', 'flow_step_id', 'flow_tenant', 'flow_actor')
398
+ values.push(
399
+ sql`${flow.traceId}::uuid`, sql`${flow.rootId}::uuid`, sql`${flow.stepId}::uuid`,
400
+ sql`${flow.tenant}::text`, sql`${flow.actor}::text`,
401
+ )
402
+
403
+ if (exclusion === undefined) {
404
+ if (opts.exclusion !== undefined) {
405
+ throw new Error(
406
+ `[pipework] enqueue() received exclusion values, but queue "${table}" has no exclusion config.\n\n` +
407
+ ' Silently dropping them would enqueue a job without its conflict data.\n' +
408
+ ' Configure the queue: createQueue({ exclusion: { arrayColumn: ..., blocksAllColumn: ..., tenantColumn: ... } }).\n',
409
+ )
410
+ }
411
+ } else {
412
+ const given = opts.exclusion ?? {}
413
+ if (given.array !== undefined && given.array !== null && exclusion.arrayColumn === undefined) {
414
+ throw new Error(
415
+ `[pipework] enqueue() received exclusion.array, but queue "${table}" configures no arrayColumn.\n\n` +
416
+ ' Add arrayColumn to the queue exclusion config, or drop the value.\n',
417
+ )
418
+ }
419
+ if (given.blocksAll !== undefined && exclusion.blocksAllColumn === undefined) {
420
+ throw new Error(
421
+ `[pipework] enqueue() received exclusion.blocksAll, but queue "${table}" configures no blocksAllColumn.\n\n` +
422
+ ' Add blocksAllColumn to the queue exclusion config, or drop the value.\n',
423
+ )
424
+ }
425
+ if (given.tenant !== undefined && exclusion.tenantColumn === undefined) {
426
+ throw new Error(
427
+ `[pipework] enqueue() received exclusion.tenant, but queue "${table}" configures no tenantColumn.\n\n` +
428
+ ' Add tenantColumn to the queue exclusion config, or drop the value.\n',
429
+ )
430
+ }
431
+ if (exclusion.tenantColumn !== undefined) {
432
+ if (given.tenant === undefined) {
433
+ throw new Error(
434
+ `[pipework] enqueue() requires exclusion.tenant — queue "${table}" scopes exclusion by "${exclusion.tenantColumn}".\n\n` +
435
+ ' Every job needs a tenant: enqueue(db, { ..., exclusion: { tenant: <id> } }).\n',
436
+ )
437
+ }
438
+ if (scopedTenant !== null && given.tenant !== scopedTenant) {
439
+ foreignTenant('enqueue', scopedTenant, String(given.tenant), 'exclusion.tenant names')
440
+ }
441
+ columns.push(quoteIdentifier(exclusion.tenantColumn, 'exclusion column'))
442
+ values.push(sql`${given.tenant}::uuid`)
443
+ }
444
+ if (exclusion.arrayColumn !== undefined && given.array !== undefined && given.array !== null) {
445
+ columns.push(quoteIdentifier(exclusion.arrayColumn, 'exclusion column'))
446
+ values.push(sql`${pgArrayLiteral(given.array)}::${sql.raw(exclusion.arrayElementType)}[]`)
447
+ }
448
+ if (exclusion.blocksAllColumn !== undefined && given.blocksAll !== undefined) {
449
+ columns.push(quoteIdentifier(exclusion.blocksAllColumn, 'exclusion column'))
450
+ values.push(sql`${given.blocksAll}`)
451
+ }
452
+ }
453
+
454
+ if (opts.pendingKey !== undefined) {
455
+ columns.push('pending_key')
456
+ values.push(sql`${opts.pendingKey}::text`)
457
+ const pendingKey = opts.pendingKey
458
+ const rows = await db.transaction(async (tx) => {
459
+ await tx.execute(sql`SELECT ${pendingKeyLock(sql`${pendingKey}::text`, sql`coalesce(${flow.tenant}::text, '')`)}`)
460
+ return tx.execute(
461
+ sql`INSERT INTO ${sql.raw(tableQ)} (${sql.raw(columns.join(', '))})
462
+ VALUES (${sql.join(values, sql`, `)})
463
+ ON CONFLICT (pending_key, (${sql.raw(PENDING_KEY_TENANT_EXPR)})) WHERE ${sql.raw(PENDING_KEY_PREDICATE)}
464
+ DO UPDATE SET payload = EXCLUDED.payload
465
+ RETURNING id::text, (xmax = 0) AS created`
466
+ )
467
+ })
468
+ return { id: rows[0]!['id'] as string, created: rows[0]!['created'] === true }
469
+ }
470
+
471
+ if (opts.liveKey === undefined) {
472
+ const rows = await db.execute(
473
+ sql`INSERT INTO ${sql.raw(tableQ)} (${sql.raw(columns.join(', '))})
474
+ VALUES (${sql.join(values, sql`, `)})
475
+ RETURNING id::text`
476
+ )
477
+ return { id: rows[0]!['id'] as string, created: true }
478
+ }
479
+
480
+ columns.push('live_key')
481
+ values.push(sql`${opts.liveKey}::text`)
482
+ const insert = sql`INSERT INTO ${sql.raw(tableQ)} (${sql.raw(columns.join(', '))})
483
+ VALUES (${sql.join(values, sql`, `)})
484
+ ON CONFLICT (live_key, (${sql.raw(LIVE_KEY_TENANT_EXPR)})) WHERE ${sql.raw(LIVE_KEY_PREDICATE)} DO NOTHING
485
+ RETURNING id::text`
486
+ // A separate statement for the lookup: a CTE would share the insert's snapshot and could not see
487
+ // a conflicting row a concurrent enqueuer committed after it began.
488
+ const lookup = sql`SELECT id::text FROM ${sql.raw(tableQ)}
489
+ WHERE live_key = ${opts.liveKey}::text AND ${sql.raw(LIVE_KEY_TENANT_EXPR)} = coalesce(${flow.tenant}::text, '')
490
+ AND status IN ('pending', 'claimed', 'running')
491
+ LIMIT 1`
492
+ for (let attempt = 0; attempt < LIVE_KEY_TRIES; attempt++) {
493
+ const inserted = await db.execute(insert)
494
+ if (inserted.length > 0) return { id: inserted[0]!['id'] as string, created: true }
495
+ const live = await db.execute(lookup)
496
+ if (live.length > 0) return { id: live[0]!['id'] as string, created: false }
497
+ }
498
+ throw new Error(
499
+ `[pipework] enqueue() on queue "${table}" could not settle liveKey "${opts.liveKey}" after ${LIVE_KEY_TRIES} tries.\n\n` +
500
+ ' Each try found the key held by a live job that finished before it could be read, then lost the\n' +
501
+ ' insert again. Retry the enqueue.\n',
502
+ )
503
+ }
504
+
505
+ function buildClaimQuery(workerId: string, types?: readonly string[], release?: string | null): SQL {
506
+ const typeClause = types !== undefined ? sql` AND candidate.job_type = ANY(${pgArrayLiteral(types)}::text[])` : sql``
507
+ const claimFilterClause = config.claimFilter
508
+ ? ` AND (${config.claimFilter('candidate')})`
509
+ : ''
510
+ const exclusionClause = exclusion !== undefined
511
+ ? ` AND ${exclusionClaimClause(tableQ, exclusion)}`
512
+ : ''
513
+ const exclusionReturning = exclusion !== undefined
514
+ ? [exclusion.tenantColumn, exclusion.arrayColumn, exclusion.blocksAllColumn]
515
+ .filter((c): c is string => c !== undefined)
516
+ .map(c => `, ${quoteIdentifier(c, 'exclusion column')}`)
517
+ .join('')
518
+ : ''
519
+
520
+ return sql`
521
+ UPDATE ${sql.raw(tableQ)} SET
522
+ ${openClaim(workerId, release ?? null)},
523
+ status = 'claimed',
524
+ claimed_at = now(),
525
+ heartbeat_at = now(),
526
+ worker_id = ${workerId},
527
+ worker_release = ${release ?? null},
528
+ claim_token = gen_random_uuid()
529
+ WHERE id = (
530
+ SELECT candidate.id FROM ${sql.raw(tableQ)} candidate
531
+ WHERE candidate.status = 'pending' AND (candidate.next_retry_at IS NULL OR candidate.next_retry_at <= now())${sql.raw(exclusionClause)}${sql.raw(claimFilterClause)}${typeClause}
532
+ ORDER BY candidate.priority DESC, candidate.queued_at ASC
533
+ FOR UPDATE SKIP LOCKED
534
+ LIMIT 1
535
+ )
536
+ RETURNING id::text, job_type, priority, payload, created_lsn::text, claim_token::text, attempt_count, max_attempts, error_payload->>'reason' AS previous_reason, error_payload->>'workerId' AS previous_worker_id, flow_trace_id::text, flow_root_id::text, flow_step_id::text, flow_tenant, flow_actor, live_key, pending_key, claims #- ARRAY[(jsonb_array_length(claims) - 1)::text] AS claims_before${sql.raw(exclusionReturning)}`
537
+ }
538
+
539
+ function previousOf(row: Record<string, unknown>): ClaimedJob['previous'] {
540
+ const reason = row['previous_reason']
541
+ if (reason !== 'reaped' && reason !== 'handed-back' && reason !== 'later') return null
542
+ return { reason, workerId: (row['previous_worker_id'] as string | null | undefined) ?? null }
543
+ }
544
+
545
+ function mapClaimedRow(row: Record<string, unknown>): ClaimedJob {
546
+ const base = {
547
+ id: row['id'] as string,
548
+ type: row['job_type'] as string,
549
+ priority: row['priority'] as number,
550
+ payload: row['payload'],
551
+ createdLsn: row['created_lsn'] as string,
552
+ claimToken: row['claim_token'] as string,
553
+ attempt: (row['attempt_count'] as number) + 1,
554
+ maxAttempts: row['max_attempts'] as number,
555
+ previous: previousOf(row),
556
+ claims: parseClaims(row['claims_before']),
557
+ liveKey: (row['live_key'] as string | null | undefined) ?? null,
558
+ pendingKey: (row['pending_key'] as string | null | undefined) ?? null,
559
+ flow: {
560
+ traceId: (row['flow_trace_id'] as string | null | undefined) ?? null,
561
+ rootId: (row['flow_root_id'] as string | null | undefined) ?? null,
562
+ stepId: (row['flow_step_id'] as string | null | undefined) ?? null,
563
+ tenant: (row['flow_tenant'] as string | null | undefined) ?? null,
564
+ actor: (row['flow_actor'] as string | null | undefined) ?? null,
565
+ },
566
+ }
567
+ if (exclusion === undefined) return base
568
+ return {
569
+ ...base,
570
+ exclusion: {
571
+ array: exclusion.arrayColumn !== undefined ? (row[exclusion.arrayColumn] as string[] | null) : null,
572
+ blocksAll: exclusion.blocksAllColumn !== undefined ? (row[exclusion.blocksAllColumn] as boolean) : false,
573
+ tenant: exclusion.tenantColumn !== undefined ? (row[exclusion.tenantColumn] as string) : null,
574
+ },
575
+ }
576
+ }
577
+
578
+ /** Full-jitter backoff for transient claim conflicts — identical schedules re-collide, jitter decorrelates the herd. */
579
+ function conflictDelay(attempt: number): number {
580
+ return Math.random() * Math.min(1000, 50 * 2 ** attempt)
581
+ }
582
+
583
+ async function claim(handle: DB, workerId: string, signal?: AbortSignal, types?: readonly string[], release?: string | null): Promise<ClaimedJob | null> {
584
+ const db = crossTenant(handle, 'claim')
585
+ const query = buildClaimQuery(workerId, types, release)
586
+
587
+ // Serialization failures (40001) carry Postgres's safe-retry guarantee:
588
+ // the conflicting transaction committed, so every abort means another
589
+ // claimer made progress and a retry cannot fail the same way again.
590
+ // Retrying until a clean attempt keeps the contract exact — null means
591
+ // "nothing claimable", never "gave up under contention". A fixed retry
592
+ // budget here starves non-conflicting claimers under sustained load.
593
+ // The loop's only other exit is shutdown: the signal cancels the backoff
594
+ // sleep and the next attempt, returning null — without it a worker stuck
595
+ // under sustained contention can never observe shutdown.
596
+ for (let attempt = 0; ; attempt++) {
597
+ if (signal?.aborted === true) return null
598
+ try {
599
+ // Two workers evaluating the exclusion NOT EXISTS checks against the
600
+ // same MVCC snapshot can both pass and claim conflicting jobs (write
601
+ // skew) — the exclusion claim query must run under SERIALIZABLE.
602
+ const rows = exclusion !== undefined
603
+ ? await serializable(db, async (tx) => tx.execute(query), { maxAttempts: 1 })
604
+ : await db.execute(query)
605
+ if (rows.length === 0) return null
606
+ return mapClaimedRow(rows[0]!)
607
+ } catch (err: unknown) {
608
+ if (!isTransientConcurrencyError(err)) throw err
609
+ await abortableDelay(conflictDelay(attempt), signal)
610
+ }
611
+ }
612
+ }
613
+
614
+ /** Rows a claim-scoped statement may touch: the claim's own token while it is still held. */
615
+ function claimScope(claimToken: string | undefined): SQL {
616
+ return claimToken === undefined
617
+ ? sql``
618
+ : sql` AND claim_token = ${claimToken}::uuid AND status IN ('claimed', 'running')`
619
+ }
620
+
621
+ async function heartbeat(handle: DB, jobId: string, claimToken?: string): Promise<boolean> {
622
+ const db = crossTenant(handle, 'heartbeat')
623
+ const rows = await db.execute(
624
+ sql`UPDATE ${sql.raw(tableQ)} SET heartbeat_at = now() WHERE id = ${jobId}::uuid${claimScope(claimToken)} RETURNING id`
625
+ )
626
+ return rows.length > 0
627
+ }
628
+
629
+ async function complete(handle: DB, jobId: string, output?: unknown, claimToken?: string): Promise<boolean> {
630
+ const db = crossTenant(handle, 'complete')
631
+ const outputJson = output !== undefined ? JSON.stringify(output) : null
632
+ const rows = await db.execute(
633
+ sql`UPDATE ${sql.raw(tableQ)} SET ${closeClaim('completed', progressQ)}, status = 'completed', completed_at = now(), claim_token = NULL, output = ${outputJson !== null ? sql`${outputJson}::jsonb` : sql`NULL`}
634
+ WHERE id = ${jobId}::uuid${claimScope(claimToken)} RETURNING id`
635
+ )
636
+ if (rows.length === 0) return false
637
+ await db.execute(
638
+ sql`SELECT pg_notify(${channel}, ${jobId})`
639
+ )
640
+ return true
641
+ }
642
+
643
+ function calculateDelay(attemptCount: number): number {
644
+ if (!backoff) return 0
645
+ if (backoff.type === 'fixed') return backoff.delayMs
646
+ const delay = backoff.delayMs * Math.pow(2, attemptCount)
647
+ return backoff.maxDelayMs !== undefined ? Math.min(delay, backoff.maxDelayMs) : delay
648
+ }
649
+
650
+ /** The advisory lock an enqueue and a return-to-pending both take, per (table, pendingKey, tenant), before touching the index. */
651
+ function pendingKeyLock(key: SQL, tenant: SQL): SQL {
652
+ return sql`pg_advisory_xact_lock(hashtextextended(${table}::text || chr(31) || ${key} || chr(31) || ${tenant}, 0))`
653
+ }
654
+
655
+ /** Takes the lock for a job's own key and tenant; nothing when the job has no pendingKey. */
656
+ async function lockPendingKeyOf(tx: DB, jobId: string): Promise<void> {
657
+ await tx.execute(
658
+ sql`SELECT ${pendingKeyLock(sql`pending_key`, sql`coalesce(flow_tenant, '')`)} FROM ${sql.raw(tableQ)}
659
+ WHERE id = ${jobId}::uuid AND pending_key IS NOT NULL`
660
+ )
661
+ }
662
+
663
+ /**
664
+ * The SET fragments that put a claimed job back to pending — unless a newer pending job holds its
665
+ * pendingKey, in which case the job is cancelled instead (claim closed as 'cancelled' with `cancel`
666
+ * `{ reason: 'superseded', message }`, error_payload `{ reason: 'superseded', by: <twin id> }`), so the twin with the newer payload is the one that runs.
667
+ * Decided inside the UPDATE, from the row as it is when the statement runs. Callers must hold
668
+ * `lockPendingKeyOf` for the row first, in the same transaction, so a racing enqueue cannot add a twin unseen.
669
+ */
670
+ function returnToPending(reason: JobClaimReason, errorPayload: SQL): SQL {
671
+ const twinId = sql`(SELECT twin.id::text FROM ${sql.raw(tableQ)} AS twin
672
+ WHERE twin.pending_key = ${sql.raw(tableQ)}.pending_key
673
+ AND coalesce(twin.flow_tenant, '') = coalesce(${sql.raw(tableQ)}.flow_tenant, '')
674
+ AND twin.status = 'pending' AND twin.id <> ${sql.raw(tableQ)}.id
675
+ LIMIT 1)`
676
+ const superseded = sql`(pending_key IS NOT NULL AND ${twinId} IS NOT NULL)`
677
+ const supersededClaim = sql`CASE WHEN ${superseded}
678
+ THEN jsonb_build_object('cancel', jsonb_build_object('reason', 'superseded', 'message', 'superseded by ' || ${twinId}))
679
+ ELSE '{}'::jsonb END`
680
+ return sql`${closeClaim(sql`CASE WHEN ${superseded} THEN 'cancelled' ELSE ${reason}::text END`, progressQ, supersededClaim)},
681
+ status = CASE WHEN ${superseded} THEN 'cancelled' ELSE 'pending' END,
682
+ completed_at = CASE WHEN ${superseded} THEN now() ELSE completed_at END,
683
+ error_payload = CASE WHEN ${superseded} THEN jsonb_build_object('reason', 'superseded', 'by', ${twinId}) ELSE ${errorPayload} END`
684
+ }
685
+
686
+ /** Wakes waitFor callers of a job `returnToPending` cancelled. */
687
+ async function notifyIfSuperseded(tx: DB, jobId: string, rows: readonly Record<string, unknown>[]): Promise<boolean> {
688
+ const superseded = rows[0]?.['status'] === 'cancelled'
689
+ if (superseded) await tx.execute(sql`SELECT pg_notify(${channel}, ${jobId})`)
690
+ return superseded
691
+ }
692
+
693
+ /**
694
+ * Counts the attempt as failed: back to pending with backoff while attempts
695
+ * remain, dead letter (and a notify) after. The row must already be locked by
696
+ * the caller's transaction. Shared by fail and reap so the two cannot drift.
697
+ */
698
+ async function retryOrDeadLetter(
699
+ tx: DB, job: JobRow, errorJson: string, reason: 'failed' | 'reaped-stale', permanent = false,
700
+ ): Promise<'retry' | 'dead_letter' | 'superseded'> {
701
+ const { jobId, attemptCount, maxAttempts: jobMaxAttempts } = job
702
+ const nextAttempt = attemptCount + 1
703
+
704
+ if (!permanent && nextAttempt < jobMaxAttempts) {
705
+ const delayMs = calculateDelay(attemptCount)
706
+ const delaySeconds = `${Math.ceil(delayMs / 1000)} seconds`
707
+ await lockPendingKeyOf(tx, jobId)
708
+ const rows = await tx.execute(
709
+ sql`UPDATE ${sql.raw(tableQ)} SET
710
+ ${returnToPending(reason, sql`${errorJson}::jsonb`)},
711
+ attempt_count = ${nextAttempt},
712
+ next_retry_at = ${delayMs > 0 ? sql`now() + ${delaySeconds}::interval` : sql`NULL`},
713
+ claimed_at = NULL,
714
+ heartbeat_at = NULL,
715
+ worker_id = NULL,
716
+ claim_token = NULL
717
+ WHERE id = ${jobId}::uuid
718
+ RETURNING status`
719
+ )
720
+ return (await notifyIfSuperseded(tx, jobId, rows)) ? 'superseded' : 'retry'
721
+ }
722
+ await tx.execute(
723
+ sql`UPDATE ${sql.raw(tableQ)} SET
724
+ ${closeClaim(reason, progressQ)},
725
+ status = 'dead_letter',
726
+ attempt_count = ${nextAttempt},
727
+ completed_at = now(),
728
+ claim_token = NULL,
729
+ error_payload = ${errorJson}::jsonb
730
+ WHERE id = ${jobId}::uuid`
731
+ )
732
+ await tx.execute(
733
+ sql`SELECT pg_notify(${channel}, ${jobId})`
734
+ )
735
+ for (const listener of deadLetterListeners) {
736
+ try {
737
+ await tx.transaction(async (savepoint) => {
738
+ await listener(savepoint, {
739
+ id: jobId, type: job.type, tenant: job.tenant, payload: job.payload, error: JSON.parse(errorJson) as unknown, flow: job.flow,
740
+ })
741
+ })
742
+ } catch (err) {
743
+ getBaseLogger().error({ err, jobId, jobType: job.type }, 'Dead-letter listener failed')
744
+ void recordFault('queue-listener', faultMessage(err), { jobId, jobType: job.type })
745
+ }
746
+ }
747
+ return 'dead_letter'
748
+ }
749
+
750
+ async function fail(db: DB, jobId: string, error: unknown, claimToken?: string, opts?: { permanent?: boolean }): Promise<boolean> {
751
+ crossTenant(db, 'fail')
752
+ return (await failAttempt(db, jobId, error, claimToken, opts)) !== null
753
+ }
754
+
755
+ async function failAttempt(
756
+ handle: DB, jobId: string, error: unknown, claimToken?: string, opts?: { permanent?: boolean },
757
+ ): Promise<FailedAttempt | null> {
758
+ const db = crossTenant(handle, 'failAttempt')
759
+ const errorJson = JSON.stringify(serializeError(error))
760
+
761
+ return db.transaction(async (tx) => {
762
+ const jobRows = await tx.execute(
763
+ sql`SELECT job_type, payload, flow_tenant, attempt_count, max_attempts, flow_trace_id::text, flow_root_id::text,
764
+ flow_step_id::text, flow_actor FROM ${sql.raw(tableQ)} WHERE id = ${jobId}::uuid${claimScope(claimToken)} FOR UPDATE`
765
+ )
766
+ const job = jobRows[0]
767
+ if (!job) {
768
+ if (claimToken !== undefined) return null
769
+ throw new Error(`[pipework] Cannot fail job "${jobId}" — job not found.`)
770
+ }
771
+ const outcome = await retryOrDeadLetter(tx, {
772
+ jobId,
773
+ type: job['job_type'] as string,
774
+ payload: job['payload'],
775
+ tenant: (job['flow_tenant'] as string | null) ?? null,
776
+ attemptCount: job['attempt_count'] as number,
777
+ maxAttempts: job['max_attempts'] as number,
778
+ flow: flowOf(job),
779
+ }, errorJson, 'failed', opts?.permanent === true)
780
+ return { outcome, error: JSON.parse(errorJson) as unknown }
781
+ })
782
+ }
783
+
784
+ async function handBack(handle: DB, jobId: string, claimToken: string): Promise<boolean> {
785
+ const db = crossTenant(handle, 'handBack')
786
+ return db.transaction(async (tx) => {
787
+ await lockPendingKeyOf(tx, jobId)
788
+ const rows = await tx.execute(
789
+ sql`UPDATE ${sql.raw(tableQ)} SET
790
+ ${returnToPending('shutdown-returned', sql`jsonb_build_object('reason', 'handed-back', 'workerId', worker_id)`)},
791
+ next_retry_at = NULL,
792
+ claimed_at = NULL,
793
+ heartbeat_at = NULL,
794
+ claim_token = NULL,
795
+ worker_id = NULL
796
+ WHERE id = ${jobId}::uuid${claimScope(claimToken)}
797
+ RETURNING status`
798
+ )
799
+ await notifyIfSuperseded(tx, jobId, rows)
800
+ return rows.length > 0
801
+ })
802
+ }
803
+
804
+ async function later(handle: DB, jobId: string, error: JobLaterError, claimToken: string): Promise<boolean> {
805
+ const db = crossTenant(handle, 'later')
806
+ return db.transaction(async (tx) => {
807
+ const found = await tx.execute(
808
+ sql`SELECT attempt_count FROM ${sql.raw(tableQ)} WHERE id = ${jobId}::uuid${claimScope(claimToken)} FOR UPDATE`
809
+ )
810
+ const row = found[0]
811
+ if (!row) return false
812
+ const delayMs = error.delayMs !== undefined
813
+ ? Math.max(0, error.delayMs)
814
+ : Math.max(1000, calculateDelay(row['attempt_count'] as number))
815
+ await lockPendingKeyOf(tx, jobId)
816
+ const updated = await tx.execute(
817
+ sql`UPDATE ${sql.raw(tableQ)} SET
818
+ ${returnToPending('later', sql`jsonb_build_object('reason', 'later', 'message', ${error.message}::text, 'workerId', worker_id)`)},
819
+ next_retry_at = now() + make_interval(secs => ${delayMs / 1000}::double precision),
820
+ claimed_at = NULL,
821
+ heartbeat_at = NULL,
822
+ claim_token = NULL,
823
+ worker_id = NULL
824
+ WHERE id = ${jobId}::uuid
825
+ RETURNING status`
826
+ )
827
+ await notifyIfSuperseded(tx, jobId, updated)
828
+ return true
829
+ })
830
+ }
831
+
832
+ async function cancel(handle: DB, jobId: string, why: { reason: string; message: string }): Promise<void> {
833
+ if (why.reason === '') {
834
+ throw new Error(
835
+ `[pipework] cancel() on queue "${table}" received an empty reason.\n\n` +
836
+ ' The reason is stored in error_payload so a cancelled job says why. Pass a non-empty reason, e.g. { reason: "operator", message: "..." }.\n',
837
+ )
838
+ }
839
+ const { db, tenant } = unscopedExecutor(handle)
840
+ const cancel = sql`jsonb_build_object('reason', ${why.reason}::text, 'message', ${why.message}::text)`
841
+ await db.execute(
842
+ sql`UPDATE ${sql.raw(tableQ)} SET ${closeClaim('cancelled', progressQ, sql`jsonb_build_object('cancel', ${cancel})`)}, status = 'cancelled', completed_at = now(), claim_token = NULL,
843
+ error_payload = ${cancel}
844
+ WHERE id = ${jobId}::uuid AND status IN ('pending', 'claimed')${tenantAnd(tenant)}`
845
+ )
846
+ }
847
+
848
+ async function retryNow(handle: DB, jobId?: string): Promise<number> {
849
+ const { db, tenant } = unscopedExecutor(handle)
850
+ const rows = await db.execute(
851
+ sql`UPDATE ${sql.raw(tableQ)} SET next_retry_at = now()
852
+ WHERE status = 'pending' AND next_retry_at > now()${jobId === undefined ? sql`` : sql` AND id = ${jobId}::uuid`}${tenantAnd(tenant)}
853
+ RETURNING id`
854
+ )
855
+ return rows.length
856
+ }
857
+
858
+ async function flowOfJob(handle: DB, jobId: string): Promise<ClaimedFlow | null> {
859
+ const { db, tenant } = unscopedExecutor(handle)
860
+ const rows = await db.execute(
861
+ sql`SELECT flow_trace_id::text, flow_root_id::text, flow_step_id::text, flow_tenant, flow_actor
862
+ FROM ${sql.raw(tableQ)} WHERE id = ${jobId}::uuid${tenantAnd(tenant)}`
863
+ )
864
+ return rows.length === 0 ? null : flowOf(rows[0] as Record<string, unknown>)
865
+ }
866
+
867
+ async function totals(handle: DB): Promise<QueueTotals> {
868
+ const { db, tenant } = unscopedExecutor(handle)
869
+ const rows = await db.execute(
870
+ sql`SELECT job_type,
871
+ (count(*) FILTER (WHERE status = 'pending' AND (next_retry_at IS NULL OR next_retry_at <= clock_timestamp())))::int AS ready,
872
+ (count(*) FILTER (WHERE status IN ('claimed', 'running')))::int AS running,
873
+ (extract(epoch FROM clock_timestamp() - min(queued_at) FILTER (WHERE status = 'pending' AND (next_retry_at IS NULL OR next_retry_at <= clock_timestamp()))) * 1000)::bigint AS oldest_ms
874
+ FROM ${sql.raw(tableQ)}
875
+ WHERE status IN ('pending', 'claimed', 'running')${tenantAnd(tenant)}
876
+ GROUP BY GROUPING SETS ((job_type), ())`
877
+ )
878
+ let overall: QueueTypeTotals = { ready: 0, running: 0, oldestReadyWaitMs: null }
879
+ const byType: Record<string, QueueTypeTotals> = {}
880
+ for (const r of rows) {
881
+ const t: QueueTypeTotals = {
882
+ ready: Number(r['ready']),
883
+ running: Number(r['running']),
884
+ oldestReadyWaitMs: r['oldest_ms'] === null ? null : Number(r['oldest_ms']),
885
+ }
886
+ if (r['job_type'] === null) overall = t
887
+ else if (t.ready > 0 || t.running > 0) byType[String(r['job_type'])] = t
888
+ }
889
+ return { overall, byType }
890
+ }
891
+
892
+ async function reap(handle: DB): Promise<ReapedJob[]> {
893
+ const db = crossTenant(handle, 'reap')
894
+ const staleInterval = `${Math.floor(staleTimeoutMs / 1000)} seconds`
895
+ return db.transaction(async (tx) => {
896
+ const stale = await tx.execute(
897
+ sql`SELECT id::text, job_type, payload, attempt_count, max_attempts, worker_id, flow_trace_id::text, flow_root_id::text,
898
+ flow_step_id::text, flow_tenant, flow_actor
899
+ FROM ${sql.raw(tableQ)}
900
+ WHERE status IN ('claimed', 'running')
901
+ AND heartbeat_at < now() - ${staleInterval}::interval
902
+ FOR UPDATE SKIP LOCKED`
903
+ )
904
+ const reaped: ReapedJob[] = []
905
+ for (const row of stale) {
906
+ const id = row['id'] as string
907
+ const attemptCount = row['attempt_count'] as number
908
+ const jobMaxAttempts = row['max_attempts'] as number
909
+ const errorJson = JSON.stringify({
910
+ reason: 'reaped',
911
+ message: 'worker lost: heartbeat timeout',
912
+ workerId: (row['worker_id'] as string | null) ?? null,
913
+ })
914
+ const outcome = await retryOrDeadLetter(tx, {
915
+ jobId: id,
916
+ type: row['job_type'] as string,
917
+ payload: row['payload'],
918
+ tenant: (row['flow_tenant'] as string | null) ?? null,
919
+ attemptCount,
920
+ maxAttempts: jobMaxAttempts,
921
+ flow: flowOf(row),
922
+ }, errorJson, 'reaped-stale')
923
+ reaped.push({
924
+ jobId: id,
925
+ type: row['job_type'] as string,
926
+ attempt: attemptCount + 1,
927
+ outcome,
928
+ maxAttempts: jobMaxAttempts,
929
+ flow: flowOf(row),
930
+ })
931
+ }
932
+ return reaped
933
+ })
934
+ }
935
+
936
+ async function requeueDeadLetter(handle: DB, jobId: string): Promise<void> {
937
+ const { db, tenant } = unscopedExecutor(handle)
938
+ let rows: readonly Record<string, unknown>[]
939
+ try {
940
+ rows = await db.execute(
941
+ sql`UPDATE ${sql.raw(tableQ)} SET
942
+ status = 'pending', attempt_count = 0, max_attempts = ${maxAttempts},
943
+ completed_at = NULL, next_retry_at = NULL, claimed_at = NULL,
944
+ heartbeat_at = NULL, worker_id = NULL, claim_token = NULL
945
+ WHERE id = ${jobId}::uuid AND status = 'dead_letter'${tenantAnd(tenant)}
946
+ RETURNING id`
947
+ )
948
+ } catch (err) {
949
+ if (isLiveKeyViolation(err, pendingKeyIndexName(table))) {
950
+ const twin = await db.execute(
951
+ sql`SELECT t.pending_key, (SELECT p.id::text FROM ${sql.raw(tableQ)} p
952
+ WHERE p.pending_key = t.pending_key AND coalesce(p.flow_tenant, '') = coalesce(t.flow_tenant, '') AND p.status = 'pending' LIMIT 1) AS twin
953
+ FROM ${sql.raw(tableQ)} t WHERE t.id = ${jobId}::uuid`
954
+ )
955
+ throw new Error(
956
+ `[pipework] Cannot requeue job "${jobId}" — pending job "${String(twin[0]?.['twin'])}" already holds its pendingKey "${String(twin[0]?.['pending_key'])}".\n\n` +
957
+ ' Requeueing would make a second pending job with the same key. Let the pending job run or cancel it, then requeue.\n',
958
+ { cause: err },
959
+ )
960
+ }
961
+ if (!isLiveKeyViolation(err, liveKeyIndexName(table))) throw err
962
+ const held = await db.execute(sql`SELECT live_key FROM ${sql.raw(tableQ)} WHERE id = ${jobId}::uuid`)
963
+ throw new Error(
964
+ `[pipework] Cannot requeue job "${jobId}" — a live job already holds its liveKey "${String(held[0]?.['live_key'])}".\n\n` +
965
+ ' Requeueing would make a second live job with the same key. Let the live job finish or cancel it, then requeue.\n',
966
+ { cause: err },
967
+ )
968
+ }
969
+ if (rows.length === 0) {
970
+ throw new Error(`[pipework] Cannot requeue job "${jobId}" — job not found or not in dead_letter status.`)
971
+ }
972
+ }
973
+
974
+ async function startListener(client: ListenClient): Promise<void> {
975
+ await listener.start(client)
976
+ }
977
+
978
+ async function stopListener(): Promise<void> {
979
+ await listener.stop()
980
+ }
981
+
982
+ async function waitFor(handle: DB, jobId: string, opts: WaitOptions): Promise<WaitResult> {
983
+ const { db, tenant } = unscopedExecutor(handle)
984
+ return listener.waitFor(db, jobId, opts, tenant)
985
+ }
986
+
987
+ async function enqueueAndWait(db: DB, opts: EnqueueOptions, waitOpts: WaitOptions): Promise<WaitResult> {
988
+ const jobId = await enqueue(db, opts)
989
+ return waitFor(db, jobId, waitOpts)
990
+ }
991
+
992
+ const sched = createScheduler({ enqueue, enqueueJob }, tableQ)
993
+
994
+ return {
995
+ definition,
996
+ door: exclusion?.tenantColumn !== undefined ? queueDoorName(table) : null,
997
+ onDeadLetter(listener) {
998
+ deadLetterListeners.add(listener)
999
+ return () => { deadLetterListeners.delete(listener) }
1000
+ },
1001
+ enqueue, claim, heartbeat, complete, fail, failAttempt, later, handBack, cancel, retryNow, flowOf: flowOfJob, totals, reap, requeueDeadLetter,
1002
+ startListener, stopListener, waitFor, enqueueAndWait,
1003
+ schedule: (db, spec) => sched.schedule(crossTenant(db, 'schedule'), spec),
1004
+ syncSchedules: (db, declared, owned) => sched.sync(crossTenant(db, 'syncSchedules'), declared, owned),
1005
+ fireSchedule: (db, name) => sched.fire(crossTenant(db, 'fireSchedule'), name),
1006
+ runDueSchedules: (db, names) => sched.tick(crossTenant(db, 'runDueSchedules'), names),
1007
+ unschedule: (db, name) => sched.unschedule(crossTenant(db, 'unschedule'), name),
1008
+ listSchedules: (db) => sched.listSchedules(crossTenant(db, 'listSchedules')),
1009
+ startScheduler: (db, opts) => sched.start(crossTenant(db, 'startScheduler'), opts),
1010
+ stopScheduler: () => sched.stop(),
1011
+ }
1012
+ }