@adonis-agora/durable 0.25.0 → 0.26.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 (38) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/assets/spa/assets/{index-CV232q_B.js → index-ClD9G7pa.js} +10 -10
  3. package/dist/assets/spa/assets/index-ClD9G7pa.js.map +1 -0
  4. package/dist/assets/spa/assets/index-Ds6dCWMa.css +1 -0
  5. package/dist/assets/spa/index.html +2 -2
  6. package/dist/providers/durable_provider.d.ts.map +1 -1
  7. package/dist/providers/durable_provider.js +25 -0
  8. package/dist/providers/durable_provider.js.map +1 -1
  9. package/dist/src/dashboard/index.d.ts +1 -1
  10. package/dist/src/dashboard/index.js +1 -1
  11. package/dist/src/engine.d.ts +7 -0
  12. package/dist/src/engine.d.ts.map +1 -1
  13. package/dist/src/engine.js +11 -0
  14. package/dist/src/engine.js.map +1 -1
  15. package/dist/src/event-trigger-bridge.d.ts +32 -0
  16. package/dist/src/event-trigger-bridge.d.ts.map +1 -0
  17. package/dist/src/event-trigger-bridge.js +122 -0
  18. package/dist/src/event-trigger-bridge.js.map +1 -0
  19. package/dist/src/index.d.ts +3 -1
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/index.js +3 -1
  22. package/dist/src/index.js.map +1 -1
  23. package/dist/src/workflow-discovery.d.ts.map +1 -1
  24. package/dist/src/workflow-discovery.js +13 -1
  25. package/dist/src/workflow-discovery.js.map +1 -1
  26. package/dist/src/workflow-events.d.ts +85 -0
  27. package/dist/src/workflow-events.d.ts.map +1 -0
  28. package/dist/src/workflow-events.js +58 -0
  29. package/dist/src/workflow-events.js.map +1 -0
  30. package/package.json +5 -2
  31. package/skills/durable-cluster/SKILL.md +308 -0
  32. package/skills/durable-determinism/SKILL.md +260 -0
  33. package/skills/durable-reliability/SKILL.md +352 -0
  34. package/skills/durable-setup/SKILL.md +293 -0
  35. package/skills/durable-transports-stores/SKILL.md +296 -0
  36. package/skills/durable-workflows/SKILL.md +399 -0
  37. package/dist/assets/spa/assets/index-CTsYsi6p.css +0 -1
  38. package/dist/assets/spa/assets/index-CV232q_B.js.map +0 -1
