Giter Club home page Giter Club logo

bpmn-elements's People

Contributors

javierlopezaircall avatar jonklein avatar paed01 avatar saeedtabrizi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

bpmn-elements's Issues

Export internal Behaviour classes

Hi

I'm using bpmn-engine to execute the whole process flow in my application, but the execution of any script expressions (e.g. the code of a ScriptTask or the condition of a SequenceFlow) needs to be executed by my application. Furthermore, I need a bit more control over how activities are generally executed.

As a proof of concept, I have therefore copied & pasted the implementation of ScriptTaskBehaviour and used my custom implementation in the engine, which is working well so far. Now I'm not a big fan of copying and pasting and would like to do this in a cleaner way by extending the default ScriptTaskBehaviour rather than copying it over.

For this to work, the package would however need to export the Behaviour classes, which it currently doesn't. Would you be open to changing this minor detail?

For reference, what I'm currently doing:

const engine = Engine({
	source,
	listener,
	elements: {
		ScriptTask: MyScriptTask,
	}
})

// -------------------------

import * as BpmnElements from 'bpmn-elements'
export function MyScriptTask(activityDefinition: any, context: any) {
	return new (BpmnElements as any).Activity(MyScriptTaskBehaviour, activityDefinition, context)
}

class MyScriptTaskBehaviour {
	loopCharacteristics
	constructor(private activity, private context) {
		this.loopCharacteristics = activity.behaviour.loopCharacteristics && new activity.behaviour.loopCharacteristics.Behaviour(this.activity, activity.behaviour.loopCharacteristics);
	}
	
	async execute(executeMessage) {
		const { content } = executeMessage
		const { id, type, broker, behaviour } = this.activity
		const { environment } = this.context
		
		if (this.loopCharacteristics && content.isRootScope) {
			return this.loopCharacteristics.execute(executeMessage);
		}
		
		try {
			await myCustomFunctionProvidedByMyApplication()
			broker.publish('execution', 'execute.completed', { ...content })
		} catch(err) {
			// handle error according to https://github.com/paed01/bpmn-engine/issues/182
		}
	}
}

and what I'd like to do instead:

const engine = Engine({
	source,
	listener,
	elements: {
		ScriptTask: MyScriptTask,
	}
})

// -------------------------

import { Activity, ScriptTaskBehaviour } from 'bpmn-elements' // Activity is already exported but not included in the .d.ts type declarations
export function MyScriptTask(activityDefinition: any, context: any) {
	return new Activity(MyScriptTaskBehaviour, activityDefinition, context)
}

class MyScriptTaskBehaviour extends ScriptTaskBehaviour {
	async execute(executeMessage) {
            // same code as in the previous example
	}
}

PS: I know that there's little code reuse in the above example as ScriptTaskBehaviour doesn't contain much more than the execute method, but I'll also need to override the behaviours of a few other similar cases where it's not as straight-forward (e.g. StartEventBehaviour).

Thanks for your support and the great work you've already put into this!

[Feature] , load process and definition extensions before load activity extensions .

There is loadExtention method and works well for loading activity extensions .
But there is no way to load Process Extensions before loading activity extensions .
I recommend to have a loadProcessExtensions in process the following code in the custom extensions :
<bpmn:process id="Process_103i680" name="Team Choosing Process" isExecutable="true" nowjs:candidateStarterGroups="Admins,Publishers" nowjs:candidateStarterUsers="Saeed,Hamid" nowjs:versionTag="1"> <bpmn:extensionElements> <nowjs:executionListener event="start"> <nowjs:script scriptFormat="javascript">function(t,i){ console.log("Hello Start Execution Process"); }</nowjs:script> </nowjs:executionListener> <nowjs:executionListener event="end"> <nowjs:script scriptFormat="javascript">function(t,i){ console.log("Hello End Execution Process"); }</nowjs:script> </nowjs:executionListener> </bpmn:extensionElements>
Thanks

Get task name

Hello, I'm trying this library and got useful results on the execution of the workflow.

However I'm having problems getting the attributes of the tasks, like the task name.

My code:

'use strict';

var BpmnModdle = require('bpmn-moddle');

var camundaModdle = require('camunda-bpmn-moddle/resources/camunda');


const {Engine} = require('bpmn-engine');
const {EventEmitter} = require('events');

