@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/durable",
3
- "version": "0.49.1",
3
+ "version": "0.50.0",
4
4
  "description": "Durable cross-app workflows for AdonisJS — deterministic replay engine, AdonisJS binding, ace commands, dashboard, OpenTelemetry, Telescope, testing harness and Redis admission, all in one package. Part of the Agora ecosystem.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -125,8 +125,7 @@
125
125
  ],
126
126
  "sideEffects": false,
127
127
  "dependencies": {
128
- "@adonis-agora/filter": "0.11.0",
129
- "zod": "4.6.7"
128
+ "@adonis-agora/filter": "0.11.0"
130
129
  },
131
130
  "peerDependencies": {
132
131
  "@adonisjs/assembler": "^8.4.0",
@@ -170,7 +169,7 @@
170
169
  }
171
170
  },
172
171
  "devDependencies": {
173
- "@adonis-agora/durable-dashboard": "0.8.0",
172
+ "@adonis-agora/durable-dashboard": "0.8.1",
174
173
  "@adonisjs/assembler": "8.6.0",
175
174
  "@adonisjs/core": "7.6.0",
176
175
  "@adonisjs/lucid": "22.4.2",
@@ -183,11 +182,14 @@
183
182
  "@tanstack/intent": "0.5.6",
184
183
  "@types/better-sqlite3": "9.6.0",
185
184
  "@types/node": "26.6.5",
185
+ "arktype": "2.2.9",
186
186
  "better-sqlite3": "13.0.3",
187
187
  "bullmq": "6.3.17",
188
188
  "cron-parser": "5.10.2",
189
189
  "ioredis": "6.0.0",
190
+ "pg": "8.23.1",
190
191
  "typescript": "7.0.2",
192
+ "valibot": "1.5.0",
191
193
  "vitest": "5.0.5",
192
194
  "zod": "4.6.7"
193
195
  },
@@ -8,13 +8,14 @@ description: >-
8
8
  (compatible/degraded/incompatible) and capability routing (requires → blocked
9
9
  runs), layered tenant auth (signTenantToken/hmacTenantVerifier/verifyTenant),
10
10
  runGateway as the role-portable read/control surface, namespace vs partition,
11
- and Python (durable-worker) / NestJS interop over the bullmq wire. Use when
11
+ tenant pods over bullmq, the @adonisjs/queue transport or the db transport, and Python
12
+ (durable-worker) / NestJS interop over the bullmq wire. Use when
12
13
  splitting web from workers, isolating tenants, or adding polyglot workers.
13
14
  license: MIT
14
15
  metadata:
15
16
  type: core
16
17
  library: "@adonis-agora/durable"
17
- library_version: "0.25.0"
18
+ library_version: "0.51.0"
18
19
  framework: adonisjs
19
20
  sources:
20
21
  - 'DavideCarvalho/adonis-durable:docs/cluster/index.mdx'
@@ -44,11 +45,11 @@ execute changes.
44
45
  Split fleet: a pure coordinator plus store-less tenant workers.
45
46
 
