@5minds/node-red-contrib-processcube 1.16.1 → 1.16.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.
Files changed (2) hide show
  1. package/externaltask-input.js +168 -44
  2. package/package.json +1 -1
@@ -1,9 +1,110 @@
1
1
  const EventEmitter = require('node:events');
2
2
 
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;
23
+ }
24
+
25
+ checkIfCompletedWithoutSend(nodeId) {
26
+ const nodeState = this.nodeStades[nodeId];
27
+ const result = (nodeState && nodeState.gotCompleted && !nodeState.gotSend);
28
+
29
+ return result;
30
+ }
31
+ }
32
+
3
33
  module.exports = function (RED) {
4
34
 
5
35
  const os = require('os');
6
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)) {
101
+ raiseExternalTaskError(completeEvent.msg.flowNodeInstanceId, completeEvent.msg.etw_input_node_id, completeEvent.node.id);
102
+ }
103
+ }
104
+ }
105
+
106
+ RED.hooks.add("onComplete", onCompleted);
107
+
7
108
  function ExternalTaskInput(config) {
8
109
  RED.nodes.createNode(this, config);
9
110
  var node = this;
@@ -165,60 +266,68 @@ module.exports = function (RED) {
165
266
  };
166
267
 
167
268
  const onPreDeliver = (sendEvent) => {
168
- if (node.isHandling() && node.ownMessage(sendEvent.msg)) {
169
-
170
- const sourceNode = sendEvent?.source?.node;
171
- const destinationNode = sendEvent?.destination?.node;
269
+ try {
270
+ if (node.isHandling() && node.ownMessage(sendEvent.msg)) {
172
271
 
173
- node._step = `${destinationNode.name || destinationNode.type}`;
272
+ const sourceNode = sendEvent?.source?.node;
273
+ const destinationNode = sendEvent?.destination?.node;
174
274
 
175
- node.showStatus();
275
+ node._step = `${destinationNode.name || destinationNode.type}`;
176
276
 
177
- const debugMsg = {
178
- event: 'enter',
179
- sourceName: sourceNode.name,
180
- sourceType: sourceNode.type,
181
- destinationNodeName: destinationNode.name,
182
- destinationNodeType: destinationNode.type,
183
- timestamp: new Date().toISOString(),
184
- };
277
+ node.showStatus();
278
+
279
+ const debugMsg = {
280
+ event: 'enter',
281
+ sourceName: sourceNode.name,
282
+ sourceType: sourceNode.type,
283
+ destinationNodeName: destinationNode.name,
284
+ destinationNodeType: destinationNode.type,
285
+ timestamp: new Date().toISOString(),
286
+ };
185
287
 
186
- node.traceExecution(debugMsg);
288
+ node.traceExecution(debugMsg);
187
289
 
188
- if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true' || process.env.NODERED_ETW_STEP_LOGGING == 'true') {
189
- node._trace = `'${sourceNode.name || sourceNode.type}'->'${destinationNode.name || destinationNode.type}'`;
190
- node.log(`preDeliver: ${node._trace}`);
290
+ if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true' || process.env.NODERED_ETW_STEP_LOGGING == 'true') {
291
+ node._trace = `'${sourceNode.name || sourceNode.type}'->'${destinationNode.name || destinationNode.type}'`;
292
+ node.log(`preDeliver: ${node._trace}`);
293
+ }
191
294
  }
295
+ } catch (error) {
296
+ node.error(`Error in onPreDeliver: ${error?.message}`, { error: error });
192
297
  }
193
298
  };
194
- RED.hooks.add('preDeliver', onPreDeliver);
299
+ RED.hooks.add(`preDeliver.etw-input-${node.id}`, onPreDeliver);
195
300
 
196
301
  const onPostDeliver = (sendEvent) => {
197
- if (node.isHandling() && node.ownMessage(sendEvent.msg)) {
198
- const sourceNode = sendEvent?.source?.node;
199
- const destinationNode = sendEvent?.destination?.node;
200
-
201
- node.decrMsgOnNode(sourceNode, sendEvent.msg);
202
- node.incMsgOnNode(destinationNode, sendEvent.msg);
203
-
204
- const debugMsg = {
205
- event: 'exit',
206
- sourceName: sourceNode.name,
207
- sourceType: sourceNode.type,
208
- destinationNodeName: destinationNode.name,
209
- destinationNodeType: destinationNode.type,
210
- timestamp: new Date().toISOString(),
211
- };
302
+ try {
303
+ if (node.isHandling() && node.ownMessage(sendEvent.msg)) {
304
+ const sourceNode = sendEvent?.source?.node;
305
+ const destinationNode = sendEvent?.destination?.node;
306
+
307
+ node.decrMsgOnNode(sourceNode, sendEvent.msg);
308
+ node.incMsgOnNode(destinationNode, sendEvent.msg);
309
+
310
+ const debugMsg = {
311
+ event: 'exit',
312
+ sourceName: sourceNode.name,
313
+ sourceType: sourceNode.type,
314
+ destinationNodeName: destinationNode.name,
315
+ destinationNodeType: destinationNode.type,
316
+ timestamp: new Date().toISOString(),
317
+ };
212
318
 
213
- node.traceExecution(debugMsg);
319
+ node.traceExecution(debugMsg);
214
320
 
215
- if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true' || process.env.NODERED_ETW_STEP_LOGGING == 'true') {
216
- node._trace = `'${sourceNode.name || sourceNode.type}'->'${destinationNode.name || destinationNode.type}'`;
217
- node.log(`postDeliver: ${node._trace}`);
321
+ if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true' || process.env.NODERED_ETW_STEP_LOGGING == 'true') {
322
+ node._trace = `'${sourceNode.name || sourceNode.type}'->'${destinationNode.name || destinationNode.type}'`;
323
+ node.log(`postDeliver: ${node._trace}`);
324
+ }
218
325
  }
326
+ } catch (error) {
327
+ node.error(`Error in onPostDeliver: ${error?.message}`, { error: error });
219
328
  }
220
329
  };
221
- RED.hooks.add('postDeliver', onPostDeliver);
330
+ RED.hooks.add(`postDeliver.etw-input-${node.id}`, onPostDeliver);
222
331
 
223
332
  node.setSubscribedStatus = () => {
224
333
  this._subscribed = true;
@@ -349,15 +458,30 @@ module.exports = function (RED) {
349
458
  return;
350
459
  }
351
460
  const etwCallback = async (payload, externalTask) => {
461
+
462
+ globalExternalTaskStates[externalTask.flowNodeInstanceId] = new ExternalTaskNodeStates(externalTask.flowNodeInstanceId);
463
+
352
464
  const saveHandleCallback = (data, callback, msg) => {
353
465
  try {
354
466
  callback(data);
355
467
  node.log(`send to engine *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}', topic '${node.topic}' and *processInstanceId* ${externalTask.processInstanceId}`);
468
+
469
+ // Remove ExternalTaskState from global dictionary
470
+ if (globalExternalTaskStates[externalTask.flowNodeInstanceId]) {
471
+ delete globalExternalTaskStates[externalTask.flowNodeInstanceId];
472
+ }
473
+
356
474
  node.setFinishHandlingTaskStatus(externalTask);
357
475
  } catch (error) {
476
+ // Remove ExternalTaskState from global dictionary on error as well
477
+ if (globalExternalTaskStates[externalTask.flowNodeInstanceId]) {
478
+ delete globalExternalTaskStates[externalTask.flowNodeInstanceId];
479
+ }
480
+
358
481
  node.setErrorFinishHandlingTaskStatus(externalTask, error);
359
482
  msg.error = error;
360
483
  node.error(`failed send to engine *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}', topic '${node.topic}' and *processInstanceId* ${externalTask.processInstanceId}: ${error?.message}`, msg);
484
+ callback(error);
361
485
  }
362
486
  };
363
487
 
@@ -397,7 +521,7 @@ module.exports = function (RED) {
397
521
  msg.etw_duration = new Date(msg.etw_finished_at) - new Date(msg.etw_started_at);
398
522
  }
399
523
  } catch (error) {
400
- node.error(`failed to calculate duration: ${error?.message}`, msg);
524
+ node.error(`failed to calculate duration: ${error?.message}`, msg);
401
525
  }
402
526
 
403
527
  node.log(
@@ -488,12 +612,12 @@ module.exports = function (RED) {
488
612
 
489
613
  node.on('close', () => {
490
614
  try {
491
- RED.hooks.remove('preDeliver', onPreDeliver);
492
- RED.hooks.remove('postDeliver', onPostDeliver);
493
615
  externalTaskWorker.stop();
616
+ RED.hooks.remove(`preDeliver.etw-input-${node.id}`);
617
+ RED.hooks.remove(`postDeliver.etw-input-${node.id}`);
494
618
  node.log('External Task Worker closed.');
495
- } catch {
496
- node.error('Client close failed', {});
619
+ } catch (error) {
620
+ node.error(`Client close failed: ${JSON.stringify(error)}`);
497
621
  }
498
622
  });
499
623
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube",
3
- "version": "1.16.1",
3
+ "version": "1.16.2",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED nodes for ProcessCube",
6
6
  "scripts": {