Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

node-red-contrib-actionflows

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

node-red-contrib-actionflows - npm Package Compare versions

Comparing version 0.0.1 to 1.0.0

actionflows/demo/bench2.jpg

746

actionflows/actionflows.js
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>",

# 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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc