Merge branch 'dev' into pr_3438

This commit is contained in:
Nick O'Leary
2022-04-26 13:53:35 +01:00
committed by GitHub
228 changed files with 28117 additions and 21477 deletions

View File

@@ -0,0 +1,5 @@
<!--
05-junction.html
This file exists so that the runtime loads the Junction node into the registry,
but it is empty so it doesn't appear in the editor palette
-->

View File

@@ -0,0 +1,12 @@
module.exports = function(RED) {
"use strict";
function JunctionNode(n) {
RED.nodes.createNode(this,n);
this.on("input",function(msg, send, done) {
send(msg);
done();
});
}
RED.nodes.registerType("junction",JunctionNode);
}

View File

@@ -389,13 +389,6 @@
var id = $("#inject-time-type-select").val();
$(".inject-time-row").hide();
$("#inject-time-row-"+id).show();
if ((id == "none") || (id == "interval") || (id == "interval-time")) {
$("#node-once").show();
}
else {
$("#node-once").hide();
$("#node-input-once").prop('checked', false);
}
// Scroll down
var scrollDiv = $("#dialog-form").parent();

View File

@@ -74,7 +74,7 @@
RED.nodes.registerType('debug',{
category: 'common',
defaults: {
name: {value:""},
name: {value:"_DEFAULT_"},
active: {value:true},
tosidebar: {value:true},
console: {value:false},
@@ -160,6 +160,10 @@
},
messageSourceClick: function(sourceId, aliasId, path) {
// Get all of the nodes that could have logged this message
if (RED.nodes.workspace(sourceId)) {
RED.view.reveal(sourceId);
return
}
var candidateNodes = [RED.nodes.node(sourceId)]
if (path) {
for (var i=2;i<path.length;i++) {
@@ -235,10 +239,11 @@
// sourceNode should be the top-level node - one that is on a flow.
var sourceNode;
var pathParts;
var pathHierarchy;
if (o.path) {
// Path is a `/`-separated list of ids that identifies the
// complete parentage of the node that generated this message.
// flow-id/subflow-A-instance/subflow-A-type/subflow-B-instance/subflow-B-type/node-id
// flow-id/subflow-A-instance/subflow-B-instance
// If it has one id, that is a top level flow
// each subsequent id is the instance id of a subflow node
@@ -251,11 +256,41 @@
// Highlight the subflow instance node.
sourceNode = RED.nodes.node(pathParts[1]);
}
pathHierarchy = pathParts.map((id,index) => {
if (index === 0) {
return {
id: id,
label: RED.nodes.workspace(id).label
}
} else {
var instanceNode = RED.nodes.node(id)
return {
id: id,
label: (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8)).name)
}
}
})
if (pathParts.length === 1) {
pathHierarchy.push({
id: o.id,
label: sourceNode.name || sourceNode.type+":"+sourceNode.id
})
}
if (o._alias) {
let aliasNode = RED.nodes.node(o._alias)
if (aliasNode) {
pathHierarchy.push({
id: o._alias,
label: aliasNode.name || aliasNode.type+":"+aliasNode.id
})
}
}
} else {
// This is probably redundant...
sourceNode = RED.nodes.node(o.id) || RED.nodes.node(o.z);
}
if (sourceNode) {
var sourceFlow = RED.nodes.workspace(sourceNode.z)
o._source = {
id:sourceNode.id,
z:sourceNode.z,
@@ -266,7 +301,9 @@
// the top-level subflow instance node.
// This means the node's name is displayed in the sidebar.
_alias:o._alias,
path: pathParts
flowName: sourceFlow?(sourceFlow.label||sourceNode.z):sourceNode.z,
path: pathParts,
pathHierarchy: pathHierarchy
};
}
RED.debug.handleDebugMessage(o);
@@ -507,6 +544,12 @@
$("#node-input-complete").val($("#node-input-typed-complete").typedInput('value'));
}
$("#node-input-statusVal").val($("#node-input-typed-status").typedInput('value'));
},
onadd: function() {
if (this.name === '_DEFAULT_') {
this.name = ''
RED.actions.invoke("core:generate-node-names", this)
}
}
});
})();

View File

