@adonis-agora/durable 0.49.1 → 0.50.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 (324) hide show
  1. package/CHANGELOG.md +251 -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/configure.d.ts.map +1 -1
  10. package/dist/configure.js +1 -0
  11. package/dist/configure.js.map +1 -1
  12. package/dist/providers/dashboard_provider.d.ts +2 -0
  13. package/dist/providers/dashboard_provider.d.ts.map +1 -1
  14. package/dist/providers/dashboard_provider.js +50 -6
  15. package/dist/providers/dashboard_provider.js.map +1 -1
  16. package/dist/providers/durable_provider.d.ts.map +1 -1
  17. package/dist/providers/durable_provider.js +143 -34
  18. package/dist/providers/durable_provider.js.map +1 -1
  19. package/dist/src/admission-redis/redis-admission-backend.d.ts.map +1 -1
  20. package/dist/src/admission-redis/redis-admission-backend.js +3 -1
  21. package/dist/src/admission-redis/redis-admission-backend.js.map +1 -1
  22. package/dist/src/ambient-step.d.ts +24 -0
  23. package/dist/src/ambient-step.d.ts.map +1 -0
  24. package/dist/src/ambient-step.js +42 -0
  25. package/dist/src/ambient-step.js.map +1 -0
  26. package/dist/src/base-workflow.d.ts +10 -1
  27. package/dist/src/base-workflow.d.ts.map +1 -1
  28. package/dist/src/base-workflow.js.map +1 -1
  29. package/dist/src/codec-state-store.d.ts +28 -2
  30. package/dist/src/codec-state-store.d.ts.map +1 -1
  31. package/dist/src/codec-state-store.js +61 -3
  32. package/dist/src/codec-state-store.js.map +1 -1
  33. package/dist/src/commands/worker.d.ts +4 -1
  34. package/dist/src/commands/worker.d.ts.map +1 -1
  35. package/dist/src/commands/worker.js +27 -3
  36. package/dist/src/commands/worker.js.map +1 -1
  37. package/dist/src/config_types.d.ts +6 -0
  38. package/dist/src/config_types.d.ts.map +1 -1
  39. package/dist/src/control-plane-redis/redis-control-plane.d.ts.map +1 -1
  40. package/dist/src/control-plane-redis/redis-control-plane.js +22 -6
  41. package/dist/src/control-plane-redis/redis-control-plane.js.map +1 -1
  42. package/dist/src/control-planes/factory.d.ts +6 -0
  43. package/dist/src/control-planes/factory.d.ts.map +1 -1
  44. package/dist/src/control-planes/factory.js +1 -0
  45. package/dist/src/control-planes/factory.js.map +1 -1
  46. package/dist/src/dashboard/gateway-adapter.d.ts +15 -5
  47. package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
  48. package/dist/src/dashboard/gateway-adapter.js +71 -7
  49. package/dist/src/dashboard/gateway-adapter.js.map +1 -1
  50. package/dist/src/dashboard/handlers.d.ts +53 -1
  51. package/dist/src/dashboard/handlers.d.ts.map +1 -1
  52. package/dist/src/dashboard/handlers.js +200 -20
  53. package/dist/src/dashboard/handlers.js.map +1 -1
  54. package/dist/src/dashboard/index.d.ts +2 -2
  55. package/dist/src/dashboard/index.d.ts.map +1 -1
  56. package/dist/src/dashboard/index.js +2 -2
  57. package/dist/src/dashboard/index.js.map +1 -1
  58. package/dist/src/dashboard/openapi.d.ts.map +1 -1
  59. package/dist/src/dashboard/openapi.js +71 -1
  60. package/dist/src/dashboard/openapi.js.map +1 -1
  61. package/dist/src/define_config.d.ts +122 -8
  62. package/dist/src/define_config.d.ts.map +1 -1
  63. package/dist/src/define_config.js.map +1 -1
  64. package/dist/src/diagnostics-bridge.d.ts +16 -3
  65. package/dist/src/diagnostics-bridge.d.ts.map +1 -1
  66. package/dist/src/diagnostics-bridge.js +9 -3
  67. package/dist/src/diagnostics-bridge.js.map +1 -1
  68. package/dist/src/discovery-roots.d.ts +92 -0
  69. package/dist/src/discovery-roots.d.ts.map +1 -0
  70. package/dist/src/discovery-roots.js +150 -0
  71. package/dist/src/discovery-roots.js.map +1 -0
  72. package/dist/src/engine.d.ts +418 -20
  73. package/dist/src/engine.d.ts.map +1 -1
  74. package/dist/src/engine.js +1523 -159
  75. package/dist/src/engine.js.map +1 -1
  76. package/dist/src/entities.d.ts +36 -1
  77. package/dist/src/entities.d.ts.map +1 -1
  78. package/dist/src/entities.js +65 -4
  79. package/dist/src/entities.js.map +1 -1
  80. package/dist/src/errors.d.ts +36 -0
  81. package/dist/src/errors.d.ts.map +1 -1
  82. package/dist/src/errors.js +44 -0
  83. package/dist/src/errors.js.map +1 -1
  84. package/dist/src/event-trigger-bridge.d.ts +8 -0
  85. package/dist/src/event-trigger-bridge.d.ts.map +1 -1
  86. package/dist/src/event-trigger-bridge.js +24 -3
  87. package/dist/src/event-trigger-bridge.js.map +1 -1
  88. package/dist/src/handshake/announced.d.ts +27 -0
  89. package/dist/src/handshake/announced.d.ts.map +1 -1
  90. package/dist/src/handshake/announced.js +62 -0
  91. package/dist/src/handshake/announced.js.map +1 -1
  92. package/dist/src/handshake/directory.d.ts +92 -0
  93. package/dist/src/handshake/directory.d.ts.map +1 -0
  94. package/dist/src/handshake/directory.js +139 -0
  95. package/dist/src/handshake/directory.js.map +1 -0
  96. package/dist/src/hooks/barrel-sources.d.ts +28 -0
  97. package/dist/src/hooks/barrel-sources.d.ts.map +1 -0
  98. package/dist/src/hooks/barrel-sources.js +92 -0
  99. package/dist/src/hooks/barrel-sources.js.map +1 -0
  100. package/dist/src/hooks/steps.d.ts +12 -3
  101. package/dist/src/hooks/steps.d.ts.map +1 -1
  102. package/dist/src/hooks/steps.js +7 -11
  103. package/dist/src/hooks/steps.js.map +1 -1
  104. package/dist/src/hooks/workflows.d.ts +12 -3
  105. package/dist/src/hooks/workflows.d.ts.map +1 -1
  106. package/dist/src/hooks/workflows.js +7 -13
  107. package/dist/src/hooks/workflows.js.map +1 -1
  108. package/dist/src/index.d.ts +12 -3
  109. package/dist/src/index.d.ts.map +1 -1
  110. package/dist/src/index.js +12 -3
  111. package/dist/src/index.js.map +1 -1
  112. package/dist/src/interfaces.d.ts +309 -21
  113. package/dist/src/interfaces.d.ts.map +1 -1
  114. package/dist/src/interfaces.js +10 -0
  115. package/dist/src/interfaces.js.map +1 -1
  116. package/dist/src/metrics.d.ts.map +1 -1
  117. package/dist/src/metrics.js +5 -0
  118. package/dist/src/metrics.js.map +1 -1
  119. package/dist/src/namespaces.d.ts +17 -0
  120. package/dist/src/namespaces.d.ts.map +1 -0
  121. package/dist/src/namespaces.js +24 -0
  122. package/dist/src/namespaces.js.map +1 -0
  123. package/dist/src/otel/durable-metrics.d.ts.map +1 -1
  124. package/dist/src/otel/durable-metrics.js +5 -0
  125. package/dist/src/otel/durable-metrics.js.map +1 -1
  126. package/dist/src/otel/durable-otel.d.ts.map +1 -1
  127. package/dist/src/otel/durable-otel.js +5 -0
  128. package/dist/src/otel/durable-otel.js.map +1 -1
  129. package/dist/src/pollers.d.ts +10 -1
  130. package/dist/src/pollers.d.ts.map +1 -1
  131. package/dist/src/pollers.js +15 -5
  132. package/dist/src/pollers.js.map +1 -1
  133. package/dist/src/protocol.d.ts +12 -0
  134. package/dist/src/protocol.d.ts.map +1 -1
  135. package/dist/src/protocol.js +27 -1
  136. package/dist/src/protocol.js.map +1 -1
  137. package/dist/src/remote-event.d.ts +6 -0
  138. package/dist/src/remote-event.d.ts.map +1 -0
  139. package/dist/src/remote-event.js +19 -0
  140. package/dist/src/remote-event.js.map +1 -0
  141. package/dist/src/remote-workflow-executor.d.ts +34 -12
  142. package/dist/src/remote-workflow-executor.d.ts.map +1 -1
  143. package/dist/src/remote-workflow-executor.js +42 -40
  144. package/dist/src/remote-workflow-executor.js.map +1 -1
  145. package/dist/src/run-detail-summary.d.ts +90 -3
  146. package/dist/src/run-detail-summary.d.ts.map +1 -1
  147. package/dist/src/run-detail-summary.js +360 -2
  148. package/dist/src/run-detail-summary.js.map +1 -1
  149. package/dist/src/run-facets.d.ts +48 -0
  150. package/dist/src/run-facets.d.ts.map +1 -0
  151. package/dist/src/run-facets.js +100 -0
  152. package/dist/src/run-facets.js.map +1 -0
  153. package/dist/src/run-gateway/interface.d.ts +27 -1
  154. package/dist/src/run-gateway/interface.d.ts.map +1 -1
  155. package/dist/src/run-gateway/proxy-run-gateway.d.ts +32 -4
  156. package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
  157. package/dist/src/run-gateway/proxy-run-gateway.js +101 -5
  158. package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
  159. package/dist/src/run-gateway/run-request-responder.d.ts +18 -5
  160. package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
  161. package/dist/src/run-gateway/run-request-responder.js +155 -32
  162. package/dist/src/run-gateway/run-request-responder.js.map +1 -1
  163. package/dist/src/run-gateway/store-run-gateway.d.ts +23 -1
  164. package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
  165. package/dist/src/run-gateway/store-run-gateway.js +38 -0
  166. package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
  167. package/dist/src/search-attributes-schema.d.ts +44 -0
  168. package/dist/src/search-attributes-schema.d.ts.map +1 -0
  169. package/dist/src/search-attributes-schema.js +51 -0
  170. package/dist/src/search-attributes-schema.js.map +1 -0
  171. package/dist/src/singleton-admission.d.ts +75 -0
  172. package/dist/src/singleton-admission.d.ts.map +1 -0
  173. package/dist/src/singleton-admission.js +156 -0
  174. package/dist/src/singleton-admission.js.map +1 -0
  175. package/dist/src/singleton-gate.d.ts +23 -8
  176. package/dist/src/singleton-gate.d.ts.map +1 -1
  177. package/dist/src/singleton-gate.js +59 -12
  178. package/dist/src/singleton-gate.js.map +1 -1
  179. package/dist/src/standard-schema.d.ts +83 -0
  180. package/dist/src/standard-schema.d.ts.map +1 -0
  181. package/dist/src/standard-schema.js +31 -0
  182. package/dist/src/standard-schema.js.map +1 -0
  183. package/dist/src/step-discovery.d.ts +21 -11
  184. package/dist/src/step-discovery.d.ts.map +1 -1
  185. package/dist/src/step-discovery.js +86 -45
  186. package/dist/src/step-discovery.js.map +1 -1
  187. package/dist/src/step-ref.d.ts +38 -12
  188. package/dist/src/step-ref.d.ts.map +1 -1
  189. package/dist/src/step-ref.js +5 -13
  190. package/dist/src/step-ref.js.map +1 -1
  191. package/dist/src/stores/factory.d.ts +6 -0
  192. package/dist/src/stores/factory.d.ts.map +1 -1
  193. package/dist/src/stores/factory.js +4 -1
  194. package/dist/src/stores/factory.js.map +1 -1
  195. package/dist/src/stores/lucid-helpers.d.ts +2 -0
  196. package/dist/src/stores/lucid-helpers.d.ts.map +1 -1
  197. package/dist/src/stores/lucid-helpers.js +7 -0
  198. package/dist/src/stores/lucid-helpers.js.map +1 -1
  199. package/dist/src/stores/lucid-mappers.d.ts +25 -0
  200. package/dist/src/stores/lucid-mappers.d.ts.map +1 -1
  201. package/dist/src/stores/lucid-mappers.js +81 -18
  202. package/dist/src/stores/lucid-mappers.js.map +1 -1
  203. package/dist/src/stores/lucid-schema.d.ts +59 -1
  204. package/dist/src/stores/lucid-schema.d.ts.map +1 -1
  205. package/dist/src/stores/lucid-schema.js +271 -25
  206. package/dist/src/stores/lucid-schema.js.map +1 -1
  207. package/dist/src/stores/lucid.d.ts +61 -2
  208. package/dist/src/stores/lucid.d.ts.map +1 -1
  209. package/dist/src/stores/lucid.js +346 -41
  210. package/dist/src/stores/lucid.js.map +1 -1
  211. package/dist/src/telescope/dashboard.d.ts.map +1 -1
  212. package/dist/src/telescope/dashboard.js +30 -1
  213. package/dist/src/telescope/dashboard.js.map +1 -1
  214. package/dist/src/telescope/data-providers.d.ts +19 -1
  215. package/dist/src/telescope/data-providers.d.ts.map +1 -1
  216. package/dist/src/telescope/data-providers.js +119 -3
  217. package/dist/src/telescope/data-providers.js.map +1 -1
  218. package/dist/src/telescope/extension.d.ts.map +1 -1
  219. package/dist/src/telescope/extension.js +2 -1
  220. package/dist/src/telescope/extension.js.map +1 -1
  221. package/dist/src/telescope/index.d.ts +1 -1
  222. package/dist/src/telescope/index.d.ts.map +1 -1
  223. package/dist/src/telescope/index.js +1 -1
  224. package/dist/src/telescope/index.js.map +1 -1
  225. package/dist/src/testing/in-memory-state-store.d.ts +44 -15
  226. package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
  227. package/dist/src/testing/in-memory-state-store.js +204 -41
  228. package/dist/src/testing/in-memory-state-store.js.map +1 -1
  229. package/dist/src/testing-kit/checkpoint-aggregation-contract.d.ts +16 -0
  230. package/dist/src/testing-kit/checkpoint-aggregation-contract.d.ts.map +1 -0
  231. package/dist/src/testing-kit/checkpoint-aggregation-contract.js +228 -0
  232. package/dist/src/testing-kit/checkpoint-aggregation-contract.js.map +1 -0
  233. package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
  234. package/dist/src/testing-kit/state-store-conformance.js +355 -3
  235. package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
  236. package/dist/src/transport-pool.d.ts +15 -1
  237. package/dist/src/transport-pool.d.ts.map +1 -1
  238. package/dist/src/transport-pool.js +50 -4
  239. package/dist/src/transport-pool.js.map +1 -1
  240. package/dist/src/transports/adaptive-concurrency.d.ts +168 -0
  241. package/dist/src/transports/adaptive-concurrency.d.ts.map +1 -0
  242. package/dist/src/transports/adaptive-concurrency.js +353 -0
  243. package/dist/src/transports/adaptive-concurrency.js.map +1 -0
  244. package/dist/src/transports/bullmq/adaptive-concurrency.d.ts +1 -165
  245. package/dist/src/transports/bullmq/adaptive-concurrency.d.ts.map +1 -1
  246. package/dist/src/transports/bullmq/adaptive-concurrency.js +3 -351
  247. package/dist/src/transports/bullmq/adaptive-concurrency.js.map +1 -1
  248. package/dist/src/transports/bullmq/bullmq-transport.d.ts +49 -1
  249. package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
  250. package/dist/src/transports/bullmq/bullmq-transport.js +330 -38
  251. package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
  252. package/dist/src/transports/bullmq/deps.d.ts +18 -2
  253. package/dist/src/transports/bullmq/deps.d.ts.map +1 -1
  254. package/dist/src/transports/bullmq/deps.js +44 -2
  255. package/dist/src/transports/bullmq/deps.js.map +1 -1
  256. package/dist/src/transports/db-helpers.d.ts +39 -0
  257. package/dist/src/transports/db-helpers.d.ts.map +1 -1
  258. package/dist/src/transports/db-helpers.js +73 -0
  259. package/dist/src/transports/db-helpers.js.map +1 -1
  260. package/dist/src/transports/db-remote.d.ts +53 -0
  261. package/dist/src/transports/db-remote.d.ts.map +1 -0
  262. package/dist/src/transports/db-remote.js +147 -0
  263. package/dist/src/transports/db-remote.js.map +1 -0
  264. package/dist/src/transports/db-schema.d.ts +44 -1
  265. package/dist/src/transports/db-schema.d.ts.map +1 -1
  266. package/dist/src/transports/db-schema.js +227 -88
  267. package/dist/src/transports/db-schema.js.map +1 -1
  268. package/dist/src/transports/db.d.ts +189 -6
  269. package/dist/src/transports/db.d.ts.map +1 -1
  270. package/dist/src/transports/db.js +1291 -137
  271. package/dist/src/transports/db.js.map +1 -1
  272. package/dist/src/transports/event-emitter.d.ts +8 -0
  273. package/dist/src/transports/event-emitter.d.ts.map +1 -1
  274. package/dist/src/transports/event-emitter.js +36 -3
  275. package/dist/src/transports/event-emitter.js.map +1 -1
  276. package/dist/src/transports/factory.d.ts +110 -5
  277. package/dist/src/transports/factory.d.ts.map +1 -1
  278. package/dist/src/transports/factory.js +56 -9
  279. package/dist/src/transports/factory.js.map +1 -1
  280. package/dist/src/transports/queue-mock-adapter.d.ts +7 -2
  281. package/dist/src/transports/queue-mock-adapter.d.ts.map +1 -1
  282. package/dist/src/transports/queue-mock-adapter.js +18 -2
  283. package/dist/src/transports/queue-mock-adapter.js.map +1 -1
  284. package/dist/src/transports/queue-remote.d.ts +146 -0
  285. package/dist/src/transports/queue-remote.d.ts.map +1 -0
  286. package/dist/src/transports/queue-remote.js +398 -0
  287. package/dist/src/transports/queue-remote.js.map +1 -0
  288. package/dist/src/transports/queue.d.ts +159 -6
  289. package/dist/src/transports/queue.d.ts.map +1 -1
  290. package/dist/src/transports/queue.js +747 -37
  291. package/dist/src/transports/queue.js.map +1 -1
  292. package/dist/src/transports/task-failure.d.ts +31 -0
  293. package/dist/src/transports/task-failure.d.ts.map +1 -0
  294. package/dist/src/transports/task-failure.js +50 -0
  295. package/dist/src/transports/task-failure.js.map +1 -0
  296. package/dist/src/workflow-als.d.ts +1 -1
  297. package/dist/src/workflow-als.d.ts.map +1 -1
  298. package/dist/src/workflow-ctx.d.ts +10 -0
  299. package/dist/src/workflow-ctx.d.ts.map +1 -1
  300. package/dist/src/workflow-ctx.js +97 -15
  301. package/dist/src/workflow-ctx.js.map +1 -1
  302. package/dist/src/workflow-discovery.d.ts +32 -41
  303. package/dist/src/workflow-discovery.d.ts.map +1 -1
  304. package/dist/src/workflow-discovery.js +65 -88
  305. package/dist/src/workflow-discovery.js.map +1 -1
  306. package/dist/src/workflow-ref.d.ts +24 -0
  307. package/dist/src/workflow-ref.d.ts.map +1 -1
  308. package/dist/src/workflow-ref.js.map +1 -1
  309. package/dist/stubs/config/durable.stub +6 -0
  310. package/dist/stubs/config/durable_dashboard.stub +7 -3
  311. package/dist/stubs/database/migrations/create_durable_tables.stub +2 -1
  312. package/dist/stubs/database/migrations/create_durable_transport_messages.stub +43 -0
  313. package/dist/stubs/database/migrations/create_durable_transport_tables.stub +12 -9
  314. package/dist/stubs/database/migrations/create_durable_transport_worker_keys.stub +1 -1
  315. package/dist/stubs/make/workflow/main.stub +1 -1
  316. package/package.json +6 -4
  317. package/skills/durable-cluster/SKILL.md +80 -11
  318. package/skills/durable-determinism/SKILL.md +11 -5
  319. package/skills/durable-reliability/SKILL.md +77 -37
  320. package/skills/durable-setup/SKILL.md +51 -22
  321. package/skills/durable-transports-stores/SKILL.md +128 -18
  322. package/skills/durable-workflows/SKILL.md +71 -18
  323. package/dist/assets/spa/assets/index-DdB9zxuX.js +0 -16
  324. 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,26 @@ 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 schema's output value is the run's
