@nestjs/workflows 0.0.1 → 0.0.2
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/core/results/result-waiter.d.ts +5 -0
- package/dist/core/results/result-waiter.js +17 -6
- package/dist/core/workers/leased-worker.d.ts +7 -2
- package/dist/core/workers/leased-worker.js +24 -3
- package/dist/services/workflow-execution.service.d.ts +0 -2
- package/dist/services/workflow-execution.service.js +12 -5
- package/package.json +15 -15
|
@@ -49,6 +49,11 @@ export declare class ResultWaiter<T = unknown> {
|
|
|
49
49
|
* connections idle at that moment, and one that answers later stays open until its keep-alive timeout.
|
|
50
50
|
*/
|
|
51
51
|
close(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Reads `id`, unless the wait settles first. It races a promise of its own, not one the whole wait shares: each
|
|
54
|
+
* race leaves a reaction on its inputs until they settle, so a shared one would keep one per poll.
|
|
55
|
+
*/
|
|
56
|
+
private read;
|
|
52
57
|
private add;
|
|
53
58
|
private remove;
|
|
54
59
|
}
|
|
@@ -47,13 +47,11 @@ export class ResultWaiter {
|
|
|
47
47
|
const timeoutMs = options.timeout === undefined ? Infinity : parseDuration(options.timeout);
|
|
48
48
|
const deadline = performance.now() + timeoutMs;
|
|
49
49
|
options.signal?.throwIfAborted();
|
|
50
|
-
|
|
51
|
-
const settled = new Promise((resolve) => (settle = () => resolve(SETTLED)));
|
|
52
|
-
const wait = { closed: this.closed, settle };
|
|
50
|
+
const wait = { closed: this.closed };
|
|
53
51
|
const nudge = () => wait.wake?.();
|
|
54
52
|
const close = () => {
|
|
55
53
|
wait.closed = true;
|
|
56
|
-
settle();
|
|
54
|
+
wait.settle?.();
|
|
57
55
|
nudge();
|
|
58
56
|
};
|
|
59
57
|
this.add(id, wait);
|
|
@@ -61,7 +59,7 @@ export class ResultWaiter {
|
|
|
61
59
|
options.signal?.addEventListener('abort', nudge);
|
|
62
60
|
try {
|
|
63
61
|
for (let delay = 25;; delay = Math.min(delay * 2, 1_000)) {
|
|
64
|
-
const read = wait.outcome || wait.closed ? SETTLED : await
|
|
62
|
+
const read = wait.outcome || wait.closed ? SETTLED : await this.read(id, wait);
|
|
65
63
|
if (wait.outcome) {
|
|
66
64
|
return unwrap(wait.outcome);
|
|
67
65
|
}
|
|
@@ -104,7 +102,7 @@ export class ResultWaiter {
|
|
|
104
102
|
const settled = typeof outcome === 'function' ? outcome() : outcome;
|
|
105
103
|
for (const wait of waits) {
|
|
106
104
|
wait.outcome ??= settled;
|
|
107
|
-
wait.settle();
|
|
105
|
+
wait.settle?.();
|
|
108
106
|
wait.wake?.();
|
|
109
107
|
}
|
|
110
108
|
}
|
|
@@ -117,6 +115,19 @@ export class ResultWaiter {
|
|
|
117
115
|
this.closing.abort();
|
|
118
116
|
await nextMacrotask();
|
|
119
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Reads `id`, unless the wait settles first. It races a promise of its own, not one the whole wait shares: each
|
|
120
|
+
* race leaves a reaction on its inputs until they settle, so a shared one would keep one per poll.
|
|
121
|
+
*/
|
|
122
|
+
async read(id, wait) {
|
|
123
|
+
const settled = new Promise((resolve) => (wait.settle = () => resolve(SETTLED)));
|
|
124
|
+
try {
|
|
125
|
+
return await Promise.race([this.options.read(id), settled]);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
wait.settle = undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
120
131
|
add(id, wait) {
|
|
121
132
|
let waits = this.waits.get(id);
|
|
122
133
|
if (!waits) {
|
|
@@ -49,8 +49,8 @@ export declare class LeasedWorker<T> {
|
|
|
49
49
|
/** Claims in flight: shutdown waits for them, so nothing they claim starts unobserved. */
|
|
50
50
|
private readonly claiming;
|
|
51
51
|
private stopped;
|
|
52
|
-
|
|
53
|
-
private readonly
|
|
52
|
+
/** Ends the `drain()` rounds waiting now, at shutdown. */
|
|
53
|
+
private readonly stopping;
|
|
54
54
|
private wake?;
|
|
55
55
|
private kicked;
|
|
56
56
|
private loop?;
|
|
@@ -76,6 +76,11 @@ export declare class LeasedWorker<T> {
|
|
|
76
76
|
* `shutdownTimeout` spent waiting for them before the rest are detached. Call it from `onModuleDestroy`.
|
|
77
77
|
*/
|
|
78
78
|
shutdown(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Waits for `work`, or until shutdown. It races a promise of its own, not one the worker keeps: each race leaves a
|
|
81
|
+
* reaction on its inputs until they settle, so a long-lived one would keep one per round.
|
|
82
|
+
*/
|
|
83
|
+
private untilStopped;
|
|
79
84
|
private poll;
|
|
80
85
|
private idle;
|
|
81
86
|
/** Claims due items and starts executing them (before any caller sees them). */
|
|
@@ -43,7 +43,8 @@ export class LeasedWorker {
|
|
|
43
43
|
/** Claims in flight: shutdown waits for them, so nothing they claim starts unobserved. */
|
|
44
44
|
this.claiming = new Set();
|
|
45
45
|
this.stopped = false;
|
|
46
|
-
|
|
46
|
+
/** Ends the `drain()` rounds waiting now, at shutdown. */
|
|
47
|
+
this.stopping = new Set();
|
|
47
48
|
this.kicked = false;
|
|
48
49
|
/** When the loop next calls `produce()` (`performance.now()`). */
|
|
49
50
|
this.produceAt = 0;
|
|
@@ -96,7 +97,7 @@ export class LeasedWorker {
|
|
|
96
97
|
break;
|
|
97
98
|
}
|
|
98
99
|
total += executions.length;
|
|
99
|
-
await
|
|
100
|
+
await this.untilStopped(Promise.all(executions));
|
|
100
101
|
}
|
|
101
102
|
return total;
|
|
102
103
|
}
|
|
@@ -109,7 +110,9 @@ export class LeasedWorker {
|
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
112
|
this.stopped = true;
|
|
112
|
-
this.
|
|
113
|
+
for (const stop of this.stopping) {
|
|
114
|
+
stop();
|
|
115
|
+
}
|
|
113
116
|
this.wake?.();
|
|
114
117
|
// What a claim in flight claims starts now, told to stop at once, so it is handed back before the store closes.
|
|
115
118
|
await Promise.allSettled(this.claiming);
|
|
@@ -127,6 +130,24 @@ export class LeasedWorker {
|
|
|
127
130
|
}
|
|
128
131
|
await this.loop;
|
|
129
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Waits for `work`, or until shutdown. It races a promise of its own, not one the worker keeps: each race leaves a
|
|
135
|
+
* reaction on its inputs until they settle, so a long-lived one would keep one per round.
|
|
136
|
+
*/
|
|
137
|
+
async untilStopped(work) {
|
|
138
|
+
let stop;
|
|
139
|
+
const stopped = new Promise((resolve) => (stop = resolve));
|
|
140
|
+
if (this.stopped) {
|
|
141
|
+
stop();
|
|
142
|
+
}
|
|
143
|
+
this.stopping.add(stop);
|
|
144
|
+
try {
|
|
145
|
+
await Promise.race([work, stopped]);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
this.stopping.delete(stop);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
130
151
|
async poll() {
|
|
131
152
|
while (!this.stopped) {
|
|
132
153
|
if (this.options.produce && performance.now() >= this.produceAt) {
|
|
@@ -151,8 +151,6 @@ export declare class WorkflowExecution {
|
|
|
151
151
|
private readonly childCounters;
|
|
152
152
|
/** The first interrupt that stops this execution (not a suspension). */
|
|
153
153
|
private stoppedBy;
|
|
154
|
-
/** The step whose function is running, in that function's async context. */
|
|
155
|
-
private readonly insideStep;
|
|
156
154
|
/** Each journal entry's size in bytes, by name, and their sum. */
|
|
157
155
|
private readonly entrySizes;
|
|
158
156
|
private journalBytes;
|
|
@@ -132,8 +132,6 @@ export class WorkflowExecution {
|
|
|
132
132
|
this.childCounters = new Map();
|
|
133
133
|
/** The first interrupt that stops this execution (not a suspension). */
|
|
134
134
|
this.stoppedBy = null;
|
|
135
|
-
/** The step whose function is running, in that function's async context. */
|
|
136
|
-
this.insideStep = new AsyncLocalStorage();
|
|
137
135
|
/** Each journal entry's size in bytes, by name, and their sum. */
|
|
138
136
|
this.entrySizes = new Map();
|
|
139
137
|
this.journalBytes = 0;
|
|
@@ -776,7 +774,7 @@ export class WorkflowExecution {
|
|
|
776
774
|
await this.write([current]);
|
|
777
775
|
},
|
|
778
776
|
};
|
|
779
|
-
const run = Promise.resolve().then(() => runInStepScope(ctx.idempotencyKey, () =>
|
|
777
|
+
const run = Promise.resolve().then(() => runInStepScope(ctx.idempotencyKey, () => insideStep.run({ execution: this, name, outer: insideStep.getStore() }, () => fn(ctx))));
|
|
780
778
|
return Promise.race([run, watchdog]).finally(() => {
|
|
781
779
|
settled = true;
|
|
782
780
|
clearTimeout(overall);
|
|
@@ -805,10 +803,14 @@ export class WorkflowExecution {
|
|
|
805
803
|
* replay, so it is a definition error, reported where it happens.
|
|
806
804
|
*/
|
|
807
805
|
assertNotInStep(call, advice = 'Call ctx methods from run() and pass the values the step needs into it.') {
|
|
808
|
-
|
|
809
|
-
|
|
806
|
+
let frame = insideStep.getStore();
|
|
807
|
+
while (frame && frame.execution !== this) {
|
|
808
|
+
frame = frame.outer;
|
|
809
|
+
}
|
|
810
|
+
if (!frame) {
|
|
810
811
|
return;
|
|
811
812
|
}
|
|
813
|
+
const step = frame.name;
|
|
812
814
|
const where = step.startsWith(COMPENSATE) ? `the compensation of step "${step.slice(COMPENSATE.length)}"` : `step "${step}"`;
|
|
813
815
|
throw this.setFatal(new WorkflowDefinitionError(`${this.describe()}: ${where} called ctx.${call}. A step's function runs once and replays return its ` +
|
|
814
816
|
`result, so it can't use ctx. ${advice}`));
|
|
@@ -1069,6 +1071,11 @@ function conditionsOf(name, conditions) {
|
|
|
1069
1071
|
return branches;
|
|
1070
1072
|
}
|
|
1071
1073
|
const COMPENSATE = '$compensate:';
|
|
1074
|
+
/**
|
|
1075
|
+
* The steps whose functions are running, in their async context. One store for every execution, not one each: on
|
|
1076
|
+
* Node 20 and 22, a store that ran stays in a process-wide list every async resource walks, until `disable()`.
|
|
1077
|
+
*/
|
|
1078
|
+
const insideStep = new AsyncLocalStorage();
|
|
1072
1079
|
const PARENT_CLOSE = ['cancel', 'terminate', 'abandon'];
|
|
1073
1080
|
/** The most a custom status (`ctx.setStatus()`) may take as JSON. */
|
|
1074
1081
|
const MAX_CUSTOM_STATUS_BYTES = 16_384;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/workflows",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@workflows)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"license": "MIT",
|
|
@@ -69,37 +69,37 @@
|
|
|
69
69
|
"@commitlint/cli": "21.2.3",
|
|
70
70
|
"@commitlint/config-angular": "21.2.3",
|
|
71
71
|
"@electric-sql/pglite": "0.5.8",
|
|
72
|
-
"@nestjs/common": "12.1.
|
|
73
|
-
"@nestjs/core": "12.1.
|
|
72
|
+
"@nestjs/common": "12.1.2",
|
|
73
|
+
"@nestjs/core": "12.1.2",
|
|
74
74
|
"@nestjs/cqrs": "12.1.0",
|
|
75
75
|
"@nestjs/drizzle": "0.0.1",
|
|
76
76
|
"@nestjs/locks": "0.0.1",
|
|
77
77
|
"@nestjs/mail": "0.0.1",
|
|
78
|
-
"@nestjs/outbox": "0.0
|
|
79
|
-
"@nestjs/platform-express": "12.1.
|
|
80
|
-
"@nestjs/platform-fastify": "12.1.
|
|
81
|
-
"@nestjs/testing": "12.1.
|
|
78
|
+
"@nestjs/outbox": "0.1.0",
|
|
79
|
+
"@nestjs/platform-express": "12.1.2",
|
|
80
|
+
"@nestjs/platform-fastify": "12.1.2",
|
|
81
|
+
"@nestjs/testing": "12.1.2",
|
|
82
82
|
"@prisma/adapter-mariadb": "7.10.0",
|
|
83
83
|
"@prisma/adapter-pg": "7.10.0",
|
|
84
84
|
"@prisma/client": "7.10.0",
|
|
85
|
-
"@types/node": "24.
|
|
85
|
+
"@types/node": "24.19.1",
|
|
86
86
|
"@types/pg": "8.23.1",
|
|
87
87
|
"drizzle-orm": "0.45.3",
|
|
88
88
|
"husky": "9.1.7",
|
|
89
89
|
"kysely": "0.29.6",
|
|
90
|
-
"lint-staged": "17.
|
|
90
|
+
"lint-staged": "17.6.0",
|
|
91
91
|
"mysql2": "3.24.5",
|
|
92
|
-
"oxlint": "1.
|
|
93
|
-
"pg": "8.23.
|
|
94
|
-
"prettier": "3.9.
|
|
92
|
+
"oxlint": "1.87.0",
|
|
93
|
+
"pg": "8.23.1",
|
|
94
|
+
"prettier": "3.9.9",
|
|
95
95
|
"prisma": "7.10.0",
|
|
96
96
|
"reflect-metadata": "0.2.2",
|
|
97
|
-
"release-it": "21.1.
|
|
97
|
+
"release-it": "21.1.1",
|
|
98
98
|
"rxjs": "7.8.2",
|
|
99
99
|
"typeorm": "1.1.1",
|
|
100
100
|
"typescript": "7.0.2",
|
|
101
|
-
"vite": "8.3.
|
|
102
|
-
"vitest": "5.0.
|
|
101
|
+
"vite": "8.3.3",
|
|
102
|
+
"vitest": "5.0.3"
|
|
103
103
|
},
|
|
104
104
|
"peerDependencies": {
|
|
105
105
|
"@nestjs/common": "^11.0.0 || ^12.0.0",
|