Repository: node-red/node-red (js)

Issue:
Multiple input handlers do not correctly register complete()
### Current Behavior

I have a node with multiple `on('input')` handlers. If I assign my node to the scope of a `complete` node, and watch for any injections, where a `.send()` _is not_ triggered (due to disabling `passthru`), but a `done()` is, then the `complete` node never recognises the `done()` call.

Removing all but one of the `node.on('input')` handlers fixes the issue.

### Expected Behavior

The `complete` should be triggered when `done()` is called. In the case where _multiple_ `on('input')` handlers exist, then I would likely expect that _all_ of them should need to call `done()`, as it would be impossible to know otherwise when all input handlers are correctly finished.

### Steps To Reproduce

Flow attached (Requires Dashboard 2.0 v0.4.0)

"My Slider" has a "disable passthru" option, by toggling on/off this behaviour you'll be able to see the issue through the two connected debug nodes.

### Example flow

```
[
    {
        "id": "81c68753865b79f6",
        "type": "inject",
        "z": "9cb9bb3cd8e3faee",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "20",
        "payloadType": "num",
        "x": 230,
        "y": 920,
        "wires": [
            [
                "dafd69c87505ad80"
            ]
        ]
    },
    {
        "id": "dafd69c87505ad80",
        "type": "ui-slider",
        "z": "9cb9bb3cd8e3faee",
        "group": "d392ba6fc81aece7",
        "name": "",
        "label": "My Slider",
        "tooltip": "",
        "order": 2,
        "width": "0",
        "height": "0",
        "passthru": false,
        "outs": "all",
        "topic": "topic",
        "topicType": "str",
        "min": 0,
        "max": "100",
        "step": 1,
        "className": "",
        "x": 300,
        "y": 860,
        "wires": [
            [
                "8560f6456c6482c0"
            ]
        ]
    },
    {
        "id": "8560f6456c6482c0",
        "type": "debug",
        "z": "9cb9bb3cd8e3faee",
        "name": "debug 37",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 500,
        "y": 820,
        "wires": []
    },
    {
        "id": "60a62ba9f7dc305c",
        "type": "complete",
        "z": "9cb9bb3cd8e3faee",
        "name": "",
        "scope": [
            "dafd69c87505ad80"
        ],
        "uncaught": false,
        "x": 1330,
        "y": 560,
        "wires": [
            [
                "24e9eae0e5ca72fa"
            ]
        ]
    },
    {
        "id": "24e9eae0e5ca72fa",
        "type": "debug",
        "z": "9cb9bb3cd8e3faee",
        "name": "debug 42",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1540,
        "y": 560,
        "wires": []
    },
    {
        "id": "d392ba6fc81aece7",
        "type": "ui-group",
        "name": "Group 2",
        "page": "ea42e606327de0a5",
        "width": "3",
        "height": "1",
        "order": 2,
        "disp": true
    },
    {
        "id": "ea42e606327de0a5",
        "type": "ui-page",
        "name": "Demo Page",
        "ui": "469092f13c8fd9d3",
        "path": "/page2",
        "layout": "grid",
        "theme": "afa24cae12543ca5",
        "order": 1
    },
    {
        "id": "469092f13c8fd9d3",
        "type": "ui-base",
        "name": "Dashboard",
        "path": "/dashboard"
    },
    {
        "id": "afa24cae12543ca5",
        "type": "ui-theme",
        "name": "FlowForge Theme",
        "colors": {
            "surface": "#152a47",
            "primary": "#005aff",
            "bgPage": "#e6e6e6",
            "groupBg": "#ffffff",
            "groupOutline": "#cccccc"
        }
    }
]
```

### Environment

- Node-RED version: 3.0.2
- Node.js version: 
- npm version:
- Platform/OS:
- Browser:


Pull request description:
Handle nodes with multiple input handlers properly
Fixes #4330

Fixes the tracking of how many input handlers have the `(msg, send, done)` signature so we know to call complete at the right time.

Patch:
diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js
index 7a7445a92..5a60cf6a0 100644
--- a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js
+++ b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js
@@ -42,6 +42,7 @@ function Node(n) {
     this._closeCallbacks = [];
     this._inputCallback = null;
     this._inputCallbacks = null;
+    this._expectedDoneCount = 0;
 
     if (n.name) {
         this.name = n.name;
@@ -159,6 +160,9 @@ Node.prototype.on = function(event, callback) {
     if (event == "close") {
         this._closeCallbacks.push(callback);
     } else if (event === "input") {
+        if (callback.length === 3) {
+            this._expectedDoneCount++
+        }
         if (this._inputCallback) {
             this._inputCallbacks = [this._inputCallback, callback];
             this._inputCallback = null;
@@ -218,19 +222,17 @@ Node.prototype._emitInput = function(arg) {
             } else if (node._inputCallbacks) {
                 // Multiple callbacks registered. Call each one, tracking eventual completion
                 var c = node._inputCallbacks.length;
+                let doneCount = 0
                 for (var i=0;i<c;i++) {
                     var cb = node._inputCallbacks[i];
-                    if (cb.length === 2) {
-                        c++;
-                    }
                     try {
                         cb.call(
                             node,
                             arg,
                             function() { node.send.apply(node,arguments) },
                             function(err) {
-                                c--;
-                                if (c === 0) {
+                                doneCount++;
+                                if (doneCount === node._expectedDoneCount) {
                                     node._complete(arg,err);
                                 }
                             }
@@ -257,6 +259,9 @@ Node.prototype._removeListener = Node.prototype.removeListener;
 Node.prototype.removeListener = function(name, listener) {
     var index;
     if (name === "input") {
+        if (listener.length === 3) {
+            this._expectedDoneCount--
+        }
         if (this._inputCallback && this._inputCallback === listener) {
             // Removing the only callback
             this._inputCallback = null;


Test patch:
diff --git a/test/unit/@node-red/runtime/lib/nodes/Node_spec.js b/test/unit/@node-red/runtime/lib/nodes/Node_spec.js
index 3fe676f1e..8aa9e6e28 100644
--- a/test/unit/@node-red/runtime/lib/nodes/Node_spec.js
+++ b/test/unit/@node-red/runtime/lib/nodes/Node_spec.js
@@ -183,6 +183,35 @@ describe('Node', function() {
             n.receive(message);
         });
 
+
+        it('calls parent flow handleComplete when multiple callbacks provided', function(done) {
+            var n = new RedNode({id:'123',type:'abc', _flow: {
+                handleComplete: function(node,msg) {
+                    try {
+                        doneCount.should.equal(2)
+                        msg.should.deepEqual(message);
+                        done();
+                    } catch(err) {
+                        done(err);
+                    }
+                }
+            }});
+
+            var message = {payload:"hello world"};
+            let doneCount = 0
+            n.on('input',function(msg, nodeSend, nodeDone) {
+                doneCount++
+                nodeDone();
+            });
+            // Include a callback without explicit done signature
+            n.on('input',function(msg) { });
+            n.on('input',function(msg, nodeSend, nodeDone) {
+                doneCount++
+                nodeDone();
+            });
+            n.receive(message);
+        });
+
         it('triggers onComplete hook when done callback provided', function(done) {
             var handleCompleteCalled = false;
             var hookCalled = false;