@dudousxd/nestjs-durable 0.47.0 → 0.48.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.
- package/dist/index.cjs +524 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -5
- package/dist/index.d.ts +106 -5
- package/dist/index.js +437 -200
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/skills/durable-setup/SKILL.md +6 -0
- package/skills/durable-workflows/SKILL.md +67 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-durable",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Durable workflows for NestJS — module, decorators, discovery and boot recovery",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Carvalho",
|
|
@@ -67,11 +67,12 @@
|
|
|
67
67
|
"tsup": "8.3.5",
|
|
68
68
|
"@dudousxd/nestjs-diagnostics": "0.5.0",
|
|
69
69
|
"@tanstack/intent": "0.4.0",
|
|
70
|
-
"@dudousxd/
|
|
71
|
-
"@dudousxd/nestjs-durable-
|
|
72
|
-
"@dudousxd/nestjs-durable-
|
|
73
|
-
"@dudousxd/nestjs-durable-
|
|
74
|
-
"@dudousxd/durable-
|
|
70
|
+
"@dudousxd/durable-worker": "0.11.0",
|
|
71
|
+
"@dudousxd/nestjs-durable-core": "^0.79.0",
|
|
72
|
+
"@dudousxd/nestjs-durable-dashboard": "0.51.0",
|
|
73
|
+
"@dudousxd/nestjs-durable-testing": "^0.18.0",
|
|
74
|
+
"@dudousxd/nestjs-durable-transport-bullmq": "0.22.1",
|
|
75
|
+
"@dudousxd/nestjs-durable-transport-event-emitter": "^0.5.1"
|
|
75
76
|
},
|
|
76
77
|
"module": "./dist/index.js",
|
|
77
78
|
"exports": {
|
|
@@ -116,6 +116,12 @@ DurableModule.forRootAsync({
|
|
|
116
116
|
});
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
`BullMQTransport` has two concurrency knobs. `concurrency` (default `1`; a number, `'adaptive'`, or
|
|
120
|
+
`{ mode: 'adaptive', min, max, ... }`) is how many TASKS a worker runs at once from each step queue.
|
|
121
|
+
`resultConcurrency` (default `1`) is how many step RESULTS the engine side handles at once. Raise it
|
|
122
|
+
for wide fans; one run's results still resume it in order on an instance. A non-finite or sub-1
|
|
123
|
+
`resultConcurrency` falls back to `1`.
|
|
124
|
+
|
|
119
125
|
### Split API pods from worker pods
|
|
120
126
|
|
|
121
127
|
`DurableModule.forRoot` is the ONE module; the role is inferred from which of `store`/`connection`
|
|
@@ -6,7 +6,8 @@ description: >
|
|
|
6
6
|
always dispatched, always engine-scheduled. @Step()-decorated provider methods carry the routing
|
|
7
7
|
identity (derived Class.method name, or an explicit override); a method reference dispatches a
|
|
8
8
|
same-process/typed step, a string name dispatches a cross-runtime one (e.g. a Python worker). Covers
|
|
9
|
-
retries/backoff and FatalError, Promise.all fan-out,
|
|
9
|
+
retries/backoff and FatalError, Promise.all fan-out, ctx.all over a @Step (step fans with
|
|
10
|
+
concurrency waves and waitAll/failFast), sub-process log.sub/subProcess annotations
|
|
10
11
|
inside a @Step handler, ctx.child for cross-runtime sub-workflows, and constructor dependency
|
|
11
12
|
injection inside a workflow class.
|
|
12
13
|
license: MIT
|
|
@@ -145,10 +146,49 @@ outputs in input order (a `GatherError` carries the failed indexes, as for a chi
|
|
|
145
146
|
const results = await ctx.all(this.lines.processLine, lines, { concurrency: 500 });
|
|
146
147
|
```
|
|
147
148
|
|
|
149
|
+
When a failed item is an expected outcome rather than a reason to fail the run, use
|
|
150
|
+
`ctx.allSettled` instead: the same fan, resolving with `{ status: 'fulfilled', value }` or
|
|
151
|
+
`{ status: 'rejected', id, error, code?, details? }` per input, in input order, and never throwing for an item. The rejected `code`/`details` are the `FatalError`'s (absent for a plain `Error`). Throw
|
|
152
|
+
`FatalError` in the step for a failure a retry can't fix (a schema mismatch); it settles as rejected
|
|
153
|
+
without spending the retries:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const results = await ctx.allSettled(this.lines.validateLine, lines, { concurrency: 500 });
|
|
157
|
+
const rejected = results.filter((r) => r.status === 'rejected');
|
|
158
|
+
```
|
|
159
|
+
|
|
148
160
|
Prefer it to `Promise.all` past a handful of steps: every result of a `Promise.all` replays the
|
|
149
161
|
workflow, which makes a fan of N steps cost O(N²) checkpoint reads. Prefer it to a child-workflow
|
|
150
162
|
`ctx.all` when each item is just a step: a child costs a whole run (row, turn, lease, signal).
|
|
151
163
|
|
|
164
|
+
How the step fan behaves (`AllStepsOptions`):
|
|
165
|
+
|
|
166
|
+
- `concurrency` sends strict waves of that many steps; the next wave goes out once the whole
|
|
167
|
+
previous wave has settled. It is part of the fan's identity, like `inputs` — don't change it under
|
|
168
|
+
an in-flight run. It must be a positive integer.
|
|
169
|
+
- `mode: 'waitAll'` (default) lets every step settle, then throws `GatherError` with every failed
|
|
170
|
+
index. `mode: 'failFast'` throws `GatherError` with the first failure, sends no further waves, and
|
|
171
|
+
does NOT cancel steps already in flight.
|
|
172
|
+
- A step only counts as failed once its retries (the `@Step` policy, or per-call `retries` /
|
|
173
|
+
`backoff`) are spent.
|
|
174
|
+
- Rejected: a `queue` option, and a `@Step` that declares `timeoutMs`.
|
|
175
|
+
- The steps share `parallelGroup` `steps:<firstSeq>:<size>[:failFast]`.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { GatherError } from '@dudousxd/nestjs-durable-core';
|
|
179
|
+
|
|
180
|
+
let failedAt: number | undefined;
|
|
181
|
+
try {
|
|
182
|
+
await ctx.all(this.lines.processLine, lines, { concurrency: 500, mode: 'failFast' });
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (!(err instanceof GatherError)) throw err;
|
|
185
|
+
failedAt = err.failures[0]?.index; // the first failed input's position
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
For a wide fan over `BullMQTransport`, raise `resultConcurrency` (default 1) on the instances that
|
|
190
|
+
consume results; one run's results still resume it in order on an instance.
|
|
191
|
+
|
|
152
192
|
### Sub-process events inside a step
|
|
153
193
|
|
|
154
194
|
For pure visibility *inside* one step's execution, the `@Step` handler's optional second argument
|
|
@@ -184,7 +224,7 @@ const rows = await ctx.child<{ rows: number }>('processing', { baseId });
|
|
|
184
224
|
`ctx.child` starts the child once (checkpointed) and suspends — zero compute — until it reaches a
|
|
185
225
|
terminal state, then resumes with its output (or throws if the child failed). Use `ctx.all(Class,
|
|
186
226
|
inputs)` / `ctx.all(name, inputs)` to fan out N children of the same workflow and wait for all of
|
|
187
|
-
them.
|
|
227
|
+
them, or `ctx.allSettled(...)` to get every outcome back instead of a `GatherError`.
|
|
188
228
|
|
|
189
229
|
## Common mistakes
|
|
190
230
|
|
|
@@ -247,4 +287,29 @@ async charge(order: Order) {
|
|
|
247
287
|
```
|
|
248
288
|
|
|
249
289
|
Any thrown error is retried to the step's limit; only `FatalError` short-circuits retries.
|
|
290
|
+
|
|
291
|
+
When a failure is an expected outcome rather than a reason to fail the run (a line that fails
|
|
292
|
+
validation), route its code to an `@OnError` handler. The engine starts the handler workflow
|
|
293
|
+
(`<workflow>.onError.<method>`) as run `<runId>.onError.<attempt>` with `{ runId, workflow, input, error }`
|
|
294
|
+
when the run fails with that code, and the run settles with the handler's outcome (its output, or
|
|
295
|
+
its error when the handler fails); the body never runs again, so a `ctx.all` over the workflow
|
|
296
|
+
keeps going. It works for remote (Python) bodies too:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
class InvalidLine extends FatalError {
|
|
300
|
+
static readonly code = 'invalid_line';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@OnError(InvalidLine) // or @OnError('invalid_line'); @OnError() catches anything else
|
|
304
|
+
async invalid(ctx: WorkflowCtx, failure: OnErrorInput<Line>): Promise<LineResult> {
|
|
305
|
+
await ctx.step(this.steps.recordDeadLetter, { line: failure.input, error: failure.error.message });
|
|
306
|
+
return { ok: 0, failed: 1 };
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`error.details` is the third argument of `FatalError(message, code, details)` (e.g. validation
|
|
311
|
+
issues). Retrying the run runs its body again; a new failure goes to a new handler run.
|
|
312
|
+
It matches on `code`, never `instanceof`, must return `run`'s output type, and never sees
|
|
313
|
+
cancellation (a `cancelled` run never routes), `nested_workflow_call` ctx misuse, or `non_determinism`
|
|
314
|
+
(`NonDeterminismError` carries `code: 'non_determinism'`). `@Workflow({ onError: [{ code, workflow }] })` routes to another workflow.
|
|
250
315
|
Source: packages/core/src/workflow-ctx.ts (`step` retry loop), packages/core/src/errors.ts.
|