@@ -108,7 +108,9 @@ module.exports = function(RED) {
}
})
this.on("input", function(msg, send, done) {
if (hasOwnProperty.call(msg, "status") && hasOwnProperty.call(msg.status, "source") && hasOwnProperty.call(msg.status.source, "id") && (msg.status.source.id === node.id)) {
if (hasOwnProperty.call(msg, "status") && msg.status &&
hasOwnProperty.call(msg.status, "source") && msg.status.source &&
hasOwnProperty.call(msg.status.source, "id") && (msg.status.source.id === node.id)) {
done();
return;
}
@@ -129,7 +131,8 @@ module.exports = function(RED) {
fill = "red";
st = msg.error.message;
}
if (hasOwnProperty.call(msg, "status")) {
if (hasOwnProperty.call(msg, "status") &&
msg.status) {
fill = msg.status.fill || "grey";
shape = msg.status.shape || "ring";
st = msg.status.text || "";

View File

@@ -32,14 +32,23 @@
<label for="node-input-timeout"><span data-i18n="exec.label.timeout"></span></label>
<input type="text" id="node-input-timeout" placeholder="30" style="width: 70px; margin-right: 5px;"><span data-i18n="inject.seconds"></span>
</div>
<div style="position:relative; height: 30px; text-align: right;"><div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div></div>
<div class="form-row node-input-link-row"></div>
<div class="form-row">
<label for="node-input-linkType" data-i18n="link.linkCallType"></label>
<select id="node-input-linkType" style="width: 70%">
<option value="static" data-i18n="link.staticLinkCall"></option>
<option value="dynamic" data-i18n="link.dynamicLinkCall"></option>
</select>
</div>
<div class="link-call-target-tree" style="position:relative; height: 30px; text-align: right;">
<div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div>
</div>
<div class="form-row node-input-link-row link-call-target-tree"></div>
</script>
<script type="text/javascript">
(function() {
var treeList;
let treeList;
function onEditPrepare(node,targetType) {
if (!node.links) {
@@ -47,7 +56,7 @@
}
node.oldLinks = [];
var activeSubflow = RED.nodes.subflow(node.z);
const activeSubflow = RED.nodes.subflow(node.z);
treeList = $("<div>")
.css({width: "100%", height: "100%"})
@@ -67,10 +76,10 @@
RED.view.redraw();
}
});
var candidateNodes = RED.nodes.filterNodes({type:targetType});
var candidateNodesCount = 0;
const candidateNodes = RED.nodes.filterNodes({type:targetType});
let candidateNodesCount = 0;
var search = $("#node-input-link-target-filter").searchBox({
const search = $("#node-input-link-target-filter").searchBox({
style: "compact",
delay: 300,
change: function() {
@@ -79,7 +88,7 @@
treeList.treeList("filter", null);
search.searchBox("count","");
} else {
var count = treeList.treeList("filter", function(item) {
const count = treeList.treeList("filter", function(item) {
return item.label.toLowerCase().indexOf(val) > -1 || (item.node && item.node.type.toLowerCase().indexOf(val) > -1)
});
search.searchBox("count",count+" / "+candidateNodesCount);
@@ -87,25 +96,27 @@
}
});
var flows = [];
var flowMap = {};
const flows = [];
const flowMap = {};
if (activeSubflow) {
flowMap[activeSubflow.id] = {
id: activeSubflow.id,
class: 'red-ui-palette-header',
label: "Subflow : "+(activeSubflow.name || activeSubflow.id),
label: "Subflow : " + (activeSubflow.name || activeSubflow.id),
expanded: true,
children: []
};
flows.push(flowMap[activeSubflow.id])
} else {
RED.nodes.eachWorkspace(function(ws) {
}
if (!activeSubflow || node.type === "link call") {
// Only "Link Call" can look outside of its own subflow
// Link In and Link Out nodes outside of a subflow should be ignored
RED.nodes.eachWorkspace(function (ws) {
flowMap[ws.id] = {
id: ws.id,
class: 'red-ui-palette-header',
label: (ws.label || ws.id)+(node.z===ws.id ? " *":""),
label: (ws.label || ws.id) + (node.z === ws.id ? " *" : ""),
expanded: true,
children: []
}
@@ -113,22 +124,21 @@
})
}
candidateNodes.forEach(function(n) {
candidateNodes.forEach(function (n) {
if (flowMap[n.z]) {
if (targetType === "link out" && n.mode === 'return') {
// Link In nodes looking for Link Out nodes should not
// include return-mode nodes.
return
return;
}
var isChecked = false;
isChecked = (node.links.indexOf(n.id) !== -1) || (n.links||[]).indexOf(node.id) !== -1;
const isChecked = (node.links.indexOf(n.id) !== -1) || (n.links || []).indexOf(node.id) !== -1;
if (isChecked) {
node.oldLinks.push(n.id);
}
flowMap[n.z].children.push({
id: n.id,
node: n,
label: n.name||n.id,
label: n.name || n.id,
selected: isChecked,
checkbox: node.type !== "link call",
radio: node.type === "link call"
@@ -136,8 +146,8 @@
candidateNodesCount++;
}
});
flows = flows.filter(function(f) { return f.children.length > 0 })
treeList.treeList('data',flows);
const flowsFiltered = flows.filter(function(f) { return f.children.length > 0 })
treeList.treeList('data',flowsFiltered);
setTimeout(function() {
treeList.treeList('show',node.z);
},100);
@@ -209,6 +219,10 @@
}
function onAdd() {
if (this.name === '_DEFAULT_') {
this.name = ''
RED.actions.invoke("core:generate-node-names", this)
}
for (var i=0;i<this.links.length;i++) {
var n = RED.nodes.node(this.links[i]);
if (n && n.links.indexOf(this.id) === -1) {
@@ -221,7 +235,7 @@
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
name: { value: "_DEFAULT_" },
links: { value: [], type:"link out[]" }
},
inputs:0,
@@ -257,12 +271,14 @@
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
links: { value: [], type:"link in[]"},
timeout: { value: "30",
label: RED._("node-red:link.timeout"),
validate:RED.validators.number(true)
}
name: { value: "" },
links: { value: [], type:"link in[]" },
linkType: { value:"static" },
timeout: {
value: "30",
label: RED._("node-red:link.timeout"),
validate:RED.validators.number(true)
}
},
inputs: 1,
outputs: 1,
@@ -274,7 +290,9 @@
if (this.name) {
return this.name;
}
if (this.links.length > 0) {
if (this.linkType === "dynamic") {
return this._("link.dynamicLinkLabel");
} else if (this.links.length > 0) {
var targetNode = RED.nodes.node(this.links[0]);
return targetNode && (targetNode.name || this._("link.linkCall"));
}
@@ -284,6 +302,22 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
console.log("link call oneditprepare")
const updateVisibility = function() {
const static = $('#node-input-linkType').val() !== "dynamic";
if(static) {
$("div.link-call-target-tree").show();
} else {
$("div.link-call-target-tree").hide();
}
}
$("#node-input-linkType").on("change",function(d){
updateVisibility();
});
if (["static","dynamic"].indexOf(this.linkType) < 0) {
$("#node-input-linkType").val('static');
}
updateVisibility();
onEditPrepare(this,"link in");
},
oneditsave: function() {
@@ -296,9 +330,9 @@
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
name: { value:"_DEFAULT_" },
mode: { value: "link" },// link || return
links: { value: [], type:"link in[]"}
links: { value: [], type:"link in[]" }
},
align:"right",
inputs:1,

View File

@@ -14,10 +14,119 @@
* limitations under the License.
**/
/**
* @typedef LinkTarget
* @type {object}
* @property {string} id - ID of the target node.
* @property {string} name - Name of target Node
* @property {number} flowId - ID of flow where the target node exists
* @property {string} flowName - Name of flow where the target node exists
* @property {boolean} isSubFlow - True if the link-in node exists in a subflow instance
*/
module.exports = function(RED) {
"use strict";
const crypto = require("crypto");
const targetCache = (function () {
const registry = { id: {}, name: {} };
function getIndex(/** @type {[LinkTarget]}*/ targets, id) {
for (let index = 0; index < (targets || []).length; index++) {
const element = targets[index];
if (element.id === id) {
return index;
}
}
return -1;
}
/**
* Generate a target object from a node
* @param {LinkInNode} node
* @returns {LinkTarget} a link target object
*/
function generateTarget(node) {
const isSubFlow = node._flow.TYPE === "subflow";
return {
id: node.id,
name: node.name || node.id,
flowId: node._flow.flow.id,
flowName: isSubFlow ? node._flow.subflowDef.name : node._flow.flow.label,
isSubFlow: isSubFlow
}
}
return {
/**
* Get a list of targets registerd to this name
* @param {string} name Name of the target
* @param {boolean} [excludeSubflows] set `true` to exclude
* @returns {[LinkTarget]} Targets registerd to this name.
*/
getTargets(name, excludeSubflows) {
const targets = registry.name[name] || [];
if (excludeSubflows) {
return targets.filter(e => e.isSubFlow != true);
}
return targets;
},
/**
* Get a single target by registered name.
* To restrict to a single flow, include the `flowId`
* If there is no targets OR more than one target, null is returned
* @param {string} name Name of the node
* @param {string} [flowId]
* @returns {LinkTarget} target
*/
getTarget(name, flowId) {
/** @type {[LinkTarget]}*/
let possibleTargets = this.getTargets(name);
/** @type {LinkTarget}*/
let target;
if (possibleTargets.length && flowId) {
possibleTargets = possibleTargets.filter(e => e.flowId == flowId);
}
if (possibleTargets.length === 1) {
target = possibleTargets[0];
}
return target;
},
/**
* Get a target by node ID
* @param {string} nodeId ID of the node
* @returns {LinkTarget} target
*/
getTargetById(nodeId) {
return registry.id[nodeId];
},
register(/** @type {LinkInNode} */ node) {
const target = generateTarget(node);
const tByName = this.getTarget(target.name, target.flowId);
if (!tByName || tByName.id !== target.id) {
registry.name[target.name] = registry.name[target.name] || [];
registry.name[target.name].push(target)
}
registry.id[target.id] = target;
return target;
},
remove(node) {
const target = generateTarget(node);
const tn = this.getTarget(target.name, target.flowId);
if (tn) {
const targs = this.getTargets(tn.name);
const idx = getIndex(targs, tn.id);
if (idx > -1) {
targs.splice(idx, 1);
}
if (targs.length === 0) {
delete registry.name[tn.name];
}
}
delete registry.id[target.id];
},
clear() {
registry = { id: {}, name: {} };
}
}
})();
function LinkInNode(n) {
RED.nodes.createNode(this,n);
@@ -27,12 +136,14 @@ module.exports = function(RED) {
msg._event = n.event;
node.receive(msg);
}
targetCache.register(node);
RED.events.on(event,handler);
this.on("input", function(msg, send, done) {
send(msg);
done();
});
this.on("close",function() {
targetCache.remove(node);
RED.events.removeListener(event,handler);
});
}
@@ -74,31 +185,69 @@ module.exports = function(RED) {
function LinkCallNode(n) {
RED.nodes.createNode(this,n);
const node = this;
const target = n.links[0];
const staticTarget = typeof n.links === "string" ? n.links : n.links[0];
const linkType = n.linkType;
const messageEvents = {};
let timeout = parseFloat(n.timeout || "30")*1000;
let timeout = parseFloat(n.timeout || "30") * 1000;
if (isNaN(timeout)) {
timeout = 30000;
}
function getTargetNode(msg) {
const dynamicMode = linkType === "dynamic";
const target = dynamicMode ? msg.target : staticTarget
this.on("input", function(msg, send, done) {
msg._linkSource = msg._linkSource || [];
const messageEvent = {
id: crypto.randomBytes(14).toString('hex'),
node: node.id,
////1st see if the target is a direct node id
let foundNode;
if (targetCache.getTargetById(target)) {
foundNode = RED.nodes.getNode(target)
}
messageEvents[messageEvent.id] = {
msg: RED.util.cloneMessage(msg),
send,
done,
ts: setTimeout(function() {
timeoutMessage(messageEvent.id)
}, timeout )
};
msg._linkSource.push(messageEvent);
var targetNode = RED.nodes.getNode(target);
if (targetNode) {
targetNode.receive(msg);
if (target && !foundNode && dynamicMode) {
//next, look in **this flow only** for the node
let cachedTarget = targetCache.getTarget(target, node._flow.flow.id);
if (!cachedTarget) {
//single target node not found in registry!
//get all possible targets from regular flows (exclude subflow instances)
const possibleTargets = targetCache.getTargets(target, true);
if (possibleTargets.length === 1) {
//only 1 link-in found with this name - good, lets use it
cachedTarget = possibleTargets[0];
} else if (possibleTargets.length > 1) {
//more than 1 link-in has this name, raise an error
throw new Error(`Multiple link-in nodes named '${target}' found`);
}
}
if (cachedTarget) {
foundNode = RED.nodes.getNode(cachedTarget.id);
}
}
if (foundNode instanceof LinkInNode) {
return foundNode;
}
throw new Error(`target link-in node '${target || ""}' not found`);
}
this.on("input", function (msg, send, done) {
try {
const targetNode = getTargetNode(msg);
if (targetNode instanceof LinkInNode) {
msg._linkSource = msg._linkSource || [];
const messageEvent = {
id: crypto.randomBytes(14).toString('hex'),
node: node.id,
}
messageEvents[messageEvent.id] = {
msg: RED.util.cloneMessage(msg),
send,
done,
ts: setTimeout(function () {
timeoutMessage(messageEvent.id)
}, timeout)
};
msg._linkSource.push(messageEvent);
targetNode.receive(msg);
}
} catch (error) {
node.error(error, msg);
}
});

View File

@@ -581,12 +581,45 @@ RED.debug = (function() {
var metaRow = $('<div class="red-ui-debug-msg-meta"></div>').appendTo(msg);
$('<span class="red-ui-debug-msg-date">'+ getTimestamp()+'</span>').appendTo(metaRow);
if (sourceNode) {
$('<a>',{href:"#",class:"red-ui-debug-msg-name"}).text('node: '+(o.name||sourceNode.name||sourceNode.id))
var nodeLink = $('<a>',{href:"#",class:"red-ui-debug-msg-name"}).text("node: "+(o.name||sourceNode.name||sourceNode.id))
.appendTo(metaRow)
.on("click", function(evt) {
evt.preventDefault();
config.messageSourceClick(sourceNode.id, sourceNode._alias, sourceNode.path);
});
if (sourceNode.pathHierarchy) {
RED.popover.create({
tooltip: true,
target:nodeLink,
trigger: "hover",
size: "small",
direction: "bottom",
interactive: true,
content: function() {
const content = $("<div>")
sourceNode.pathHierarchy.forEach((pathPart,idx) => {
const link = $("<a>", {href:"#" ,style:'display: block'})
.css({
paddingLeft:((idx*10)+((idx === sourceNode.pathHierarchy.length - 1)?10:0))+"px",
paddingRight:'2px'
})
.text(pathPart.label)
.appendTo(content)
.on("click", function(evt) {
evt.preventDefault();
config.messageSourceClick(pathPart.id);
})
if (idx < sourceNode.pathHierarchy.length - 1) {
$('<i class="fa fa-angle-down" style="margin-right: 3px"></i>').prependTo(link)
}
})
return content
},
delay: { show: 50, hide: 150 }
});
}
} else if (name) {
$('<span class="red-ui-debug-msg-name">'+name+'</span>').appendTo(metaRow);
}

View File

@@ -355,7 +355,7 @@
color:"#fdd0a2",
category: 'function',
defaults: {
name: {value:""},
name: {value:"_DEFAULT_"},
func: {value:"\nreturn msg;"},
outputs: {value:1},
noerr: {value:0,required:true,
@@ -413,7 +413,7 @@
$("#func-tabs-content").children().hide();
$("#" + tab.id).show();
let editor = $("#" + tab.id).find('.monaco-editor').first();
if(editor.length) {
if(editor.length) {
if(that.editor.nodered && that.editor.type == "monaco") {
that.editor.nodered.refreshModuleLibs(getLibsList());
}
@@ -619,6 +619,12 @@
this.finalizeEditor.resize();
$("#node-input-libs-container").css("height", (height - 192)+"px");
},
onadd: function() {
if (this.name === '_DEFAULT_') {
this.name = ''
RED.actions.invoke("core:generate-node-names", this)
}
}
});
})();

View File

@@ -55,6 +55,7 @@ module.exports = function(RED) {
catch(e) { return false;}
}
else if (b === "null") { return a === null; }
else if (b === "number") { return typeof a === b && !isNaN(a) }
else { return typeof a === b && !Array.isArray(a) && !Buffer.isBuffer(a) && a !== null; }
},
'head': function(a, b, c, d, parts) {

View File

@@ -122,22 +122,26 @@
}},
timeoutUnits: {value:"seconds"},
rate: {
value:"1", required:true,
value:"1",
required:true,
label:RED._("node-red:delay.label.rate"),
validate:function(v,opt) {
if (RED.validators.number(v) && (v >= 0)) {
return true;
}
return RED._("node-red:delay.errors.invalid-rate");
}},
}
},
nbRateUnits: {
value:"1", required:false,
value:"1",
required:false,
validate:function(v,opt) {
if (RED.validators.number(v) && (v >= 0)) {
if (v === undefined || (RED.validators.number(v) && (v >= 0))) {
return true;
}
return RED._("node-red:delay.errors.invalid-rate-unit");
}},
}
},
rateUnits: {value: "second"},
randomFirst: {
value:"1", required:true,

View File

@@ -65,6 +65,11 @@
/* opacity: 0.3;
pointer-events: none; */
}
.form-row.form-row-mqtt-datatype-tip > .form-tips {
width: calc(70% - 18px);
display: inline-block;
margin-top: -8px;
}
</style>
@@ -121,6 +126,7 @@
<div class="form-row">
<label for="node-input-datatype"><i class="fa fa-sign-out"></i> <span data-i18n="mqtt.label.output"></span></label>
<select id="node-input-datatype" style="width:70%;">
<option value="auto-detect" data-i18n="mqtt.output.auto-detect"></option>
<option value="auto" data-i18n="mqtt.output.auto"></option>
<option value="buffer" data-i18n="mqtt.output.buffer"></option>
<option value="utf8" data-i18n="mqtt.output.string"></option>
@@ -128,6 +134,10 @@
<option value="base64" data-i18n="mqtt.output.base64"></option>
</select>
</div>
<div class="form-row form-row-mqtt-datatype-tip">
<label> &nbsp; </label>
<div class="form-tips" id="mqtt-in-datatype-depreciated-tip"><span data-i18n="mqtt.label.auto-mode-depreciated"></span></div>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@@ -765,9 +775,8 @@
}
},
qos: {value: "2"},
datatype: {value:"auto",required:true},
broker: {type:"mqtt-broker", required:true,
label:RED._("node-red:mqtt.label.broker")},
datatype: {value:"auto-detect",required:true},
broker: {type:"mqtt-broker", required:true, label:RED._("node-red:mqtt.label.broker")},
// subscriptionIdentifier: {value:0},
nl: {value:false},
rap: {value:true},
@@ -803,6 +812,16 @@
$("div.form-row-mqtt5").toggleClass("form-row-mqtt5-active",!!v5);
$("div.form-row.form-row-mqtt-static").toggleClass("form-row-mqtt-static-disabled", !!dynamic)
}
$("#node-input-datatype").on("change", function() {
if($(this).val() === "auto") {
$(".form-row.form-row-mqtt-datatype-tip").show();
} else {
$(".form-row.form-row-mqtt-datatype-tip").hide();
}
})
$("#node-input-datatype").trigger("change");
$("#node-input-broker").on("change",function(d){
updateVisibility();
});
@@ -823,7 +842,7 @@
$("#node-input-qos").val("2");
}
if (this.datatype === undefined) {
$("#node-input-datatype").val("auto");
$("#node-input-datatype").val("auto-detect");
}
},
oneditsave: function() {

View File

@@ -20,7 +20,30 @@ module.exports = function(RED) {
var isUtf8 = require('is-utf8');
var HttpsProxyAgent = require('https-proxy-agent');
var url = require('url');
const knownMediaTypes = {
"text/css":"string",
"text/html":"string",
"text/plain":"string",
"text/html":"string",
"application/json":"json",
"application/octet-stream":"buffer",
"application/pdf":"buffer",
"application/x-gtar":"buffer",
"application/x-gzip":"buffer",
"application/x-tar":"buffer",
"application/xml":"string",
"application/zip":"buffer",
"audio/aac":"buffer",
"audio/ac3":"buffer",
"audio/basic":"buffer",
"audio/mp4":"buffer",
"audio/ogg":"buffer",
"image/bmp":"buffer",
"image/gif":"buffer",
"image/jpeg":"buffer",
"image/tiff":"buffer",
"image/png":"buffer",
}
//#region "Supporting functions"
function matchTopic(ts,t) {
if (ts == "#") {
@@ -103,7 +126,7 @@ module.exports = function(RED) {
if(src[propName] === "true" || src[propName] === true) {
dst[propName] = true;
} else if(src[propName] === "false" || src[propName] === false) {
dst[propName] = true;
dst[propName] = false;
}
} else {
if(def != undefined) dst[propName] = def;
@@ -188,24 +211,7 @@ module.exports = function(RED) {
*/
function subscriptionHandler(node, datatype ,topic, payload, packet) {
const v5 = node.brokerConn.options && node.brokerConn.options.protocolVersion == 5;
if (datatype === "buffer") {
// payload = payload;
} else if (datatype === "base64") {
payload = payload.toString('base64');
} else if (datatype === "utf8") {
payload = payload.toString('utf8');
} else if (datatype === "json") {
if (isUtf8(payload)) {
payload = payload.toString();
try { payload = JSON.parse(payload); }
catch(e) { node.error(RED._("mqtt.errors.invalid-json-parse"),{payload:payload, topic:topic, qos:packet.qos, retain:packet.retain}); return; }
}
else { node.error((RED._("mqtt.errors.invalid-json-string")),{payload:payload, topic:topic, qos:packet.qos, retain:packet.retain}); return; }
} else {
if (isUtf8(payload)) { payload = payload.toString(); }
}
var msg = {topic:topic, payload:payload, qos:packet.qos, retain:packet.retain};
var msg = {topic:topic, payload:null, qos:packet.qos, retain:packet.retain};
if(v5 && packet.properties) {
setStrProp(packet.properties, msg, "responseTopic");
setBufferProp(packet.properties, msg, "correlationData");
@@ -215,6 +221,76 @@ module.exports = function(RED) {
setStrProp(packet.properties, msg, "reasonString");
setUserProperties(packet.properties.userProperties, msg);
}
const v5isUtf8 = v5 ? msg.payloadFormatIndicator === true : null;
const v5HasMediaType = v5 ? !!msg.contentType : null;
const v5MediaTypeLC = v5 ? (msg.contentType + "").toLowerCase() : null;
if (datatype === "buffer") {
// payload = payload;
} else if (datatype === "base64") {
payload = payload.toString('base64');
} else if (datatype === "utf8") {
payload = payload.toString('utf8');
} else if (datatype === "json") {
if (v5isUtf8 || isUtf8(payload)) {
try {
payload = JSON.parse(payload.toString());
} catch (e) {
node.error(RED._("mqtt.errors.invalid-json-parse"), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
}
} else {
node.error((RED._("mqtt.errors.invalid-json-string")), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
}
} else {
//"auto" (legacy) or "auto-detect" (new default)
if (v5isUtf8 || v5HasMediaType) {
const outputType = knownMediaTypes[v5MediaTypeLC]
switch (outputType) {
case "string":
payload = payload.toString();
break;
case "buffer":
//no change
break;
case "json":
try {
//since v5 type states this should be JSON, parse it & error out if NOT JSON
payload = payload.toString()
const obj = JSON.parse(payload);
if (datatype === "auto-detect") {
payload = obj; //as mode is "auto-detect", return the parsed JSON
}
} catch (e) {
node.error(RED._("mqtt.errors.invalid-json-parse"), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
}
break;
default:
if (v5isUtf8 || isUtf8(payload)) {
payload = payload.toString(); //auto String
if (datatype === "auto-detect") {
try {
payload = JSON.parse(payload); //auto to parsed object (attempt)
} catch (e) {
/* mute error - it simply isnt JSON, just leave payload as a string */
}
}
}
break;
}
} else if (isUtf8(payload)) {
payload = payload.toString(); //auto String
if (datatype === "auto-detect") {
try {
payload = JSON.parse(payload);
} catch (e) {
/* mute error - it simply isnt JSON, just leave payload as a string */
}
}
} //else {
//leave as buffer
//}
}
msg.payload = payload;
if ((node.brokerConn.broker === "localhost")||(node.brokerConn.broker === "127.0.0.1")) {
msg._topic = topic;
}
@@ -465,7 +541,7 @@ module.exports = function(RED) {
};
if(hasProperty(opts, "willTopic")) {
//will v5 properties must be set in the "properties" sub object
node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properies");
node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properties");
};
} else {
//update options
@@ -637,24 +713,8 @@ module.exports = function(RED) {
node.deregister = function(mqttNode,done) {
delete node.users[mqttNode.id];
if (node.closing) {
return done();
}
if (Object.keys(node.users).length === 0) {
if (node.client && node.client.connected) {
// Send close message
if (node.closeMessage) {
node.publish(node.closeMessage,function(err) {
node.client.end(done);
});
} else {
node.client.end(done);
}
return;
} else {
if (node.client) { node.client.end(); }
return done();
}
if (!node.closing && node.connected && Object.keys(node.users).length === 0) {
node.disconnect();
}
done();
};
@@ -663,6 +723,7 @@ module.exports = function(RED) {
}
node.connect = function (callback) {
if (node.canConnect()) {
node.closing = false;
node.connecting = true;
setStatusConnecting(node, true);
try {
@@ -672,6 +733,7 @@ module.exports = function(RED) {
let callbackDone = false; //prevent re-connects causing node.client.on('connect' firing callback multiple times
// Register successful connect or reconnect handler
node.client.on('connect', function (connack) {
node.closing = false;
node.connecting = false;
node.connected = true;
if(!callbackDone && typeof callback == "function") {
@@ -740,6 +802,7 @@ module.exports = function(RED) {
reasonCode: rc,
reasonString: rs
}
node.connected = false;
node.log(RED._("mqtt.state.broker-disconnected", details));
setStatusDisconnected(node, true);
});
@@ -764,25 +827,31 @@ module.exports = function(RED) {
}
};
node.disconnect = function (callback) {
const _callback = function () {
const _callback = function (resetNodeConnectedState) {
setStatusDisconnected(node, true);
node.connecting = false;
node.connected = false;
if(resetNodeConnectedState) {
node.closing = true;
node.connecting = false;
node.connected = false;
}
callback && typeof callback == "function" && callback();
};
if(node.client) {
if(node.client.connected && node.closeMessage) {
node.publish(node.closeMessage, function (err) {
node.client.end(_callback);
});
} else if(node.client.connected || node.client.reconnecting) {
node.client.end(_callback);
} else if(node.client.disconnecting || node.client.connected === false) {
_callback();
}
if(node.closing) {
return _callback(false);
}
var endCallBack = function endCallBack() {
}
if(node.connected && node.closeMessage) {
node.publish(node.closeMessage, function (err) {
node.client.end(endCallBack);
_callback(true);
});
} else if(node.connected) {
node.client.end(endCallBack);
_callback(true);
} else {
_callback();
_callback(false);
}
}
node.subscriptionIds = {};
@@ -1074,6 +1143,8 @@ module.exports = function(RED) {
node.brokerConn.unsubscribe(node.topic,node.id, removed);
}
node.brokerConn.deregister(node, done);
} else {
done();
}
});
} else {
@@ -1134,7 +1205,11 @@ module.exports = function(RED) {
}
node.brokerConn.register(node);
node.on('close', function(done) {
node.brokerConn.deregister(node,done);
if (node.brokerConn) {
node.brokerConn.deregister(node,done);
} else {
done();
}
});
} else {
node.error(RED._("mqtt.errors.missing-config"));

View File

@@ -100,14 +100,107 @@
<option value="obj" data-i18n="httpin.json"></option>
</select>
</div>
<div class="form-row form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></span></div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="httpin.label.headers"></span></label>
</div>
<div class="form-row node-input-headers-container-row">
<ol id="node-input-headers-container"></ol>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></span></div>
</script>
<script type="text/javascript">
(function() {
const headerTypes = [
{ value: "Accept", label: "Accept", hasValue: false },
{ value: "Accept-Encoding", label: "Accept-Encoding", hasValue: false },
{ value: "Accept-Language", label: "Accept-Language", hasValue: false },
{ value: "Authorization", label: "Authorization", hasValue: false },
{ value: "Content-Type", label: "Content-Type", hasValue: false },
{ value: "Cache-Control", label: "Cache-Control", hasValue: false },
{ value: "User-Agent", label: "User-Agent", hasValue: false },
{ value: "Location", label: "Location", hasValue: false },
{ value: "other", label: "other", hasValue: true, icon: "red/images/typedInput/az.png" },
{ value: "msg", label: "msg.", hasValue: true },
]
const headerOptions = {};
const defaultOptions = [
{ value: "other", label: "other", hasValue: true, icon: "red/images/typedInput/az.png" },
{ value: "msg", label: "msg.", hasValue: true },
];
headerOptions["accept"] = [
{ value: "text/plain", label: "text/plain", hasValue: false },
{ value: "text/html", label: "text/html", hasValue: false },
{ value: "application/json", label: "application/json", hasValue: false },
{ value: "application/xml", label: "application/xml", hasValue: false },
...defaultOptions,
];
headerOptions["accept-encoding"] = [
{ value: "gzip", label: "gzip", hasValue: false },
{ value: "deflate", label: "deflate", hasValue: false },
{ value: "compress", label: "compress", hasValue: false },
{ value: "br", label: "br", hasValue: false },
{ value: "gzip, deflate", label: "gzip, deflate", hasValue: false },
{ value: "gzip, deflate, br", label: "gzip, deflate, br", hasValue: false },
...defaultOptions,
];
headerOptions["accept-language"] = [
{ value: "*", label: "*", hasValue: false },
{ value: "en-GB, en-US, en;q=0.9", label: "en-GB, en-US, en;q=0.9", hasValue: false },
{ value: "de-AT, de-DE;q=0.9, en;q=0.5", label: "de-AT, de-DE;q=0.9, en;q=0.5", hasValue: false },
{ value: "es-mx,es,en;q=0.5", label: "es-mx,es,en;q=0.5", hasValue: false },
{ value: "fr-CH, fr;q=0.9, en;q=0.8", label: "fr-CH, fr;q=0.9, en;q=0.8", hasValue: false },
{ value: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", label: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", hasValue: false },
{ value: "ja-JP, jp", label: "ja-JP, jp", hasValue: false },
...defaultOptions,
];
headerOptions["content-type"] = [
{ value: "text/css", label: "text/css", hasValue: false },
{ value: "text/plain", label: "text/plain", hasValue: false },
{ value: "text/html", label: "text/html", hasValue: false },
{ value: "application/json", label: "application/json", hasValue: false },
{ value: "application/octet-stream", label: "application/octet-stream", hasValue: false },
{ value: "application/pdf", label: "application/pdf", hasValue: false },
{ value: "application/xml", label: "application/xml", hasValue: false },
{ value: "application/zip", label: "application/zip", hasValue: false },
{ value: "multipart/form-data", label: "multipart/form-data", hasValue: false },
{ value: "audio/aac", label: "audio/aac", hasValue: false },
{ value: "audio/ac3", label: "audio/ac3", hasValue: false },
{ value: "audio/basic", label: "audio/basic", hasValue: false },
{ value: "audio/mp4", label: "audio/mp4", hasValue: false },
{ value: "audio/ogg", label: "audio/ogg", hasValue: false },
{ value: "image/bmp", label: "image/bmp", hasValue: false },
{ value: "image/gif", label: "image/gif", hasValue: false },
{ value: "image/jpeg", label: "image/jpeg", hasValue: false },
{ value: "image/png", label: "image/png", hasValue: false },
{ value: "image/tiff", label: "image/tiff", hasValue: false },
...defaultOptions,
];
headerOptions["cache-control"] = [
{ value: "max-age=0", label: "max-age=0", hasValue: false },
{ value: "max-age=86400", label: "max-age=86400", hasValue: false },
{ value: "no-cache", label: "no-cache", hasValue: false },
...defaultOptions,
];
headerOptions["user-agent"] = [
{ value: "Mozilla/5.0", label: "Mozilla/5.0", hasValue: false },
...defaultOptions,
];
function getHeaderOptions(headerName) {
const lc = (headerName || "").toLowerCase();
let opts = headerOptions[lc];
return opts || defaultOptions;
}
RED.nodes.registerType('http request',{
category: 'network',
color:"rgb(231, 231, 174)",
@@ -133,7 +226,8 @@
proxy: {type:"http proxy",required: false,
label:RED._("node-red:httpin.proxy-config") },
authType: {value: ""},
senderr: {value: false}
senderr: {value: false},
headers: { value: [] }
},
credentials: {
user: {type:"text"},
@@ -156,6 +250,7 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
const node = this;
$("#node-input-useAuth").on("change", function() {
if ($(this).is(":checked")) {
$(".node-input-useAuth-row").show();
@@ -169,9 +264,10 @@
$('#node-input-user').val('');
$('#node-input-password').val('');
}
RED.tray.resize();
});
$("#node-input-authType-select").on("change", function() {
var val = $(this).val();
const val = $(this).val();
$("#node-input-authType").val(val);
if (val === "basic" || val === "digest") {
$(".node-input-basic-row").show();
@@ -183,6 +279,7 @@
$('#node-span-token').show();
$('#node-input-user').val('');
}
RED.tray.resize();
});
$("#node-input-method").on("change", function() {
if ($(this).val() == "GET") {
@@ -190,17 +287,18 @@
} else {
$(".node-input-paytoqs-row").hide();
}
RED.tray.resize();
});
if (this.paytoqs === true || this.paytoqs == "query") {
if (node.paytoqs === true || node.paytoqs == "query") {
$("#node-input-paytoqs").val("query");
} else if (this.paytoqs === "body") {
} else if (node.paytoqs === "body") {
$("#node-input-paytoqs").val("body");
} else {
$("#node-input-paytoqs").val("ignore");
}
if (this.authType) {
if (node.authType) {
$('#node-input-useAuth').prop('checked', true);
$("#node-input-authType-select").val(this.authType);
$("#node-input-authType-select").val(node.authType);
$("#node-input-authType-select").change();
} else {
$('#node-input-useAuth').prop('checked', false);
@@ -213,8 +311,9 @@
} else {
$("#node-row-tls").hide();
}
RED.tray.resize();
}
if (this.tls) {
if (node.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
@@ -230,8 +329,9 @@
} else {
$("#node-input-useProxy-row").hide();
}
RED.tray.resize();
}
if (this.proxy) {
if (node.proxy) {
$("#node-input-useProxy").prop("checked", true);
} else {
$("#node-input-useProxy").prop("checked", false);
@@ -247,7 +347,70 @@
} else {
$("#tip-json").hide();
}
RED.tray.resize();
});
const hasMatch = function (arr, value) {
return arr.some(function (ht) {
return ht.value === value
});
}
const headerList = $("#node-input-headers-container").css('min-height', '150px').css('min-width', '450px').editableList({
addItem: function (container, i, header) {
const row = $('<div/>').css({
overflow: 'hidden',
whiteSpace: 'nowrap',
display: 'flex'
}).appendTo(container);
const propertNameCell = $('<div/>').css({ 'flex-grow': 1 }).appendTo(row);
const propertyName = $('<input/>', { class: "node-input-header-name", type: "text", style: "width: 100%" })
.appendTo(propertNameCell)
.typedInput({ types: headerTypes });
const propertyValueCell = $('<div/>').css({ 'flex-grow': 1, 'margin-left': '10px' }).appendTo(row);
const propertyValue = $('<input/>', { class: "node-input-header-value", type: "text", style: "width: 100%" })
.appendTo(propertyValueCell)
.typedInput({
types: getHeaderOptions(header.keyType)
});
const setup = function(_header) {
const headerTypeIsAPreset = function(h) {return hasMatch(headerTypes, h) };
const headerValueIsAPreset = function(h, v) {return hasMatch(getHeaderOptions(h), v) };
const {keyType, keyValue, valueType, valueValue} = header;
if(keyType == "msg" || keyType == "other") {
propertyName.typedInput('type', keyType);
propertyName.typedInput('value', keyValue);
} else if (headerTypeIsAPreset(keyType)) {
propertyName.typedInput('type', keyType);
} else {
propertyName.typedInput('type', "other");
propertyName.typedInput('value', keyValue);
}
if(valueType == "msg" || valueType == "other") {
propertyValue.typedInput('type', valueType);
propertyValue.typedInput('value', valueValue);
} else if (headerValueIsAPreset(propertyName.typedInput('type'), valueType)) {
propertyValue.typedInput('type', valueType);
} else {
propertyValue.typedInput('type', "other");
propertyValue.typedInput('value', valueValue);
}
}
setup(header);
propertyName.on('change', function (event) {
propertyValue.typedInput('types', getHeaderOptions(propertyName.typedInput('type')));
});
},
removable: true
});
if (node.headers) {
for (let index = 0; index < node.headers.length; index++) {
const element = node.headers[index];
headerList.editableList('addItem', node.headers[index]);
}
}
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
@@ -256,6 +419,36 @@
if (!$("#node-input-useProxy").is(":checked")) {
$("#node-input-proxy").val("_ADD_");
}
const headers = $("#node-input-headers-container").editableList('items');
const node = this;
node.headers = [];
headers.each(function(i) {
const header = $(this);
const keyType = header.find(".node-input-header-name").typedInput('type');
const keyValue = header.find(".node-input-header-name").typedInput('value');
const valueType = header.find(".node-input-header-value").typedInput('type');
const valueValue = header.find(".node-input-header-value").typedInput('value');
if (keyType !== '' || keyType === 'other' || keyType === 'msg') {
node.headers.push({
keyType, keyValue, valueType, valueValue
})
}
});
},
oneditresize: function(size) {
const dlg = $("#dialog-form");
const expandRow = dlg.find('.node-input-headers-container-row');
let height = dlg.height() - 5;
if(expandRow && expandRow.length){
const siblingRows = dlg.find('> .form-row:not(.node-input-headers-container-row)');
for (let i = 0; i < siblingRows.size(); i++) {
const cr = $(siblingRows[i]);
if(cr.is(":visible"))
height -= cr.outerHeight(true);
}
$("#node-input-headers-container").editableList('height',height);
}
}
});
})();
</script>

View File

@@ -73,7 +73,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
var paytobody = false;
var redirectList = [];
var sendErrorsToCatch = n.senderr;
node.headers = n.headers || [];
var nodeHTTPPersistent = n["persist"];
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
@@ -105,6 +105,37 @@ in your Node-RED user directory (${RED.settings.userDir}).
timingLog = RED.settings.httpRequestTimingLog;
}
/**
* Case insensitive header value update util function
* @param {object} headersObject The opt.headers object to update
* @param {string} name The header name
* @param {string} value The header value to set (if blank, header is removed)
*/
const updateHeader = function(headersObject, name, value ) {
const hn = name.toLowerCase();
const keys = Object.keys(headersObject);
const matchingKeys = keys.filter(e => e.toLowerCase() == hn)
const updateKey = (k,v) => {
delete headersObject[k]; //delete incase of case change
if(v) { headersObject[name] = v } //re-add with requested name & value
}
if(matchingKeys.length == 0) {
updateKey(name, value)
} else {
matchingKeys.forEach(k => {
updateKey(k, value);
});
}
}
/**
* @param {Object} headersObject
* @param {string} name
* @return {any} value
*/
const getHeaderValue = (headersObject, name) => {
const asLowercase = name.toLowercase();
return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)];
}
this.on("input",function(msg,nodeSend,nodeDone) {
checkNodeAgentPatch();
//reset redirectList on each request
@@ -183,7 +214,6 @@ in your Node-RED user directory (${RED.settings.userDir}).
// TODO: add UI option to auto decompress. Setting to false for 1.x compatibility
opts.decompress = false;
opts.method = method;
opts.headers = {};
opts.retry = 0;
opts.responseType = 'buffer';
opts.maxRedirects = 21;
@@ -229,34 +259,85 @@ in your Node-RED user directory (${RED.settings.userDir}).
]
}
var ctSet = "Content-Type"; // set default camel case
var clSet = "Content-Length";
let ctSet = "Content-Type"; // set default camel case
let clSet = "Content-Length";
const normaliseKnownHeader = function (name) {
const _name = name.toLowerCase();
// only normalise the known headers used later in this
// function. Otherwise leave them alone.
switch (_name) {
case "content-type":
ctSet = name;
name = _name;
break;
case "content-length":
clSet = name;
name = _name;
break;
}
return name;
}
opts.headers = {};
//add msg.headers
//NOTE: ui headers will take precidence over msg.headers
if (msg.headers) {
if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
var headerHash = msg.headers['x-node-red-request-node'];
const headerHash = msg.headers['x-node-red-request-node'];
delete msg.headers['x-node-red-request-node'];
var hash = hashSum(msg.headers);
const hash = hashSum(msg.headers);
if (hash === headerHash) {
delete msg.headers;
}
}
if (msg.headers) {
for (var v in msg.headers) {
if (msg.headers.hasOwnProperty(v)) {
var name = v.toLowerCase();
if (name !== "content-type" && name !== "content-length") {
// only normalise the known headers used later in this
// function. Otherwise leave them alone.
name = v;
}
else if (name === 'content-type') { ctSet = v; }
else { clSet = v; }
opts.headers[name] = msg.headers[v];
}
for (let hn in msg.headers) {
const name = normaliseKnownHeader(hn);
updateHeader(opts.headers, name, msg.headers[hn]);
}
}
}
//add/remove/update headers from UI.
if (node.headers.length) {
for (let index = 0; index < node.headers.length; index++) {
const header = node.headers[index];
let headerName, headerValue;
if (header.keyType === "other") {
headerName = header.keyValue
} else if (header.keyType === "msg") {
RED.util.evaluateNodeProperty(header.keyValue, header.keyType, node, msg, (err, value) => {
if (err) {
//ignore header
} else {
headerName = value;
}
});
} else {
headerName = header.keyType
}
if (!headerName) {
continue; //skip if header name is empyy
}
if (header.valueType === "other") {
headerValue = header.valueValue
} else if (header.valueType === "msg") {
RED.util.evaluateNodeProperty(header.valueValue, header.valueType, node, msg, (err, value) => {
if (err) {
//ignore header
} else {
headerValue = value;
}
});
} else {
headerValue = header.valueType
}
const hn = normaliseKnownHeader(headerName);
updateHeader(opts.headers, hn, headerValue);
}
}
if (msg.hasOwnProperty('followRedirects')) {
opts.followRedirect = !!msg.followRedirects;
}

View File

@@ -35,8 +35,6 @@ module.exports = function(RED) {
}
}
var listenerNodes = {};
var activeListenerNodes = 0;
// A node red node that sets up a local websocket server
function WebSocketListenerNode(n) {
@@ -166,7 +164,6 @@ module.exports = function(RED) {
}
if (node.isServer) {
activeListenerNodes++;
if (!serverUpgradeAdded) {
RED.server.on('upgrade', handleServerUpgrade);
serverUpgradeAdded = true
@@ -210,7 +207,7 @@ module.exports = function(RED) {
startconn(); // start outbound connection
}
node.on("close", function() {
node.on("close", function(done) {
if (node.heartbeatInterval) {
clearInterval(node.heartbeatInterval);
}
@@ -218,19 +215,25 @@ module.exports = function(RED) {
delete listenerNodes[node.fullPath];
node.server.close();
node._inputNodes = [];
activeListenerNodes--;
// if (activeListenerNodes === 0 && serverUpgradeAdded) {
// RED.server.removeListener('upgrade', handleServerUpgrade);
// serverUpgradeAdded = false;
// }
}
else {
node.closing = true;
node.server.close();
if (node.tout) {
clearTimeout(node.tout);
node.tout = null;
}
//wait 20*50 (1000ms max) for ws to close.
//call done when readyState === ws.CLOSED (or 1000ms, whichever comes fist)
const closeMonitorInterval = 20;
let closeMonitorCount = 50;
let si = setInterval(() => {
if(node.server.readyState === ws.CLOSED || closeMonitorCount <= 0) {
if (node.tout) {
clearTimeout(node.tout);
node.tout = null;
}
clearInterval(si);
return done();
}
closeMonitorCount--;
}, closeMonitorInterval);
}
});
}

View File

@@ -50,7 +50,8 @@
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:110px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional"><br/>
<input type="checkbox" id="node-input-trim" style="display:inline-block; width:auto; vertical-align:top;"> <span data-i18n="tcpin.label.reattach"></span>
</div>
<div class="form-row">
@@ -87,6 +88,7 @@
datatype:{value:"buffer"},
newline:{value:""},
topic: {value:""},
trim: {value:false},
base64: {/*deprecated*/ value:false, required:true},
tls: {type:"tls-config", value:'', required:false,
label:RED._("node-red:httpin.tls-config") }
@@ -315,7 +317,8 @@
<span id="node-units"></span>
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:162px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional"><br/>
<input type="checkbox" id="node-input-trim" style="display:inline-block; width:auto; vertical-align:top;"> <span data-i18n="tcpin.label.reattach"></span>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
@@ -339,8 +342,8 @@
ret: {value:"buffer"},
splitc: {value:"0", required:true},
newline: {value:""},
tls: {type:"tls-config", value:'', required:false,
label:RED._("node-red:httpin.tls-config") }
trim: {value:false},
tls: {type:"tls-config", value:'', required:false, label:RED._("node-red:httpin.tls-config")}
},
inputs:1,
outputs:1,

View File

@@ -88,6 +88,7 @@ module.exports = function(RED) {
this.datatype = n.datatype||'buffer'; /* buffer,utf8,base64 */
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.base64 = n.base64;
this.trim = n.trim || false;
this.server = (typeof n.server == 'boolean')?n.server:(n.server == "server");
this.closing = false;
this.connected = false;
@@ -135,7 +136,8 @@ module.exports = function(RED) {
buffer = buffer+data;
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i] + node.newline.trimEnd()};
msg = {topic:node.topic, payload:parts[i]};
if (node.trim == true) { msg.payload += node.newline; }
msg._session = {type:"tcp",id:id};
node.send(msg);
}
@@ -229,7 +231,8 @@ module.exports = function(RED) {
buffer = buffer+data;
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i] + node.newline.trimEnd(), ip:socket.remoteAddress, port:socket.remotePort};
msg = {topic:node.topic, payload:parts[i], ip:socket.remoteAddress, port:socket.remotePort};
if (node.trim == true) { msg.payload += node.newline; }
msg._session = {type:"tcp",id:id};
node.send(msg);
}
@@ -518,6 +521,7 @@ module.exports = function(RED) {
this.out = n.out;
this.ret = n.ret || "buffer";
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.trim = n.trim || false;
this.splitc = n.splitc;
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
@@ -653,7 +657,8 @@ module.exports = function(RED) {
let parts = chunk.split(node.newline);
for (var p=0; p<parts.length-1; p+=1) {
let m = RED.util.cloneMessage(msg);
m.payload = parts[p] + node.newline.trimEnd();
m.payload = parts[p];
if (node.trim == true) { m.payload += node.newline; }
nodeSend(m);
}
chunk = parts[parts.length-1];

View File

@@ -21,7 +21,7 @@
<label style="width:100%;"><span data-i18n="json.label.o2j"></span></label>
</div>
<div class="form-row node-json-to-json-options" style="padding-left: 20px;">
<input style="width:20px; vertical-align:top; margin-right: 5px;" type="checkbox" id="node-input-pretty"><label style="width: auto;" for="node-input-pretty" data-i18n="json.label.pretty"></span>
<input style="width:20px; vertical-align:top; margin-right: 5px;" type="checkbox" id="node-input-pretty"><label style="width: auto;" for="node-input-pretty" data-i18n="json.label.pretty"></label>
</div>
</script>

View File

@@ -47,7 +47,7 @@
<div class="form-row">
<input type="checkbox" id="node-input-allowEmptySequence" style="margin-left:20px; margin-right: 10px; vertical-align:top; width:auto;">
<label for="node-input-allowEmptySequence" style="width:auto;" data-i18n="batch.interval.empty"></label>
</div>
</div>
</div>
<div class="node-row-msg-concat">