136
+ input), and `static workflow = { searchAttributes }` (every
137
+ `ctx.upsertSearchAttributes` is validated against the merged attributes; type
138
+ the ctx as `WorkflowCtx<InferSearchAttributes<typeof schema>>` and read back with
139
+ `attributesOf(WorkflowClass, run)`). A failure is a `SchemaValidationError`
140
+ (`FatalError`, code `schema_validation`, issues in `details.issues`) — never
141
+ retried.
142
+
143
+ ```ts
144
+ import * as v from 'valibot'
145
+
146
+ export const charge = defineStep('billing:charge', async (input) => input.amount * 100, {
147
+ input: v.object({ amount: v.number() }), // input: { amount: number }, inferred
148
+ })
149
+ ```
150
+
123
151
  ### Pattern 2 — pausing durably: sleeps, signals, events
124
152
 
125
153
  All three suspend the run with zero compute and survive restarts.
@@ -151,15 +179,12 @@ events with `{ onEvent: ['payment.settled'] }`; coalesce chatty sources with
151
179
  `{ mode: 'batch', maxSize: 50, windowMs: 60_000 }` (receives `{ events: [...] }`).
152
180
 
153
181
  ```ts title="app/controllers/approvals_controller.ts"
154
- import { inject } from '@adonisjs/core'
155
- import { WorkflowEngine } from '@adonis-agora/durable'
182
+ import type { HttpContext } from '@adonisjs/core/http'
183
+ import engine from '@adonis-agora/durable/services/main'
156
184
 
157
- @inject()
158
185
  export default class ApprovalsController {
159
- constructor(private engine: WorkflowEngine) {}
160
-
161
186
  async approve({ params, request, response }: HttpContext) {
162
- await this.engine.signal(`approve:${params.id}`, request.body())
187
+ await engine.signal(`approve:${params.id}`, request.body())
163
188
  return response.noContent()
164
189
  }
165
190
  }
@@ -181,9 +206,9 @@ use `ctx.allSettled(Workflow, inputs, { concurrency })` instead: the same fan,
181
206
  resolving with `{ status: 'fulfilled', value } | { status: 'rejected', id, error, code?, details? }`
182
207
  per input and never throwing for a child. Child ids derive from the call position and
183
208
  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`.