@@ -0,0 +1,293 @@
1
+ ---
2
+ name: durable-setup
3
+ description: >-
4
+ Install and configure @adonis-agora/durable in an AdonisJS app: node ace add,
5
+ config/durable.ts with defineConfig and driver-by-name transports/stores maps
6
+ (memory, event-emitter, queue, db, bullmq; lucid store), the WorkflowEngine
7
+ container singleton from @adonis-agora/durable/services/main,
8
+ Workflow.dispatch vs .start, where runs execute (in-process dispatcher,
9
+ NOOP_RUN_DISPATCHER, durable:work), consumers, leaseMs. Use when setting up
10
+ durable workflows for the first time, moving from dev defaults to production
11
+ infrastructure, or diagnosing "runs vanish on restart" / "workflows compete
12
+ with HTTP traffic".
13
+ license: MIT
14
+ metadata:
15
+ type: core
16
+ library: "@adonis-agora/durable"
17
+ library_version: "0.25.0"
18
+ framework: adonisjs
19
+ sources:
20
+ - 'DavideCarvalho/adonis-durable:docs/getting-started.mdx'
21
+ - 'DavideCarvalho/adonis-durable:docs/concepts/durability.mdx'
22
+ - 'DavideCarvalho/adonis-durable:docs/transports/index.mdx'
23
+ - 'DavideCarvalho/adonis-durable:docs/reliability/failure-modes.mdx'
24
+ - 'DavideCarvalho/adonis-durable:README.md'
25
+ ---
26
+
27
+ # Setting up @adonis-agora/durable
28
+
29
+ `@adonis-agora/durable` is a durable workflow engine for AdonisJS: workflows are
30
+ plain async code whose steps are checkpointed, so a crash or deploy resumes from
31
+ the last checkpoint instead of starting over. The published `config/durable.ts`
32
+ defaults to zero infrastructure (in-process transport + in-memory store) — that
33
+ default is for development only.
34
+
35
+ ## Setup
36
+
37
+ Install via ace (registers `@adonis-agora/durable/durable_provider` in
38
+ `adonisrc.ts` and publishes `config/durable.ts`):
39
+
40
+ ```bash
41
+ node ace add @adonis-agora/durable
42
+ ```
43
+
44
+ The provider binds a singleton `WorkflowEngine` built from the config. Import it
45
+ anywhere with `import engine from '@adonis-agora/durable/services/main'`.
46
+
47
+ ```ts title="config/durable.ts"
48
+ import { defineConfig, stores, transports } from '@adonis-agora/durable'
49
+
50
+ export default defineConfig({
51
+ transport: 'event-emitter',
52
+ transports: {
53
+ memory: transports.memory(),
54
+ 'event-emitter': transports.eventEmitter(),
55
+ // queue: transports.queue({ connection: 'redis' }), // names a config/queue.ts adapter
56
+ // db: transports.db(),
57
+ },
58
+ store: 'lucid',
59
+ stores: { lucid: stores.lucid() },
60
+ })
61
+ ```
62
+
63
+ Define a workflow class under `app/workflows/` (auto-registered at boot):
64
+
65
+ ```ts title="app/workflows/checkout_workflow.ts"
66
+ import { BaseWorkflow } from '@adonis-agora/durable'
67
+ import type { WorkflowCtx } from '@adonis-agora/durable'
68
+
69
+ export default class CheckoutWorkflow extends BaseWorkflow {
70
+ static workflow = { name: 'checkout', version: '1' }
71
+
72
+ async run(ctx: WorkflowCtx, order: { id: number; total: number }) {
73
+ await ctx.localStep('reserveStock', async () => ({ reserved: true }))
74
+ const approval = await ctx.waitForSignal<{ approved: boolean }>(`approve:${order.id}`)
75
+ if (!approval.approved) return { status: 'rejected' }
76
+ return { status: 'shipped' }
77
+ }
78
+ }
79
+ ```
80
+
81
+ Start a run from a controller — `dispatch` enqueues and returns immediately:
82
+
83
+ ```ts title="app/controllers/checkout_controller.ts"
84
+ import type { HttpContext } from '@adonisjs/core/http'
85
+ import CheckoutWorkflow from '#workflows/checkout_workflow'
86
+ import Order from '#models/order'
87
+
88
+ export default class CheckoutController {
89
+ async store({ request, response }: HttpContext) {
90
+ const total = request.input('total') as number
91
+ const order = await Order.create({ total })
92
+ const { runId } = await CheckoutWorkflow.dispatch(
93
+ { id: order.id, total: order.total },
94
+ { runId: `checkout:${order.id}` }, // stable runId = idempotency key
95
+ )
96
+ return response.accepted({ runId })
97
+ }
98
+ }
99
+ ```
100
+
101
+ Source: `docs/getting-started.mdx`, `docs/index.mdx`.
102
+
103
+ ## Core patterns
104
+
105
+ ### Pattern 1 — production single-process config
106
+
107
+ For a deployed app that stays one process, pair the Lucid store with the
108
+ `eventEmitter` transport. Checkpoints survive restarts in Postgres while steps
109
+ never leave the process:
110
+
111
+ ```ts title="config/durable.ts"
112
+ import { defineConfig, stores, transports } from '@adonis-agora/durable'
113
+
114
+ export default defineConfig({
115
+ transport: 'event-emitter',
116
+ transports: { 'event-emitter': transports.eventEmitter() },
117
+ store: 'lucid',
118
+ stores: { lucid: stores.lucid() },
119
+ leaseMs: 30_000,
120
+ })
121
+ ```
122
+
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.
125
+ Source: `docs/transports/index.mdx`.
126
+
127
+ ### Pattern 2 — split web from workers
128
+
129
+ `engine.start()` never blocks the caller, but by default the body executes on a
130
+ 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:
133
+
134
+ ```ts title="config/durable.ts (API/dashboard pod)"
135
+ import { NOOP_RUN_DISPATCHER, defineConfig, stores, transports } from '@adonis-agora/durable'
136
+
137
+ export default defineConfig({
138
+ transport: 'queue',
139
+ transports: { queue: transports.queue({ connection: 'redis' }) },
140
+ store: 'lucid',
141
+ stores: { lucid: stores.lucid() },
142
+ runDispatcher: NOOP_RUN_DISPATCHER, // create runs; never execute bodies here
143
+ })
144
+ ```
145
+
146
+ ```bash title="a separate worker process"
147
+ node ace durable:work --interval=500 --drainTimeout=15000
148
+ ```
149
+
150
+ Each `durable:work` tick runs `engine.runPending()` → `recoverIncomplete()` →
151
+ `resumeDueTimers()` → `sweepTimeouts()` → fires due schedules, then drains on
152
+ `SIGINT`/`SIGTERM`. Source: `docs/concepts/durability.mdx`,
153
+ `docs/reliability/failure-modes.mdx`, `docs/cli.mdx`.
154
+
155
+ ### Pattern 3 — dispatch vs start vs waitForRun
156
+
157
+ 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.
160
+
161
+ ```ts
162
+ const { runId } = await OrderWorkflow.dispatch(order) // hot path — respond now
163
+ const result = await OrderWorkflow.start(order) // scripts / short runs only
164
+ await engine.start('order', order, runId)
165
+ const settled = await engine.waitForRun(runId) // resolves on settle
166
+ ```
167
+
168
+ `.start` blocks the calling context — avoid it on a hot HTTP path for
169
+ long-running workflows. Source: `docs/getting-started.mdx`,
170
+ `docs/authoring/app-workflows.mdx`.
171
+
172
+ ### Pattern 4 — optional peers install only when selected
173
+
174
+ Every driver lazily imports its optional peer (`@adonisjs/lucid`,
175
+ `@adonisjs/queue`, `@adonisjs/redis`, `bullmq`, `@opentelemetry/api`) only when
176
+ that driver is named in config. Add the peer to the app that uses it:
177
+
178
+ ```bash
179
+ npm i @adonisjs/lucid # lucid store / db transport
180
+ node ace configure @adonisjs/lucid && node ace migration:run
181
+ npm i bullmq # bullmq transport (cross-ecosystem fleets)
182
+ npm i cron-parser # cron-based schedules
183
+ ```
184
+
185
+ `cron-parser` is needed wherever schedules with a `cron` field fire (the worker
186
+ host). Source: `docs/stores/lucid.mdx`, `docs/authoring/scheduling.mdx`,
187
+ `packages/adonis/package.json` (peerDependencies).
188
+
189
+ ## Common mistakes
190
+
191
+ ### CRITICAL shipping the in-memory default store
192
+
193
+ Wrong:
194
+
195
+ ```ts
196
+ // Works locally, then every run and checkpoint vanishes on restart/deploy.
197
+ export default defineConfig({
198
+ transport: 'memory',
199
+ transports: { memory: transports.memory() },
200
+ })
201
+ ```
202
+
203
+ Correct:
204
+
205
+ ```ts
206
+ export default defineConfig({
207
+ transport: 'event-emitter',
208
+ transports: { 'event-emitter': transports.eventEmitter() },
209
+ store: 'lucid',
210
+ stores: { lucid: stores.lucid() },
211
+ })
212
+ ```
213
+
214
+ Mechanism: with no `store` key the engine uses the in-memory store — no error,
215
+ no warning, just state loss across restarts. The in-memory store is documented
216
+ as "for tests and local dev. Not durable."
217
+ Source: `docs/stores/index.mdx`, `docs/getting-started.mdx`.
218
+
219
+ ### HIGH using `transports.memory()` as the single-process production driver
220
+
221
+ Wrong:
222
+
223
+ ```ts
224
+ transport: 'memory' // "it's all one process anyway"
225
+ ```
226
+
227
+ Correct:
228
+
229
+ ```ts
230
+ transport: 'event-emitter',
231
+ transports: { 'event-emitter': transports.eventEmitter() },
232
+ ```
233
+
234
+ Mechanism: `memory` executes the handler **inline inside the dispatch call**;
235
+ `eventEmitter` delivers on a later tick like a real broker, so code correct on
236
+ `eventEmitter` is correct on a broker too. The docs warn explicitly:
237
+ "memory is for tests; eventEmitter is the single-process production driver".
238
+ Source: `docs/transports/index.mdx`.
239
+
240
+ ### HIGH letting workflows share the API pod's event loop
241
+
242
+ Wrong:
243
+
244
+ ```ts
245
+ // API/dashboard pod, default config — a burst of CPU-heavy runs starves requests.
246
+ export default defineConfig({ transport: 'queue', transports: { /* … */ } })
247
+ ```
248
+
249
+ Correct:
250
+
251
+ ```ts
252
+ import { NOOP_RUN_DISPATCHER, defineConfig } from '@adonis-agora/durable'
253
+
254
+ export default defineConfig({
255
+ transport: 'queue',
256
+ transports: { /* … */ },
257
+ runDispatcher: NOOP_RUN_DISPATCHER,
258
+ })
259
+ ```
260
+
261
+ plus `node ace durable:work` as its own process. Mechanism: the default
262
+ in-process dispatcher executes bodies on the same instance, event loop and DB
263
+ pool as the HTTP handler that called `start()`; a saturated pool starves both.
264
+ Source: `docs/reliability/failure-modes.mdx` ("The in-process default
265
+ surprise"), `docs/concepts/durability.mdx`.
266
+
267
+ ### MEDIUM expecting a `node ace` script to await a remote step result
268
+
269
+ Wrong:
270
+
271
+ ```ts
272
+ // inside `node ace run-report` — hangs forever on a queue/db/bullmq transport
273
+ const { runId } = await OrderWorkflow.dispatch(input)
274
+ const result = await engine.waitForRun(runId)
275
+ ```
276
+
277
+ Correct:
278
+
279
+ ```ts
280
+ export default defineConfig({
281
+ // …
282
+ consumers: 'always', // opt this process into broker consumption
283
+ })
284
+ ```
285
+
286
+ —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'`.
290
+ Source: `docs/transports/queue.mdx` ("consumers").
291
+
292
+ See also: `durable-transports-stores` — picking drivers, namespaces, migrations.
293
+ See also: `durable-workflows` — authoring the workflow classes you just wired.
@@ -0,0 +1,296 @@
1
+ ---
2
+ name: durable-transports-stores
3
+ description: >-
4
+ Wire @adonis-agora/durable drivers in config/durable.ts: transports
5
+ (memory/eventEmitter/queue over @adonisjs/queue, broker-less db over Lucid,
6
+ bullmq for cross-runtime), per-step transport pinning and failover pools,
7
+ controlPlanes.redis broadcast channel, the lucid StateStore (published
8
+ migration delegating to createDurableTables, autoSchema, repair warning,
9
+ updateRunIf compare-and-set), CodecStateStore payload encryption with a
10
+ PayloadCodec, and namespace vs partition isolation. Use when moving steps
11
+ across processes, persisting runs durably, sharing one database/broker
12
+ between pools, or encrypting payloads at rest.
13
+ license: MIT
14
+ metadata:
15
+ type: core
16
+ library: "@adonis-agora/durable"
17
+ library_version: "0.25.0"
18
+ framework: adonisjs
19
+ sources:
20
+ - 'DavideCarvalho/adonis-durable:docs/transports/index.mdx'
21
+ - 'DavideCarvalho/adonis-durable:docs/transports/queue.mdx'
22
+ - 'DavideCarvalho/adonis-durable:docs/transports/db.mdx'
23
+ - 'DavideCarvalho/adonis-durable:docs/transports/control-plane.mdx'
24
+ - 'DavideCarvalho/adonis-durable:docs/stores/index.mdx'
25
+ - 'DavideCarvalho/adonis-durable:docs/stores/lucid.mdx'
26
+ ---
27
+
28
+ # Transports & state stores
29
+
30
+ A transport carries a dispatched `ctx.step` to its handler and the result back;
31
+ a state store persists runs and checkpoints. They are independent pluggable,
32
+ config-selected drivers — mix any store with any transport. Drivers are named
33
+ from the `transports`/`stores` maps and lazily import their optional peer only
34
+ when selected.
35
+
36
+ ## Setup
37
+
38
+ ```bash
39
+ node ace configure @adonis-agora/durable # publishes config + migrations
40
+ node ace migration:run # durable tables (+ db-transport tables)
41
+ ```
42
+
43
+ ```ts title="config/durable.ts"
44
+ import { defineConfig, stores, transports } from '@adonis-agora/durable'
45
+
46
+ export default defineConfig({
47
+ transport: 'queue',
48
+ transports: {
49
+ memory: transports.memory(),
50
+ 'event-emitter': transports.eventEmitter(),
51
+ queue: transports.queue({ connection: 'redis' }), // names a config/queue.ts adapter
52
+ db: transports.db(), // pass { connection } for a non-default Lucid connection
53
+ },
54
+ store: 'lucid',
55
+ stores: { lucid: stores.lucid() },
56
+ })
57
+ ```
58
+
59
+ Switching transports is a one-line change; workflow code never changes.
60
+
61
+ ## Core patterns
62
+
63
+ ### Pattern 1 — cross-process steps without new infrastructure (`db`)
64
+
65
+ The `db` transport makes dispatched steps rows in the database you already run —
66
+ no Redis, no broker. Workers claim rows with an atomic portable lease (no
67
+ `FOR UPDATE SKIP LOCKED` needed):
68
+
69
+ ```ts title="start/worker.ts (a separate worker process)"
70
+ import db from '@adonisjs/lucid/services/db'
71
+ import { DbTransport } from '@adonis-agora/durable'
72
+
73
+ const transport = new DbTransport({ db }) // { partition } isolates this pool
74
+ transport.handle('pipeline:extract', async (input) => extract(input))
75
+ ```
76
+
77
+ Trade-off: throughput is bounded by polling + row contention — right for
78
+ workflow-scale modest rates, not high-fanout firehoses. Delivery is
79
+ at-least-once (crash between insert-result and delete-task), so handlers should
80
+ be idempotent on `step_id`. Source: `docs/transports/db.mdx`.
81
+
82
+ ### Pattern 2 — real brokers (`queue`, `bullmq`) and stalled-claim tuning
83
+
84
+ The `queue` transport rides any `@adonisjs/queue` adapter defined once in
85
+ `config/queue.ts`; `bullmq` is the cross-ecosystem wire shared byte-for-byte
86
+ with NestJS engines and Python workers:
87
+
88
+ ```ts title="config/durable.ts"
89
+ export default defineConfig({
90
+ transports: {
91
+ queue: transports.queue({
92
+ connection: 'redis',
93
+ // A claim is NEVER renewed while a worker processes it — its age is the
94
+ // step's elapsed time. Raise the threshold above your longest step:
95
+ stalledThresholdMs: 2 * 60 * 60 * 1000, // default 30min
96
+ maxStalledCount: 3, // bounds a poison job
97
+ onError: (err) => logger.error({ err }, 'durable queue transport'),
98
+ }),
99
+ },
100
+ })
101
+ ```
102
+
103
+ One instance consumes engine-side results; run one or more `node ace
104
+ durable:work` workers serving the step names. Source:
105
+ `docs/transports/queue.mdx`.
106
+
107
+ ### Pattern 3 — multi-replica fan-out: the control plane
108
+
109
+ The task transport is point-to-point; lifecycle events and cancellation need a
110
+ broadcast channel across every replica. Omit it and the engine is local-only
111
+ (correct for single-instance):
112
+
113
+ ```ts title="config/durable.ts"
114
+ import { controlPlanes, defineConfig } from '@adonis-agora/durable'
115
+
116
+ export default defineConfig({
117
+ // …
118
+ controlPlane: controlPlanes.redis({ connection: 'main' }), // config/redis.ts connection
119
+ })
120
+ ```
121
+
122
+ Channel is `${prefix}-control` (default prefix `durable`) — wire-compatible with
123
+ the NestJS fleet's channel, so an AdonisJS and NestJS deployment sharing one
124
+ Redis fan out across both runtimes. Source: `docs/transports/control-plane.mdx`.
125
+
126
+ ### Pattern 4 — schema ownership and encryption at rest
127
+
128
+ The migration **delegates** to the library instead of snapshotting DDL, so the
129
+ schema cannot drift from what the installed version writes:
130
+
131
+ ```ts title="database/migrations/…_create_durable_tables.ts"
132
+ import { BaseSchema } from '@adonisjs/lucid/schema'
133
+ import db from '@adonisjs/lucid/services/db'
134
+ import { createDurableTables, dropDurableTables } from '@adonis-agora/durable'
135
+
136
+ export default class extends BaseSchema {
137
+ static disableTransactions = true
138
+
139
+ async up() {
140
+ await createDurableTables(db, this.db.connectionName)
141
+ }
142
+
143
+ async down() {
144
+ await dropDurableTables(db, this.db.connectionName)
145
+ }
146
+ }
147
+ ```
148
+
149
+ By default the provider also provisions idempotently at boot (`ensureSchema`);
150
+ turn that off to own every change through reviewed migrations:
151
+
152
+ ```ts
153
+ export default defineConfig({ /* … */ autoSchema: false })
154
+ // then `node ace migration:run` after EVERY package upgrade
155
+ ```
156
+
157
+ Encrypt run/step/signal/event payloads by decorating any store with a codec
158
+ (searchable metadata and structured error stay plaintext):
159
+
160
+ ```ts title="config/durable.ts"
161
+ import { CodecStateStore, LucidStateStore, defineConfig } from '@adonis-agora/durable'
162
+ import type { PayloadCodec } from '@adonis-agora/durable'
163
+ import db from '@adonisjs/lucid/services/db'
164
+
165
+ const aesCodec: PayloadCodec = {
166
+ encode(value) {
167
+ const body = Buffer.from(JSON.stringify(value), 'utf8').toString('base64')
168
+ return { body }
169
+ },
170
+ decode(value) {
171
+ return JSON.parse(Buffer.from((value as { body: string }).body, 'base64').toString('utf8'))
172
+ },
173
+ }
174
+
175
+ export default defineConfig({
176
+ store: 'encrypted',
177
+ stores: { encrypted: async () => new CodecStateStore(new LucidStateStore(db), aesCodec) },
178
+ })
179
+ ```
180
+
181
+ Source: `docs/stores/lucid.mdx`, `docs/stores/index.mdx`.
182
+
183
+ ## Common mistakes
184
+
185
+ ### HIGH setting `stalledThresholdMs` below the longest legitimate step
186
+
187
+ Wrong:
188
+
189
+ ```ts
190
+ transports.queue({ connection: 'redis', stalledThresholdMs: 60_000 }) // imports take ~90min
191
+ ```
192
+
193
+ Correct:
194
+
195
+ ```ts
196
+ transports.queue({ connection: 'redis', stalledThresholdMs: 2 * 60 * 60 * 1000 })
197
+ ```
198
+
199
+ Mechanism: a claim's age equals the step's elapsed runtime because nothing
200
+ renews it mid-processing; a threshold below the longest step redelivers jobs
201
+ from merely-slow workers and double-runs the step. For tighter detection use
202
+ per-step `timeoutMs`, which IS heartbeat-aware.
203
+ Source: `docs/transports/queue.mdx` ("Stalled-job reclaim").
204
+
205
+ ### HIGH hand-copying the DDL into the migration
206
+
207
+ Wrong:
208
+
209
+ ```ts
210
+ export default class extends BaseSchema {
211
+ async up() {
212
+ this.schema.createTable('durable_workflow_runs', (t) => {
213
+ /* copied column list */
214
+ })
215
+ }
216
+ }
217
+ ```
218
+
219
+ Correct:
220
+
221
+ ```ts
222
+ export default class extends BaseSchema {
223
+ static disableTransactions = true
224
+ async up() {
225
+ await createDurableTables(db, this.db.connectionName)
226
+ }
227
+ }
228
+ ```
229
+
230
+ Mechanism: the snapshot is only right until the next release drifts past it, and
231
+ the two details are load-bearing — without `disableTransactions = true` a
232
+ `pool.max: 1` setup hangs until acquire timeout (createDurableTables checks out
233
+ its own connection), and `this.db` has no `.connection()` method, only
234
+ `.connectionName`. Source: `docs/stores/lucid.mdx`.
235
+
236
+ ### MEDIUM leaving `autoSchema: false` with no upgrade discipline
237
+
238
+ Wrong:
239
+
240
+ ```ts
241
+ export default defineConfig({ store: 'lucid', stores: { lucid: stores.lucid() }, autoSchema: false })
242
+ // …package upgraded twice, migrations never re-run
243
+ ```
244
+
245
+ Correct:
246
+
247
+ ```bash
248
+ node ace migration:run # after every @adonis-agora/durable upgrade
249
+ ```
250
+
251
+ Mechanism: with boot provisioning off, nothing repairs the schema; when the boot
252
+ path DOES repair (autoSchema on), it logs a one-time "repaired the durable
253
+ schema in place" warning meaning exactly this — the database was behind and the
254
+ fix was never recorded in migration history. Source: `docs/stores/lucid.mdx`
255
+ ("The repair warning").
256
+
257
+ ### MEDIUM using the deprecated `group` option to isolate worker pools
258
+
259
+ Wrong:
260
+
261
+ ```ts
262
+ transports.db({ group: 'tenant-a' }) // silently accepted and ignored
263
+ ```
264
+
265
+ Correct:
266
+
267
+ ```ts
268
+ transports.db({ partition: 'tenant-a' })
269
+ ```
270
+
271
+ Mechanism: routing is by handler name — an instance serves whatever names it
272
+ registers, so there is no group to declare. Isolation comes from `partition`
273
+ (suffixes routing tokens `<name>@<partition>`) plus `namespace` (scopes which
274
+ runs this engine polls). Source: `docs/transports/db.mdx` options table,
275
+ `docs/concepts/tenancy.mdx`.
276
+
277
+ ### MEDIUM wrapping a store in CodecStateStore and expecting heartbeats
278
+
279
+ Wrong:
280
+
281
+ ```ts
282
+ stores: { encrypted: async () => new CodecStateStore(new LucidStateStore(db), aesCodec) }
283
+ // then relying on persisted heartbeat progress for long remote steps
284
+ ```
285
+
286
+ Correct: accept the documented trade-off explicitly — the decorator does not
287
+ implement the optional `recordStepHeartbeat`, so persisted liveness progress is
288
+ traded for encryption at rest (the live `step.started` event still fires).
289
+
290
+ Mechanism: the optional member list defines what decorators can silently drop;
291
+ heartbeat persistence is one of them. Source: `docs/stores/index.mdx`
292
+ (optional members table + warning).
293
+
294
+ See also: `durable-reliability` — retries/timeouts interact with transport
295
+ claim semantics; the stuck-run playbook.
296
+ See also: `durable-cluster` — namespace/partition across topologies and tenants.