46
47
  ```ts title="config/durable.ts (control plane)"
47
- import { defineConfig, stores, transports } from '@adonis-agora/durable'
48
+ import { defineConfig, hmacTenantVerifier, stores, transports } from '@adonis-agora/durable'
48
49
 
49
50
  export default defineConfig({
50
51
  role: 'control-plane',
51
- transport: 'bullmq', // cross-ecosystem wire; queue/db work for Adonis-only splits
52
+ transport: 'bullmq', // or 'queue' / 'db' for Adonis-only fleets
52
53
  transports: { bullmq: transports.bullmq({ connection: { host: '127.0.0.1', port: 6379 } }) },
53
54
  store: 'lucid',
54
55
  stores: { lucid: stores.lucid({ connection: 'pg' }) },
@@ -79,6 +80,41 @@ Isolation is three-layered: type (`store` is `never` on tenant), container (no
79
80
  store binding registered), object (`WorkerRuntime`/`ProxyRunGateway` have no
80
81
  store field).
81
82
 
83
+ Tenant pods need a transport carrying the run-request protocol: `bullmq`,
84
+ `queue` or `db` (the provider feature-detects the methods). Both sides use the
85
+ same transport and backend. On `queue`, replies go to a per-pod
86
+ `<prefix>:run-replies:<instanceId>` queue and live-tail events are pushed to
87
+ each subscribed pod's `<prefix>:tenant-events:<instanceId>`; the pod announces
88
+ its subscriptions and (with `workerKeys: false`) its worker advertisement to the
89
+ control plane, which records them in its own key store — so the pod needs no
90
+ database:
91
+
92
+ ```ts title="config/durable.ts (tenant pod, queue transport)"
93
+ export default defineConfig({
94
+ role: 'tenant',
95
+ transport: 'queue',
96
+ transports: { queue: transports.queue({ connection: 'redis', workerKeys: false }) },
97
+ partition: 'acme-corp',
98
+ tenant: { token: process.env.DURABLE_TENANT_TOKEN },
99
+ })
100
+ ```
101
+
102
+ Queue-transport limits vs bullmq: replies/events are polled (not pushed), a new
103
+ subscription can miss events for about a second, each event costs one job per
104
+ subscribed pod, and live-tail needs a worker-key store on the control plane.
105
+ Source: `docs/transports/queue.mdx` (tenant pods over the queue transport).
106
+
107
+ On `db`, the pod points `transports.db()` at the control plane's database (it
108
+ still owns no durable store). Requests, starts, replies, decisions and events
109
+ are rows of `durable_transport_messages`; a reply or event is a row addressed to
110
+ one instance, and the pod writes its own `-inbox:` / `-tenant-subscriber:` keys
111
+ and worker advertisement to `durable_transport_worker_keys`. Remote workflow
112
+ turns are task rows of kind `workflow`, and each decision is addressed to the
113
+ engine replica that dispatched the turn (any replica could apply it: the engine
114
+ applies a decision by run id). Needs the
115
+ `create_durable_transport_messages` migration (or `autoCreate`). Source:
116
+ `docs/transports/db.mdx` (remote workflows and tenant pods).
117
+
82
118
  ## Core patterns
83
119
 
84
120
  ### Pattern 1 — role-portable code via `runGateway`
@@ -107,16 +143,22 @@ export default class RunsController {
107
143
  Verbs: `getRun`, `listRuns`, `getCheckpoints`, `getRunChildren`,
108
144
  `getRunDetail` / `getFanMembers` (optional; summarize and page wide fans),
109
145
  `getSearchAttributes`, `start`, `signal`, `cancel`, `redispatchPending`,
110
- `workerHealth`, `subscribe`, `topology()`. Never branch on `role`.
146
+ `workerHealth`, `subscribe`, `topology()`, and the NestJS gateway's `retry`
147
+ (in-place requeue), `continue` (breakpoint), `retryWithInput`, `runFacets`,
148
+ `runValueFacets`, `waitingFor` (optional on the interface; both built-in
149
+ gateways implement them, tenant-scoped by the responder). Never branch on `role`.
111
150
 
112
151
  ```ts
113
152
  // Each fan of more than 50 members — or declared wider than 50 — comes back as one FanSummary in `fans`.
114
153
  const detail = await runGateway.getRunDetail?.(params.id, { summarizeFansOver: 50 })
115
154
  ```
116
155
 
117
- A tenant pod's dashboard reads run detail through the `getRunDetail` verb, and
118
- an older control plane rejects unknown verbs: upgrade the control plane first,
119
- or with its tenants.
156
+ Tenants and the control plane upgrade in either order. A verb the control plane
157
+ does not know comes back as `unsupported-request` (or, before 0.50, an
158
+ `unknown run-request verb` error) and the proxy falls back at once: run detail
159
+ is built from `getRun` + `getCheckpoints` and summarized on the tenant, and the
160
+ tenant console degrades `retry` to a re-dispatch; it remembers the old control
161
+ plane for a minute. Every request gets a reply, even when the gateway throws.
120
162
  Source: `docs/cluster/roles-and-config.mdx`.
121
163
 
122
164
  ### Pattern 2 — capability-aware routing and blocked runs
@@ -147,10 +189,11 @@ imports zero store code:
147
189
 
148
190
  ```ts title="worker.ts"
149
191
  import { RedisWorkerRegistry, WorkerRuntime } from '@adonis-agora/durable/worker'
150
- import { transports } from '@adonis-agora/durable'
192
+ import { BullMQTransport, createBullMQDeps } from '@adonis-agora/durable'
151
193
  import { Redis } from 'ioredis'
152
194
 
