@adonis-agora/durable 0.49.1 → 0.51.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 (335) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/README.md +53 -22
  3. package/dist/assets/spa/assets/index-DupjrK7N.js +16 -0
  4. package/dist/assets/spa/assets/index-DupjrK7N.js.map +1 -0
  5. package/dist/assets/spa/index.html +1 -1
  6. package/dist/commands/durable_worker.d.ts.map +1 -1
  7. package/dist/commands/durable_worker.js +9 -8
  8. package/dist/commands/durable_worker.js.map +1 -1
  9. package/dist/commands/work.d.ts.map +1 -1
  10. package/dist/commands/work.js +3 -1
  11. package/dist/commands/work.js.map +1 -1
  12. package/dist/configure.d.ts.map +1 -1
  13. package/dist/configure.js +1 -0
  14. package/dist/configure.js.map +1 -1
  15. package/dist/providers/dashboard_provider.d.ts +2 -0
  16. package/dist/providers/dashboard_provider.d.ts.map +1 -1
  17. package/dist/providers/dashboard_provider.js +50 -6
  18. package/dist/providers/dashboard_provider.js.map +1 -1
  19. package/dist/providers/durable_provider.d.ts.map +1 -1
  20. package/dist/providers/durable_provider.js +146 -35
  21. package/dist/providers/durable_provider.js.map +1 -1
  22. package/dist/src/admission-redis/redis-admission-backend.d.ts.map +1 -1
  23. package/dist/src/admission-redis/redis-admission-backend.js +3 -1
  24. package/dist/src/admission-redis/redis-admission-backend.js.map +1 -1
  25. package/dist/src/ambient-step.d.ts +24 -0
  26. package/dist/src/ambient-step.d.ts.map +1 -0
  27. package/dist/src/ambient-step.js +42 -0
  28. package/dist/src/ambient-step.js.map +1 -0
  29. package/dist/src/base-workflow.d.ts +10 -1
  30. package/dist/src/base-workflow.d.ts.map +1 -1
  31. package/dist/src/base-workflow.js.map +1 -1
  32. package/dist/src/codec-state-store.d.ts +28 -2
  33. package/dist/src/codec-state-store.d.ts.map +1 -1
  34. package/dist/src/codec-state-store.js +61 -3
  35. package/dist/src/codec-state-store.js.map +1 -1
  36. package/dist/src/commands/worker.d.ts +8 -3
  37. package/dist/src/commands/worker.d.ts.map +1 -1
  38. package/dist/src/commands/worker.js +35 -8
  39. package/dist/src/commands/worker.js.map +1 -1
  40. package/dist/src/config_types.d.ts +6 -0
  41. package/dist/src/config_types.d.ts.map +1 -1
  42. package/dist/src/control-plane-redis/redis-control-plane.d.ts.map +1 -1
  43. package/dist/src/control-plane-redis/redis-control-plane.js +22 -6
  44. package/dist/src/control-plane-redis/redis-control-plane.js.map +1 -1
  45. package/dist/src/control-planes/factory.d.ts +6 -0
  46. package/dist/src/control-planes/factory.d.ts.map +1 -1
  47. package/dist/src/control-planes/factory.js +1 -0
  48. package/dist/src/control-planes/factory.js.map +1 -1
  49. package/dist/src/dashboard/gateway-adapter.d.ts +15 -5
  50. package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
  51. package/dist/src/dashboard/gateway-adapter.js +71 -7
  52. package/dist/src/dashboard/gateway-adapter.js.map +1 -1
  53. package/dist/src/dashboard/handlers.d.ts +53 -1
  54. package/dist/src/dashboard/handlers.d.ts.map +1 -1
  55. package/dist/src/dashboard/handlers.js +200 -20
  56. package/dist/src/dashboard/handlers.js.map +1 -1
  57. package/dist/src/dashboard/index.d.ts +2 -2
  58. package/dist/src/dashboard/index.d.ts.map +1 -1
  59. package/dist/src/dashboard/index.js +2 -2
  60. package/dist/src/dashboard/index.js.map +1 -1
  61. package/dist/src/dashboard/openapi.d.ts.map +1 -1
  62. package/dist/src/dashboard/openapi.js +71 -1
  63. package/dist/src/dashboard/openapi.js.map +1 -1
  64. package/dist/src/define_config.d.ts +122 -8
  65. package/dist/src/define_config.d.ts.map +1 -1
  66. package/dist/src/define_config.js.map +1 -1
  67. package/dist/src/diagnostics-bridge.d.ts +16 -3
  68. package/dist/src/diagnostics-bridge.d.ts.map +1 -1
  69. package/dist/src/diagnostics-bridge.js +9 -3
  70. package/dist/src/diagnostics-bridge.js.map +1 -1
  71. package/dist/src/discovery-roots.d.ts +92 -0
  72. package/dist/src/discovery-roots.d.ts.map +1 -0
  73. package/dist/src/discovery-roots.js +150 -0
  74. package/dist/src/discovery-roots.js.map +1 -0
  75. package/dist/src/engine.d.ts +428 -21
  76. package/dist/src/engine.d.ts.map +1 -1
  77. package/dist/src/engine.js +1566 -164
  78. package/dist/src/engine.js.map +1 -1
  79. package/dist/src/entities.d.ts +36 -1
  80. package/dist/src/entities.d.ts.map +1 -1
  81. package/dist/src/entities.js +65 -4
  82. package/dist/src/entities.js.map +1 -1
  83. package/dist/src/errors.d.ts +46 -0
  84. package/dist/src/errors.d.ts.map +1 -1
  85. package/dist/src/errors.js +48 -0
  86. package/dist/src/errors.js.map +1 -1
  87. package/dist/src/event-trigger-bridge.d.ts +8 -0
  88. package/dist/src/event-trigger-bridge.d.ts.map +1 -1
  89. package/dist/src/event-trigger-bridge.js +24 -3
  90. package/dist/src/event-trigger-bridge.js.map +1 -1
  91. package/dist/src/handshake/announced.d.ts +27 -0
  92. package/dist/src/handshake/announced.d.ts.map +1 -1
  93. package/dist/src/handshake/announced.js +62 -0
  94. package/dist/src/handshake/announced.js.map +1 -1
  95. package/dist/src/handshake/directory.d.ts +92 -0
  96. package/dist/src/handshake/directory.d.ts.map +1 -0
  97. package/dist/src/handshake/directory.js +139 -0
  98. package/dist/src/handshake/directory.js.map +1 -0
  99. package/dist/src/hooks/barrel-sources.d.ts +28 -0
  100. package/dist/src/hooks/barrel-sources.d.ts.map +1 -0
  101. package/dist/src/hooks/barrel-sources.js +92 -0
  102. package/dist/src/hooks/barrel-sources.js.map +1 -0
  103. package/dist/src/hooks/steps.d.ts +12 -3
  104. package/dist/src/hooks/steps.d.ts.map +1 -1
  105. package/dist/src/hooks/steps.js +7 -11
  106. package/dist/src/hooks/steps.js.map +1 -1
  107. package/dist/src/hooks/workflows.d.ts +12 -3
  108. package/dist/src/hooks/workflows.d.ts.map +1 -1
  109. package/dist/src/hooks/workflows.js +7 -13
  110. package/dist/src/hooks/workflows.js.map +1 -1
  111. package/dist/src/index.d.ts +12 -3
  112. package/dist/src/index.d.ts.map +1 -1
  113. package/dist/src/index.js +12 -3
  114. package/dist/src/index.js.map +1 -1
  115. package/dist/src/interfaces.d.ts +309 -21
  116. package/dist/src/interfaces.d.ts.map +1 -1
  117. package/dist/src/interfaces.js +10 -0
  118. package/dist/src/interfaces.js.map +1 -1
  119. package/dist/src/metrics.d.ts.map +1 -1
  120. package/dist/src/metrics.js +5 -0
  121. package/dist/src/metrics.js.map +1 -1
  122. package/dist/src/namespaces.d.ts +17 -0
  123. package/dist/src/namespaces.d.ts.map +1 -0
  124. package/dist/src/namespaces.js +24 -0
  125. package/dist/src/namespaces.js.map +1 -0
  126. package/dist/src/on-error.d.ts +4 -1
  127. package/dist/src/on-error.d.ts.map +1 -1
  128. package/dist/src/on-error.js +15 -2
  129. package/dist/src/on-error.js.map +1 -1
  130. package/dist/src/otel/durable-metrics.d.ts.map +1 -1
  131. package/dist/src/otel/durable-metrics.js +5 -0
  132. package/dist/src/otel/durable-metrics.js.map +1 -1
  133. package/dist/src/otel/durable-otel.d.ts.map +1 -1
  134. package/dist/src/otel/durable-otel.js +5 -0
  135. package/dist/src/otel/durable-otel.js.map +1 -1
  136. package/dist/src/pollers.d.ts +10 -1
  137. package/dist/src/pollers.d.ts.map +1 -1
  138. package/dist/src/pollers.js +15 -5
  139. package/dist/src/pollers.js.map +1 -1
  140. package/dist/src/protocol.d.ts +12 -0
  141. package/dist/src/protocol.d.ts.map +1 -1
  142. package/dist/src/protocol.js +27 -1
  143. package/dist/src/protocol.js.map +1 -1
  144. package/dist/src/remote-event.d.ts +6 -0
  145. package/dist/src/remote-event.d.ts.map +1 -0
  146. package/dist/src/remote-event.js +19 -0
  147. package/dist/src/remote-event.js.map +1 -0
  148. package/dist/src/remote-workflow-executor.d.ts +34 -12
  149. package/dist/src/remote-workflow-executor.d.ts.map +1 -1
  150. package/dist/src/remote-workflow-executor.js +42 -40
  151. package/dist/src/remote-workflow-executor.js.map +1 -1
  152. package/dist/src/run-detail-summary.d.ts +90 -3
  153. package/dist/src/run-detail-summary.d.ts.map +1 -1
  154. package/dist/src/run-detail-summary.js +360 -2
  155. package/dist/src/run-detail-summary.js.map +1 -1
  156. package/dist/src/run-facets.d.ts +48 -0
  157. package/dist/src/run-facets.d.ts.map +1 -0
  158. package/dist/src/run-facets.js +100 -0
  159. package/dist/src/run-facets.js.map +1 -0
  160. package/dist/src/run-gateway/interface.d.ts +27 -1
  161. package/dist/src/run-gateway/interface.d.ts.map +1 -1
  162. package/dist/src/run-gateway/proxy-run-gateway.d.ts +32 -4
  163. package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
  164. package/dist/src/run-gateway/proxy-run-gateway.js +101 -5
  165. package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
  166. package/dist/src/run-gateway/run-request-responder.d.ts +18 -5
  167. package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
  168. package/dist/src/run-gateway/run-request-responder.js +155 -32
  169. package/dist/src/run-gateway/run-request-responder.js.map +1 -1
  170. package/dist/src/run-gateway/store-run-gateway.d.ts +23 -1
  171. package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
  172. package/dist/src/run-gateway/store-run-gateway.js +38 -0
  173. package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
  174. package/dist/src/scheduler.d.ts +44 -3
  175. package/dist/src/scheduler.d.ts.map +1 -1
  176. package/dist/src/scheduler.js +36 -1
  177. package/dist/src/scheduler.js.map +1 -1
  178. package/dist/src/search-attributes-schema.d.ts +44 -0
  179. package/dist/src/search-attributes-schema.d.ts.map +1 -0
  180. package/dist/src/search-attributes-schema.js +51 -0
  181. package/dist/src/search-attributes-schema.js.map +1 -0
  182. package/dist/src/singleton-admission.d.ts +75 -0
  183. package/dist/src/singleton-admission.d.ts.map +1 -0
  184. package/dist/src/singleton-admission.js +156 -0
  185. package/dist/src/singleton-admission.js.map +1 -0
  186. package/dist/src/singleton-gate.d.ts +23 -8
  187. package/dist/src/singleton-gate.d.ts.map +1 -1
  188. package/dist/src/singleton-gate.js +59 -12
  189. package/dist/src/singleton-gate.js.map +1 -1
  190. package/dist/src/standard-schema.d.ts +86 -0
  191. package/dist/src/standard-schema.d.ts.map +1 -0
  192. package/dist/src/standard-schema.js +33 -0
  193. package/dist/src/standard-schema.js.map +1 -0
  194. package/dist/src/step-discovery.d.ts +21 -11
  195. package/dist/src/step-discovery.d.ts.map +1 -1
  196. package/dist/src/step-discovery.js +86 -45
  197. package/dist/src/step-discovery.js.map +1 -1
  198. package/dist/src/step-ref.d.ts +38 -12
  199. package/dist/src/step-ref.d.ts.map +1 -1
  200. package/dist/src/step-ref.js +5 -13
  201. package/dist/src/step-ref.js.map +1 -1
  202. package/dist/src/stores/factory.d.ts +6 -0
  203. package/dist/src/stores/factory.d.ts.map +1 -1
  204. package/dist/src/stores/factory.js +4 -1
  205. package/dist/src/stores/factory.js.map +1 -1
  206. package/dist/src/stores/lucid-helpers.d.ts +2 -0
  207. package/dist/src/stores/lucid-helpers.d.ts.map +1 -1
  208. package/dist/src/stores/lucid-helpers.js +7 -0
  209. package/dist/src/stores/lucid-helpers.js.map +1 -1
  210. package/dist/src/stores/lucid-mappers.d.ts +25 -0
  211. package/dist/src/stores/lucid-mappers.d.ts.map +1 -1
  212. package/dist/src/stores/lucid-mappers.js +81 -18
  213. package/dist/src/stores/lucid-mappers.js.map +1 -1
  214. package/dist/src/stores/lucid-schema.d.ts +59 -1
  215. package/dist/src/stores/lucid-schema.d.ts.map +1 -1
  216. package/dist/src/stores/lucid-schema.js +271 -25
  217. package/dist/src/stores/lucid-schema.js.map +1 -1
  218. package/dist/src/stores/lucid.d.ts +61 -2
  219. package/dist/src/stores/lucid.d.ts.map +1 -1
  220. package/dist/src/stores/lucid.js +346 -41
  221. package/dist/src/stores/lucid.js.map +1 -1
  222. package/dist/src/telescope/dashboard.d.ts.map +1 -1
  223. package/dist/src/telescope/dashboard.js +30 -1
  224. package/dist/src/telescope/dashboard.js.map +1 -1
  225. package/dist/src/telescope/data-providers.d.ts +19 -1
  226. package/dist/src/telescope/data-providers.d.ts.map +1 -1
  227. package/dist/src/telescope/data-providers.js +119 -3
  228. package/dist/src/telescope/data-providers.js.map +1 -1
  229. package/dist/src/telescope/extension.d.ts.map +1 -1
  230. package/dist/src/telescope/extension.js +2 -1
  231. package/dist/src/telescope/extension.js.map +1 -1
  232. package/dist/src/telescope/index.d.ts +1 -1
  233. package/dist/src/telescope/index.d.ts.map +1 -1
  234. package/dist/src/telescope/index.js +1 -1
  235. package/dist/src/telescope/index.js.map +1 -1
  236. package/dist/src/testing/in-memory-state-store.d.ts +44 -15
  237. package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
  238. package/dist/src/testing/in-memory-state-store.js +204 -41
  239. package/dist/src/testing/in-memory-state-store.js.map +1 -1
  240. package/dist/src/testing-kit/checkpoint-aggregation-contract.d.ts +16 -0
  241. package/dist/src/testing-kit/checkpoint-aggregation-contract.d.ts.map +1 -0
  242. package/dist/src/testing-kit/checkpoint-aggregation-contract.js +228 -0
  243. package/dist/src/testing-kit/checkpoint-aggregation-contract.js.map +1 -0
  244. package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
  245. package/dist/src/testing-kit/state-store-conformance.js +355 -3
  246. package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
  247. package/dist/src/transport-pool.d.ts +15 -1
  248. package/dist/src/transport-pool.d.ts.map +1 -1
  249. package/dist/src/transport-pool.js +50 -4
  250. package/dist/src/transport-pool.js.map +1 -1
  251. package/dist/src/transports/adaptive-concurrency.d.ts +168 -0
  252. package/dist/src/transports/adaptive-concurrency.d.ts.map +1 -0
  253. package/dist/src/transports/adaptive-concurrency.js +353 -0
  254. package/dist/src/transports/adaptive-concurrency.js.map +1 -0
  255. package/dist/src/transports/bullmq/adaptive-concurrency.d.ts +1 -165
  256. package/dist/src/transports/bullmq/adaptive-concurrency.d.ts.map +1 -1
  257. package/dist/src/transports/bullmq/adaptive-concurrency.js +3 -351
  258. package/dist/src/transports/bullmq/adaptive-concurrency.js.map +1 -1
  259. package/dist/src/transports/bullmq/bullmq-transport.d.ts +49 -1
  260. package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
  261. package/dist/src/transports/bullmq/bullmq-transport.js +330 -38
  262. package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
  263. package/dist/src/transports/bullmq/deps.d.ts +18 -2
  264. package/dist/src/transports/bullmq/deps.d.ts.map +1 -1
  265. package/dist/src/transports/bullmq/deps.js +44 -2
  266. package/dist/src/transports/bullmq/deps.js.map +1 -1
  267. package/dist/src/transports/db-helpers.d.ts +39 -0
  268. package/dist/src/transports/db-helpers.d.ts.map +1 -1
  269. package/dist/src/transports/db-helpers.js +73 -0
  270. package/dist/src/transports/db-helpers.js.map +1 -1
  271. package/dist/src/transports/db-remote.d.ts +53 -0
  272. package/dist/src/transports/db-remote.d.ts.map +1 -0
  273. package/dist/src/transports/db-remote.js +147 -0
  274. package/dist/src/transports/db-remote.js.map +1 -0
  275. package/dist/src/transports/db-schema.d.ts +44 -1
  276. package/dist/src/transports/db-schema.d.ts.map +1 -1
  277. package/dist/src/transports/db-schema.js +227 -88
  278. package/dist/src/transports/db-schema.js.map +1 -1
  279. package/dist/src/transports/db.d.ts +189 -6
  280. package/dist/src/transports/db.d.ts.map +1 -1
  281. package/dist/src/transports/db.js +1291 -137
  282. package/dist/src/transports/db.js.map +1 -1
  283. package/dist/src/transports/event-emitter.d.ts +8 -0
  284. package/dist/src/transports/event-emitter.d.ts.map +1 -1
  285. package/dist/src/transports/event-emitter.js +36 -3
  286. package/dist/src/transports/event-emitter.js.map +1 -1
  287. package/dist/src/transports/factory.d.ts +110 -5
  288. package/dist/src/transports/factory.d.ts.map +1 -1
  289. package/dist/src/transports/factory.js +56 -9
  290. package/dist/src/transports/factory.js.map +1 -1
  291. package/dist/src/transports/queue-mock-adapter.d.ts +7 -2
  292. package/dist/src/transports/queue-mock-adapter.d.ts.map +1 -1
  293. package/dist/src/transports/queue-mock-adapter.js +18 -2
  294. package/dist/src/transports/queue-mock-adapter.js.map +1 -1
  295. package/dist/src/transports/queue-remote.d.ts +146 -0
  296. package/dist/src/transports/queue-remote.d.ts.map +1 -0
  297. package/dist/src/transports/queue-remote.js +398 -0
  298. package/dist/src/transports/queue-remote.js.map +1 -0
  299. package/dist/src/transports/queue.d.ts +159 -6
  300. package/dist/src/transports/queue.d.ts.map +1 -1
  301. package/dist/src/transports/queue.js +747 -37
  302. package/dist/src/transports/queue.js.map +1 -1
  303. package/dist/src/transports/task-failure.d.ts +31 -0
  304. package/dist/src/transports/task-failure.d.ts.map +1 -0
  305. package/dist/src/transports/task-failure.js +50 -0
  306. package/dist/src/transports/task-failure.js.map +1 -0
  307. package/dist/src/workflow-als.d.ts +1 -1
  308. package/dist/src/workflow-als.d.ts.map +1 -1
  309. package/dist/src/workflow-ctx.d.ts +10 -0
  310. package/dist/src/workflow-ctx.d.ts.map +1 -1
  311. package/dist/src/workflow-ctx.js +97 -15
  312. package/dist/src/workflow-ctx.js.map +1 -1
  313. package/dist/src/workflow-discovery.d.ts +32 -41
  314. package/dist/src/workflow-discovery.d.ts.map +1 -1
  315. package/dist/src/workflow-discovery.js +65 -88
  316. package/dist/src/workflow-discovery.js.map +1 -1
  317. package/dist/src/workflow-ref.d.ts +26 -0
  318. package/dist/src/workflow-ref.d.ts.map +1 -1
  319. package/dist/src/workflow-ref.js.map +1 -1
  320. package/dist/stubs/config/durable.stub +6 -0
  321. package/dist/stubs/config/durable_dashboard.stub +7 -3
  322. package/dist/stubs/database/migrations/create_durable_tables.stub +2 -1
  323. package/dist/stubs/database/migrations/create_durable_transport_messages.stub +43 -0
  324. package/dist/stubs/database/migrations/create_durable_transport_tables.stub +12 -9
  325. package/dist/stubs/database/migrations/create_durable_transport_worker_keys.stub +1 -1
  326. package/dist/stubs/make/workflow/main.stub +1 -1
  327. package/package.json +6 -4
  328. package/skills/durable-cluster/SKILL.md +80 -11
  329. package/skills/durable-determinism/SKILL.md +11 -5
  330. package/skills/durable-reliability/SKILL.md +77 -37
  331. package/skills/durable-setup/SKILL.md +51 -22
  332. package/skills/durable-transports-stores/SKILL.md +128 -18
  333. package/skills/durable-workflows/SKILL.md +83 -18
  334. package/dist/assets/spa/assets/index-DdB9zxuX.js +0 -16
  335. package/dist/assets/spa/assets/index-DdB9zxuX.js.map +0 -1
