@5minds/node-red-contrib-processcube 2.0.0-feature-629c78-m2dq1ygt → 7.6.0-develop-51b534-mjy4rzoh

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 (68) hide show
  1. package/README.md +3 -58
  2. package/check-authorization.html +138 -0
  3. package/check-authorization.js +27 -0
  4. package/dataobject-instance-query.html +141 -0
  5. package/dataobject-instance-query.js +45 -0
  6. package/endevent-finished-listener.js +14 -25
  7. package/examples/Check-Authorization-Sample.json +109 -0
  8. package/examples/Dataobject-Instance-Query-Sample.json +109 -0
  9. package/externaltask-error.html +8 -3
  10. package/externaltask-error.js +43 -29
  11. package/externaltask-event-listener.js +11 -28
  12. package/externaltask-input.html +48 -3
  13. package/externaltask-input.js +581 -114
  14. package/externaltask-output.js +20 -16
  15. package/icons/data-object-query.svg +5 -0
  16. package/message-event-trigger.html +1 -1
  17. package/message-event-trigger.js +8 -7
  18. package/package.json +74 -67
  19. package/process-event-listener.js +166 -225
  20. package/process-start.html +6 -0
  21. package/process-start.js +29 -6
  22. package/process-terminate.html +1 -1
  23. package/process-terminate.js +7 -5
  24. package/processcube-engine-config.html +25 -7
  25. package/processcube-engine-config.js +25 -135
  26. package/processcube-google-docs-mail-template.html +150 -0
  27. package/processcube-google-docs-mail-template.js +158 -0
  28. package/processdefinition-deploy.html +44 -0
  29. package/processdefinition-deploy.js +28 -0
  30. package/processdefinition-query.html +18 -13
  31. package/processdefinition-query.js +33 -31
  32. package/processinstance-delete-advanced.html +82 -0
  33. package/processinstance-delete-advanced.js +33 -0
  34. package/processinstance-delete.html +60 -8
  35. package/processinstance-delete.js +84 -30
  36. package/processinstance-query.html +116 -109
  37. package/processinstance-query.js +28 -5
  38. package/signal-event-trigger.js +8 -6
  39. package/usertask-event-listener.html +123 -1
  40. package/usertask-event-listener.js +30 -45
  41. package/usertask-input.html +119 -0
  42. package/usertask-input.js +7 -9
  43. package/usertask-output.js +15 -8
  44. package/wait-for-usertask.html +122 -6
  45. package/wait-for-usertask.js +44 -47
  46. package/.github/workflows/build-and-publish.yml +0 -72
  47. package/.processcube/authority/config/config.json +0 -36
  48. package/.processcube/authority/config/upeSeedingData.json +0 -12
  49. package/Dockerfile +0 -9
  50. package/doc_generator/_process_instances_query.md +0 -115
  51. package/doc_generator/generator.js +0 -41
  52. package/doc_generator/generator_with_swagger.js +0 -72
  53. package/doc_generator/package-lock.json +0 -176
  54. package/doc_generator/package.json +0 -15
  55. package/doc_generator/query_template.mustache +0 -20
  56. package/doc_generator/swagger.json +0 -4110
  57. package/docker-compose.yml +0 -44
  58. package/nodered/flows.json +0 -2156
  59. package/nodered/flows_cred.json +0 -3
  60. package/nodered/settings.js +0 -562
  61. package/nodered/static/ProcessCube_Logo.svg +0 -53
  62. package/processes/Call-Activity-Sample.bpmn +0 -88
  63. package/processes/External-Task-Auth-Sample.bpmn +0 -82
  64. package/processes/External-Task-Sample.bpmn +0 -94
  65. package/processes/SampleEvent.bpmn +0 -73
  66. package/processes/User-Task-Auth-Sample.bpmn +0 -63
  67. package/processes/User-Task-Sample.bpmn +0 -76
  68. package/processes/Wait-For-Usertask.bpmn +0 -74
@@ -1,170 +1,637 @@
1
1
  const EventEmitter = require('node:events');
2
2
 