const source = `
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions 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:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.2.1">
  <bpmn:collaboration id="Collaboration_125kmlt">
    <bpmn:participant id="Participant_1m824gz" processRef="Process_1" />
  </bpmn:collaboration>
  <bpmn:process id="Process_1" isExecutable="true">
    <bpmn:task id="Task_0lql6pm" name="Task">
      <bpmn:incoming>SequenceFlow_1lmtknf</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_0vn6u9s</bpmn:outgoing>
    </bpmn:task>
    <bpmn:endEvent id="EndEvent_05qevh9" name="Fim">
      <bpmn:incoming>SequenceFlow_1uuhwhq</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:startEvent id="StartEvent_1" name="Inicio">
      <bpmn:outgoing>SequenceFlow_1lmtknf</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:task id="Task_03tvoig" name="Task 2">
      <bpmn:extensionElements>
        <camunda:properties>
          <camunda:property name="bla" value="blue" />
        </camunda:properties>
      </bpmn:extensionElements>
      <bpmn:incoming>SequenceFlow_0vn6u9s</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_129st9b</bpmn:outgoing>
    </bpmn:task>
    <bpmn:task id="Task_1rf5iiv" name="Task 4">
      <bpmn:incoming>SequenceFlow_0d0d8x7</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_1uuhwhq</bpmn:outgoing>
    </bpmn:task>
    <bpmn:userTask id="Task_0es7s5u" name="Task 3">
      <bpmn:extensionElements>
        <camunda:formData>
          <camunda:formField id="FormField_0tk57s0" type="string" />
          <camunda:formField id="FormField_2i2ovp5" type="long" />
          <camunda:formField id="FormField_09cjc22" type="boolean" />
          <camunda:formField id="FormField_318salp" type="date" />
        </camunda:formData>
      </bpmn:extensionElements>
      <bpmn:incoming>SequenceFlow_129st9b</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_0d0d8x7</bpmn:outgoing>
    </bpmn:userTask>
    <bpmn:sequenceFlow id="SequenceFlow_1uuhwhq" sourceRef="Task_1rf5iiv" targetRef="EndEvent_05qevh9" />
    <bpmn:sequenceFlow id="SequenceFlow_0d0d8x7" sourceRef="Task_0es7s5u" targetRef="Task_1rf5iiv" />
    <bpmn:sequenceFlow id="SequenceFlow_129st9b" sourceRef="Task_03tvoig" targetRef="Task_0es7s5u" />
    <bpmn:sequenceFlow id="SequenceFlow_0vn6u9s" sourceRef="Task_0lql6pm" targetRef="Task_03tvoig" />
    <bpmn:sequenceFlow id="SequenceFlow_1lmtknf" sourceRef="StartEvent_1" targetRef="Task_0lql6pm" />
    <bpmn:textAnnotation id="TextAnnotation_1dku5an">
      <bpmn:text>BLA BLA</bpmn:text>
    </bpmn:textAnnotation>
    <bpmn:association id="Association_0dmanpm" sourceRef="StartEvent_1" targetRef="TextAnnotation_1dku5an" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_125kmlt">
      <bpmndi:BPMNShape id="Participant_1m824gz_di" bpmnElement="Participant_1m824gz" isHorizontal="true">
        <dc:Bounds x="122" y="81" width="1280" height="260" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="TextAnnotation_1dku5an_di" bpmnElement="TextAnnotation_1dku5an">
        <dc:Bounds x="210" y="103" width="100" height="30" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Task_1blbewn_di" bpmnElement="Task_0lql6pm">
        <dc:Bounds x="348" y="163" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="EndEvent_05qevh9_di" bpmnElement="EndEvent_05qevh9">
        <dc:Bounds x="1345" y="283" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="1354" y="326" width="19" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="StartEvent_0dxnxgb_di" bpmnElement="StartEvent_1">
        <dc:Bounds x="173" y="185" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="178" y="228" width="26" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Task_03tvoig_di" bpmnElement="Task_03tvoig">
        <dc:Bounds x="587" y="163" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Task_1rf5iiv_di" bpmnElement="Task_1rf5iiv">
        <dc:Bounds x="1065" y="163" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="UserTask_0vz3ffk_di" bpmnElement="Task_0es7s5u">
        <dc:Bounds x="826" y="163" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Association_0dmanpm_di" bpmnElement="Association_0dmanpm">
        <di:waypoint x="202" y="189" />
        <di:waypoint x="248" y="133" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_1uuhwhq_di" bpmnElement="SequenceFlow_1uuhwhq">
        <di:waypoint x="1165" y="203" />
        <di:waypoint x="1255" y="203" />
        <di:waypoint x="1255" y="301" />
        <di:waypoint x="1345" y="301" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_0d0d8x7_di" bpmnElement="SequenceFlow_0d0d8x7">
        <di:waypoint x="926" y="203" />
        <di:waypoint x="1065" y="203" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_129st9b_di" bpmnElement="SequenceFlow_129st9b">
        <di:waypoint x="687" y="203" />
        <di:waypoint x="826" y="203" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_0vn6u9s_di" bpmnElement="SequenceFlow_0vn6u9s">
        <di:waypoint x="448" y="203" />
        <di:waypoint x="587" y="203" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_1lmtknf_di" bpmnElement="SequenceFlow_1lmtknf">
        <di:waypoint x="209" y="203" />
        <di:waypoint x="348" y="203" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

`;

const engine = Engine({
  name: 'Test',
  source,
  moddleOptions: {
    camunda: require('camunda-bpmn-moddle/resources/camunda')
  }
});

const listener = new EventEmitter();

