node-red-contrib-actionflows
Advanced tools
Comparing version 0.0.1 to 1.0.0
module.exports = function(RED) { | ||
RED.nodes.registerType("actionflows", actionflows); | ||
function actionflows(config) { | ||
var node = this; | ||
// Create our node and event handler | ||
RED.nodes.createNode(this, config); | ||
var node = this; | ||
node.context().global.set('actionflows', getActionFlows()); | ||
var nodeID = config.id; | ||
if (typeof config._alias != 'undefined') { | ||
nodeID = config._alias; | ||
} | ||
var event = "af:" + nodeID; | ||
var event = "af:" + config.id; | ||
var handler = function(msg) { | ||
msg._af["noinc0"] = true; // Returning flag, do not increment from zero | ||
node.receive(msg); | ||
if (typeof msg._af != "undefined") { | ||
msg._af["noinc0"] = true; // Returning flag, do not increment from zero | ||
node.receive(msg); | ||
} | ||
} | ||
RED.events.on(event, handler); | ||
this.on("input", function(msg) { | ||
// Check loop conditional | ||
if (stopLoop()) { | ||
// Move on | ||
if (msg._af["stack"].length == 0) { | ||
delete msg._af; | ||
} | ||
node.status({}); | ||
node.send(msg); | ||
return; | ||
// Clean up event handler on close | ||
this.on("close",function() { | ||
RED.events.removeListener(event, handler); | ||
node.status({}); | ||
}); | ||
// Create our global context actionflows object for mapping details | ||
var af = node.context().global.get('actionflows'); | ||
if (typeof af == "undefined") { | ||
af = new Object(); | ||
af["afs"] = new Object(); | ||
af["ins"] = new Object(); | ||
} | ||
if (typeof af["map"] == "undefined") { | ||
af["map"] = function(e) { | ||
if (e.id == "runtime-state") { | ||
purge(); | ||
map(); | ||
} | ||
var af = node.context().global.get('actionflows'); | ||
if (typeof msg._af == 'undefined') { | ||
msg._af = {}; | ||
msg._af["stack"] = []; | ||
} | ||
RED.events.on("runtime-event", af["map"]); | ||
} | ||
af["afs"][config.id] = config; | ||
af["map"] = map; | ||
node.context().global.set('actionflows', af); | ||
// Purge flows from prior deployment | ||
function purge() { | ||
var af = node.context().global.get("actionflows"); | ||
var afs_object = af["afs"]; | ||
for (var id in afs_object) { | ||
if (afs_object[id].mapped == true) { | ||
delete afs_object[id]; | ||
} | ||
} | ||
var ins_object = af["ins"]; | ||
for (var id in ins_object) { | ||
if (ins_object[id].mapped == true) { | ||
delete ins_object[id]; | ||
} | ||
} | ||
af["afs"] = afs_object; | ||
af["ins"] = ins_object; | ||
node.context().global.set('actionflows', af); | ||
} | ||
// Map actionflows with `action in` assocations on scope settings | ||
function map() { | ||
// Separate our actions from our ins | ||
var af = node.context().global.get("actionflows"); | ||
var RED2 = require.main.require('node-red'); | ||
var flows = RED2.nodes.getFlows().flows; | ||
var afs_object = af["afs"]; | ||
var ins_object = af["ins"]; | ||
if (typeof msg._af[nodeID] == "undefined") { | ||
msg._af[nodeID] = { | ||
execTime: process.hrtime(), | ||
ins: af[nodeID].ins, | ||
index: 0 | ||
}; | ||
node.status({fill:"green",shape:"dot",text: "running" }); | ||
// Purge `action`s on disabled tabs | ||
for (var id in afs_object) { | ||
var t = findTab(afs_object[id]); | ||
if (t == false || t.disabled == true) { | ||
delete afs_object[id]; | ||
}else{ | ||
afs_object[id].ins = []; | ||
} | ||
if (msg._af[nodeID].index < msg._af[nodeID].ins.length) { | ||
msg._af["stack"].push(event); | ||
msg._af[nodeID].index++; | ||
RED.events.emit("af:" + msg._af[nodeID].ins[msg._af[nodeID].index - 1].id, msg); | ||
}else{ | ||
if (config.perf) { | ||
var t = process.hrtime(msg._af[nodeID].execTime); | ||
node.warn("Action cycle execution time: " + t[0] + "s and " + t[1]/1000000 + "ms"); | ||
// Mark for mapped | ||
afs_object[id].mapped = true; | ||
} | ||
// Purge `action in`s on disabled tabs | ||
for (var id in ins_object) { | ||
var t = findTab(ins_object[id]); | ||
if (t == false || t.disabled == true) { | ||
delete ins_object[id]; | ||
} | ||
// Mark for mapped | ||
ins_object[id].mapped = true; | ||
} | ||
// Build associations between actions and their matching ins | ||
var actions = Object.assign({}, afs_object); | ||
for (var id in actions) { | ||
var a = actions[id]; | ||
var ins = Object.assign({}, ins_object); | ||
// Match actionflows on same z plane, regardless of scope | ||
for (var id in ins) { | ||
var i = ins[id]; | ||
if (i.z == a.z) { | ||
if (prefixMatch(i.name).startsWith(prefixMatch(a.name))) { | ||
a.ins.push(i); | ||
delete ins[id]; | ||
} | ||
} | ||
delete msg._af[nodeID]; | ||
} | ||
// Match any global actionflows | ||
if (a.scope == "global") { | ||
for (var id in ins) { | ||
var i = ins[id]; | ||
if (i.scope == "global") { | ||
if (prefixMatch(i.name).startsWith(prefixMatch(a.name))) { | ||
a.ins.push(i); | ||
delete ins[i]; | ||
} | ||
} | ||
} | ||
} | ||
// Match protected actionflows to explicitly named ins | ||
if (a.scope == "protected") { | ||
var name = a.name; | ||
var parent = getParent(a); | ||
while (parent != false && parent.type != "tab") { | ||
name = parent.name + " " + name; | ||
for (var id in ins) { | ||
var i = ins[id]; | ||
if (i.scope == "protected" && i.z == parent.z) { | ||
if (prefixMatch(i.name).startsWith(prefixMatch(name))) { | ||
a.ins.push(i); | ||
delete ins[id]; | ||
} | ||
} | ||
} | ||
parent = getParent(parent); | ||
} | ||
} | ||
} | ||
// Bump loop and restart action | ||
if (config.loop != "none") { | ||
if (bumpLoop()) { | ||
RED.events.emit(event, msg); | ||
}else{ | ||
// Stop on bump error | ||
node.status({fill:"red",shape:"ring",text: "error" }); | ||
return; | ||
// Match protected ins with explicity named actions | ||
var ins = Object.assign({}, ins_object); | ||
for (var id in ins) { | ||
var i = ins[id]; | ||
if (i.scope == "protected") { | ||
var name = i.name; | ||
var parent = getParent(i); | ||
while (parent != false && parent.type != "tab") { | ||
name = parent.name + " " + name; | ||
for (var id in actions) { | ||
var a = actions[id]; | ||
if (a.scope == "protected" && a.z == parent.z) { | ||
if (prefixMatch(a.name).startsWith(prefixMatch(name))) { | ||
a.ins.push(i); | ||
} | ||
} | ||
} | ||
parent = getParent(parent); | ||
} | ||
} | ||
} | ||
// Sort matched ins by priority | ||
for (var id in actions) { | ||
actions[id].ins.sort(function(a, b) { | ||
return parseInt(a.priority)-parseInt(b.priority); | ||
}); | ||
} | ||
af["invoke"] = invokeActionIn; | ||
af["actions"] = actions; | ||
node.context().global.set('actionflows', af); | ||
// Return the parent (tab or subflow) of the given item or false | ||
function getParent(item) { | ||
var parent = false; | ||
if (item.type == "tab") { | ||
return parent; | ||
} | ||
var subs = getSubflows(); | ||
for (var i = 0; i < subs.length; i++) { | ||
if (subs[i].id == item.z) { | ||
parent = subs[i]; | ||
break; | ||
} | ||
} | ||
if (parent == false) { | ||
var tabs = getTabs(); | ||
for (var i = 0; i < tabs.length; i++) { | ||
if (tabs[i].id == item.z) { | ||
parent = tabs[i]; | ||
break; | ||
} | ||
} | ||
} | ||
return parent; | ||
} | ||
// Return all subflows recursively from the given array | ||
function getSubflows(scan) { | ||
if (typeof scan == "undefined") { | ||
scan = flows; | ||
} | ||
var items = []; | ||
for (var i = 0; i < scan.length; i++) { | ||
if (scan[i].type.startsWith("subflow:")) { | ||
items.push({ | ||
name: getSubflowName(scan[i]), | ||
type: scan[i].type, | ||
id: scan[i].id, | ||
z: scan[i].z | ||
}); | ||
} | ||
} | ||
// Return the subflow name, or its default | ||
function getSubflowName(ss) { | ||
if (typeof ss.name == "undefined" || ss.name == "") { | ||
var name = ""; | ||
for (var f = 0; f < flows.length; f++) { | ||
if (flows[f].id == ss.type.substr(8)) { | ||
name = flows[f].name; | ||
break; | ||
} | ||
} | ||
return name; | ||
}else{ | ||
// Move on | ||
return ss.name; | ||
} | ||
}; | ||
var more = []; | ||
items.forEach(function(f) { | ||
var sub = RED.nodes.getNode(f.id); | ||
if (sub != null) { | ||
if (typeof sub.instanceNodes != "undefined") { | ||
var inst = sub.instanceNodes; | ||
for(var id in inst) { | ||
if (id != f.id) { | ||
var subsub = Object.assign({}, inst[id]); | ||
if (subsub.type.startsWith("subflow:")) { | ||
more.push({ | ||
name: getSubflowName(subsub), | ||
type: subsub.type, | ||
id: subsub.id, | ||
z: subsub.z | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
if (more.length > 0) { | ||
var subsub = getSubflows(more); | ||
if (subsub.length > 0) { | ||
items = items.concat(subsub); | ||
} | ||
} | ||
return items; | ||
} | ||
// Support dividers; dot, dash, space, underscore | ||
function prefixMatch(s) { | ||
return s.replace(new RegExp("_", 'g'), " ") | ||
.replace(new RegExp("-", 'g'), " ") | ||
.replace(new RegExp("\\.", 'g'), " ") + " "; | ||
} | ||
// Given the item instance, return the tab it lives on or false | ||
function findTab(item) { | ||
if (item.type == "tab") { | ||
return item; | ||
} | ||
var tabs = getTabs(); | ||
var t = tabs.find(function(t) { | ||
return item.z == t.id; | ||
}); | ||
if (typeof t == "undefined") { | ||
var subs = getSubflows(); | ||
t = subs.find(function(s) { | ||
return item.z == s.id; | ||
}); | ||
} | ||
if (typeof t == "undefined") { | ||
return false; | ||
} | ||
return findTab(t); | ||
} | ||
// Return all tabs | ||
function getTabs() { | ||
var tabs = []; | ||
for (var i = 0; i < flows.length; i++) { | ||
if (flows[i].type == "tab") { | ||
tabs.push(flows[i]); | ||
} | ||
} | ||
return tabs; | ||
} | ||
// Furnish invoke function for JavaScript authors | ||
function invokeActionIn(sName, msg) { | ||
// Match action ins with the given name | ||
var ains = []; | ||
for (var id in ins) { | ||
if (prefixMatch(ins[id].name).startsWith(prefixMatch(sName))) { | ||
ains.push(ins[id]); | ||
} | ||
} | ||
// Sort `action in` by priority | ||
ains.sort(function(a, b) { | ||
return parseInt(a.priority)-parseInt(b.priority); | ||
}); | ||
if (typeof msg._af == "undefined") { | ||
msg._af = {}; | ||
msg._af.stack = []; | ||
} | ||
var done; | ||
var event = "af:" + RED2.util.generateId(); | ||
var handler = function(msg) { | ||
if (ains.length > 0) { | ||
msg._af["stack"].push(event); | ||
RED.events.emit("af:" + ains.shift().id, msg); | ||
}else{ | ||
RED.events.removeListener(event, handler); | ||
if (msg._af["stack"].length == 0) { | ||
delete msg._af; | ||
} | ||
node.status({}); | ||
node.send(msg); | ||
done(msg); | ||
} | ||
} | ||
RED.events.on(event, handler); | ||
var p = new Promise(function(resolve) { | ||
done = resolve; | ||
}); | ||
if (ains.length > 0) { | ||
var id = ains.shift().id; | ||
msg._af["stack"].push(event); | ||
RED.events.emit("af:" + id, msg); | ||
} | ||
return p; | ||
} | ||
invokeActionIn("#deployed", {payload:""}); | ||
} | ||
this.on("input", function(msg) { | ||
// Check if stopLoop condition met | ||
function stopLoop() { | ||
// Check no matching `action in`s, just move along | ||
var af = node.context().global.get('actionflows'); | ||
if (typeof af == "undefined") { | ||
node.status({}); | ||
node.send(msg); | ||
return; | ||
} | ||
var actions = af["actions"]; | ||
if (typeof actions[config.id] == "undefined") { | ||
node.status({}); | ||
node.send(msg); | ||
return; | ||
} | ||
// Support increment from zero | ||
if (typeof msg._af != "undefined") { | ||
if (typeof msg._af["noinc0"] == "undefined" && config.loop == "inc0") { | ||
setContextPropertyValue(config.proptype, config.prop, 0); | ||
}else{ | ||
delete msg._af["noinc0"]; | ||
} | ||
} | ||
// Check loop conditional | ||
if (stopLoop()) { | ||
// Move on | ||
if (msg._af.stack.length == 0) { | ||
delete msg._af; | ||
} | ||
node.status({}); | ||
node.send(msg); | ||
return; | ||
} | ||
if (typeof msg._af == "undefined") { | ||
msg._af = {}; | ||
msg._af.stack = []; | ||
} | ||
if (typeof msg._af[config.id] == "undefined") { | ||
msg._af[config.id] = actions[config.id]; | ||
msg._af[config.id].execTime = process.hrtime(); | ||
msg._af[config.id].index = 0; | ||
node.status({fill:"green",shape:"dot",text: "running" }); | ||
} | ||
var sl = false; | ||
if (config.loop == "none") return sl; | ||
var prop = getTypeInputValue(config.proptype, config.prop); | ||
if (msg._af[config.id].index < msg._af[config.id].ins.length) { | ||
msg._af.stack.push(event); | ||
msg._af[config.id].index++; | ||
var ain = msg._af[config.id].ins[msg._af[config.id].index - 1]; | ||
if (config.seq) { | ||
node.warn("`" + config.name + "` (" + config.id + ") -> `" + ain.name + "` (" + ain.id + ")"); | ||
} | ||
RED.events.emit("af:" + ain.id, msg); | ||
}else{ | ||
if (config.perf) { | ||
var t = process.hrtime(msg._af[config.id].execTime); | ||
node.warn("Action cycle execution time: " + t[0] + "s and " + t[1]/1000000 + "ms"); | ||
} | ||
delete msg._af[config.id]; | ||
// Initialized undefined variables | ||
if (typeof prop == "undefined") { | ||
// Bump loop and restart action | ||
if (config.loop != "none") { | ||
if (bumpLoop()) { | ||
RED.events.emit(event, msg); | ||
}else{ | ||
// Stop on bump error | ||
node.status({fill:"red",shape:"ring",text: "error" }); | ||
return; | ||
} | ||
}else{ | ||
// Move on | ||
if (msg._af["stack"].length == 0) { | ||
delete msg._af; | ||
} | ||
node.status({}); | ||
node.send(msg); | ||
} | ||
} | ||
// String logic operator inits to empty string | ||
if (config.until == "cont" || config.until == "notc") { | ||
prop = ""; | ||
}else{ | ||
// Check if stopLoop condition met | ||
function stopLoop() { | ||
// Math logic operator inits to zero | ||
prop = 0; | ||
} | ||
setContextPropertyValue(config.proptype, config.prop, prop); | ||
// Support increment from zero | ||
if (typeof msg._af != "undefined") { | ||
if (typeof msg._af["noinc0"] == "undefined" && config.loop == "inc0") { | ||
setContextPropertyValue(config.proptype, config.prop, 0); | ||
}else{ | ||
delete msg._af["noinc0"]; | ||
} | ||
var untilprop = getTypeInputValue(config.untilproptype, config.untilprop); | ||
switch(config.until) { | ||
case "eq": | ||
sl = (prop == untilprop); | ||
break; | ||
case "neq": | ||
sl = (prop != untilprop); | ||
break; | ||
case "lt": | ||
sl = (prop < untilprop); | ||
break; | ||
case "lte": | ||
sl = (prop <= untilprop); | ||
break; | ||
case "gt": | ||
sl = (prop > untilprop); | ||
break; | ||
case "gte": | ||
sl = (prop >= untilprop); | ||
break; | ||
case "cont": | ||
sl = (prop.indexOf(untilprop) != -1); | ||
break; | ||
case "notc": | ||
sl = (prop.indexOf(untilprop) == -1); | ||
break; | ||
} | ||
return sl; | ||
} | ||
// Bump any loop increment, decrement | ||
function bumpLoop() { | ||
if (config.loop == "none") return true; | ||
var prop = getTypeInputValue(config.proptype, config.prop); | ||
switch(config.loop) { | ||
case "inc0": | ||
case "inc": | ||
if (typeof prop == "number") { | ||
prop++; | ||
}else{ | ||
node.error("Cannot increment loop variable. " + config.prop + " is not a number."); | ||
return false; | ||
} | ||
break; | ||
case "dec": | ||
if (typeof prop == "number") { | ||
prop--; | ||
}else{ | ||
node.error("Cannot decrement loop variable. " + config.prop + " is not a number."); | ||
return false; | ||
} | ||
break; | ||
case "watch": | ||
break; | ||
var sl = false; | ||
if (config.loop == "none") return sl; | ||
var prop = getTypeInputValue(config.proptype, config.prop); | ||
// Initialized undefined variables | ||
if (typeof prop == "undefined") { | ||
// String logic operator inits to empty string | ||
if (config.until == "cont" || config.until == "notc") { | ||
prop = ""; | ||
}else{ | ||
// Math logic operator inits to zero | ||
prop = 0; | ||
} | ||
setContextPropertyValue(config.proptype, config.prop, prop); | ||
return true; | ||
} | ||
var untilprop = getTypeInputValue(config.untilproptype, config.untilprop); | ||
switch(config.until) { | ||
case "eq": | ||
sl = (prop == untilprop); | ||
break; | ||
case "neq": | ||
sl = (prop != untilprop); | ||
break; | ||
case "lt": | ||
sl = (prop < untilprop); | ||
break; | ||
case "lte": | ||
sl = (prop <= untilprop); | ||
break; | ||
case "gt": | ||
sl = (prop > untilprop); | ||
break; | ||
case "gte": | ||
sl = (prop >= untilprop); | ||
break; | ||
case "cont": | ||
sl = (prop.indexOf(untilprop) != -1); | ||
break; | ||
case "notc": | ||
sl = (prop.indexOf(untilprop) == -1); | ||
break; | ||
} | ||
return sl; | ||
} | ||
// Decode typeInput value by type/value | ||
function getTypeInputValue(t, v) { | ||
var r = ''; | ||
switch(t) { | ||
// Bump any loop increment, decrement | ||
function bumpLoop() { | ||
if (config.loop == "none") return true; | ||
var prop = getTypeInputValue(config.proptype, config.prop); | ||
switch(config.loop) { | ||
case "inc0": | ||
case "inc": | ||
if (typeof prop == "number") { | ||
prop++; | ||
}else{ | ||
node.error("Cannot increment loop variable. " + config.prop + " is not a number."); | ||
return false; | ||
} | ||
break; | ||
case "dec": | ||
if (typeof prop == "number") { | ||
prop--; | ||
}else{ | ||
node.error("Cannot decrement loop variable. " + config.prop + " is not a number."); | ||
return false; | ||
} | ||
break; | ||
case "watch": | ||
break; | ||
} | ||
setContextPropertyValue(config.proptype, config.prop, prop); | ||
return true; | ||
} | ||
// Decode typeInput value by type/value | ||
function getTypeInputValue(t, v) { | ||
var r = ''; | ||
switch(t) { | ||
case "msg": | ||
r = RED.util.getMessageProperty(msg, v); | ||
break; | ||
case "flow": | ||
r = flowContext.get(v); | ||
break; | ||
case "global": | ||
r = globalContext.get(v); | ||
break; | ||
case "str": | ||
try { | ||
r = unescape(JSON.parse('"'+v+'"'));; | ||
}catch(e){ | ||
r = v; | ||
} | ||
break; | ||
case "num": | ||
r = parseFloat(v); | ||
break; | ||
case 'bool': | ||
r = (v=='true'); | ||
break; | ||
default: | ||
r = v; | ||
} | ||
return r; | ||
} | ||
// Set the context property value | ||
function setContextPropertyValue(context, property, value) { | ||
// Assign value to given object and property | ||
switch(context) { | ||
case "msg": | ||
r = RED.util.getMessageProperty(msg, v); | ||
RED.util.setMessageProperty(msg, property, value); | ||
break; | ||
case "flow": | ||
r = flowContext.get(v); | ||
flowContext.set(property, value); | ||
break; | ||
case "global": | ||
r = globalContext.get(v); | ||
globalContext.set(property, value); | ||
break; | ||
case "str": | ||
try { | ||
r = unescape(JSON.parse('"'+v+'"'));; | ||
}catch(e){ | ||
r = v; | ||
} | ||
break; | ||
case "num": | ||
r = parseFloat(v); | ||
break; | ||
case 'bool': | ||
r = (v=='true'); | ||
break; | ||
default: | ||
r = v; | ||
} | ||
return r; | ||
} | ||
// Set the context property value | ||
function setContextPropertyValue(context, property, value) { | ||
// Assign value to given object and property | ||
switch(context) { | ||
case "msg": | ||
RED.util.setMessageProperty(msg, property, value); | ||
break; | ||
case "flow": | ||
flowContext.set(property, value); | ||
break; | ||
case "global": | ||
globalContext.set(property, value); | ||
break; | ||
} | ||
} | ||
} | ||
}); | ||
this.on("close",function() { | ||
RED.events.removeListener(event, handler); | ||
node.status({}); | ||
}); | ||
} | ||
RED.nodes.registerType("actionflows_in", actionflows_in); | ||
function actionflows_in(config) { | ||
var node = this; | ||
// Create our node and event handler | ||
RED.nodes.createNode(this, config); | ||
var node = this; | ||
var nodeID = config.id; | ||
if (typeof config._alias != 'undefined') { | ||
nodeID = config._alias; | ||
} | ||
var event = "af:" + nodeID; | ||
var event = "af:" + config.id; | ||
var handler = function(msg) { | ||
@@ -227,8 +554,19 @@ node.receive(msg); | ||
RED.events.on(event, handler); | ||
// Clean up event handler | ||
this.on("close",function() { | ||
RED.events.removeListener(event, handler); | ||
}); | ||
// Create our global context actionflows object for mapping details | ||
var af = node.context().global.get('actionflows'); | ||
if (typeof af == "undefined") { | ||
af = new Object(); | ||
af["afs"] = new Object(); | ||
af["ins"] = new Object(); | ||
} | ||
// Save details | ||
af["ins"][config.id] = config; | ||
node.context().global.set('actionflows', af); | ||
this.on("input", function(msg) { | ||
this.send(msg); | ||
}); | ||
this.on("close",function() { | ||
RED.events.removeListener(event, handler); | ||
}); | ||
} | ||
@@ -240,51 +578,7 @@ RED.nodes.registerType("actionflows_out", actionflows_out); | ||
this.on('input', function(msg) { | ||
RED.events.emit(msg._af["stack"].pop(), msg); // return to the action orig. flow | ||
if (typeof msg._af != "undefined") { | ||
RED.events.emit(msg._af["stack"].pop(), msg); // return to the action orig. flow | ||
} | ||
}); | ||
} | ||
/** | ||
* getActionFlows returns all actionflows nodes | ||
*/ | ||
function getActionFlows() { | ||
var RED2 = require.main.require('node-red'); | ||
var flows = RED2.nodes.getFlows().flows; | ||
var actionflows = []; | ||
var ins = []; | ||
flows.forEach(function(f) { | ||
if (f.type.substring(0, 11) == 'actionflows') { | ||
if (f.type == 'actionflows') { | ||
actionflows.push(f); | ||
} | ||
if (f.type == 'actionflows_in') { | ||
ins.push(f); | ||
} | ||
} | ||
}); | ||
// Sort actionflows_in by priority | ||
ins.sort(function(a, b) { | ||
return parseInt(a.priority)-parseInt(b.priority); | ||
}); | ||
var af = {}; | ||
// Associate actionflows with ins | ||
actionflows.forEach(function(a) { | ||
a.ins = []; | ||
ins.forEach(function(i) { | ||
// Enforce private settings | ||
if ((a.private == false && i.private == false) || | ||
(a.private == true && a.z == i.z) || | ||
(i.private == true && a.z == i.z)) { | ||
if (i.name.replace(new RegExp("_", 'g'), " ") // search for prefix while preventing | ||
.replace(new RegExp("-", 'g'), " ") // substr match (i.e. 'he' in 'head') | ||
.replace(new RegExp("\\.", 'g'), " ") // support domain format | ||
.startsWith(a.name + " ")) { | ||
a.ins.push(i); | ||
} | ||
} | ||
}); | ||
af[a.id] = a; | ||
}); | ||
return af; | ||
} | ||
} |
[ | ||
{ | ||
"id": "e47c0628.8d996", | ||
"id": "3b4ff8d0.970fd8", | ||
"type": "inject", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "", | ||
@@ -14,6 +14,6 @@ "topic": "", | ||
"x": 100, | ||
"y": 40, | ||
"y": 60, | ||
"wires": [ | ||
[ | ||
"d769f95b.ab5fd8" | ||
"602338ac.7ba688" | ||
] | ||
@@ -23,5 +23,5 @@ ] | ||
{ | ||
"id": "af6a998b.83bde", | ||
"id": "f46f813f.01c9b8", | ||
"type": "debug", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "", | ||
@@ -32,9 +32,9 @@ "active": true, | ||
"x": 550, | ||
"y": 40, | ||
"y": 60, | ||
"wires": [] | ||
}, | ||
{ | ||
"id": "d769f95b.ab5fd8", | ||
"id": "602338ac.7ba688", | ||
"type": "change", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "", | ||
@@ -56,6 +56,6 @@ "rules": [ | ||
"x": 260, | ||
"y": 40, | ||
"y": 60, | ||
"wires": [ | ||
[ | ||
"ce1ce404.7f661" | ||
"d323143f.32a8e" | ||
] | ||
@@ -65,5 +65,5 @@ ] | ||
{ | ||
"id": "ce1ce404.7f661", | ||
"id": "d323143f.32a8e", | ||
"type": "actionflows", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"info": "Describe your action API here.", | ||
@@ -77,9 +77,10 @@ "untilproptype": "num", | ||
"loop": "none", | ||
"perf": false, | ||
"private": false, | ||
"scope": "global", | ||
"perf": true, | ||
"seq": false, | ||
"x": 410, | ||
"y": 40, | ||
"y": 60, | ||
"wires": [ | ||
[ | ||
"af6a998b.83bde" | ||
"f46f813f.01c9b8" | ||
] | ||
@@ -89,14 +90,14 @@ ] | ||
{ | ||
"id": "60f6e66.31e2118", | ||
"id": "3927d9b6.d8d016", | ||
"type": "actionflows_in", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "action in", | ||
"priority": "50", | ||
"links": [], | ||
"private": true, | ||
"scope": "private", | ||
"x": 140, | ||
"y": 100, | ||
"y": 120, | ||
"wires": [ | ||
[ | ||
"62c830ed.2bce2" | ||
"a6e044b8.1a8d98" | ||
] | ||
@@ -106,15 +107,15 @@ ] | ||
{ | ||
"id": "6854d839.fe442", | ||
"id": "4979fa78.fdd5fc", | ||
"type": "actionflows_out", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "action out", | ||
"links": [], | ||
"x": 540, | ||
"y": 100, | ||
"y": 120, | ||
"wires": [] | ||
}, | ||
{ | ||
"id": "62c830ed.2bce2", | ||
"id": "a6e044b8.1a8d98", | ||
"type": "change", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "", | ||
@@ -138,6 +139,6 @@ "rules": [ | ||
"x": 340, | ||
"y": 100, | ||
"y": 120, | ||
"wires": [ | ||
[ | ||
"6854d839.fe442" | ||
"4979fa78.fdd5fc" | ||
] | ||
@@ -147,14 +148,14 @@ ] | ||
{ | ||
"id": "33576e42.80c352", | ||
"id": "9a40d66c.a6ab", | ||
"type": "actionflows_in", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "action 2", | ||
"priority": "50", | ||
"links": [], | ||
"private": true, | ||
"scope": "private", | ||
"x": 130, | ||
"y": 160, | ||
"y": 180, | ||
"wires": [ | ||
[ | ||
"2afccd26.eeb6c2" | ||
"96ca323b.659958" | ||
] | ||
@@ -164,15 +165,15 @@ ] | ||
{ | ||
"id": "3f3be948.8507ae", | ||
"id": "45b30884.46ac4", | ||
"type": "actionflows_out", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "action 2", | ||
"links": [], | ||
"x": 540, | ||
"y": 160, | ||
"y": 180, | ||
"wires": [] | ||
}, | ||
{ | ||
"id": "2afccd26.eeb6c2", | ||
"id": "96ca323b.659958", | ||
"type": "change", | ||
"z": "344509a4.4e05ee", | ||
"z": "3082c225.0a2e3e", | ||
"name": "change World for Mars", | ||
@@ -196,9 +197,9 @@ "rules": [ | ||
"x": 340, | ||
"y": 160, | ||
"y": 180, | ||
"wires": [ | ||
[ | ||
"3f3be948.8507ae" | ||
"45b30884.46ac4" | ||
] | ||
] | ||
} | ||
] | ||
] |
[ | ||
{ | ||
"id": "abe837c2.6055d8", | ||
"id": "596beda5.4a29fc", | ||
"type": "inject", | ||
"z": "d2e07100.9ad6c", | ||
"z": "3e28489d.522bc8", | ||
"name": "", | ||
@@ -13,7 +13,7 @@ "topic": "", | ||
"once": false, | ||
"x": 100, | ||
"y": 40, | ||
"x": 140, | ||
"y": 80, | ||
"wires": [ | ||
[ | ||
"cbb5939f.a84658" | ||
"c3c15c1a.1bd448" | ||
] | ||
@@ -23,5 +23,5 @@ ] | ||
{ | ||
"id": "cbb5939f.a84658", | ||
"id": "c3c15c1a.1bd448", | ||
"type": "actionflows", | ||
"z": "d2e07100.9ad6c", | ||
"z": "3e28489d.522bc8", | ||
"info": "Describe your action API here.", | ||
@@ -35,6 +35,7 @@ "untilproptype": "num", | ||
"loop": "inc0", | ||
"scope": "global", | ||
"perf": false, | ||
"private": false, | ||
"x": 290, | ||
"y": 40, | ||
"seq": false, | ||
"x": 330, | ||
"y": 80, | ||
"wires": [ | ||
@@ -45,14 +46,14 @@ [] | ||
{ | ||
"id": "4f225489.490ed4", | ||
"id": "3d1eb128.6416e6", | ||
"type": "actionflows_in", | ||
"z": "d2e07100.9ad6c", | ||
"z": "3e28489d.522bc8", | ||
"name": "action in", | ||
"priority": "50", | ||
"links": [], | ||
"private": true, | ||
"x": 120, | ||
"y": 100, | ||
"scope": "global", | ||
"x": 160, | ||
"y": 140, | ||
"wires": [ | ||
[ | ||
"e4e78e49.b2cb68" | ||
"343e14d9.49de14" | ||
] | ||
@@ -62,15 +63,15 @@ ] | ||
{ | ||
"id": "b3f8d51a.dafa68", | ||
"id": "a89ea39a.f9bf58", | ||
"type": "actionflows_out", | ||
"z": "d2e07100.9ad6c", | ||
"z": "3e28489d.522bc8", | ||
"name": "action out", | ||
"links": [], | ||
"x": 480, | ||
"y": 100, | ||
"x": 520, | ||
"y": 140, | ||
"wires": [] | ||
}, | ||
{ | ||
"id": "e4e78e49.b2cb68", | ||
"id": "343e14d9.49de14", | ||
"type": "change", | ||
"z": "d2e07100.9ad6c", | ||
"z": "3e28489d.522bc8", | ||
"name": "", | ||
@@ -100,8 +101,8 @@ "rules": [ | ||
"reg": false, | ||
"x": 300, | ||
"y": 100, | ||
"x": 340, | ||
"y": 140, | ||
"wires": [ | ||
[ | ||
"b3f8d51a.dafa68", | ||
"32d4d58f.af8252" | ||
"a89ea39a.f9bf58", | ||
"8d58cc53.74aaf" | ||
] | ||
@@ -111,5 +112,5 @@ ] | ||
{ | ||
"id": "32d4d58f.af8252", | ||
"id": "8d58cc53.74aaf", | ||
"type": "debug", | ||
"z": "d2e07100.9ad6c", | ||
"z": "3e28489d.522bc8", | ||
"name": "", | ||
@@ -119,6 +120,6 @@ "active": true, | ||
"complete": "false", | ||
"x": 490, | ||
"y": 40, | ||
"x": 530, | ||
"y": 80, | ||
"wires": [] | ||
} | ||
] | ||
] |
[ | ||
{ | ||
"id": "a1800f9.86d5cf", | ||
"id": "bb04598c.2a6e08", | ||
"type": "inject", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "", | ||
@@ -17,3 +17,3 @@ "topic": "", | ||
[ | ||
"1550b861.4569a" | ||
"aade01b6.d4183" | ||
] | ||
@@ -23,5 +23,5 @@ ] | ||
{ | ||
"id": "bbdca960.1973", | ||
"id": "c9b356c7.cbea48", | ||
"type": "debug", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "", | ||
@@ -36,5 +36,5 @@ "active": true, | ||
{ | ||
"id": "1550b861.4569a", | ||
"id": "aade01b6.d4183", | ||
"type": "actionflows", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"info": "Describe your action API here.", | ||
@@ -48,4 +48,5 @@ "untilproptype": "num", | ||
"loop": "none", | ||
"scope": "global", | ||
"perf": false, | ||
"private": false, | ||
"seq": false, | ||
"x": 270, | ||
@@ -55,3 +56,3 @@ "y": 40, | ||
[ | ||
"bbdca960.1973" | ||
"c9b356c7.cbea48" | ||
] | ||
@@ -61,9 +62,9 @@ ] | ||
{ | ||
"id": "cb1a48b5.6f60a8", | ||
"id": "27eca698.fc234a", | ||
"type": "actionflows_in", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "action in", | ||
"priority": "50", | ||
"links": [], | ||
"private": true, | ||
"scope": "private", | ||
"x": 100, | ||
@@ -73,3 +74,3 @@ "y": 100, | ||
[ | ||
"30f435e3.4c6992" | ||
"74763ac0.6a36cc" | ||
] | ||
@@ -79,5 +80,5 @@ ] | ||
{ | ||
"id": "ae3055ed.a45f18", | ||
"id": "a9d26880.b36a88", | ||
"type": "actionflows_out", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "action out", | ||
@@ -90,9 +91,9 @@ "links": [], | ||
{ | ||
"id": "c2c92da6.4bf0e8", | ||
"id": "b6306ae9.203198", | ||
"type": "actionflows_in", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "action in 2", | ||
"priority": "50", | ||
"links": [], | ||
"private": true, | ||
"scope": "private", | ||
"x": 100, | ||
@@ -102,3 +103,3 @@ "y": 160, | ||
[ | ||
"febb3c1e.1a8d28" | ||
"508f51d1.70ab3" | ||
] | ||
@@ -108,5 +109,5 @@ ] | ||
{ | ||
"id": "924bb61f.5ef16", | ||
"id": "3714777a.814718", | ||
"type": "actionflows_out", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "action out 2", | ||
@@ -119,5 +120,5 @@ "links": [], | ||
{ | ||
"id": "987064aa.020bc8", | ||
"id": "6fc98d69.9dfe64", | ||
"type": "actionflows", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"info": "Describe your action API here.", | ||
@@ -131,4 +132,5 @@ "untilproptype": "num", | ||
"loop": "none", | ||
"scope": "global", | ||
"perf": false, | ||
"private": false, | ||
"seq": false, | ||
"x": 410, | ||
@@ -138,3 +140,3 @@ "y": 160, | ||
[ | ||
"924bb61f.5ef16" | ||
"3714777a.814718" | ||
] | ||
@@ -144,5 +146,5 @@ ] | ||
{ | ||
"id": "30f435e3.4c6992", | ||
"id": "74763ac0.6a36cc", | ||
"type": "delay", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "", | ||
@@ -163,3 +165,3 @@ "pauseType": "delay", | ||
[ | ||
"ae3055ed.a45f18" | ||
"a9d26880.b36a88" | ||
] | ||
@@ -169,5 +171,5 @@ ] | ||
{ | ||
"id": "febb3c1e.1a8d28", | ||
"id": "508f51d1.70ab3", | ||
"type": "delay", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "", | ||
@@ -188,3 +190,3 @@ "pauseType": "delay", | ||
[ | ||
"987064aa.020bc8" | ||
"6fc98d69.9dfe64" | ||
] | ||
@@ -194,9 +196,9 @@ ] | ||
{ | ||
"id": "27d44431.27c0a4", | ||
"id": "6fcd6f13.af569", | ||
"type": "actionflows_in", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "nested in", | ||
"priority": "50", | ||
"links": [], | ||
"private": true, | ||
"scope": "private", | ||
"x": 240, | ||
@@ -206,3 +208,3 @@ "y": 220, | ||
[ | ||
"4fbd990a.1f8cf8" | ||
"4dc033d2.9966bc" | ||
] | ||
@@ -212,5 +214,5 @@ ] | ||
{ | ||
"id": "8f01de9a.6f11a8", | ||
"id": "1e062fcf.5e67d8", | ||
"type": "actionflows_out", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "nested out", | ||
@@ -223,5 +225,5 @@ "links": [], | ||
{ | ||
"id": "4fbd990a.1f8cf8", | ||
"id": "4dc033d2.9966bc", | ||
"type": "delay", | ||
"z": "7cb254cb.f5bbdc", | ||
"z": "5911470f.4efe88", | ||
"name": "", | ||
@@ -242,6 +244,6 @@ "pauseType": "delay", | ||
[ | ||
"8f01de9a.6f11a8" | ||
"1e062fcf.5e67d8" | ||
] | ||
] | ||
} | ||
] | ||
] |
{ | ||
"name": "node-red-contrib-actionflows", | ||
"version": "0.0.1", | ||
"version": "1.0.0", | ||
"description": "Create extendable, loopable, and reusable design patterns for flows.", | ||
@@ -5,0 +5,0 @@ "author": "Stephen J. Carnam <steveorevo@gmail.com>", |
263
README.md
# node-red-contrib-actionflows | ||
Provides three nodes that allow you to create extensible, reusable, | ||
looped, and prioritized flows. ActionFlows includes performance benchmarks with | ||
nanosecond precision. Advanced use enables the ability to group flows into | ||
"libraries" by using Node-RED's subflow capabilities. You can organize flows for | ||
ActionFlows brings easy to use loops and OOP (object oriented programming) | ||
features to Node-RED's flow programming paradigm. Three nodes allow you to | ||
create extensible, scoped, looped, and prioritized flows. Utilities include | ||
performance benchmarks with nanosecond precision. Advanced use enables the | ||
ability to group flows into "libraries" using Node-RED's native subflow | ||
capabilities and invocation via JavaScript. You can organize flows for | ||
readability and create extendable design patterns. To understand ActionFlows, | ||
@@ -23,6 +25,7 @@ review each section starting with Basics below and each section's examples. | ||
* "Late binding"; extend flows without modifying the original flow | ||
* Looping; call flow segments with conditional iteration | ||
* Prioritize flows; allow for OOP-like overrides | ||
* Private and shared flows | ||
* "Late binding"; extend complex flows without modifying the original flow | ||
* Looping; call flow segments repeatedly with conditional iteration | ||
* Create OOP-like "classes" (subflows) with public/private flows | ||
* Prioritize flows; allow for OOP-like overrides & inheritance | ||
* Flow scopes; private, protected, and global flows | ||
@@ -46,4 +49,4 @@ Simply include the `action` flow inline at specific points where you would like | ||
callable by the `action` node. For instance, an `action` node named "Sample", | ||
will activate any `action in` nodes with names like "Sample in", "Sample-in", | ||
"Sample_Exercise", or "Sample.Acme.com". | ||
will call any `action in` nodes with names like "Sample in", "Sample-in", | ||
"Sample_Exercise", or "sample.acme.com". | ||
@@ -59,3 +62,3 @@ ``` | ||
![ActionFlow Sequence](/actionflows/demo/basic3.png?raw=true "Sequential Flow Segments") | ||
![ActionFlows Sequence](/actionflows/demo/basic3.png?raw=true "Sequential Flow Segments") | ||
@@ -71,5 +74,5 @@ In the example above: | ||
can be created or imported dynamically (such as with the `flowman` node). Flows | ||
can be defined on other tabs or within subflows (see the "Libraries" section | ||
below) or restricted to the same tab or subflow where the calling `action` node | ||
has been defined. | ||
can be defined on other tabs or within subflows (see the "Libraries and Scope" | ||
section below) or restricted to the same tab or subflow where the calling | ||
`action` node has been defined. | ||
@@ -81,11 +84,11 @@ Flow sequence order can also be changed by the `action in` node's settings (see | ||
### Benchmarks | ||
### Benchmarks and Debugging | ||
Benchmarks in the `action` node allow you to see how long all `action in` flow | ||
sequences take to execute. Use the checkbox labelled "Debug action cycle | ||
execution time?" to see debug output indicating how long it took to run all of | ||
execution time" to see debug output indicating how long it took to run all of | ||
the corresponding `action in/out` flow segments before returning to the calling | ||
action. | ||
![ActionFlow Benchmarks](/actionflows/demo/bench2.png?raw=true "Debug Execution Time") | ||
![ActionFlows Benchmarks](/actionflows/demo/bench2.jpg?raw=true "Debug Execution Time") | ||
@@ -96,12 +99,15 @@ > Note: Benchmarks report how long it takes to run all matching `action in/out` | ||
Use the "Debug invocation sequence" checkbox to reveal the name of each | ||
`action in` that is called, it's sequence order, and node id in the debug tab. | ||
### Priorities | ||
Priorities allow you to define ActionFlows that take precedence over other | ||
ActionFlows. Inspired by [WordPress' core actions and filters API](https://codex.wordpress.org/Plugin_API#Hooks:_Actions_and_Filters), Priorities | ||
are at the heart of manageable extendability. In our Basic example sequence | ||
we see that two `action in/out` flow segments have been defined; each changing | ||
the "Hello World" in `msg.payload` to eventually become "Hello Mars, and Solar | ||
System!". However, if we simply change the `action in/out` flow sequences, we | ||
end up with "Hello Mars" in the `msg.payload`. | ||
ActionFlows. Inspired by [WordPress' core actions and filters API](https://codex.wordpress.org/Plugin_API#Hooks:_Actions_and_Filters), | ||
Priorities are at the heart of manageable extendability. In our Basic example | ||
sequence we see that two `action in/out` flow segments have been defined; each | ||
changing the "Hello World" in `msg.payload` to eventually become "Hello Mars, | ||
and Solar System!". However, if we simply change the `action in/out` flow | ||
sequences, we end up with "Hello Mars" in the `msg.payload`. | ||
![ActionFlow Priorities](/actionflows/demo/priority2.png?raw=true "Flow Priorities") | ||
![ActionFlows Priorities](/actionflows/demo/priority2.png?raw=true "Flow Priorities") | ||
@@ -132,3 +138,4 @@ Here we modify the node "action in" and "action 2" to execute in the reverse | ||
that in turn, invokes additional `action in/out` flow segments. One way to trace | ||
an ActionFlows' sequence is to use the `delay` node. Be sure to set the delay | ||
an ActionFlows' sequence is to use the "Debug invocation sequence" checkbox or, | ||
(as illustrated below) by using the `delay` node. Be sure to set the delay | ||
to above 2 seconds to see the blue dot appear in the `action in/out` flow path | ||
@@ -138,3 +145,3 @@ and for the green dot and "running" indicator under the active `action` node. | ||
![ActionFlow Nesting](/actionflows/demo/nested.gif?raw=true "ActionFlow Nesting") | ||
![ActionFlows Nesting](/actionflows/demo/nested.gif?raw=true "ActionFlows Nesting") | ||
@@ -155,3 +162,3 @@ In this simple animation, the main `action` node calls two defined flows; one | ||
![ActionFlow Looping](/actionflows/demo/loop2.jpg?raw=true "ActionFlow Looping") | ||
![ActionFlows Looping](/actionflows/demo/loop2.jpg?raw=true "ActionFlows Looping") | ||
@@ -170,3 +177,3 @@ > Note: The `action` node icon will change from a lightening bolt | ||
![ActionFlow Increment from zero](/actionflows/demo/loop.png?raw=true "ActionFlow Increment from zero") | ||
![ActionFlows Increment from zero](/actionflows/demo/loop.png?raw=true "ActionFlows Increment from zero") | ||
@@ -258,39 +265,177 @@ The `msg.loop` variable is accessible to our `change` node allowing us to inject | ||
## Libraries | ||
You can use multiple ActionFlows on tabs. However, you can also encapsulate | ||
functionality by placing the nodes inside a subflow. The subflow does not need | ||
to have any inputs or outputs. Placing an instance of the subflow on your tab | ||
will enable the ActionFlows. You can use the Private settings in the `action` | ||
node and `action in` nodes to expose functionality to other tabs and subflows or | ||
to limit their access and restrict functionality to the given subflow or tab. | ||
## Libraries and Scope | ||
Scope provides functionality for flows that are more commonly found in OOP | ||
(object oriented programming) environments. Using scopes with ActionFlows allows | ||
you to build reusable flow libraries that may act as base for other flows. | ||
Regardless of the scope setting, `action` nodes will invoke all matching | ||
`action in` flows that are on the same "z plane" (same tab or within the same | ||
subflow). However, there are many benefits to using the different scope modes | ||
and in different combinations. Here are the three main levels of scope which | ||
define ActionFlows' behaviors: | ||
### Private actions | ||
Use the private checkbox in the `action` node's settings to restrict calling any | ||
`action in/out` flow sequences to the same tab or within the given subflow. | ||
Uncheck the checkbox to allow invoking flow sequences defined on other tabs, or | ||
subflows. | ||
#### global | ||
The "global" scope is the default mode. The "global" setting allows you to use | ||
ActionFlows across multiple tabs or within different subflows. An `action` node | ||
will invoke any `action in` flow segment across the system, regardless of where | ||
they are defined (within subflows or other tabs). Flows will be invoked if the | ||
`action in` node's name begins with the name of the corresponding `action` node. | ||
Use the global scope to allow other developers to extend a flow on their own tab | ||
or without having to modify an existing flow no matter where it is located | ||
(i.e. deep within a subflows). Placing a group of global ActionFlows within a | ||
subflow is an easy way to distribute modular behaviors or add vendor specific | ||
functionality. | ||
### Private flows | ||
Use the private flow checkbox to limit this <code>action in</code> node's | ||
flow to `action` nodes calling from within the same tab or within the | ||
same subflow. Uncheck to allow actions to invoke the `action in` node | ||
from other tabs or subflows (where the `action` node's own private | ||
checkbox is unchecked. | ||
#### protected | ||
Using the "protected" scope setting for ActionFlows allows you to group | ||
functionality while avoiding conflicts with common names that could occur with | ||
global scope. Unlike global scope, protected scope restricts `action` and | ||
corresponding `action in` nodes to the same tab. Furthermore, protected scope | ||
places restrictions on accessing ActionFlows within a subflow; you may still | ||
access them but must first declare a prefix that is the subflow's name. This | ||
allows you to work with multiple subflows as **object instances** in a similar | ||
fashion that OOP developers use classes and objects with public or private | ||
methods. | ||
## Advanced | ||
The nodes used in ActionFlows keep their priority, private settings, and names | ||
associations within the global property "actionflows" (`global.actionflows`). | ||
The variable contains a list of `action` nodes and their associated `action in` | ||
nodes within a property called `ins`, arranged by priority order. | ||
During runtime of an `action in/out` segment, a `msg._af` property variable is | ||
present that determines the calling `action` node to return to after running | ||
the sequential segments. In addition, any nested flows will be held within the | ||
`msg._asf['stack']` property. The `msg._af` property is destroyed after the | ||
flows exit the parent `action` node. | ||
![ActionFlows Scope: Protected](/actionflows/demo/protected.jpg?raw=true "ActionFlows Scope: Protected") | ||
By manipulating the ActionFlow global and `msg._af` properties, you can changed | ||
the runtime behavior of ActionFlows (i.e. such as override, replace, or disable | ||
`action in/out` flow segments). | ||
ActionFlows can address other ActionFlows within subflows using an explicit | ||
prefix to identify the subflow location of other ActionFlows nodes. The prefix | ||
is the name of the subflow where the corresponding `action` or `action in` node | ||
exists. In the screenshot above we have two examples: | ||
**An example of an `action` node calling a flow segment defined outside the subflow.** | ||
1a) The subflow is defined on the tab with the name "acme", it is invoked with | ||
an injector supplying the string "Hello". | ||
1b) The injector activates the subflow's `action` node named "action". | ||
1c) The flow segment outside the subflow is found by the name `acme.action` | ||
because the `action in` node's name starts with the subflow name and the | ||
`action` node's name within it "action". | ||
The flow segment contains a change node that alters the "Hello" and changes it | ||
to "Hi". | ||
**An example of a flow segment defined inside a subflow and accessed from outside.** | ||
2a) The `action` node named "acme.sample in" finds the defined flow segment | ||
inside the subflow named "acme". | ||
2b) Within the "acme" subflow is the `action in` node named "sample". | ||
The flow segment has a change node that changes the injector's "Hello" | ||
string to "Good bye". | ||
[Download the Protected Scope example flow here.](/actionflows/demo/protected.json) | ||
The following namespace-like rules apply to using ActionFlows with "protected" | ||
scope inside of subflows: | ||
* Both `action` and `action in` names must match within a subflow. I.e. an | ||
`action` named "sample" will invoke any `action in` beginning with the name | ||
"sample". The subflow name as a prefix is not necessary from inside the subflow. | ||
* ActionFlows defined outside of the subflow must declare the subflow name as | ||
apart of the prefix. For example, an `action` node named "apple" within | ||
a subflow named "fruits" could invoke an `action in` node at the tab level if | ||
the `action in` node's name begins with the subflow name, i.e. "fruits.apple". | ||
Likewise, sub-subflows (subflows that exist within subflows) would require | ||
additional prefixes to address the innermost node. | ||
* Protected scope nodes can only invoke one another within the same tab. | ||
#### private | ||
Private flows are useful if your actions have a commonly used name and/or you | ||
wish to restrict extendability to within a subflow or tab. Unlike protected | ||
scope, private scope inhibits the ability to invoke or respond to ActionFlows | ||
that are defined outside of the given subflow or tab where the ActionFlows | ||
exists. Using private scope helps avoid naming conflicts but prevents | ||
extensibility. | ||
### Mixed Scope Modes | ||
Note that both `action` and `action in` nodes have a scope setting. For example | ||
within a subflow, a global scope `action` next to private scope `action in` will | ||
have a unique ability; this pattern ensures that the internal private | ||
`action in` is always invoked once within the subflow and only for that subflow | ||
instance. Any other `action in` nodes of the same name elsewhere could also be | ||
called but the internal `action in` can never be invoked from other instances | ||
of the subflow. | ||
### Scope Icons | ||
Scope settings are reflected in the ActionFlows node icons. The icons for | ||
`action in` nodes, `action` nodes (in single or loop mode) will depict a small | ||
"hint" icon in the upper right hand corner to indicate the scope setting. | ||
![Scope Hint Icons](/actionflows/demo/scope-icons.jpg?raw=true "Scope Hint Icons") | ||
## ActionFlows and JavaScript | ||
ActionFlows creates a global object called "actionflows" that you can obtain a | ||
reference to in JavaScript. The object contains a number of data structures and | ||
methods that determine the runtime behavior of ActionFlows. For instance, the | ||
ActionFlows' `action in` nodes can be pragmatically invoked using Node-RED's | ||
native JavaScript function node. To invoke a given `action in` node, you will | ||
need to obtain a reference to the "actionflows" global object. Line 1 in the | ||
screenshot below shows how to get a reference to "actionflows" in the | ||
variable `af`. From there you may use the `af` object's `invoke` method to call | ||
an existing `action in` node (line 2). The `invoke` method expects two | ||
parameters; the first should be a string representing the name that any matching | ||
`action in` node should begin with. The second should be the `msg` object to be | ||
passed into the matching `action in` node. | ||
![JavaScript invoke](/actionflows/demo/invoke.jpg?raw=true "JavaScript Invoke") | ||
In the given screenshot, the JavaScript function node invokes the `action in` | ||
node with the name "action". The flow is executed and appends the string | ||
" World" to the injector node's "Hello" resulting in "Hello World" in the debug | ||
window. | ||
[Download the JavaScript Invoke example flow here.](/actionflows/demo/invoke.json) | ||
The `actionflows` global object contains the following methods and properties of | ||
interest: | ||
### Methods | ||
**invoke** - Invokes any matching `action in` nodes with the name found in the | ||
first parameter. The second parameter should be the `msg` object to be passed | ||
into the flow segment. A promise object is returned with a single incoming | ||
parameter containing the returned `msg` object. Note: the invoke method ignores | ||
scope settings and can be used to invoke any `action in` node by name. | ||
**map** - The map method processes all the found `action` and `action in` nodes | ||
and builds an associative map. This method is called once internally at | ||
deployment and determines the order in which each `action in` node is called | ||
for it's corresponding `action` node. The results are updated in the `actions` | ||
property (see below). | ||
### Properties | ||
**actions** - An object containing the calculated associative map from the `map` | ||
method (defined above) that is used internally by ActionFlows. The map is a list | ||
of enabled `action` node instances with a special `ins` property containing | ||
corresponding, `action in` node instances based on their priority and scope | ||
settings. The map allows ActionFlows to quickly execute sequential flows at | ||
runtime. Editing this list will alter the ActionFlows behavior (use with | ||
caution). The object can be reset by recalling the `map` method or re-deploying | ||
to restore the original design-time flow settings. | ||
**afs** - An object containing all the `action` nodes in the system. This property | ||
is used internally by the `map` method to determine the runtime behavior of | ||
ActionFlows. Altering this list prior to calling the `map` method will | ||
permanently change the runtime behavior of ActionFlows. Alteration is not | ||
recommended as this will disable the ability to reset the behavior until | ||
re-deployment. | ||
**ins** - An object containing all the `action in` nodes in the system. This | ||
property is used internally by the `map` method to determine the runtime | ||
behavior of ActionFlows. Altering this list prior to calling the `map` method | ||
will permanently change the runtime behavior of ActionFlows. Alteration is not | ||
recommended as this will disable the ability to reset the behavior until | ||
re-deployment. | ||
### Reserved Action Names | ||
Currently, ActionFlows has only one reserved `action in` node name: | ||
``` | ||
#deployed | ||
``` | ||
Any `action in` nodes that start with `#deployed` in their name will be invoked | ||
at deployment. This would be the equivalent of paring an inject node with the | ||
option for "Inject once at start" set to invoke a flow segment defined by | ||
ActionFlows. | ||
## Installation | ||
@@ -297,0 +442,0 @@ Run the following command in your Node-RED user directory (typically ~/.node-red): |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1203952
27
1457
1
437