3
- function showStatus(node, msgCounter) {
4
- if (msgCounter >= 1) {
5
- node.status({ fill: 'blue', shape: 'dot', text: `handling tasks ${msgCounter}.` });
6
- } else {
7
- node.status({ fill: 'blue', shape: 'ring', text: `subcribed.` });
3
+ class ExternalTaskNodeStates {
4
+ constructor(flowNodeInstanceId) {
5
+ this.flowNodeInstanceId = flowNodeInstanceId;
6
+ this.nodeStades = {}; // Track send calls per nodeId
7
+ }
8
+
9
+ markSended(nodeId) {
10
+ if (!this.nodeStades[nodeId]) {
11
+ this.nodeStades[nodeId] = { gotSend: false, gotCompleted: false };
12
+ }
13
+
14
+ this.nodeStades[nodeId].gotSend = true;
15
+ }
16
+
17
+ markCompleted(nodeId) {
18
+ if (!this.nodeStades[nodeId]) {
19
+ this.nodeStades[nodeId] = { gotSend: false, gotCompleted: false };
20
+ }
21
+
22
+ this.nodeStades[nodeId].gotCompleted = true;
8
23
  }
9
- }
10
24
 
11
- const started_external_tasks = {};
25
+ checkIfCompletedWithoutSend(nodeId) {
26
+ const nodeState = this.nodeStades[nodeId];
27
+ const result = (nodeState && nodeState.gotCompleted && !nodeState.gotSend);
28
+
29
+ return result;
30
+ }
31
+ }
12
32
 
13
33
  module.exports = function (RED) {
34
+
35
+ const os = require('os');
36
+
37
+ // Global dictionary for tracking external tasks by flowNodeInstanceId
38
+ const globalExternalTaskStates = {};
39
+
40
+ const raiseExternalTaskError = (flowNodeInstanceId, etwInputNodeId, nodeId) => {
41
+ const fullNode = RED.nodes.getNode(nodeId);
42
+
43
+ const wires = fullNode?.wires;
44
+ const hasConnectedOutputs = wires && wires.some(wireArray => wireArray && wireArray.length > 0);
45
+
46
+ if (hasConnectedOutputs) {
47
+ const inputNode = RED.nodes.getNode(etwInputNodeId);
48
+
49
+ if (inputNode && inputNode.eventEmitter) {
50
+ const errorMessage = `Node ${nodeId} (${fullNode.name || fullNode.type}) completed without sending output`;
51
+ const error = new Error(errorMessage);
52
+ error.errorCode = 'NODE_NO_OUTPUT';
53
+ error.errorDetails = RED.util.encodeObject({
54
+ flowNodeInstanceId: flowNodeInstanceId,
55
+ nodeId: nodeId,
56
+ nodeName: fullNode.name,
57
+ nodeType: fullNode.type
58
+ });
59
+
60
+ inputNode.eventEmitter.emit(`handle-${flowNodeInstanceId}`, error, true);
61
+ }
62
+ }
63
+ };
64
+
65
+ // Example synchronous onSend hook
66
+ RED.hooks.add("onSend", (sendEvents) => {
67
+ for (const sendEvent of sendEvents) {
68
+
69
+ // Call send method on ExternalTaskState if this message has a flowNodeInstanceId
70
+ if (sendEvent.msg?.flowNodeInstanceId) {
71
+ let externalTaskNodeStates = globalExternalTaskStates[sendEvent.msg.flowNodeInstanceId];
72
+
73
+ if (!externalTaskNodeStates) {
74
+ externalTaskNodeStates = new ExternalTaskNodeStates(sendEvent.msg.flowNodeInstanceId);
75
+ globalExternalTaskStates[sendEvent.msg.flowNodeInstanceId] = externalTaskNodeStates;
76
+ }
77
+
78
+ externalTaskNodeStates.markSended(sendEvent.source.node.id)
79
+
80
+ if (externalTaskNodeStates.checkIfCompletedWithoutSend(sendEvent.source.node.id)) {
81
+ raiseExternalTaskError(sendEvent.msg.flowNodeInstanceId, sendEvent.msg.etw_input_node_id, sendEvent.source.node.id);
82
+ }
83
+ }
84
+ }
85
+ });
86
+
87
+ const onCompleted = (completeEvent) => {
88
+
89
+ // Check if this is an external task message
90
+ if (completeEvent.msg?.flowNodeInstanceId) {
91
+ let externalTaskNodeStates = globalExternalTaskStates[completeEvent.msg.flowNodeInstanceId];
92
+
93
+ if (!externalTaskNodeStates) {
94
+ externalTaskNodeStates = new ExternalTaskNodeStates(completeEvent.msg.flowNodeInstanceId);
95
+ globalExternalTaskStates[completeEvent.msg.flowNodeInstanceId] = externalTaskNodeStates;
96
+ }
97
+
98
+ externalTaskNodeStates.markCompleted(completeEvent.node.id);
99
+
100
+ if (externalTaskNodeStates.checkIfCompletedWithoutSend(completeEvent.node.id) && !completeEvent.error) {
101
+
102
+ raiseExternalTaskError(completeEvent.msg.flowNodeInstanceId, completeEvent.msg.etw_input_node_id, completeEvent.node.id);
103
+ }
104
+ }
105
+ }
106
+
107
+ RED.hooks.add("onComplete", onCompleted);
108
+
14
109
  function ExternalTaskInput(config) {
15
110
  RED.nodes.createNode(this, config);
16
111
  var node = this;
17
- var flowContext = node.context().flow;
18
112
 
19
- const engine = RED.nodes.getNode(config.engine);
113
+ node.started_external_tasks = {};
114
+
115
+ node.engine = RED.nodes.getNode(config.engine);
20
116
 
21
- const client = engine.engineClient;
117
+ node.eventEmitter = new EventEmitter();
22
118
 
23
- if (!client) {
24
- node.error('No engine configured.');
25
- return;
119
+ let options = RED.util.evaluateNodeProperty(config.workerConfig, config.workerConfigType, node);
120
+ let topic = node.topic = RED.util.evaluateNodeProperty(config.topic, config.topicType, node)
121
+ this.workername = RED.util.evaluateNodeProperty(config.workername, config.workernameType, node);
122
+ node._traces = config.traces || [];
123
+
124
+ if (!options['workerId']) {
125
+
126
+ if (!this.workername) {
127
+ this.workername = `nodered:${process.env.NODERED_NAME || ''}-host:${os.hostname()}-pid:${process.pid}-id:${node.id}`;
128
+ }
129
+
130
+ options['workerId'] = this.workername;
26
131
  }
27
132
 
28
- var eventEmitter = flowContext.get('emitter');
133
+ if (!options['lockDuration'] && (process.env.NODE_RED_ETW_LOCK_DURATION || process.env.NODERED_ETW_LOCK_DURATION)) {
134
+ options['lockDuration'] = parseInt(process.env.NODE_RED_ETW_LOCK_DURATION || process.env.NODERED_ETW_LOCK_DURATION) || undefined;
135
+ }
29
136
 
30
- if (!eventEmitter) {
31
- flowContext.set('emitter', new EventEmitter());
32
- eventEmitter = flowContext.get('emitter');
137
+ if (!options['longpollingTimeout']) {
138
+ options['longpollingTimeout'] = parseInt(process.env.NODE_RED_ETW_LONGPOLLING_TIMEOUT || process.env.NODERED_ETW_LONGPOLLING_TIMEOUT) || undefined;
33
139
  }
34
140
 
35
- const etwCallback = async (payload, externalTask) => {
36
- const saveHandleCallback = (data, callback) => {
37
- try {
38
- callback(data);
39
- } catch (error) {
40
- node.error(`Error in callback 'saveHandleCallback': ${error.message}`);
141
+ if (!options['idleTimeout']) {
142
+ options['idleTimeout'] = parseInt(process.env.NODE_RED_ETW_IDLE_TIMEOUT || process.env.NODERED_ETW_IDLE_TIMEOUT) || undefined;
143
+ }
144
+
145
+ node._subscribed = true;
146
+ node._subscribed_error = null;
147
+ node._trace = '';
148
+ node._step = '';
149
+ node._tracking_nodes = {};
150
+ node._join_inputs = {};
151
+ node._tracking_for_etw = {};
152
+
153
+ node.isHandling = () => {
154
+ return Object.keys(node.started_external_tasks).length > 0;
155
+ };
156
+
157
+ node.ownMessage = (msg) => {
158
+ return msg.etw_input_node_id === node.id;
159
+ };
160
+
161
+ node.clearTracking = (msg) => {
162
+ if (msg.flowNodeInstanceId) {
163
+ node._tracking_for_etw[msg.flowNodeInstanceId].forEach((theNode) => {
164
+ node.decrMsgOnNode(theNode, msg);
165
+ });
166
+ }
167
+ };
168
+
169
+ node.incMsgOnNode = (theNode, msg) => {
170
+ if (node.id === theNode.id) {
171
+ return;
172
+ }
173
+
174
+ if (!node._tracking_nodes[theNode.id]) {
175
+ node._tracking_nodes[theNode.id] = {
176
+ node: theNode,
177
+ count: 1,
178
+ };
179
+ } else {
180
+ node._tracking_nodes[theNode.id].count++;
181
+ }
182
+
183
+ // bei nodes vom type 'join' müssen die eingänge gezählt werden,
184
+ // dass diese dann wieder beim verlassen am ausgang gesamt entfernt werden müssem
185
+ if (theNode.type === 'join') {
186
+ if (!node._join_inputs[theNode.id]) {
187
+ node._join_inputs[theNode.id] = {};
41
188
  }
42
- };
43
189
 
44
- return await new Promise((resolve, reject) => {
45
- const handleFinishTask = (msg) => {
46
- let result = RED.util.encodeObject(msg.payload);
190
+ if (!node._join_inputs[theNode.id][msg.flowNodeInstanceId]) {
191
+ node._join_inputs[theNode.id][msg.flowNodeInstanceId] = 1;
192
+ } else {
193
+ node._join_inputs[theNode.id][msg.flowNodeInstanceId]++;
194
+ }
195
+ }
196
+
197
+ if (!node._tracking_for_etw[msg.flowNodeInstanceId]) {
198
+ node._tracking_for_etw[msg.flowNodeInstanceId] = [];
199
+ node._tracking_for_etw[msg.flowNodeInstanceId].push(theNode);
200
+ } else {
201
+ node._tracking_for_etw[msg.flowNodeInstanceId].push(theNode);
202
+ }
203
+
204
+ theNode.status({ fill: 'blue', shape: 'dot', text: `tasks(${node._tracking_nodes[theNode.id].count})` });
205
+ };
47
206
 
48
- node.log(
49
- `handle event for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* ${externalTask.processInstanceId} with result ${result} on msg._msgid ${msg._msgid}.`
50
- );
207
+ node.decrMsgOnNode = (theNode, msg) => {
208
+ if (node.id === theNode.id) {
209
+ return;
210
+ }
51
211
 
52
- if (externalTask.flowNodeInstanceId) {
53
- delete started_external_tasks[externalTask.flowNodeInstanceId];
54
- }
212
+ // bei nodes vom type 'join' müssen die eingänge gezählt werden,
213
+ // dass diese dann wieder beim verlassen am ausgang gesamt entfernt werden müssen
214
+ let dec_count = 1;
55
215
 
56
- showStatus(node, Object.keys(started_external_tasks).length);
216
+ if (theNode.type === 'join') {
217
+ if (!node._join_inputs[theNode.id]) {
218
+ node._join_inputs[theNode.id] = {};
219
+ }
220
+
221
+ if (node._join_inputs[theNode.id][msg.flowNodeInstanceId]) {
222
+ dec_count = node._join_inputs[theNode.id][msg.flowNodeInstanceId];
223
+ delete node._join_inputs[theNode.id][msg.flowNodeInstanceId];
224
+ }
225
+ }
57
226
 
58
- //resolve(result);
59
- saveHandleCallback(result, resolve);
227
+ if (!node._tracking_nodes[theNode.id]) {
228
+ node._tracking_nodes[theNode.id] = {
229
+ node: theNode,
230
+ count: 0,
60
231
  };
232
+ } else {
233
+ //node._tracking_nodes[theNode.id].count--;
234
+ node._tracking_nodes[theNode.id].count =- dec_count;
61
235
 
62
- const handleErrorTask = (msg) => {
63
- node.log(
64
- `handle error event for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}' on *msg._msgid* '${msg._msgid}'.`
65
- );
236
+ if (node._tracking_nodes[theNode.id].count <= 0) {
237
+ node._tracking_nodes[theNode.id].count = 0;
238
+ }
239
+ }
66
240
 
67
- if (externalTask.flowNodeInstanceId) {
68
- delete started_external_tasks[externalTask.flowNodeInstanceId];
69
- }
241
+ if (node._tracking_for_etw[msg.flowNodeInstanceId]) {
242
+ const count_nodes = node._tracking_for_etw[msg.flowNodeInstanceId].filter(item => item !== theNode)
243
+
244
+ if (count_nodes <= 0) {
245
+ delete node._tracking_for_etw[msg.flowNodeInstanceId];
246
+ }
247
+ }
70
248
 
71
- showStatus(node, Object.keys(started_external_tasks).length);
249
+ theNode.status({ fill: 'blue', shape: 'dot', text: `tasks(${node._tracking_nodes[theNode.id].count})` });
250
+ };
72
251
 
73
- // TODO: with reject, the default error handling is proceed
74
- // SEE: https://github.com/5minds/ProcessCube.Engine.Client.ts/blob/develop/src/ExternalTaskWorker.ts#L180
75
- // reject(result);
76
- //resolve(msg);
77
- saveHandleCallback(msg, resolve);
252
+ node.traceExecution = (msg) => {
253
+ if ((node._traces || []).includes(msg.event)) {
254
+
255
+ const message = {
256
+ id: node.id,
257
+ z: node.z,
258
+ _alias: node._alias,
259
+ path: node._flow.path,
260
+ name: node.name,
261
+ topic: node.topic,
262
+ msg: msg
78
263
  };
79
264
 
80
- eventEmitter.once(`handle-${externalTask.flowNodeInstanceId}`, (msg, isError = false) => {
81
- node.log(
82
- `handle event for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}' with *msg._msgid* '${msg._msgid}' and *isError* '${isError}'`
83
- );
265
+ RED.comms.publish("debug", message);
266
+ }
267
+ };
84
268
 
85
- if (isError) {
86
- handleErrorTask(msg);
87
- } else {
88
- handleFinishTask(msg);
269
+ const onPreDeliver = (sendEvent) => {
270
+ try {
271
+ if (node.isHandling() && node.ownMessage(sendEvent.msg)) {
272
+
273
+ const sourceNode = sendEvent?.source?.node;
274
+ const destinationNode = sendEvent?.destination?.node;
275
+
276
+ node._step = `${destinationNode.name || destinationNode.type}`;
277
+
278
+ node.showStatus();
279
+
280
+ const debugMsg = {
281
+ event: 'enter',
282
+ sourceName: sourceNode.name,
283
+ sourceType: sourceNode.type,
284
+ destinationNodeName: destinationNode.name,
285
+ destinationNodeType: destinationNode.type,
286
+ timestamp: new Date().toISOString(),
287
+ };
288
+
289
+ node.traceExecution(debugMsg);
290
+
291
+ if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true' || process.env.NODERED_ETW_STEP_LOGGING == 'true') {
292
+ node._trace = `'${sourceNode.name || sourceNode.type}'->'${destinationNode.name || destinationNode.type}'`;
293
+ node.log(`preDeliver: ${node._trace}`);
89
294
  }
90
- });
295
+ }
296
+ } catch (error) {
297
+ node.error(`Error in onPreDeliver: ${error?.message}`, { error: error });
298
+ }
299
+ };
300
+ RED.hooks.add(`preDeliver.etw-input-${node.id}`, onPreDeliver);
301
+
302
+ const onPostDeliver = (sendEvent) => {
303
+ try {
304
+ if (node.isHandling() && node.ownMessage(sendEvent.msg)) {
305
+ const sourceNode = sendEvent?.source?.node;
306
+ const destinationNode = sendEvent?.destination?.node;
307
+
308
+ node.decrMsgOnNode(sourceNode, sendEvent.msg);
309
+ node.incMsgOnNode(destinationNode, sendEvent.msg);
310
+
311
+ const debugMsg = {
312
+ event: 'exit',
313
+ sourceName: sourceNode.name,
314
+ sourceType: sourceNode.type,
315
+ destinationNodeName: destinationNode.name,
316
+ destinationNodeType: destinationNode.type,
317
+ timestamp: new Date().toISOString(),
318
+ };
319
+
320
+ node.traceExecution(debugMsg);
321
+
322
+ if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true' || process.env.NODERED_ETW_STEP_LOGGING == 'true') {
323
+ node._trace = `'${sourceNode.name || sourceNode.type}'->'${destinationNode.name || destinationNode.type}'`;
324
+ node.log(`postDeliver: ${node._trace}`);
325
+ }
326
+ }
327
+ } catch (error) {
328
+ node.error(`Error in onPostDeliver: ${error?.message}`, { error: error });
329
+ }
330
+ };
331
+ RED.hooks.add(`postDeliver.etw-input-${node.id}`, onPostDeliver);
91
332
 
92
- started_external_tasks[externalTask.flowNodeInstanceId] = externalTask;
333
+ node.setSubscribedStatus = () => {
334
+ this._subscribed = true;
335
+ this._subscribed_error = null;
336
+ this.showStatus();
337
+ };
93
338
 
94
- showStatus(node, Object.keys(started_external_tasks).length);
339
+ node.setUnsubscribedStatus = (error) => {
340
+ this._subscribed = false;
341
+ this._subscribed_error = error;
95
342
 
96
- let msg = {
97
- _msgid: RED.util.generateId(),
98
- task: RED.util.encodeObject(externalTask),
99
- payload: payload,
100
- flowNodeInstanceId: externalTask.flowNodeInstanceId,
101
- processInstanceId: externalTask.processInstanceId
102
- };
343
+ const info = `subscription failed (topic: ${node.topic}) [error: ${error?.message}].`;
344
+
345
+ this.error(info, JSON.stringify(error));
103
346
 
104
- node.log(
105
- `Received *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}' with *msg._msgid* '${msg._msgid}'`
106
- );
347
+ this.showStatus();
348
+ };
107
349
 
108
- node.send(msg);
109
- });
350
+ node.setStartHandlingTaskStatus = (externalTask) => {
351
+ this._subscribed = true;
352
+ this._subscribed_error = null;
353
+ this.started_external_tasks[externalTask.flowNodeInstanceId] = externalTask;
354
+
355
+ const debugMsg = {
356
+ event: 'start',
357
+ topic: node.topic,
358
+ flowNodeInstanceId: externalTask.flowNodeInstanceId,
359
+ timestamp: new Date().toISOString(),
360
+ };
361
+
362
+ node.traceExecution(debugMsg);
363
+
364
+ this.showStatus();
110
365
  };
111
366
 
112
- let options = {};
367
+ node.setFinishHandlingTaskStatus = (externalTask) => {
368
+ if (externalTask.flowNodeInstanceId) {
369
+ delete this.started_external_tasks[externalTask.flowNodeInstanceId];
370
+ node._trace = '';
371
+ node._step = '';
372
+ }
373
+
374
+ this._subscribed = true;
375
+ this._subscribed_error = null;
376
+
377
+ const debugMsg = {
378
+ event: 'finish',
379
+ topic: node.topic,
380
+ flowNodeInstanceId: externalTask.flowNodeInstanceId,
381
+ timestamp: new Date().toISOString(),
382
+ };
113
383
 
114
- if (!!config.workerConfig) {
115
- options = RED.util.evaluateNodeProperty(config.workerConfig, 'json', node);
116
- }
384
+ node.traceExecution(debugMsg);
117
385
 
118
- client.externalTasks
119
- .subscribeToExternalTaskTopic(config.topic, etwCallback, options)
120
- .then(async (externalTaskWorker) => {
121
- node.status({ fill: 'blue', shape: 'ring', text: 'subcribed' });
386
+ this.clearTracking(externalTask); // as msg
387
+ this.showStatus();
388
+ };
122
389
 
123
- externalTaskWorker.identity = engine.identity;
124
- engine.registerOnIdentityChanged((identity) => {
125
- externalTaskWorker.identity = identity;
126
- });
390
+ node.setErrorFinishHandlingTaskStatus = (externalTask, error) => {
391
+ if (externalTask.flowNodeInstanceId) {
392
+ delete this.started_external_tasks[externalTask.flowNodeInstanceId];
393
+ }
127
394
 
128
- // export type WorkerErrorHandler = (errorType: 'fetchAndLock' | 'extendLock' | 'processExternalTask' | 'finishExternalTask', error: Error, externalTask?: ExternalTask<any>) => void;
129
- externalTaskWorker.onWorkerError((errorType, error, externalTask) => {
130
- switch (errorType) {
131
- case 'extendLock':
132
- case 'finishExternalTask':
133
- case 'processExternalTask':
134
- node.error(
135
- `Worker error ${errorType} for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}': ${error.message}`
136
- );
137
-
138
- if (externalTask) {
139
- delete started_external_tasks[externalTask.flowNodeInstanceId];
140
- }
395
+ this._subscribed_error = error;
396
+ this.error(`finished task failed (topic: ${node.topic}).`);
141
397
 
142
- showStatus(node, Object.keys(started_external_tasks).length);
143
- break;
144
- default:
145
- // reduce noise error logs
146
- // node.error(`Worker error ${errorType}: ${error.message}`);
147
- break;
398
+ const debugMsg = {
399
+ event: 'error',
400
+ topic: node.topic,
401
+ flowNodeInstanceId: externalTask.flowNodeInstanceId,
402
+ timestamp: new Date().toISOString(),
403
+ };
404
+
405
+ node.traceExecution(debugMsg);
406
+
407
+
408
+ this.showStatus();
409
+ };
410
+
411
+ node.sendStatus = (status, message) => {
412
+ RED.events.emit("processcube:healthcheck:update", {
413
+ nodeId: node.id,
414
+ status: status,
415
+ nodeName: `topic: ${node.topic}`,
416
+ nodeType: 'externaltask-input',
417
+ message: message
418
+ });
419
+ };
420
+
421
+ node.showStatus = () => {
422
+ const msgCounter = Object.keys(this.started_external_tasks).length;
423
+
424
+ if (this._subscribed === false) {
425
+ this.status({ fill: 'red', shape: 'ring', text: `subscription failed (${msgCounter})` });
426
+
427
+ this.sendStatus('NotOk', `subscription failed (${msgCounter})`);
428
+ } else {
429
+ if (msgCounter >= 1) {
430
+ if (node._step) {
431
+ this.status({ fill: 'blue', shape: 'dot', text: `tasks(${msgCounter}) ->'${node._step}'` });
432
+ this.sendStatus('Ok', `tasks(${msgCounter}) ->'${node._step}'.`);
433
+ } else {
434
+ this.status({ fill: 'blue', shape: 'dot', text: `tasks(${msgCounter})` });
435
+ this.sendStatus('Ok', `tasks(${msgCounter})`);
148
436
  }
149
- });
437
+ this.log(`handling tasks ${msgCounter}.`);
438
+ } else {
439
+ this.status({ fill: 'green', shape: 'ring', text: `subcribed` });
440
+ this.sendStatus('Ok', `subcribed`);
441
+ }
442
+ }
443
+ };
150
444
 
445
+ const register = async () => {
446
+ if (node.etw) {
151
447
  try {
152
- externalTaskWorker.start();
153
- } catch (error) {
154
- node.error(`Worker start 'externalTaskWorker.start' failed: ${error.message}`);
448
+ node.etw.stop();
449
+ node.log(`old etw closed: ${JSON.stringify(node.etw)}`);
450
+ } catch (e) {
451
+ node.log(`cant close etw: ${JSON.stringify(node.etw)}`);
155
452
  }
453
+ }
454
+
455
+ const client = node.engine.engineClient;
456
+
457
+ if (!client) {
458
+ node.error('No engine configured.', {});
459
+ return;
460
+ }
461
+ const etwCallback = async (payload, externalTask) => {
462
+
463
+ globalExternalTaskStates[externalTask.flowNodeInstanceId] = new ExternalTaskNodeStates(externalTask.flowNodeInstanceId);
464
+
465
+ const saveHandleCallback = (data, callback, msg) => {
466
+ try {
467
+ callback(data);
468
+ node.log(`send to engine *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}', topic '${node.topic}' and *processInstanceId* ${externalTask.processInstanceId}`);
469
+
470
+ // Remove ExternalTaskState from global dictionary
471
+ if (globalExternalTaskStates[externalTask.flowNodeInstanceId]) {
472
+ delete globalExternalTaskStates[externalTask.flowNodeInstanceId];
473
+ }
474
+
475
+ node.setFinishHandlingTaskStatus(externalTask);
476
+ } catch (error) {
477
+ // Remove ExternalTaskState from global dictionary on error as well
478
+ if (globalExternalTaskStates[externalTask.flowNodeInstanceId]) {
479
+ delete globalExternalTaskStates[externalTask.flowNodeInstanceId];
480
+ }
481
+
482
+ node.setErrorFinishHandlingTaskStatus(externalTask, error);
483
+ msg.error = error;
484
+ node.error(`failed send to engine *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}', topic '${node.topic}' and *processInstanceId* ${externalTask.processInstanceId}: ${error?.message}`, msg);
485
+ callback(error);
486
+ }
487
+ };
488
+
489
+ return await new Promise((resolve, reject) => {
490
+ const handleFinishTask = (msg) => {
491
+ let result = RED.util.encodeObject(msg.payload);
492
+
493
+ // remote msg and format from result
494
+ delete result.format;
495
+ delete result.msg;
496
+
497
+ node.log(
498
+ `handle event for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* ${externalTask.processInstanceId} with result ${JSON.stringify(result)} on msg._msgid ${msg._msgid}.`
499
+ );
500
+
501
+ //resolve(result);
502
+ saveHandleCallback(result, resolve, msg);
503
+ };
504
+
505
+ const handleErrorTask = (error) => {
506
+ node.log(
507
+ `handle error event for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}' on *msg._msgid* '${error.errorDetails?._msgid}'.`
508
+ );
509
+
510
+ // TODO: with reject, the default error handling is proceed
511
+ // SEE: https://github.com/5minds/ProcessCube.Engine.Client.ts/blob/develop/src/ExternalTaskWorker.ts#L180
512
+ // reject(result);
513
+ //resolve(msg);
514
+ saveHandleCallback(error, resolve, error);
515
+ };
516
+
517
+ node.eventEmitter.once(`handle-${externalTask.flowNodeInstanceId}`, (msg, isError = false) => {
518
+ try {
519
+ msg.etw_finished_at = new Date().toISOString();
520
+
521
+ if (msg.etw_started_at) {
522
+ msg.etw_duration = new Date(msg.etw_finished_at) - new Date(msg.etw_started_at);
523
+ }
524
+ } catch (error) {
525
+ node.error(`failed to calculate duration: ${error?.message}`, msg);
526
+ }
527
+
528
+ node.log(
529
+ `handle event for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}' with *msg._msgid* '${msg._msgid}' and *isError* '${isError}' *duration* '${msg.etw_duration}' mSek`
530
+ );
531
+
532
+
533
+ if (isError) {
534
+ handleErrorTask(msg);
535
+ } else {
536
+ handleFinishTask(msg);
537
+ }
538
+ });
539
+
540
+ node.setStartHandlingTaskStatus(externalTask);
541
+
542
+ let msg = {
543
+ _msgid: RED.util.generateId(),
544
+ task: RED.util.encodeObject(externalTask),
545
+ payload: payload,
546
+ flowNodeInstanceId: externalTask.flowNodeInstanceId,
547
+ processInstanceId: externalTask.processInstanceId,
548
+ etw_input_node_id: node.id,
549
+ etw_started_at: new Date().toISOString()
550
+ };
551
+
552
+ node.log(
553
+ `Received *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}' with *msg._msgid* '${msg._msgid}'`
554
+ );
555
+
556
+ node.send(msg);
557
+ });
558
+ };
559
+
560
+ client.externalTasks
561
+ .subscribeToExternalTaskTopic(topic, etwCallback, options)
562
+ .then(async (externalTaskWorker) => {
563
+ node.status({ fill: 'blue', shape: 'ring', text: 'subcribed' });
564
+
565
+ node.etw = externalTaskWorker;
566
+
567
+ externalTaskWorker.onHeartbeat((event, external_task_id) => {
568
+ node.setSubscribedStatus();
569
+
570
+ if (process.env.NODE_RED_ETW_HEARTBEAT_LOGGING == 'true' || process.env.NODERED_ETW_HEARTBEAT_LOGGING == 'true') {
571
+ if (external_task_id) {
572
+ this.log(`subscription (heartbeat:topic ${node.topic}, ${event} for ${external_task_id}).`);
573
+ } else {
574
+ this.log(`subscription (heartbeat:topic ${node.topic}, ${event}).`);
575
+ }
576
+ }
577
+ });
578
+
579
+ externalTaskWorker.onWorkerError((errorType, error, externalTask) => {
580
+ switch (errorType) {
581
+ case 'extendLock':
582
+ case 'finishExternalTask':
583
+ case 'processExternalTask':
584
+ node.error(
585
+ `Worker error ${errorType} for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}': ${error?.message}`
586
+ );
587
+
588
+ node.setUnsubscribedStatus(error);
589
+
590
+ if (process.env.NODE_RED_ETW_STOP_IF_FAILED == 'true' || process.env.NODERED_ETW_STOP_IF_FAILED == 'true') {
591
+ // abort the external task MM: waiting for a fix in the client.ts
592
+ externalTaskWorker.abortExternalTaskIfPresent(externalTask.id);
593
+ // mark the external task as finished, cause it is gone
594
+ node.setFinishHandlingTaskStatus(externalTask);
595
+ node.log(`Cancel external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}'.`)
596
+ }
597
+
598
+ break;
599
+ case 'fetchAndLock':
600
+ node.setUnsubscribedStatus(error);
601
+ break;
602
+ default:
603
+ // reduce noise error logs
604
+ break;
605
+ }
606
+ });
156
607
 
157
- node.on('close', () => {
158
608
  try {
159
- externalTaskWorker.stop();
160
- } catch {
161
- node.error('Client close failed');
609
+ externalTaskWorker.start();
610
+ } catch (error) {
611
+ node.error(`Worker start 'externalTaskWorker.start' failed: ${error.message}`, {});
162
612
  }
613
+
614
+ node.on('close', () => {
615
+ try {
616
+ externalTaskWorker.stop();
617
+ RED.hooks.remove(`preDeliver.etw-input-${node.id}`);
618
+ RED.hooks.remove(`postDeliver.etw-input-${node.id}`);
619
+ node.log('External Task Worker closed.');
620
+ } catch (error) {
621
+ node.error(`Client close failed: ${JSON.stringify(error)}`);
622
+ }
623
+ });
624
+ })
625
+ .catch((error) => {
626
+ node.error(`Error in subscribeToExternalTaskTopic: ${error.message}`, {});
163
627
  });
164
- })
165
- .catch((error) => {
166
- node.error(`Error in subscribeToExternalTaskTopic: ${error.message}`);
628
+ };
629
+
630
+ if (node.engine) {
631
+ register().catch((error) => {
632
+ node.error(error, {});
167
633
  });
634
+ }
168
635
  }
169
636
 
170
637
  RED.nodes.registerType('externaltask-input', ExternalTaskInput);