var oldEmit = listener.emit;

listener.emit = function() {
    var emitArgs = arguments;
    console.log("Received event", emitArgs[0]);
    oldEmit.apply(listener, arguments);
}

listener.on('activity.end', (a) => {
    console.log("activity.end", a.id, a.name);
});
  
engine.execute({
  listener
}).then ( () => {
     console.log('Finished');
});

Output:
image

Expected result:
I was expecting to see the task name in the object:

image

Migrating to typescript

Would it be possible to move the implementation to typescript?
It would make some parts of documentation obsolete.

Gateway Asynchronous Execution

The basic javascript implementation of your bpmn-engine expects a callback function to be passed, so we can write async JS Functions. But your are not using this kind of callback everywhere.

For example the Gateway Implementation:

  if (flow.evaluateCondition(executeMessage, onEvaluateError)) {
        conditionMet = true;
        outbound.push({id: flow.id, action: 'take'});
      } else {
        if (evaluateError) return broker.publish('execution', 'execute.error', cloneContent(content, {error: evaluateError}));
        outbound.push({id: flow.id, action: 'discard'});
      }

Here we can never use async functions at all. The callback just sets the error object and that's it.

The script tasks utilizes it correclty:

 return script.execute(ExecutionScope(activity, executeMessage), scriptCallback);

    function scriptCallback(err, output) {

Right now i've extended the Gateways to check if the condition is a promise and then awaits the result.

So is this behaviour intended? And I have to extend all classes with promise awaiting?

ParallelLoop task index Bug?

src/tasks/LoopCharacteristics.js from line 169:

ParallelLoopCharacteristics.prototype._onCompleteMessage = function onCompleteMessage(routingKey, message) {
  const chr = this.characteristics;
  const {
    content
  } = message;
  if (content.output !== undefined) chr.output[content.index] = content.output;
  if (routingKey === 'execute.discard') {
    this.discarded++;
  }
  this.running--;
  this.activity.broker.publish('execution', 'execute.iteration.completed', {
    ...content,
    ...chr.getContent(),
    index: this.index,                    // >>>>>>>  this line 
    running: this.running,
    discarded: this.discarded,
    output: chr.output,
    state: 'iteration.completed',
    preventComplete: true
  });

The marked line should probably be removed, as the index is already included in the content, whereas in parallel tasks, the value of this.index = tasks.length +1 after the parallel tasks are started in batches

Stop and resume after error differ depending on service task implementation

This test requires bpm-engine, I don't know how to replicate it without the engine (apologize).

The test case is based on a bpmn that has this main flow:

  1. a service task is called
  2. service task raises and error
  3. error is catched by boundary event
  4. flow continues to a user task
    continuation is irrelevant at this stage.

at step 4 the current state is collected, the engine is stopped and then resumed.
on the first testcase the service task implementation is a javascript timeout that raises the error in the second case the callback is called directly .
In the first case the state is returned only with a limited set of messages and when engine is resumed it enters into a process.error in the second case process is correctly resumed.

I would expect that both scenarios lead to a correct process resume.

const {EventEmitter} = require('events');
const {Engine} = require('bpmn-engine');
const { expect } = require('chai');
const Debug = require('debug');

Debug.enable('-nock*,bpmn*,test*');

function dumpQueue(state) {
  const queues = state.definitions[0].execution.processes[0].broker.queues;
  queues.forEach(q => {
    const mess = q.messages;
    console.error(q.options);
    mess.forEach(m => {
      const {content: c} = m;
      console.error('%o: %o %o', q.name, c.id, c.state);
    });
  });
}

describe('Error handling on save and resume', () => {
  const source = `
?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>
  <process id="error-state" isExecutable="true">
    <startEvent id="StartEvent_1" name="Start">
      <outgoing>AfterStartSequenceFlow</outgoing>
    </startEvent>
    <sequenceFlow id="AfterStartSequenceFlow" sourceRef="StartEvent_1" targetRef="makeRequestService" />
    <serviceTask id="makeRequestService" name="Make request" implementation="\${environment.services.makeRequestService}">
      <incoming>AfterStartSequenceFlow</incoming>
      <incoming>APIreadySequenceFlow</incoming>
      <outgoing>gotResponse</outgoing>
    </serviceTask>
    <endEvent id="end" name="End">
      <incoming>toEnd-flow</incoming>
    </endEvent>
    <sequenceFlow id="gotResponse" sourceRef="makeRequestService" targetRef="joinGateway" />
    <sequenceFlow id="checkErrorOrResponseFlow" name="" sourceRef="joinGateway" targetRef="statusGateway" />
    <boundaryEvent id="requestErrorEvent" name="Errored" attachedToRef="makeRequestService">
      <outgoing>errorFlow</outgoing>
      <errorEventDefinition errorRef="ErrorOnRequest" />
    </boundaryEvent>
    <sequenceFlow id="errorFlow" sourceRef="requestErrorEvent" targetRef="joinGateway" />
    <sequenceFlow id="APIreadySequenceFlow" sourceRef="waitForSignalTask" targetRef="makeRequestService" />
    <sequenceFlow id="toTerminateFlow" name="Yep!" sourceRef="retryGateway" targetRef="terminateEvent">
      <conditionExpression xsi:type="tFormalExpression">\${environment.variables.retry}</conditionExpression>
    </sequenceFlow>
    <endEvent id="terminateEvent" name="Terminate">
      <incoming>toTerminateFlow</incoming>
      <terminateEventDefinition />
    </endEvent>
    <exclusiveGateway id="statusGateway" name="Success?" default="toRetryGW-flow">
      <incoming>checkErrorOrResponseFlow</incoming>
      <outgoing>toEnd-flow</outgoing>
      <outgoing>toRetryGW-flow</outgoing>
    </exclusiveGateway>
    <sequenceFlow id="toEnd-flow" name="Yay!" sourceRef="statusGateway" targetRef="end">
      <conditionExpression xsi:type="tFormalExpression">\${environment.services.statusCodeOk(environment.variables)}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="toManualDefaultFlow" name="Try again" sourceRef="retryGateway" targetRef="waitForSignalTask" />
    <exclusiveGateway id="retryGateway" name="Retried?" default="toManualDefaultFlow">
      <incoming>toRetryGW-flow</incoming>
      <outgoing>toTerminateFlow</outgoing>
      <outgoing>toManualDefaultFlow</outgoing>
    </exclusiveGateway>
    <sequenceFlow id="toRetryGW-flow" name="No..." sourceRef="statusGateway" targetRef="retryGateway" />
    <exclusiveGateway id="joinGateway" name="" default="checkErrorOrResponseFlow">
      <incoming>gotResponse</incoming>
      <incoming>errorFlow</incoming>
      <outgoing>checkErrorOrResponseFlow</outgoing>
    </exclusiveGateway>
    <boundaryEvent id="signalTimeoutEvent" name="You have got 10 minutes" attachedToRef="waitForSignalTask">
      <timerEventDefinition id="TimerEventDefinition_0849v2v">
        <timeDuration xsi:type="tFormalExpression">\${environment.variables.timeout}</timeDuration>
      </timerEventDefinition>
    </boundaryEvent>
    <userTask id="waitForSignalTask" name="Signal when API ready">
      <incoming>toManualDefaultFlow</incoming>
      <outgoing>APIreadySequenceFlow</outgoing>
    </userTask>
    <sequenceFlow id="ApiNotReadyTimeoutFlow" sourceRef="signalTimeoutEvent" targetRef="NotReadyTerminateEvent" />
    <endEvent id="NotReadyTerminateEvent">
      <incoming>ApiNotReadyTimeoutFlow</incoming>
      <terminateEventDefinition id="TerminateEventDefinition_1dj4zf7" />
    </endEvent>
  </process>
  <error id="ErrorOnRequest" name="requestError" errorCode="code000" />
</definitions>
`;
  it('does not handle the error on resume', () => {
    const services = {
      statusCodeOk: () => {
        return false;
      }
      , makeRequestService: (message, callback) => {
        setTimeout(() =>{
          callback({ name: 'requestError'
            , description: 'Error happened'
            , code: 'code000'
          }, null);
        }, 1);
      }
    };
    return new Promise((res, rej) => {
      const engine = new Engine({
        name: 'test-engine'
        , services
        , source
        , variables: {
          timeout: 'PT1S'
        }});

      let state;
      const listener = new EventEmitter();
      listener.on('activity.wait', (api) => {
        if (api.id === 'waitForSignalTask') {
          engine.getState().then(_state => {
            state = _state;
            dumpQueue(state);
            engine.stop();
          });
        }
      });
      engine.execute({ listener }, (err) => {
        if (err) rej(err);

        expect(state).to.be.ok;

        const listener2 = new EventEmitter();
        listener2.on('activity.wait', (api) => {
          if (api.id === 'waitForSignalTask') {
            res();
          }
          rej('SHOULD WAIT ONLY ON waitForSignalTask');
        });
        const engine2 = new Engine({
          source, services
        }).recover(state);

        engine2.resume({ listener: listener2}, (error) => {
          if (error) rej(error);
        });
      });
    });
  });

  it('handles the error on resume', () => {
    const services = {
      statusCodeOk: () => {
        return false;
      }
      , makeRequestService: (message, callback) => {
        callback({ name: 'requestError'
          , description: 'Error happened'
          , code: 'code000'
        }, null);
      }
    };
    return new Promise((res, rej) => {
      const engine = new Engine({
        name: 'test-engine'
        , services
        , source
        , variables: {
          timeout: 'PT1S'
        }
      });

      let state;
      const listener = new EventEmitter();
      listener.on('activity.wait', (api) => {
        if (api.id === 'waitForSignalTask') {
          engine.getState().then(_state => {
            state = _state;
            dumpQueue(state);
            engine.stop();
          });
        }
      });
      engine.execute({ listener }, (err) => {
        if (err) rej(err);

        const listener2 = new EventEmitter();
        listener2.on('activity.wait', (api) => {
          if (api.id === 'waitForSignalTask') {
            res();
          }
          rej('SHOULD WAIT ONLY ON waitForSignalTask');
        });
        const engine2 = new Engine({
          source, services
        }).recover(state);

        engine2.resume({ listener: listener2}, (error) => {
          if (error) rej(error);
        });
      });
    });
  });
});

BPM Engine and Element Multiple instances

Hi there!

Thanks for this repos!!!

I'm a little confused about the differences between bpm-engine and bpm-elements.

Other thing if, if i want to have a lot of instances of workflow working in a express app.. how you suggest that thi should work?

bests

camunda:formKey on StartEvent does not trigger activity.wait

As explained in subject.

Below is a test-case that breaks stating that "task" is not "start" beacuse there is no activity.wait on the startEvent element.

import testHelpers from '../helpers/testHelpers';
import Definition from '../../src/definition/Definition';
import camunda from 'camunda-bpmn-moddle/resources/camunda';

const extensions = {
  camunda: {
    moddleOptions: camunda,
  },
};


Feature('StartEvent Bug', () => {
  Scenario('FormKey on StartEvent', () => {
    let definition;
    Given('a process with form start event that should wait', async () => {
        const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <process id="theProcess" isExecutable="true">
    <startEvent id="start" camunda:formKey='startFormKey'/>
    <userTask id="task" camunda:formKey='taskFormKey'/>
    <endEvent id="end" />
    <sequenceFlow id="flow1" sourceRef="start" targetRef="task" />
    <sequenceFlow id="flow2" sourceRef="task" targetRef="end" />
  </process>
</definitions>`;
          const context = await testHelpers.context(source);
          definition = new Definition(context);
    });

    let end;
    let waitActivity;
    When('ran', () => {
      end = definition.waitFor('end');
      waitActivity = definition.waitFor('wait', (_, api) => {
        return api.content.id;
      });
      definition.run();
    });
    Then('stops at StartEvent', async () => {
        let api = await waitActivity;
        expect(api.id).to.equal('start');
    });
  });
});

Expected attribute loopCounter for StandardLoopCharacteristics

I am trying to use bpmn-engine on a simple example, like this bpmn I just uploaded.

loopSimple.txt

As described in BPMN 2.0 specifics, there should be an attribute loopCounter used at runtime to count the number of loops. I tried to use it on the bpmn but it does not work.

Is it possible to implement it, according to the specifics? Else, what variable should I use for the loopCounter?

Thank you very much.

exception resuming SequenceFlow discard - timing of engine save?

I'm seeing an occasional exception when resuming the engine on a user form task. The exception is coming from the processing of a postponed sequence flow discard message. What's odd to me is that the sequence flow in question is not anywhere near the task being resumed - this flow is at the end of the process, while the execution task is in the middle.

action: "discard"
id: "SequenceFlow_1fys6qm"
isDefault: true
isSequenceFlow: true
parent: {id: "Process_1n2nxgh", type: "bpmn:Process", executionId: "Process_1n2nxgh_320ae9a2"}
sequenceId: "SequenceFlow_1fys6qm_280a0a09"
sourceId: "Task_0i7bvta"
state: "pre-flight"
targetId: "EndEvent_1d0yx81"
type: "bpmn:SequenceFlow"

Partial stacktrace:

< (node:75899) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'resume' of undefined
<     at postponed.slice.forEach (/Users/jk/Development/bpmn/node_modules/bpmn-elements/dist/src/process/ProcessExecution.js:151:38)
<     at Array.forEach (<anonymous>)
<     at resume (/Users/jk/Development/bpmn/node_modules/bpmn-elements/dist/src/process/ProcessExecution.js:149:23)

In debugging, I found that I could resolve the issue by introducing a small delay in saving the engine state: when saving the engine on either flow.discard or activity.wait, the flow discard event is processed after. If I delay until that message is processed, the save/resume works correctly.

So two issues, I guess:

  1. It seems like the engine is not handling these messages correctly on resume, if my reading of the code is correct.

  2. When is the "correct" time to save engine state? Is this happening because I'm incorrectly saving the engine with messages left in the queue?

Process order definition in collaboration leads to wrong execution

In the following example I put 2 tests.
BPMN is exactly the same, only the order of the process definitions in the XML changes.
When running the test this is what happens>
In first case (process2 starts before process1) the execution completes correctly.
In second case (process2 starts after process1) the execution does not terminate.
This is because (I would say) engine enter process1, then enter process2 (ok) but then it starts process1 and does not start process2. This leads the message sent from process1 is lost.

I would expect both scenarios lead to execution completed as in the first case.

const {Engine} = require('bpmn-engine');

describe('Process order', () => {
  it('behaves as expected', () => {
    const source = `
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions 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"
 id="Definitions_05spjsy"
 targetNamespace="http://bpmn.io/schema/bpmn">
  <bpmn:collaboration id="collaboration">
    <bpmn:participant id="participant-1" name="one" processRef="process-1" />
    <bpmn:participant id="participant-2" name="two" processRef="process-2" />
    <bpmn:messageFlow id="message1-flow" sourceRef="send-message-1" targetRef="start-event-2" />
    <bpmn:messageFlow id="message2-flow" sourceRef="end-event-2" targetRef="wait-for-message2" />
  </bpmn:collaboration>
  <bpmn:process id="process-2" name="receiver" isExecutable="true">
    <bpmn:startEvent id="start-event-2">
      <bpmn:outgoing>flow2-1</bpmn:outgoing>
      <bpmn:messageEventDefinition id="message1def-2" messageRef="message1-id" />
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="flow2-1" sourceRef="start-event-2" targetRef="end-event-2" />
    <bpmn:endEvent id="end-event-2">
      <bpmn:incoming>flow2-1</bpmn:incoming>
      <bpmn:messageEventDefinition id="message2def-2" messageRef="message2-id" />
    </bpmn:endEvent>
  </bpmn:process>
  <bpmn:process id="process-1" name="sender" isExecutable="true">
    <bpmn:startEvent id="start-event-1">
      <bpmn:outgoing>flow1-1</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:intermediateThrowEvent id="send-message-1">
      <bpmn:incoming>flow1-1</bpmn:incoming>
      <bpmn:outgoing>flow1-2</bpmn:outgoing>
      <bpmn:messageEventDefinition id="message1def-1" messageRef="message1-id">
      </bpmn:messageEventDefinition>
    </bpmn:intermediateThrowEvent>
    <bpmn:intermediateCatchEvent id="wait-for-message2">
      <bpmn:incoming>flow1-2</bpmn:incoming>
      <bpmn:outgoing>flow1-3</bpmn:outgoing>
      <bpmn:messageEventDefinition id="message2def-1" messageRef="message2-id" />
    </bpmn:intermediateCatchEvent>
    <bpmn:sequenceFlow id="flow1-1" sourceRef="start-event-1" targetRef="send-message-1" />
    <bpmn:sequenceFlow id="flow1-2" sourceRef="send-message-1" targetRef="wait-for-message2" />
    <bpmn:sequenceFlow id="flow1-3" sourceRef="wait-for-message2" targetRef="end-event-1" />
    <bpmn:endEvent id="end-event-1">
      <bpmn:incoming>flow1-3</bpmn:incoming>
    </bpmn:endEvent>
  </bpmn:process>
  <bpmn:message id="message1-id" name="message1" />
  <bpmn:message id="message2-id" name="message2" />
</bpmn:definitions>`;
    return new Promise((res, rej) => {
      const engine = new Engine({
        name: 'test-engine'
        , source});

      engine.execute({ }, (err) => {
        if (err) rej(err);
        res();
      });
    });
  });
  it('does not behave as expected', () => {
    const source = `
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions 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"
 id="Definitions_05spjsy"
 targetNamespace="http://bpmn.io/schema/bpmn">
  <bpmn:collaboration id="collaboration">
    <bpmn:participant id="participant-1" name="one" processRef="process-1" />
    <bpmn:participant id="participant-2" name="two" processRef="process-2" />
    <bpmn:messageFlow id="message1-flow" sourceRef="send-message-1" targetRef="start-event-2" />
    <bpmn:messageFlow id="message2-flow" sourceRef="end-event-2" targetRef="wait-for-message2" />
  </bpmn:collaboration>
  <bpmn:process id="process-1" name="sender" isExecutable="true">
    <bpmn:startEvent id="start-event-1">
      <bpmn:outgoing>flow1-1</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:intermediateThrowEvent id="send-message-1">
      <bpmn:incoming>flow1-1</bpmn:incoming>
      <bpmn:outgoing>flow1-2</bpmn:outgoing>
      <bpmn:messageEventDefinition id="message1def-1" messageRef="message1-id">
      </bpmn:messageEventDefinition>
    </bpmn:intermediateThrowEvent>
    <bpmn:intermediateCatchEvent id="wait-for-message2">
      <bpmn:incoming>flow1-2</bpmn:incoming>
      <bpmn:outgoing>flow1-3</bpmn:outgoing>
      <bpmn:messageEventDefinition id="message2def-1" messageRef="message2-id" />
    </bpmn:intermediateCatchEvent>
    <bpmn:sequenceFlow id="flow1-1" sourceRef="start-event-1" targetRef="send-message-1" />
    <bpmn:sequenceFlow id="flow1-2" sourceRef="send-message-1" targetRef="wait-for-message2" />
    <bpmn:sequenceFlow id="flow1-3" sourceRef="wait-for-message2" targetRef="end-event-1" />
    <bpmn:endEvent id="end-event-1">
      <bpmn:incoming>flow1-3</bpmn:incoming>
    </bpmn:endEvent>
  </bpmn:process>
  <bpmn:process id="process-2" name="receiver" isExecutable="true">
    <bpmn:startEvent id="start-event-2">
      <bpmn:outgoing>flow2-1</bpmn:outgoing>
      <bpmn:messageEventDefinition id="message1def-2" messageRef="message1-id" />
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="flow2-1" sourceRef="start-event-2" targetRef="end-event-2" />
    <bpmn:endEvent id="end-event-2">
      <bpmn:incoming>flow2-1</bpmn:incoming>
      <bpmn:messageEventDefinition id="message2def-2" messageRef="message2-id" />
    </bpmn:endEvent>
  </bpmn:process>
  <bpmn:message id="message1-id" name="message1" />
  <bpmn:message id="message2-id" name="message2" />
</bpmn:definitions>`;
    return new Promise((res, rej) => {
      const engine = new Engine({
        name: 'test-engine'
        , source});

      engine.execute({ }, (err) => {
        if (err) rej(err);
        res();
      });
    });
  });
});

image

Resuming an engine seems to lose services

I'm looking for a way to configure a fairly complex flow behind a HTML form submission. The flow is hard-coded now but for our customers, we want to make it configurable. I'm making a POC but am stumbling on the user tasks. What I want is an Express app that uses the BPMN to navigate through the screens.

This is a simple BPMN I have now:

<?xml version="1.0" encoding="UTF-8"?>
<definitions id="testProcess" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <process id="theProcess1" isExecutable="true">
    <startEvent id="theStart" />
    <userTask id="loginPage" />
    <exclusiveGateway id="checkLogin" />
    <serviceTask id="invalidLogin" implementation="{environment.service.invalidLogin}" />
    <serviceTask id="validLogin" implementation="{environment.service.validLogin}" />

    <sequenceFlow id="flow1" sourceRef="theStart" targetRef="loginPage" />
    <sequenceFlow id="flow2" sourceRef="loginPage" targetRef="checkLogin" />
    <sequenceFlow id="flow3a" sourceRef="checkLogin" targetRef="invalidLogin">
        <conditionExpression xsi:type="tFormalExpression">\${environment.services.isFalse(environment.variables.isValidLogin)}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow3b" sourceRef="checkLogin" targetRef="validLogin">
        <conditionExpression xsi:type="tFormalExpression">\${environment.services.isTrue(environment.variables.isValidLogin)}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow4" sourceRef="validLogin" targetRef="end1" />
    <sequenceFlow id="flow5" sourceRef="invalidLogin" targetRef="end2" />

    <endEvent id="end1" />
    <endEvent id="end2" />
  </process>
</definitions>

When I GET the root URL, my app starts the engine:

app.get('/', async (req, res) => {
    const id = Math.floor(Math.random() * 10000);
    engine = new Engine({
        name: 'execution example',
        source,
        variables: {
            id
        },
        listener,
        services: {
            req,
            res,
            isTrue: (val) => val === true,
            isFalse: (va) => val === false
        }
    });

    await engine.execute();
});

The listener listens to several events, but this is the important one:

listener.on('activity.wait', async (api, execution) => {
    if (api.type === "bpmn:UserTask") {
        await userTasks[api.id](api);
    }

    api.stop();
    state = await execution.getState();
});

The userTasks[api.id](api) basically calls the following logic:

function loginPage(api, execution) {
    api.environment.services.res.render('login');
}

Now this works on the GET. The state is stored and the engine is stopped. But when I POST the form, I want to continue the flow where I left off:

app.post('/', async (req, res) => {
    engine = new Engine().recover(state);
    
    await engine.resume({
        listener,
        variables: {
            isValidLogin: true
        },
        services: {
            req,
            res,
            isTrue: (val) => val === true,
            isFalse: (va) => val === false
        }
    })
});

First I was thinking it would take the correct flow after the loginPage task, but it seems it continues inside the loginPage task. Makes sense, as I haven't ended that activity yet.

So I was going to inspect the request object which I pass to the services option. However, on resuming my engine, I see that all services are undefined (req, res, isTrue and `isFalse).

What is the correct way to resume an engine and include the necessary services again so I can continue "as if nothing happened"?

Async ExpressionHandler

Hello!
apparently I can extend the behaviour of an activity - but how to extend the behaviour of a SequenceFlow?

I try to use an ExpressionHandler with an asynchronous resolveExpression function, which is not allowed in the interface but could be solved easily by overwriting the ExpressionCondition.prototype.execute method as follows:

ExpressionCondition.prototype.execute = function execute(message, callback) {
const owner = this._owner;
   try {
    const result = owner.environment.resolveExpression(this.expression, owner.createMessage(message));
    return Promise.resolve(result).then(r =>
        callback ? callback(null, r) : result).catch(err =>
        callback ? callback(err) : err)
    //if (callback) return callback(null, result);
    //return result;
  } catch (err) {
    if (callback) return callback(err);
    throw err;
  }
};

or is there any other mechanism to deal with an async ExpressionHandler?

Thank you!
Florian

race condition of start events with eventDefinition

In the following simple example, there are two start events - a plain start event, and a signal event. The problem is that the plain event is executed first and flow immediately proceeds to the throw, before the signal event is executed and listening. As a result, Task2 is never invoked.

Screen Shot 2019-07-24 at 10 41 30 AM

I was able to resolve this in src/process/ProcessExecution.js by sorting the startActivities by the presence of eventDefinitions, so that start activities that have some condition are run before those that run unconditionally. I'm not sure that's the correct solution - if it is, I'd be happy to put in a PR.

I encountered this while trying to implement a LinkEvent, but the existing signal event demonstrates the issue as well.

Is this plugin enough for state machine?

Hello @paed01 , thank you very much for your awesome work on this and other library for bpmn.

Since bpmn seem doesn't support for client-side web app, i don't have any other choice than using this library.

My use case is:

I want to define a work flow with bpmn.js and base on it result to perform different action for use. One by one

Example:

image

I'v tried this lib a little bit. It run well. But i don't know how to make it run one by one, stop as condition and wait for user interact, and then base on user interact result, i'll do some action then jump to other step...

Can this library handle that and what do i need to pay attention to?

Thank you in advanced ❤️

Memory leak in SubProcess.js

Hello,
I'm running bpmn-elements from bpmn-engine and it seems that SubProcess causes memory leak.
How to reproduce:
Create process definition with:

  • SubProcess
  • Loop cardinality e.g 200

processExecutions in SubProcess.js will keep growing without any clean up policy

As a workaround I modified SubProcess.js and then it seems to run fine

  if (loopCharacteristics && content.isRootScope) {
      broker.subscribeTmp('execution', `execution.completed`, onEventMessage, { noAck: true, priority: 200 });
      broker.subscribeTmp('api', `activity.#.${rootExecutionId}`, onApiRootMessage, {
          noAck: true,
          consumerTag: `_api-${rootExecutionId}`,
          priority: 200,
  });

  function onEventMessage(routingKey, message) {
      if(message.content.executionId !== rootExecutionId) {
          return;
      }

      const messageType = message.properties.type;
      switch (messageType) {
          case 'completed':
              processExecutions.splice(0,processExecutions.length)
              break;
      }
  }

Is there any clean up policy when process is executed/completed?
I'm calling child process from parent process and it seems that the only approach is to create engine per each process instance which also causes to use a lot of memory

Invoking Task after restore

Hi,

I have a requirement to invoke Task signal knowing only the taskId or the executionId but not from inside an event, therefore, I don't have an api?

More specifically, I am providing a persistence layer, so I restore instances of definitions from a state.

User Tasks worked fine, since you kindly raise the 'wait' event on restore, but timer-tasks and event-based-tasks remain in 'enter' mode.

Thanks very much

When the implementation field of a serviceTask includes a string with comma symbols inside, it split the string

In getPropertyValue.js file, line 54:


if (args) {
      callArguments = callArguments.concat(args.split(','));
      callArguments = callArguments.map(argument => {
        return getFunctionArgument(base, argument.trim(), fnScope);
      });
} else {
      callArguments.push(base);
}

If args is of the form "'This is a string, with a comma inside'", the split() method will split it into two wrong arguments.

To avoid this you should use a function that handles the string arguments in a different way than the rest of the arguments. It can be done with a stack, or a very smart regexp, but a simple implementation could be something like this:

function splitArguments(args, base){
    let insideString = false;
    let argCompleted = false;
    let arg = '';

    let callArguments = [];

    for(let i=0;i<args.length;i++){
      let charPos = args.charAt(i);
      if(!insideString && charPos === ',') {
        argCompleted = true;
      }
      else{
        arg += charPos;

        if (charPos === '\'') {

          if(insideString){
            argCompleted = true;
          }
          else{
            insideString = true;
          }
        }
      }

      if(argCompleted){
        callArguments.push(getFunctionArgument(base, arg.trim(),fnScope));
        arg = '';
        insideString = false;
        argCompleted = false;
      }
    }

    if(arg.trim() != ''){
      callArguments.push(getFunctionArgument(base, arg.trim(),fnScope));
    }

    return callArguments;
  }

function executeFn(fn, args, base) {
    if (!fn) return;
    let callArguments = [];

    if (args) {

      callArguments = splitArguments(args, base);
      // callArguments = callArguments.concat(args.split(','));
      // callArguments = callArguments.map(argument => {
      //   return getFunctionArgument(base, argument.trim(), fnScope);
      // });
    } else {
      callArguments.push(base);
    }

    if (!fnScope) return fn.apply(null, callArguments);
    return function ScopedIIFE() {
      // eslint-disable-line no-extra-parens
      return fn.apply(this, callArguments);
    }.call(fnScope);
  }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.