Skip to content

Commit 1b28cfd

Browse files
committed
fix recover with options not forwarding engine options #199
1 parent 85216e0 commit 1b28cfd

File tree

6 files changed

+292
-7
lines changed

6 files changed

+292
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [24.0.1] - 2025-03-14
4+
5+
- fix recover engine with options not keeping scripts and logger from when initiated
6+
7+
> NB! Next major versions will not accept recovering a running engine.
8+
39
## [24.0.0] - 2025-02-08
410

511
- major update [`bpmn-elements@17`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md), something about mitigating weird formatting behaviour

lib/index.cjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,14 +280,23 @@ Engine.prototype.stop = function stop() {
280280
};
281281

282282
Engine.prototype.recover = function recover(savedState, recoverOptions) {
283+
if (this[kExecution]?.isRunning) {
284+
this.logger.error('recover during running execution will be deprecated next major version');
285+
this.logger.debug(`<${this.name}> stopping current running execution`);
286+
this[kExecution].stop();
287+
}
288+
283289
if (!savedState) return this;
284290

285291
let name = this.name;
286292
if (!name) name = this.name = savedState.name;
287293

288294
this.logger.debug(`<${name}> recover`);
289295

290-
if (recoverOptions) this[kEnvironment] = new Elements__namespace.Environment(recoverOptions);
296+
if (recoverOptions) {
297+
this[kEnvironment] = this[kEnvironment].clone(recoverOptions);
298+
}
299+
291300
if (savedState.environment) this[kEnvironment] = this[kEnvironment].recover(savedState.environment);
292301

293302
if (!savedState.definitions) return this;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "bpmn-engine",
33
"description": "BPMN 2.0 execution engine. Open source javascript workflow engine.",
4-
"version": "24.0.0",
4+
"version": "24.0.1",
55
"type": "module",
66
"module": "./src/index.js",
77
"main": "./lib/index.cjs",

src/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,23 @@ Engine.prototype.stop = function stop() {
134134
};
135135

136136
Engine.prototype.recover = function recover(savedState, recoverOptions) {
137+
if (this[kExecution]?.isRunning) {
138+
this.logger.error('recover during running execution will be deprecated next major version');
139+
this.logger.debug(`<${this.name}> stopping current running execution`);
140+
this[kExecution].stop();
141+
}
142+
137143
if (!savedState) return this;
138144

139145
let name = this.name;
140146
if (!name) name = this.name = savedState.name;
141147

142148
this.logger.debug(`<${name}> recover`);
143149

144-
if (recoverOptions) this[kEnvironment] = new Elements.Environment(recoverOptions);
150+
if (recoverOptions) {
151+
this[kEnvironment] = this[kEnvironment].clone(recoverOptions);
152+
}
153+
145154
if (savedState.environment) this[kEnvironment] = this[kEnvironment].recover(savedState.environment);
146155

147156
if (!savedState.definitions) return this;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Engine } from '../../../src/index.js';
2+
3+
const source = `
4+
<?xml version="1.0" encoding="UTF-8"?>
5+
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:bioc="http://bpmn.io/schema/bpmn/biocolor/1.0" xmlns:color="http://www.omg.org/spec/BPMN/non-normative/color/1.0" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="bpmn-js (https://demo.bpmn.io)" exporterVersion="18.1.1">
6+
<bpmn:process id="ReproduceProcess" name="Reproduce" isExecutable="true">
7+
<bpmn:startEvent id="StartEvent_1" name="Start">
8+
<bpmn:outgoing>Flow_1</bpmn:outgoing>
9+
</bpmn:startEvent>
10+
<bpmn:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="ScriptTask_1" />
11+
<bpmn:scriptTask id="ScriptTask_1" name="Script task 1" scriptFormat="Javascript">
12+
<bpmn:incoming>Flow_1</bpmn:incoming>
13+
<bpmn:outgoing>Flow_2</bpmn:outgoing>
14+
<bpmn:script>this.environment.services.log('hello from Script task 1');
15+
next();</bpmn:script>
16+
</bpmn:scriptTask>
17+
<bpmn:sequenceFlow id="Flow_2" sourceRef="ScriptTask_1" targetRef="UserTask_1" />
18+
<bpmn:userTask id="UserTask_1" name="User task 1">
19+
<bpmn:incoming>Flow_2</bpmn:incoming>
20+
<bpmn:outgoing>Flow_3</bpmn:outgoing>
21+
</bpmn:userTask>
22+
<bpmn:sequenceFlow id="Flow_3" sourceRef="UserTask_1" targetRef="ScriptTask2" />
23+
<bpmn:scriptTask id="ScriptTask2" name="Sript task 2" scriptFormat="Javascript">
24+
<bpmn:incoming>Flow_3</bpmn:incoming>
25+
<bpmn:outgoing>Flow_4</bpmn:outgoing>
26+
<bpmn:script>this.environment.services.log('hello from Script task 2');
27+
next();</bpmn:script>
28+
</bpmn:scriptTask>
29+
<bpmn:sequenceFlow id="Flow_4" sourceRef="ScriptTask2" targetRef="EndEvent_1" />
30+
<bpmn:endEvent id="EndEvent_1" name="End">
31+
<bpmn:incoming>Flow_4</bpmn:incoming>
32+
</bpmn:endEvent>
33+
</bpmn:process>
34+
</bpmn:definitions>
35+
`;
36+
37+
Feature('issue 199 - Issue with Script Tasks After State Recovery in bpmn-engine', () => {
38+
Scenario('execute, recover resume with same engine instance', () => {
39+
let engine;
40+
Given('an engine with user task flanked by two script tasks', () => {
41+
engine = new Engine({
42+
name: 'first',
43+
source,
44+
services: {
45+
log() {},
46+
},
47+
});
48+
});
49+
50+
let execution;
51+
When('executed', async () => {
52+
execution = await engine.execute();
53+
});
54+
55+
Then('engine should be in a running state', () => {
56+
expect(execution.state).to.equal('running');
57+
});
58+
59+
let end;
60+
When('user task is signalled', () => {
61+
end = engine.waitFor('end');
62+
execution.signal({ id: 'UserTask_1' });
63+
});
64+
65+
Then('run completed', () => {
66+
return end;
67+
});
68+
69+
Given('a new engine instance', () => {
70+
engine = new Engine({
71+
name: 'second',
72+
source,
73+
services: {
74+
log() {},
75+
},
76+
});
77+
});
78+
79+
let state;
80+
When('executed and get state', async () => {
81+
execution = await engine.execute();
82+
state = execution.getState();
83+
});
84+
85+
Then('engine should be in a running state', () => {
86+
expect(execution.state).to.equal('running');
87+
});
88+
89+
When('same instance is recovered with options', () => {
90+
engine.recover(state, {
91+
services: {
92+
log() {},
93+
},
94+
});
95+
});
96+
97+
And('resumed', () => {
98+
engine.resume();
99+
});
100+
101+
Then('engine is still in a running state', () => {
102+
expect(engine.execution.state).to.equal('running');
103+
});
104+
105+
When('resumed execution user task is signalled', () => {
106+
end = engine.waitFor('end');
107+
engine.execution.signal({ id: 'UserTask_1' });
108+
});
109+
});
110+
});

test/feature/resume-feature.js

Lines changed: 155 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Feature('Resume execution', () => {
1717
});
1818

1919
Given('an engine with source', () => {
20-
engine = Engine({
20+
engine = new Engine({
2121
name: 'Engine feature',
2222
source,
2323
settings: {
@@ -63,7 +63,7 @@ Feature('Resume execution', () => {
6363

6464
let recovered;
6565
When('engine is recovered with a new setting and one overridden setting', () => {
66-
recovered = Engine({
66+
recovered = new Engine({
6767
name: 'Recovered engine',
6868
}).recover(state, {
6969
settings: {
@@ -231,7 +231,7 @@ Feature('Resume execution', () => {
231231
});
232232

233233
When('engine is recovered with a the slimmer state and sourceContext', async () => {
234-
recovered = Engine({
234+
recovered = new Engine({
235235
sourceContext: await testHelpers.context(source),
236236
}).recover(slimmerState);
237237
});
@@ -266,7 +266,7 @@ Feature('Resume execution', () => {
266266
</process>
267267
</definitions>`;
268268

269-
recovered = Engine({
269+
recovered = new Engine({
270270
name: 'Recovered engine',
271271
sourceContext: await testHelpers.context(otherSource),
272272
});
@@ -278,4 +278,155 @@ Feature('Resume execution', () => {
278278
}).to.throw(Error);
279279
});
280280
});
281+
282+
Scenario('recover with options', () => {
283+
let engine, listener, source;
284+
Given('a bpmn source with user task, timer and script', () => {
285+
source = `
286+
<definitions id="Def_1" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
287+
<process id="theProcess" isExecutable="true">
288+
<task id="task" />
289+
<sequenceFlow id="to-timer" sourceRef="task" targetRef="timer" />
290+
<intermediateCatchEvent id="timer">
291+
<timerEventDefinition>
292+
<timeDuration xsi:type="tFormalExpression">PT1S</timeDuration>
293+
</timerEventDefinition>
294+
</intermediateCatchEvent>
295+
<sequenceFlow id="to-script" sourceRef="timer" targetRef="script" />
296+
<scriptTask id="script" scriptFormat="js">
297+
<script>this.environment.services.serviceFn(next)</script>
298+
</scriptTask>
299+
</process>
300+
</definitions>`;
301+
});
302+
303+
const events = [];
304+
Given('an engine with source', () => {
305+
listener = new EventEmitter();
306+
307+
listener.on('activity.end', (api) => {
308+
events.push(api.id);
309+
});
310+
311+
engine = new Engine({
312+
name: 'Resume feature',
313+
source,
314+
listener,
315+
services: {
316+
serviceFn(...args) {
317+
args.pop()();
318+
},
319+
},
320+
});
321+
});
322+
323+
let execution1;
324+
When('engine is executed', async () => {
325+
execution1 = await engine.execute();
326+
});
327+
328+
Then('timer is waiting', () => {
329+
expect(execution1.activityStatus).to.equal('timer');
330+
});
331+
332+
let execution2;
333+
When('same instance is recovered options and resumed', async () => {
334+
engine.recover(execution1.getState(), {
335+
settings: {
336+
mySetting: 1,
337+
},
338+
});
339+
340+
execution2 = await engine.resume();
341+
});
342+
343+
Then('first execution is stopped', () => {
344+
expect(execution1.state).to.equal('stopped');
345+
});
346+
347+
And('only one timer is running', () => {
348+
expect(engine.environment.timers.executing).to.have.length(1);
349+
});
350+
351+
And('a new timer is waiting', () => {
352+
expect(execution2.activityStatus).to.equal('timer');
353+
});
354+
355+
let end;
356+
When('resumed timer times out', () => {
357+
end = engine.waitFor('end');
358+
engine.environment.timers.executing.pop().callback();
359+
});
360+
361+
Then('run completes', () => {
362+
return end;
363+
});
364+
365+
And('listener has captured events', () => {
366+
expect(events).to.deep.equal(['task', 'timer', 'script']);
367+
});
368+
369+
Given('a new engine instance with same source', () => {
370+
events.splice(0);
371+
372+
engine = new Engine({
373+
name: 'Proper resume',
374+
source,
375+
listener,
376+
services: {
377+
serviceFn(...args) {
378+
args.pop()();
379+
},
380+
},
381+
});
382+
});
383+
384+
When('engine is executed', async () => {
385+
execution1 = await engine.execute();
386+
});
387+
388+
Then('timer is waiting', () => {
389+
expect(execution1.activityStatus).to.equal('timer');
390+
});
391+
392+
let state;
393+
Given('run is stopped and state is saved', async () => {
394+
await engine.stop();
395+
state = execution1.getState();
396+
});
397+
398+
When('a new instance is recovered with listener and service options and resumed', async () => {
399+
engine = new Engine({
400+
name: 'Proper recover',
401+
});
402+
403+
engine.recover(state, {
404+
listener,
405+
services: {
406+
serviceFn(...args) {
407+
args.pop()();
408+
},
409+
},
410+
});
411+
412+
execution2 = await engine.resume();
413+
});
414+
415+
Then('a timer is running', () => {
416+
expect(engine.environment.timers.executing).to.have.length(1);
417+
});
418+
419+
When('recovered and resumed timer times out', () => {
420+
end = engine.waitFor('end');
421+
engine.environment.timers.executing.pop().callback();
422+
});
423+
424+
Then('run completes', () => {
425+
return end;
426+
});
427+
428+
And('listener has captured events', () => {
429+
expect(events).to.deep.equal(['task', 'timer', 'script']);
430+
});
431+
});
281432
});

0 commit comments

Comments
 (0)