153
- const transport = await transports.bullmq({ connection: process.env.REDIS_URL })({ app })
195
+ // No Adonis app here, so build the transport directly instead of through `transports.bullmq()`.
196
+ const transport = new BullMQTransport({ deps: await createBullMQDeps(process.env.REDIS_URL) })
154
197
  const runtime = new WorkerRuntime({
155
198
  transport,
156
199
  partition: 'acme-corp',
@@ -294,6 +337,28 @@ the queues the deployed fleet consumes — so the local process steals productio
294
337
  tasks; docs call this out under Tenancy pitfalls ("competes for, and steals, the
295
338
  deployed fleet's tasks"). Source: `docs/concepts/tenancy.mdx`.
296
339
 
340
+ ### HIGH a db-transport tenant pod on a database of its own
341
+
342
+ Wrong:
343
+
344
+ ```ts
345
+ // the pod's default Lucid connection is its own app database
346
+ defineConfig({ role: 'tenant', transport: 'db', transports: { db: transports.db() }, partition: 'acme' })
347
+ ```
348
+
349
+ Correct:
350
+
351
+ ```ts
352
+ // a connection to the CONTROL PLANE's database (config/database.ts), used only for the transport tables
353
+ defineConfig({ role: 'tenant', transport: 'db', transports: { db: transports.db({ connection: 'durable' }) }, partition: 'acme' })
354
+ ```
355
+
356
+ Mechanism: on `db` the wire IS the transport tables, so a pod writing its
357
+ requests to another database is never answered — every `runGateway` call times
358
+ out after `requestTimeoutMs` and no step reaches it. Pods that cannot reach the
359
+ control plane's database need `queue` or `bullmq`. Source:
360
+ `docs/transports/db.mdx` (remote workflows and tenant pods).
361
+
297
362
  ### MEDIUM stamping runs into a namespace nothing polls
298
363
 
299
364
  Wrong:
@@ -311,7 +376,11 @@ await engine.start('checkout', order, id, { namespace: 'eu-west' })
311
376
 
312
377
  Mechanism: between namespaced engines there is no fallback owner; a namespace
313
378
  with neither an engine of its own nor an operator above it is a dead end where
314
- runs sit `pending` forever. Source: `docs/concepts/tenancy.mdx` (pitfalls).
379
+ runs sit `pending` forever. Since 0.51 only a `control-plane` with no
380
+ `namespace` is the operator; a `standalone` engine with no `namespace` is the
381
+ `'default'` pool. Before 0.51 an unset namespace made ANY engine an operator,
382
+ so a standalone app sharing a store drove every pool's runs. Source:
383
+ `docs/concepts/tenancy.mdx` (pitfalls).
315
384
 
316
385
  See also: `durable-setup` — the standalone execution model this scales out of.
317
386
  See also: `durable-transports-stores` — bullmq/queue/db driver details,
@@ -6,14 +6,15 @@ description: >-
6
6
  checkpointed ctx.now() and ctx.sideEffect() capture sources,
7
7
  NonDeterminismError at replay time, workflow versions for breaking shape
8
8
  changes (skew protection), ctx.patched for surgical in-place guards, and the
9
- @adonis-agora/durable-eslint-plugin no-nondeterminism rule with its
10
- recommended preset. Use when authoring or changing workflow code that has
9
+ @adonis-agora/durable-eslint-plugin rules (no-nondeterminism,
10
+ rethrow-control-flow-signals, no-io-in-workflow-body) and its recommended
11
+ preset. Use when authoring or changing workflow code that has
11
12
  runs in flight, fixing NonDeterminismError, or wiring determinism linting.
12
13
  license: MIT
13
14
  metadata:
14
15
  type: core
15
16
  library: "@adonis-agora/durable"
16
- library_version: "0.25.0"
17
+ library_version: "0.51.0"
17
18
  framework: adonisjs
18
19
  sources:
19
20
  - 'DavideCarvalho/adonis-durable:docs/concepts/durability.mdx'
@@ -75,15 +76,20 @@ export default [
75
76
  plugins: { '@adonis-agora/durable': durable },
76
77
  rules: {
77
78
  '@adonis-agora/durable/no-nondeterminism': 'error',
79
+ '@adonis-agora/durable/rethrow-control-flow-signals': 'error',
80
+ '@adonis-agora/durable/no-io-in-workflow-body': 'error',
78
81
  },
79
82
  },
80
83
  ]
81
84
 
82
- // or simply:
85
+ // or simply (enables all three):
83
86
  // export default [durable.configs.recommended]
84
87
  ```
85
88
 
86
- Each report names its replacement ("use `ctx.now()`"). The plugin is the cheap
89
+ `rethrow-control-flow-signals` flags a `catch` around an awaited ctx op that
90
+ does not rethrow `isWorkflowControlFlowSignal(error)`; `no-io-in-workflow-body`
91
+ flags a global `fetch()` and raw `engine.*` calls in the body. Each report names
92
+ its replacement ("use `ctx.now()`"). The plugin is the cheap
87
93
  early layer; `NonDeterminismError` is the replay-time backstop for calls it
88
94
  cannot see (transitive helpers). Source: `docs/tooling/linting.mdx`.
89
95
 
@@ -5,23 +5,25 @@ description: >-
5
5
  backoff and jitter, FatalError and worker retryable:false, timeoutMs liveness
6
6
  vs the durable suspend path, pickupTimeoutMs, log.heartbeat, saga
7
7
  compensation (compensate callbacks/refs, UndoOf, compensationRetries,
8
- cancel({compensate:true})), flow-control queues (registerQueue, concurrency,
8
+ cancel({compensate:true})), flow-control queues (config queues / registerQueue, concurrency,
9
9
  rateLimit, fairnessKey) with Redis-backed global admission (admission-redis),
10
- the dead-letter queue (maxRecoveryAttempts, onDead, retryWithInput), the
10
+ the dead-letter queue (maxRecoveryAttempts, deadLetterWorkflow, deadLetter()
11
+ method, onDead, retryWithInput), retention (maxAge/maxCount policies), the
11
12
  stuck-run playbook (stranded signature, redispatchPending,
12
- remoteRedispatchMs), ace ops commands (durable:work/runs/retry --stale), and
13
+ remoteRedispatchMs, remoteAdvanceSilenceMs for remote workflow turns), ace ops commands (durable:work/runs/retry --stale), and
13
14
  the testing harness (createTestEngine, tick, failOnce, conformance kits).
14
15
  license: MIT
15
16
  metadata:
16
17
  type: core
17
18
  library: "@adonis-agora/durable"
18
- library_version: "0.25.0"
19
+ library_version: "0.51.0"
19
20
  framework: adonisjs
20
21
  sources:
21
22
  - 'DavideCarvalho/adonis-durable:docs/reliability/retries.mdx'
22
23
  - 'DavideCarvalho/adonis-durable:docs/reliability/sagas.mdx'
23
24
  - 'DavideCarvalho/adonis-durable:docs/reliability/dead-letter.mdx'
24
25
  - 'DavideCarvalho/adonis-durable:docs/reliability/flow-control.mdx'
26
+ - 'DavideCarvalho/adonis-durable:docs/reliability/retention.mdx'
25
27
  - 'DavideCarvalho/adonis-durable:docs/reliability/failure-modes.mdx'
26
28
  - 'DavideCarvalho/adonis-durable:docs/cli.mdx'
27
29
  - 'DavideCarvalho/adonis-durable:docs/testing.mdx'
@@ -40,7 +42,7 @@ Retries are declared per step; `FatalError` opts a deterministic business
40
42
  failure out of the retry path:
41
43
 
42
44
  ```ts title="app/steps/charge.ts"
43
- import { defineStep } from '@adonis-agora/durable'
45
+ import { FatalError, defineStep } from '@adonis-agora/durable'
44
46
 
45
47
  export const chargeCard = defineStep(
46
48
  'payments:charge-card',
@@ -48,7 +50,7 @@ export const chargeCard = defineStep(
48
50
  const res = await stripe.charge(input)
49
51
  if (res.declined) {
50
52
  // deterministic verdict — don't make the engine retry it
51
- throw Object.assign(new Error('card declined'), { code: 'declined', retryable: false })
53
+ throw new FatalError('card declined', 'declined')
52
54
  }
53
55
  return { chargeId: res.id }
54
56
  },
@@ -56,7 +58,9 @@ export const chargeCard = defineStep(
56
58
  )
57
59
  ```
58
60
 
59
- A dispatched step WITHOUT `timeoutMs` takes the durable path: failure → persist
61
+ `FatalError` carries `retryable: false`, which the step result builders read; a
62
+ worker in another runtime gets the same effect by throwing any error with
63
+ `retryable: false` (and an optional `code`). A dispatched step WITHOUT `timeoutMs` takes the durable path: failure → persist
60
64
  `wakeAt = now + backoff(attempt)` on the checkpoint → suspend → the timer poller
61
65
  re-dispatches. Crash-safe, never held in memory.
62
66
 
@@ -90,16 +94,20 @@ envelope (`{ input, output }`). Engine-level `compensationRetries` (default 1)
90
94
  bounds transient undo failures; failing compensations are skipped so they can't
91
95
  mask the original error. Trigger deliberately with
92
96
  `await engine.cancel(runId, { compensate: true })` or
93
- `engine.cancelWhere(filter, { compensate: true })`. Every undo surfaces as a
94
- visible `compensate:<step>` event. Source: `docs/reliability/sagas.mdx`.
97
+ `engine.cancelWhere(filter, { compensate: true })`. The cancel is durable (a
98
+ `cancel:<runId>` marker) and honored on every exit of the turn running when it
99
+ lands: return, throw, continue-as-new or suspend all compensate and settle
100
+ `cancelled`. Every undo surfaces as a visible `compensate:<step>` event.
101
+ Source: `docs/reliability/sagas.mdx`.
95
102
 
96
103
  ### Pattern 2 — flow-control queues + fleet-wide admission
97
104
 
98
105
  Cap concurrency/rate durably — a blocked call re-suspends with a persisted
99
106
  `retryAt`, driven by the timer poller (nothing waits in memory):
100
107
 
101
- ```ts title="start/durable.ts"
102
- engine.registerQueue({ name: 'emails', concurrency: 10, rateLimit: { limit: 1000, periodMs: 3_600_000 } })
108
+ ```ts title="config/durable.ts"
109
+ queues: [{ name: 'emails', concurrency: 10, rateLimit: { limit: 1000, periodMs: 3_600_000 } }],
110
+ // or at runtime: engine.registerQueue({ name: 'emails', … }) — same shape, replaces a same-named one
103
111
  ```
104
112
 
105
113
  ```ts
@@ -130,7 +138,12 @@ Source: `docs/reliability/flow-control.mdx`.
130
138
 
131
139
  Crash-recovery reclaims orphaned runs within ~`leaseMs`; a run that crashes the
132
140
  process every time would loop forever. Cap it and route the terminal `dead`
133
- status into a handler workflow (idempotent by the `dlq:<runId>` start id):
141
+ status into a handler workflow — the engine starts it as `dlq:<runId>`
142
+ (idempotent) with a `DeadLetter` input `{ deadRunId, workflow, input, error }`
143
+ in the dead run's namespace. A run
144
+ whose remote step is still in flight is parked `suspended`, never counted, and a
145
+ retry (`requeue` / `durable:retry`) of a `dead`/`failed` run resets
146
+ `recoveryAttempts` to 0:
134
147
 
135
148
  ```ts title="config/durable.ts"
136
149
  export default defineConfig({
@@ -139,20 +152,40 @@ export default defineConfig({
139
152
  })
140
153
  ```
141
154
 
142
- ```ts title="start/durable.ts"
143
- engine.onDead((run) => {
144
- void engine.start(
145
- 'dlq',
146
- { deadRunId: run.id, workflow: run.workflow, input: run.input, error: run.error },
147
- `dlq:${run.id}`,
148
- )
149
- })
155
+ ```ts title="app/workflows/pipeline_workflow.ts"
156
+ export default class PipelineWorkflow extends BaseWorkflow {
157
+ static workflow = { name: 'pipeline', version: '1' }
158
+ async run(ctx: WorkflowCtx, input: PipelineInput) { /* … */ }
159
+ // registered as `pipeline.dlq`; shares this instance's injected services
160
+ async deadLetter(ctx: WorkflowCtx, dl: DeadLetter<PipelineInput>) { /* alert, ticket */ }
161
+ }
150
162
  ```
151
163
 
164
+ Precedence: inline `deadLetter()` > per-workflow `deadLetterWorkflow`
165
+ (`static workflow` / `register` / `registerRemote` option) > config
166
+ `deadLetterWorkflow` (fleet default). Declaring both a method and the option
167
+ throws. A `dlq:` handler run that dies is not routed again; a failed start
168
+ (handler not registered) is `console.warn`-ed. `engine.onDead(run => …)` still
169
+ fires first and is the manual escape hatch (keep the `dlq:<runId>` id).
170
+
152
171
  A dead run stays inspectable and retriable (`durable:retry <runId>`, dashboard
153
172
  retry, or `engine.retryWithInput(runId, fixedInput)` for bad data).
154
173
  Source: `docs/reliability/dead-letter.mdx`.
155
174
 
175
+ Retention bounds terminal history (swept by `durable:work`, once a minute,
176
+ `onEvict` archives first). Scoped policies take `maxAge`, `maxCount` (keep the N
177
+ newest by start time within the scope; `0` deletes all matching), or both (most
178
+ restrictive wins); a policy with neither fails boot:
179
+
180
+ ```ts title="config/durable.ts"
181
+ retention: {
182
+ completed: '30d',
183
+ policies: [{ statuses: ['completed', 'failed'], maxCount: 200, scope: { namespace: 'acme' } }],
184
+ },
185
+ ```
186
+
187
+ Source: `docs/reliability/retention.mdx`.
188
+
156
189
  ### Pattern 4 — the stuck-run playbook
157
190
 
158
191
  A lost remote dispatch does **not** self-heal by default: the run cycles
@@ -166,22 +199,14 @@ node ace durable:retry checkout:1234
166
199
  ```
167
200
 
168
201
  ```ts title="app/controllers/ops_controller.ts"
169
- import { inject } from '@adonisjs/core'
170
- import {
171
- DEFAULT_STALE_MS,
172
- WorkflowEngine,
173
- attachLiveness,
174
- filterStale,
175
- listRuns,
176
- } from '@adonis-agora/durable'
202
+ import type { HttpContext } from '@adonisjs/core/http'
203
+ import { DEFAULT_STALE_MS, attachLiveness, filterStale, listRuns } from '@adonis-agora/durable'
204
+ import engine from '@adonis-agora/durable/services/main'
177
205
 
178
- @inject()
179
206
  export default class OpsController {
180
- constructor(private engine: WorkflowEngine) {}
181
-
182
207
  async strandedRuns({ response }: HttpContext) {
183
- const runs = await listRuns(this.engine, { statuses: ['running', 'suspended'], limit: 200 })
184
- const stale = filterStale(await attachLiveness(this.engine, runs), DEFAULT_STALE_MS)
208
+ const runs = await listRuns(engine, { statuses: ['running', 'suspended'], limit: 200 })
209
+ const stale = filterStale(await attachLiveness(engine, runs), DEFAULT_STALE_MS)
185
210
  return response.ok({ stranded: stale.map(({ run }) => run.id) })
186
211
  }
187
212
  }
@@ -198,6 +223,20 @@ Three nets, in preference order:
198
223
  3. `engine.redispatchPending(runId)` — the operator escape hatch, safe on a
199
224
  healthy run (only touches already-pending checkpoints).
200
225
 
226
+ Remote (polyglot) workflow TURNS have their own net: `remoteAdvanceSilenceMs`
227
+ (config; `RemoteWorkflowExecutor`'s `timeoutMs` per workflow, shorter wins). The
228
+ engine records the turn's random `taskId` on the run (`awaitingDecisionTaskId`),
229
+ releases the lease, then enqueues; whichever replica consumes the decision
230
+ applies it by run id (stale/duplicate decisions dropped), so a point-to-point
231
+ `<P>-decisions` queue across replicas is safe. That is 0.51+: before 0.51 a
232
+ replica that had not dispatched the turn consumed the decision, found no waiter
233
+ and dropped it, leaving the run leased forever — upgrade any multi-replica fleet
234
+ running remote (Python/NestJS) workflows. A turn with no decision and no
235
+ run-scoped heartbeat (`{ runId, seq: 0, group }`, sent every 5s by the Python
236
+ and TS workers) for the window is re-dispatched by the timer poller — never
237
+ failed. Unset = wait for the decision forever. Source:
238
+ `docs/reliability/multi-instance.mdx` (remote workflow turns).
239
+
201
240
  Source: `docs/reliability/failure-modes.mdx`, `docs/cli.mdx`.
202
241
 
203
242
  ### Pattern 5 — test workflows without infrastructure
@@ -282,7 +321,7 @@ Correct:
282
321
 
283
322
  ```ts
284
323
  const inFlight = counts['run.started'] - (counts['run.completed'] + counts['run.failed'])
285
- // or read the store: listRuns({ status: ['running', 'suspended'] })
324
+ // or read the store: listRuns(engine, { statuses: ['running', 'suspended'] })
286
325
  ```
287
326
 
288
327
  Mechanism: `run.suspended` fires on EVERY park — each sleep, signal wait, child
@@ -296,7 +335,7 @@ Source: `docs/observability/events-and-interceptors.mdx`.
296
335
  Wrong:
297
336
 
298
337
  ```ts
299
- const stalled = (await engine.workerHealth()).some((g) => g.stalled) // always false on queue/db
338
+ const stalled = (await engine.workerHealth()).some((g) => g.stalled) // always false where no group health is reported (eventEmitter, queue with workerKeys: false)
300
339
  ```
301
340
 
302
341
  Correct:
@@ -308,9 +347,10 @@ const stale = filterStale(
308
347
  )
309
348
  ```
310
349
 
311
- Mechanism: group health is BullMQ-only — on `queue`, `db` or `eventEmitter`,
350
+ Mechanism: only `bullmq`, `queue` (with a worker-key store) and `db` report group
351
+ health — on `eventEmitter`, or `queue` with `workerKeys: false`,
312
352
  `workerHealth()` returns `[]`, not an error, so "no stalled groups reported" says
313
- nothing. Use the stranded-signature query instead. Source:
353
+ nothing. On `queue`, `depth` counts pending jobs only; on `db`, pending + claimed rows. Use the stranded-signature query instead. Source:
314
354
  `docs/reliability/failure-modes.mdx`.
315
355
 
316
356
  ### MEDIUM writing non-idempotent saga compensations
@@ -6,7 +6,7 @@ description: >-
6
6
  (memory, event-emitter, queue, db, bullmq; lucid store), the WorkflowEngine
7
7
  container singleton from @adonis-agora/durable/services/main,
8
8
  Workflow.dispatch vs .start, where runs execute (in-process dispatcher,
9
- NOOP_RUN_DISPATCHER, durable:work), consumers, leaseMs. Use when setting up
9
+ drive: false, durable:work), consumers, leaseMs, shutdown drain. Use when setting up
10
10
  durable workflows for the first time, moving from dev defaults to production
11
11
  infrastructure, or diagnosing "runs vanish on restart" / "workflows compete
12
12
  with HTTP traffic".
@@ -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/getting-started.mdx'
@@ -34,15 +34,20 @@ default is for development only.
34
34
 
35
35
  ## Setup
36
36
 
37
- Install via ace (registers `@adonis-agora/durable/durable_provider` in
38
- `adonisrc.ts` and publishes `config/durable.ts`):
37
+ Install via ace. The `configure` hook registers the durable and dashboard
38
+ providers, the Ace commands and the `app/workflows` / `app/steps` codegen hooks
39
+ in `adonisrc.ts`, adds the `#workflows/*` / `#steps/*` subpath imports to
40
+ `package.json`, and publishes `config/durable.ts`, `config/durable_dashboard.ts`
41
+ and the Lucid migrations:
39
42
 
40
43
  ```bash
41
44
  node ace add @adonis-agora/durable
42
45
  ```
43
46
 
44
47
  The provider binds a singleton `WorkflowEngine` built from the config. Import it
45
- anywhere with `import engine from '@adonis-agora/durable/services/main'`.
48
+ anywhere with `import engine from '@adonis-agora/durable/services/main'` — never
49
+ `app.container.make(WorkflowEngine)` in app code. On a store-less `tenant` pod
50
+ the engine is `undefined`; use the same module's `runGateway` export instead.
46
51
 
47
52
  ```ts title="config/durable.ts"
48
53
  import { defineConfig, stores, transports } from '@adonis-agora/durable'
@@ -120,49 +125,69 @@ export default defineConfig({
120
125
  })
121
126
  ```
122
127
 
123
- Run `node ace durable:work` alongside (or rely on the embedded worker) so
124
- recovery, timers and schedules are driven even if you later split processes.
128
+ Run `node ace durable:work` alongside (or set `worker: { embedded: true }` to
129
+ run the same loop inside the web process) so recovery, timers and schedules are
130
+ driven even if you later split processes.
125
131
  Source: `docs/transports/index.mdx`.
126
132
 
127
133
  ### Pattern 2 — split web from workers
128
134
 
129
135
  `engine.start()` never blocks the caller, but by default the body executes on a
130
136
  microtask **on the calling instance**. On an API/dashboard pod that means heavy
131
- runs compete with HTTP traffic and the DB pool. Pass `NOOP_RUN_DISPATCHER` so
132
- the API pod only creates runs, and let `durable:work` execute them:
137
+ runs compete with HTTP traffic and the DB pool. Set `drive: false` (with
138
+ `consumers: 'never'`) so the API pod only creates runs, and let `durable:work`
139
+ execute them:
133
140
 
134
141
  ```ts title="config/durable.ts (API/dashboard pod)"
135
- import { NOOP_RUN_DISPATCHER, defineConfig, stores, transports } from '@adonis-agora/durable'
142
+ import { defineConfig, stores, transports } from '@adonis-agora/durable'
136
143
 
137
144
  export default defineConfig({
138
145
  transport: 'queue',
139
146
  transports: { queue: transports.queue({ connection: 'redis' }) },
140
147
  store: 'lucid',
141
148
  stores: { lucid: stores.lucid() },
142
- runDispatcher: NOOP_RUN_DISPATCHER, // create runs; never execute bodies here
149
+ drive: false, // create runs; never execute bodies here
150
+ consumers: 'never', // claim no broker jobs either
143
151
  })
144
152
  ```
145
153
 
154
+ `durable:work` drives through its poll whatever `drive` says, so one config file
155
+ can serve both pods (runs a worker pod starts itself, e.g. children, are picked
156
+ up on its next tick rather than inline). A resume handled on the API pod (an
157
+ `engine.signal` or webhook) still executes there under the run's lease. An
158
+ explicit `runDispatcher` wins over `drive`; `drive: false` plus
159
+ `worker: { embedded: true }` throws at boot. Every store-backed process drains
160
+ in-flight runs on shutdown (`worker.drainTimeoutMs`, else `drainTimeoutMs`,
161
+ default 10s) before the transport closes; no `app.terminating` hook
162
+ needed.
163
+
146
164
  ```bash title="a separate worker process"
147
- node ace durable:work --interval=500 --drainTimeout=15000
165
+ node ace durable:work --interval=500 --drain-timeout=15000
148
166
  ```
149
167
 
150
168
  Each `durable:work` tick runs `engine.runPending()` → `recoverIncomplete()` →
151
169
  `resumeDueTimers()` → `sweepTimeouts()` → fires due schedules, then drains on
152
- `SIGINT`/`SIGTERM`. Source: `docs/concepts/durability.mdx`,
170
+ `SIGINT`/`SIGTERM`. `--drain-timeout` bounds the whole shutdown: the wait for
171
+ the tick in flight is deducted from the drain. The recovery fallbacks are
172
+ `config/durable.ts` fields: `reconcileMs` (lost-wake re-drive, default 300000,
173
+ `0` off) and `blockedPollMs` (blocked-run re-check, default 5000). Source: `docs/concepts/durability.mdx`,
153
174
  `docs/reliability/failure-modes.mdx`, `docs/cli.mdx`.
154
175
 
155
176
  ### Pattern 3 — dispatch vs start vs waitForRun
156
177
 
157
178
  One rule: `.dispatch(input)` = fire-and-forget (`{ runId }`); `.start(input)` =
158
- block until the run settles (terminal state or suspended) and get the result;
159
- `engine.waitForRun(runId)` resolves a previously started run.
179
+ block until the run is **terminal** (it waits through sleeps/signals) and get its
180
+ output; `engine.waitForRun(runId)` resolves a previously started run once it
181
+ **settles** (terminal or suspended — except a `suspended` run still awaiting a
182
+ remote workflow turn's decision, which keeps waiting until the decision lands) —
183
+ pass `{ terminal: true }` to wait for the end.
160
184
 
161
185
  ```ts
162
186
  const { runId } = await OrderWorkflow.dispatch(order) // hot path — respond now
163
- const result = await OrderWorkflow.start(order) // scripts / short runs only
187
+ const output = await OrderWorkflow.start(order) // scripts / short runs only
164
188
  await engine.start('order', order, runId)
165
- const settled = await engine.waitForRun(runId) // resolves on settle
189
+ const settled = await engine.waitForRun(runId) // RunResult, resolves on settle
190
+ const done = await engine.waitForRun(runId, { terminal: true, timeoutMs: 60_000 })
166
191
  ```
167
192
 
168
193
  `.start` blocks the calling context — avoid it on a hot HTTP path for
@@ -249,12 +274,13 @@ export default defineConfig({ transport: 'queue', transports: { /* … */ } })
249
274
  Correct:
250
275
 
251
276
  ```ts
252
- import { NOOP_RUN_DISPATCHER, defineConfig } from '@adonis-agora/durable'
277
+ import { defineConfig } from '@adonis-agora/durable'
253
278
 
254
279
  export default defineConfig({
255
280
  transport: 'queue',
256
281
  transports: { /* … */ },
257
- runDispatcher: NOOP_RUN_DISPATCHER,
282
+ drive: false,
283
+ consumers: 'never',
258
284
  })
259
285
  ```
260
286
 
@@ -284,9 +310,12 @@ export default defineConfig({
284
310
  ```
285
311
 
286
312
  —or don't round-trip inline: start the run, exit, let the worker fleet finish
287
- it. Mechanism: console/repl processes deliberately do not start broker consumer
288
- loops (they would claim jobs and die holding them), so nothing receives results
289
- in that process unless `consumers: 'always'`.
313
+ it. Mechanism: with `consumers: 'auto'` (the default) console/repl processes
314
+ deliberately do not start broker consumer loops (they would claim jobs and die
315
+ holding them), so nothing receives results in that process unless
316
+ `consumers: 'always'`. Before 0.50 the bullmq and db transports ignored this and
317
+ a one-off `node ace` command consumed jobs; upgrade if you see that.
318
+ `consumers: 'never'` makes a web fleet a pure producer.
290
319
  Source: `docs/transports/queue.mdx` ("consumers").
291
320
 
292
321
  See also: `durable-transports-stores` — picking drivers, namespaces, migrations.