209
+ children run at once (waves of `floor(n / 2)`, two at a time). The wave is part of
210
+ each child id (`<runId>.all.<firstSeq>.w<k>.<i>`), so changing it for an in-flight
211
+ run is non-deterministic, like changing `inputs`.
187
212
 
188
213
  For the same step over many inputs, pass the step reference (`defineStep` ref
189
214
  or `@Step` method) to `ctx.all`. It
@@ -203,8 +228,10 @@ Prefer it to `Promise.all` past a handful of steps (every result of a
203
228
  child-workflow `ctx.all` when each item is just a step. A `queue` or a
204
229
  `timeoutMs` is rejected on a step fan.
205
230
 
206
- Serialize runs per subject with a singleton — runs sharing a key queue FIFO and
207
- are race-free across instances:
231
+ Serialize runs per subject with a singleton — runs sharing a key queue FIFO.
232
+ Admission is an atomic store claim (`StateStore.tryAdmitSingleton`, built into the
233
+ Lucid and in-memory stores), and a holder keeps its slot until it settles
234
+ terminally, so replicas never over-admit and a later start never displaces it:
208
235
 
209
236
  ```ts
210
237
  export default class SyncShopWorkflow extends BaseWorkflow {
@@ -354,7 +381,7 @@ each replay breaks that dedup and re-fires the child. Source:
354
381
  Wrong:
355
382
 
356
383
  ```ts
357
- const result = await CheckoutWorkflow.start(order) // request held open until settle
384
+ const result = await CheckoutWorkflow.start(order) // request held open until the run is terminal
358
385
  ```
359
386
 
360
387
  Correct:
@@ -364,8 +391,9 @@ const { runId } = await CheckoutWorkflow.dispatch(order, { runId: `checkout:${or
364
391
  return response.accepted({ runId })
365
392
  ```
366
393
 
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`
394
+ Mechanism: `.start` resolves only when the run reaches a terminal state — it
395
+ keeps waiting through every sleep/signal suspension, hours later for a long
396
+ workflow. Source: `docs/getting-started.mdx`
369
397
  (warning callout).
370
398
 
371
399
  ### MEDIUM awaiting a webhook with no timeout
@@ -419,5 +447,30 @@ caller suspended on `ctx.callEntity`; model expected rejections as returned
419
447
  discriminated results and reserve throws for genuine programmer errors.
420
448
  Source: `docs/authoring/entities.mdx` (warning callout).
421
449
 
450
+ ### MEDIUM the same workflow or step name in two discovery directories
451
+
452
+ Wrong:
453
+
454
+ ```ts
455
+ // config/durable.ts — both contexts declare a workflow named 'charge' (version 1)
456
+ export default defineConfig({
457
+ workflowsPath: ['app/orders/flows', 'app/billing/flows'],
458
+ })
459
+ ```
460
+
461
+ Correct:
462
+
463
+ ```ts
464
+ // app/billing/flows/charge_workflow.ts
465
+ static workflow = { name: 'billing.charge', version: '1' }
466
+ ```
467
+
468
+ Mechanism: names route runs and steps, so with several `workflowsPath` /
469
+ `stepsPath` directories boot throws `DuplicateDiscoveryError` naming both files
470
+ when two directories define the same workflow name + version or step name with
471
+ different classes. Prefix names by context. The hooks' `source` list must name
472
+ the same directories, otherwise the barrel and the runtime scan disagree.
473
+ Source: `docs/authoring/app-workflows.mdx` ("Several directories").
474
+
422
475
  See also: `durable-determinism` — why the body must stay deterministic and how
423
476
  to change it safely once live.