@@ -14,7 +14,7 @@ license: MIT
14
14
  metadata:
15
15
  type: core
16
16
  library: "@adonis-agora/durable"
17
- library_version: "0.25.0"
17
+ library_version: "0.51.0"
18
18
  framework: adonisjs
19
19
  sources:
20
20
  - 'DavideCarvalho/adonis-durable:docs/transports/index.mdx'
@@ -59,6 +59,11 @@ export default defineConfig({
59
59
 
60
60
  Switching transports is a one-line change; workflow code never changes.
61
61
 
62
+ `transports.eventEmitter({ onError })` reports a failing result/heartbeat
63
+ handler (default `console.error`) and redelivers a failed result from memory
64
+ with backoff until `close()`. Before 0.50 such a failure was an unhandled
65
+ rejection — fatal under Node's default — and the result was lost.
66
+
62
67
  ## Core patterns
63
68
 
64
69
  ### Pattern 1 — cross-process steps without new infrastructure (`db`)
@@ -75,10 +80,51 @@ const transport = new DbTransport({ db }) // { partition } isolates this pool
75
80
  transport.handle('pipeline:extract', async (input) => extract(input))
76
81
  ```
77
82
 
83
+ A worker runs ONE task row at a time unless `concurrency` is raised
84
+ (`transports.db({ concurrency: 8, resultConcurrency: 8 })`, or `'adaptive'` with
85
+ the same controller and fields as `queue`/`bullmq`); one poll loop serves every
86
+ handler name and claims only as many rows as it has free slots. Every held
87
+ claim is renewed every `leaseMs / 3`, and a row is recorded (task delete +
88
+ result insert, one transaction) only while `claimed_by` is still the worker's
89
+ token — a worker whose claim was taken over drops its result and reports it to
90
+ `onError`. (Before 0.50 the loop claimed 20 rows per poll, renewed only the
91
+ running one, and double-ran the rest of the batch when a step outlived
92
+ `leaseMs` — upgrade if `db` steps run twice.) Idle task/heartbeat/control loops back off up to `idleBackoffMax`
93
+ (default 4) poll intervals; results never back off.
94
+
95
+ Workers beat `durable[-<ns>]-worker-heartbeat:<token>:<instance>` into
96
+ `durable_transport_worker_keys` with their `WorkerStatus`, and `DbTransport`
97
+ implements `groupHealth`: `depth` = pending + claimed task rows of the group.
98
+ Each takeover of a lapsed claim bumps the row's `reclaims` column; past
99
+ `maxStalledCount` (default 3) the row is deleted and a retryable failed
100
+ `StepResult` (`remote step worker failed: claim lapsed more than N times`) is
101
+ published. `close()` (after `closeTimeoutMs`, default 5s) clears every claim it
102
+ still holds, uncounted, so another worker takes the rows at once and a deploy
103
+ never pushes a step toward the poison bound; only a crash (no close) counts.
104
+ Dispatch `priority` is stored on the row (`0` when absent = normal) and rows
105
+ are claimed highest priority first, then oldest. `autoCreate` /
106
+ `createDurableTransportTables` add `reclaims` and `priority` to an existing
107
+ table; with `autoCreate: false` add them yourself (integer, not null, default
108
+ 0), or the poison bound / priority ordering stay off.
109
+
110
+ `db` also carries remote workflow turns (task rows of kind `workflow`, claimed
111
+ with the token's steps; each decision is a `durable_transport_messages` row
112
+ addressed to the engine replica that dispatched the turn, though any replica
113
+ could apply it) and store-less tenant
114
+ pods that reach the same database: start-run/run-request/step-event rows go to
115
+ any consumer, a reply is a row addressed to the requester, a tenant event is one
116
+ row per live subscriber (pods write their own `-inbox:` / `-tenant-subscriber:`
117
+ keys to `durable_transport_worker_keys`). A janitor on every instance that
118
+ addresses rows deletes those older than `inboxTtlMs` whose recipient's inbox key
119
+ is gone. Ships as the `create_durable_transport_messages` migration (also adds
120
+ the tasks `kind` column) or via `autoCreate`; without it the protocol is off and
121
+ said once through `onError`. Tune with `inboxTtlMs`, `tenantSubscribersCacheMs`,
122
+ `inboxSweepIntervalMs`. See `durable-cluster`.
123
+
78
124
  Trade-off: throughput is bounded by polling + row contention — right for
79
125
  workflow-scale modest rates, not high-fanout firehoses. Delivery is
80
- at-least-once (crash between insert-result and delete-task), so handlers should
81
- be idempotent on `step_id`. Source: `docs/transports/db.mdx`.
126
+ at-least-once (a worker that dies after its handler ran re-runs elsewhere), so
127
+ handlers should be idempotent on `step_id`. Source: `docs/transports/db.mdx`.
82
128
 
83
129
  ### Pattern 2 — real brokers (`queue`, `bullmq`) and stalled-claim tuning
84
130
 
@@ -91,9 +137,9 @@ export default defineConfig({
91
137
  transports: {
92
138
  queue: transports.queue({
93
139
  connection: 'redis',
94
- // A claim is NEVER renewed while a worker processes it — its age is the
95
- // step's elapsed time. Raise the threshold above your longest step:
96
- stalledThresholdMs: 2 * 60 * 60 * 1000, // default 30min
140
+ // Claims are renewed while a step runs, so this bounds worker SILENCE
141
+ // (crash, or a blocked event loop), not step length. Default 2min:
142
+ stalledThresholdMs: 5 * 60 * 1000,
97
143
  maxStalledCount: 3, // bounds a poison job
98
144
  onError: (err) => logger.error({ err }, 'durable queue transport'),
99
145
  }),
@@ -102,11 +148,37 @@ export default defineConfig({
102
148
  ```
103
149
 
104
150
  One instance consumes engine-side results; run one or more `node ace
105
- durable:work` workers serving the step names. Source:
151
+ durable:work` workers serving the step names. A worker runs ONE job of a step
152
+ name at a time unless `concurrency` is raised (`transports.queue({ connection,
153
+ concurrency: 8, resultConcurrency: 8 })`); the slots share one poll loop.
154
+ `concurrency: 'adaptive'` (or `{ mode: 'adaptive', min, max, start, tickMs,
155
+ ramCeilingPct, cpuCeilingPct }`) runs the same controller as `bullmq` (below);
156
+ each step name's loop reads the live limit every poll. Idle
157
+ task/heartbeat/control loops back off up to `idleBackoffMax × pollIntervalMs`
158
+ (default 4 × 200ms; `1` disables).
159
+
160
+ A `queue` worker beats `<prefix>-worker-heartbeat:<token>:<instance>` into the
161
+ worker-key store (10s, live 35s) with its `WorkerStatus`, and the transport
162
+ implements `groupHealth` when a store is configured (the default with a database;
163
+ absent with `workerKeys: false`). Its `depth` is the PENDING count (`sizeOf`) only —
164
+ the adapter contract can't count jobs in flight or delayed retries. A task that
165
+ stalls past `maxStalledCount` is failed AND bridged into a retryable failed
166
+ `StepResult` (`remote step worker failed: stalled more than N times`) on
167
+ `@adonisjs/queue` 0.7; on 0.6 the adapter deletes it internally and nothing is
168
+ published, so rely on `timeoutMs` / `remoteRedispatchMs` there. Source:
106
169
  `docs/transports/queue.mdx`.
107
170
 
171
+ `queue` also carries remote workflow turns (`<prefix>:decisions`,
172
+ `<prefix>:step-events`) and store-less tenant pods: start-run/run-request are
173
+ plain jobs to the control plane, replies go to a per-pod
174
+ `<prefix>:run-replies:<instanceId>` queue, and tenant events are pushed to each
175
+ subscribed pod's `<prefix>:tenant-events:<instanceId>` (subscriptions announced
176
+ on `<prefix>:inbox-registrations`, recorded in the control plane's key store;
177
+ tune with `inboxTtlMs`, `tenantSubscribersCacheMs`, `inboxSweepIntervalMs`).
178
+ See `durable-cluster`.
179
+
108
180
  For wide fans (`ctx.all` over thousands of steps), raise how many results an
109
- instance settles at once on `bullmq` (`queue` and `db` have no such option),
181
+ instance settles at once on `bullmq`, `queue` or `db` (`resultConcurrency`),
110
182
  let its workers size their step concurrency, and size the engine's due-timer
111
183
  batch:
112
184
 
@@ -115,7 +187,7 @@ export default defineConfig({
115
187
  timerBatchSize: 1000, // due runs per timer pass, most overdue first (default 500)
116
188
  transports: {
117
189
  bullmq: transports.bullmq({
118
- connection: { host: '127.0.0.1', port: 6379 },
190
+ connection: { host: '127.0.0.1', port: 6379 }, // or 'redis://…' / 'rediss://…' (0.50+)
119
191
  resultConcurrency: 16, // default 1
120
192
  // A number pins each task queue's concurrency (default 1). 'adaptive' moves it
121
193
  // every tickMs: +1 while steps saturate the limit at steady latency, down by the
@@ -133,6 +205,17 @@ work and progress); a non-finite or out-of-range field falls back to its default
133
205
  throughput covers the trailing minute, decaying while the worker is idle and reading 0 once a minute passes with no completed step; with a fixed limit the heartbeat still reports `cpuPct`, sampled at most once every 2s. In adaptive mode the decisions, step-events, start-run and run-request consumers run
134
206
  at 1 (results run at `resultConcurrency`). Sources: `docs/transports/index.mdx`, `docs/cluster/interop.mdx`.
135
207
 
208
+ Two `bullmq` safety nets: `pingIntervalMs` (default 30s; `0`/`false` off) PINGs
209
+ every pub/sub subscriber (heartbeats, run replies, tenant events) and reconnects
210
+ one a VPN/NAT/idle timeout dropped silently, so autoResubscribe restores it; each
211
+ subscriber also gets an `error` listener (reported once per burst to `onError`).
212
+ `stepTimeoutMs` (off by default) fails a step handler still running at the
213
+ ceiling as RETRYABLE, so a wedged `await` cannot hold its job forever (BullMQ
214
+ renews the lock on a timer, so the job never stalls). A `redis://` /
215
+ `rediss://` connection string is parsed into ioredis options since 0.50; before
216
+ 0.50 it was silently ignored and every client connected to `127.0.0.1:6379`.
217
+ Source: `docs/transports/bullmq.mdx`.
218
+
136
219
  ### Pattern 3 — multi-replica fan-out: the control plane
137
220
 
138
221
  The task transport is point-to-point; lifecycle events and cancellation need a
@@ -214,30 +297,56 @@ without it the engine loads the run's checkpoints per result), implement
214
297
  (without it a blind save can put a settled step back to `pending`), and return
215
298
  `listDueTimers` most overdue first, with or without a `limit`. Run
216
299
  `runStateStoreContract` from `@adonis-agora/durable/testing/conformance`, which
217
- covers all three and fails a store missing either method.
300
+ covers all three and fails a store missing either method. The contract also
301
+ requires `saveCheckpointsIfPending` (batched result settling), `deleteRuns`
302
+ (batched retention/purge deletes), `runFacets`, `pruneBufferedEvents`
303
+ (`eventBufferTtl`) and the `checkpointStats` + `queryCheckpoints` pair (the run
304
+ detail from aggregates, with an ETag the console polls with `If-None-Match`).
305
+
306
+ The Lucid schema check runs at every boot but is gated by a fingerprint row in
307
+ `durable_schema_meta`: a current schema costs two statements. A stale or missing
308
+ marker runs the full check under `pg_advisory_xact_lock` / MySQL `GET_LOCK`, so
309
+ pods booting together don't race `CREATE TABLE`. Bump `DURABLE_SCHEMA_REVISION`
310
+ with any DDL change; a hand change behind the marker needs `{ force: true }`. On
311
+ MySQL the JSON columns are `longtext` (an old `text` column is widened in place;
312
+ a table rebuild, so run the migration in a maintenance window for big tables),
313
+ and `stores.lucid({ collation })` aligns the tables to the app's collation. A
314
+ JSON column that does not parse reads back as `undefined` and is reported once
315
+ (`onCorruptJsonColumn`) instead of failing every read that touches the row.
316
+
317
+ To run singleton workflows a custom store also needs `tryAdmitSingleton` (one transaction that
318
+ locks before it reads, built on the exported `singletonAdmissionTags` /
319
+ `singletonAdmissionDenialPatch` including the pre-0.51 holder count, and a terminal `updateRun`/`updateRunIf` that
320
+ drops both singleton markers (`durable:singleton:admitted` and `durable:singleton:tracked`, via `withoutSingletonMarkers`)); without it singletons fall back
321
+ to the old non-atomic scan and the engine warns once naming `StateStore.tryAdmitSingleton`.
218
322
 
219
323
  Source: `docs/stores/lucid.mdx`, `docs/stores/index.mdx`.
220
324
 
221
325
  ## Common mistakes
222
326
 
223
- ### HIGH setting `stalledThresholdMs` below the longest legitimate step
327
+ ### HIGH blocking the event loop longer than `stalledThresholdMs`
224
328
 
225
329
  Wrong:
226
330
 
227
331
  ```ts
228
- transports.queue({ connection: 'redis', stalledThresholdMs: 60_000 }) // imports take ~90min
332
+ // a step body that renders a report synchronously for ~3 minutes, default 2min threshold
333
+ transports.queue({ connection: 'redis' })
229
334
  ```
230
335
 
231
336
  Correct:
232
337
 
233
338
  ```ts
234
- transports.queue({ connection: 'redis', stalledThresholdMs: 2 * 60 * 60 * 1000 })
339
+ transports.queue({ connection: 'redis', stalledThresholdMs: 5 * 60 * 1000 })
340
+ // or keep the heavy work async / off the main thread
235
341
  ```
236
342
 
237
- Mechanism: a claim's age equals the step's elapsed runtime because nothing
238
- renews it mid-processing; a threshold below the longest step redelivers jobs
239
- from merely-slow workers and double-runs the step. For tighter detection use
240
- per-step `timeoutMs`, which IS heartbeat-aware.
343
+ Mechanism: the transport renews every in-flight claim each half threshold
344
+ (`adapter.renewJobs`), so a long but async step keeps its claim forever. A
345
+ synchronous body starves the renewal timer; once the claim goes unrenewed past
346
+ the threshold the sweep re-delivers it and the step runs twice — reported to
347
+ `onError` as `lost N of M claim(s)`. Raising the threshold to cover step LENGTH
348
+ is no longer needed: before 0.50 the `queue` transport never renewed claims
349
+ (30min default threshold, long steps re-delivered); upgrade instead.
241
350
  Source: `docs/transports/queue.mdx` ("Stalled-job reclaim").
242
351
 
243
352
  ### HIGH hand-copying the DDL into the migration
@@ -309,7 +418,8 @@ transports.db({ partition: 'tenant-a' })
309
418
  Mechanism: routing is by handler name — an instance serves whatever names it
310
419
  registers, so there is no group to declare. Isolation comes from `partition`
311
420
  (suffixes routing tokens `<name>@<partition>`) plus `namespace` (scopes which
312
- runs this engine polls). Source: `docs/transports/db.mdx` options table,
421
+ runs this engine polls; absent = `'default'` on standalone, every pool on a
422
+ control plane). Source: `docs/transports/db.mdx` options table,
313
423
  `docs/concepts/tenancy.mdx`.
314
424
 
315
425
  ### MEDIUM wrapping a store in CodecStateStore and expecting heartbeats
@@ -17,7 +17,7 @@ license: MIT
17
17
  metadata:
18
18
  type: core
19
19
  library: "@adonis-agora/durable"
20
- library_version: "0.25.0"
20
+ library_version: "0.51.0"
21
21
  framework: adonisjs
22
22
  sources:
23
23
  - 'DavideCarvalho/adonis-durable:docs/authoring/app-workflows.mdx'
@@ -37,10 +37,17 @@ and registers every exported class — you never call `engine.register` by hand
37
37
  (it remains the low-level escape hatch). The convention mirrors `@adonisjs/queue`
38
38
  jobs under `app/jobs`; scaffold with `node ace make:workflow order`.
39
39
 
40
+ A modular app (one folder per bounded context) lists several directories:
41
+ `workflowsPath` and `stepsPath` in `config/durable.ts` take an array, and so
42
+ does the `source` option of the `workflowsHook` / `stepsHook` codegen hooks.
43
+ Names must be unique across those directories. To register one more directory
44
+ from a preload, call `registerWorkflowsFromDir(engine, dir)` and
45
+ `registerStepsFromDir(engine, dir)`. The engine serves steps through its
46
+ transport (`engine.handle`).
47
+
40
48
  ## Setup
41
49
 
42
50
  ```ts title="app/workflows/checkout_workflow.ts"
43
- import { inject } from '@adonisjs/core'
44
51
  import { BaseWorkflow } from '@adonis-agora/durable'
45
52
  import type { WorkflowCtx } from '@adonis-agora/durable'
46
53
 
@@ -62,7 +69,8 @@ export default class CheckoutWorkflow extends BaseWorkflow {
62
69
  ```
63
70
 
64
71
  Run it from anywhere: `CheckoutWorkflow.dispatch(input, { runId })` enqueues;
65
- `CheckoutWorkflow.start(input)` awaits the settled result. Both statics are
72
+ `CheckoutWorkflow.start(input)` blocks until the run is terminal and returns its
73
+ output. Both statics are
66
74
  context-aware — inside a running body they become a linked child /
67
75
  fire-and-forget child instead of a top-level run.
68
76
 
@@ -75,7 +83,7 @@ Steps are discovered from `app/steps` and served by name automatically. A
75
83
 
76
84
  ```ts title="app/steps/payment_steps.ts"
77
85
  import { Step } from '@adonis-agora/durable'
78
- import { z } from 'zod'
86
+ import { z } from 'zod' // any Standard Schema library: zod, valibot, arktype, …
79
87
 
80
88
  export default class PaymentSteps {
81
89
  @Step({
@@ -120,6 +128,28 @@ constructor as wiring; keep per-run state in input and step outputs, never on
120
128
  `this`. Injected services are for **composing**, not doing work: reach them
121
129
  through step refs so the work is checkpointed.
122
130
 
131
+ Schemas are **any Standard Schema** (zod 3.24+/4, valibot, arktype, effect
132
+ schema, …) — the package depends on none of them. They fit every schema slot:
133
+ `@Step({ input, output })` / `defineStep` (with an unannotated handler the types
134
+ are inferred from the schema), `static workflow = { inputSchema }` (validated at
135
+ `start`, nothing created on failure; the run stores the input as sent and
136
+ `run` receives the schema's output, re-parsed before every turn — keep the schema
137
+ deterministic and bump `version` when it changes), and `static workflow = { searchAttributes }` (every
138
+ `ctx.upsertSearchAttributes` is validated against the merged attributes; type
139
+ the ctx as `WorkflowCtx<InferSearchAttributes<typeof schema>>` and read back with
140
+ `attributesOf(WorkflowClass, run)`). A failure is a `SchemaValidationError`
141
+ (`FatalError`, code `invalid_input` / `invalid_output` — `schema_validation` for a
142
+ `searchAttributes` write — issues in `details.issues`) — never
143
+ retried.
144
+
145
+ ```ts
146
+ import * as v from 'valibot'
147
+
148
+ export const charge = defineStep('billing:charge', async (input) => input.amount * 100, {
149
+ input: v.object({ amount: v.number() }), // input: { amount: number }, inferred
150
+ })
151
+ ```
152
+
123
153
  ### Pattern 2 — pausing durably: sleeps, signals, events
124
154
 
125
155
  All three suspend the run with zero compute and survive restarts.
@@ -151,15 +181,12 @@ events with `{ onEvent: ['payment.settled'] }`; coalesce chatty sources with
151
181
  `{ mode: 'batch', maxSize: 50, windowMs: 60_000 }` (receives `{ events: [...] }`).
152
182
 
153
183
  ```ts title="app/controllers/approvals_controller.ts"
154
- import { inject } from '@adonisjs/core'
155
- import { WorkflowEngine } from '@adonis-agora/durable'
184
+ import type { HttpContext } from '@adonisjs/core/http'
185
+ import engine from '@adonis-agora/durable/services/main'
156
186
 
157
- @inject()
158
187
  export default class ApprovalsController {
159
- constructor(private engine: WorkflowEngine) {}
160
-
161
188
  async approve({ params, request, response }: HttpContext) {
162
- await this.engine.signal(`approve:${params.id}`, request.body())
189
+ await engine.signal(`approve:${params.id}`, request.body())
163
190
  return response.noContent()
164
191
  }
165
192
  }
@@ -181,9 +208,9 @@ use `ctx.allSettled(Workflow, inputs, { concurrency })` instead: the same fan,
181
208
  resolving with `{ status: 'fulfilled', value } | { status: 'rejected', id, error, code?, details? }`
182
209
  per input and never throwing for a child. Child ids derive from the call position and
183
210
  are replay-stable; never pass a random one. `{ concurrency: n }` caps how many
184
- children run at once (waves of `floor(n / 2)`, two at a time), and the wave goes into each child
185
- id), so changing it for an in-flight run is non-deterministic, like changing
186
- `inputs`.
211
+ children run at once (waves of `floor(n / 2)`, two at a time). The wave is part of
212
+ each child id (`<runId>.all.<firstSeq>.w<k>.<i>`), so changing it for an in-flight
213
+ run is non-deterministic, like changing `inputs`.
187
214
 
188
215
  For the same step over many inputs, pass the step reference (`defineStep` ref
189
216
  or `@Step` method) to `ctx.all`. It
@@ -203,8 +230,10 @@ Prefer it to `Promise.all` past a handful of steps (every result of a
203
230
  child-workflow `ctx.all` when each item is just a step. A `queue` or a
204
231
  `timeoutMs` is rejected on a step fan.
205
232
 
206
- Serialize runs per subject with a singleton — runs sharing a key queue FIFO and
207
- are race-free across instances:
233
+ Serialize runs per subject with a singleton — runs sharing a key queue FIFO.
234
+ Admission is an atomic store claim (`StateStore.tryAdmitSingleton`, built into the
235
+ Lucid and in-memory stores), and a holder keeps its slot until it settles
236
+ terminally, so replicas never over-admit and a later start never displaces it:
208
237
 
209
238
  ```ts
210
239
  export default class SyncShopWorkflow extends BaseWorkflow {
@@ -264,6 +293,16 @@ export default class DailyReportWorkflow extends BaseWorkflow {
264
293
  }
265
294
  ```
266
295
 
296
+ In `config/durable.ts`, name the workflow with a lazy import, as Adonis does for
297
+ controllers — never a direct class import, which can hang the boot (a service the
298
+ workflow imports awaits `app.booted` while the config is still loading):
299
+
300
+ ```ts
301
+ schedules: [
302
+ { key: 'daily-report', workflow: () => import('#workflows/daily_report_workflow'), cron: '0 2 * * *' },
303
+ ]
304
+ ```
305
+
267
306
  Each schedule window starts exactly once across a fleet via an idempotent
268
307
  time-bucket run id.
269
308
 
@@ -354,7 +393,7 @@ each replay breaks that dedup and re-fires the child. Source:
354
393
  Wrong:
355
394
 
356
395
  ```ts
357
- const result = await CheckoutWorkflow.start(order) // request held open until settle
396
+ const result = await CheckoutWorkflow.start(order) // request held open until the run is terminal
358
397
  ```
359
398
 
360
399
  Correct:
@@ -364,8 +403,9 @@ const { runId } = await CheckoutWorkflow.dispatch(order, { runId: `checkout:${or
364
403
  return response.accepted({ runId })
365
404
  ```
366
405
 
367
- Mechanism: `.start` resolves only when the run reaches a terminal state or
368
- `suspends` — hours later for a long workflow. Source: `docs/getting-started.mdx`
406
+ Mechanism: `.start` resolves only when the run reaches a terminal state — it
407
+ keeps waiting through every sleep/signal suspension, hours later for a long
408
+ workflow. Source: `docs/getting-started.mdx`
369
409
  (warning callout).
370
410
 
371
411
  ### MEDIUM awaiting a webhook with no timeout
@@ -419,5 +459,30 @@ caller suspended on `ctx.callEntity`; model expected rejections as returned
419
459
  discriminated results and reserve throws for genuine programmer errors.
420
460
  Source: `docs/authoring/entities.mdx` (warning callout).
421
461
 
462
+ ### MEDIUM the same workflow or step name in two discovery directories
463
+
464
+ Wrong:
465
+
466
+ ```ts
467
+ // config/durable.ts — both contexts declare a workflow named 'charge' (version 1)
468
+ export default defineConfig({
469
+ workflowsPath: ['app/orders/flows', 'app/billing/flows'],
470
+ })
471
+ ```
472
+
473
+ Correct:
474
+
475
+ ```ts
476
+ // app/billing/flows/charge_workflow.ts
477
+ static workflow = { name: 'billing.charge', version: '1' }
478
+ ```
479
+
480
+ Mechanism: names route runs and steps, so with several `workflowsPath` /
481
+ `stepsPath` directories boot throws `DuplicateDiscoveryError` naming both files
482
+ when two directories define the same workflow name + version or step name with
483
+ different classes. Prefix names by context. The hooks' `source` list must name
484
+ the same directories, otherwise the barrel and the runtime scan disagree.
485
+ Source: `docs/authoring/app-workflows.mdx` ("Several directories").
486
+
422
487
  See also: `durable-determinism` — why the body must stay deterministic and how
423
488
  to change it safely once live.