From 038d821a7c988d5fe461f5d1e1c5932b2616ca48 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 2 Jul 2018 15:21:13 +0100 Subject: [PATCH 01/34] Apply fGC to all global contexts for default values --- red/runtime/nodes/context/index.js | 144 ++++++++++----- red/runtime/nodes/context/memory.js | 6 +- test/red/runtime/nodes/context/index_spec.js | 172 ++++++++++++++---- test/red/runtime/nodes/context/memory_spec.js | 28 +-- 4 files changed, 239 insertions(+), 111 deletions(-) diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index 9db6e107a..bd336d371 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -19,21 +19,23 @@ var log = require("../../log"); var memory = require("./memory"); var settings; + +// A map of scope id to context instance var contexts = {}; -var globalContext = null; -var externalContexts = {}; -var hasExternalContext = false; + +// A map of store name to instance +var stores = {}; + +// Whether there context storage has been configured or left as default +var hasConfiguredStore = false; + +var defaultStore = "_"; function init(_settings) { settings = _settings; - externalContexts = {}; - - // init memory plugin + stores = {}; var seed = settings.functionGlobalContext || {}; - externalContexts["_"] = memory(); - externalContexts["_"].setGlobalContext(seed); - globalContext = createContext("global",seed); - contexts['global'] = globalContext; + contexts['global'] = createContext("global",seed); } function load() { @@ -43,6 +45,8 @@ function load() { var defaultIsAlias = false; var promises = []; if (plugins) { + var hasDefault = plugins.hasOwnProperty('default'); + var defaultName; for (var pluginName in plugins) { if (plugins.hasOwnProperty(pluginName)) { // "_" is a reserved name - do not allow it to be overridden @@ -59,6 +63,9 @@ function load() { defaultIsAlias = true; continue; } + if (!hasDefault && !defaultName) { + defaultName = pluginName; + } var plugin; if (plugins[pluginName].hasOwnProperty("module")) { // Get the provided config and copy in the 'approved' top-level settings (eg userDir) @@ -79,7 +86,7 @@ function load() { } try { // Create a new instance of the plugin by calling its module function - externalContexts[pluginName] = plugin(config); + stores[pluginName] = plugin(config); } catch(err) { return reject(new Error(log._("context.error-loading-module",{module:pluginName,message:err.toString()}))); } @@ -91,23 +98,39 @@ function load() { } // Open all of the configured contexts - for (var plugin in externalContexts) { - if (externalContexts.hasOwnProperty(plugin)) { - promises.push(externalContexts[plugin].open()); + for (var plugin in stores) { + if (stores.hasOwnProperty(plugin)) { + promises.push(stores[plugin].open()); } } - // If 'default' is an alias, point it at the right module - we have already - // checked that it exists - if (defaultIsAlias) { - externalContexts["default"] = externalContexts[plugins["default"]]; - } + // There is a 'default' listed in the configuration + if (hasDefault) { + // If 'default' is an alias, point it at the right module - we have already + // checked that it exists. If it isn't an alias, then it will + // already be set to a configured store + if (defaultIsAlias) { + stores["default"] = stores[plugins["default"]]; + } + stores["_"] = stores["default"]; + } else if (defaultName) { + // No 'default' listed, so pick first in list as the default + stores["default"] = stores[defaultName]; + stores["_"] = stores["default"]; + } // else there were no stores list the config object - fall through + // to below where we default to a memory store } if (promises.length === 0) { - promises.push(externalContexts["_"].open()) + // No stores have been configured. Setup the default as an instance + // of memory storage + stores["_"] = memory(); + stores["default"] = stores["_"]; + promises.push(stores["_"].open()) } else { - hasExternalContext = true; + // if there's configured storage then the lifecycle is slightly different + // - specifically, we don't delete node context on redeploy + hasConfiguredStore = true; } return resolve(Promise.all(promises)); }); @@ -122,12 +145,12 @@ function copySettings(config, settings){ } function getContextStorage(storage) { - if (externalContexts.hasOwnProperty(storage)) { + if (stores.hasOwnProperty(storage)) { // A known context - return externalContexts[storage]; - } else if (externalContexts.hasOwnProperty("default")) { + return stores[storage]; + } else if (stores.hasOwnProperty("default")) { // Not known, but we have a default to fall back to - return externalContexts["default"]; + return stores["default"]; } else { // Not known and no default configured var contextError = new Error(log._("context.error-use-undefined-storage", {storage:storage})); @@ -136,14 +159,19 @@ function getContextStorage(storage) { } } + function createContext(id,seed) { + // Seed is only set for global context - sourced from functionGlobalContext var scope = id; var obj = seed || {}; - + var seedKeys; + if (seed) { + seedKeys = Object.keys(seed); + } obj.get = function(key, storage, callback) { var context; if (!storage && !callback) { - context = externalContexts["_"]; + context = stores["_"]; } else { if (typeof storage === 'function') { callback = storage; @@ -154,12 +182,34 @@ function createContext(id,seed) { } context = getContextStorage(storage); } - return context.get(scope, key, callback); + if (seed) { + // Get the value from the underlying store. If it is undefined, + // check the seed for a default value. + if (callback) { + context.get(scope,key,function(err, v) { + if (v === undefined) { + callback(err, seed[key]); + } else { + callback(err, v); + } + }) + } else { + // No callback, attempt to do this synchronously + var storeValue = context.get(scope,key); + if (storeValue === undefined) { + return seed[key]; + } else { + return storeValue; + } + } + } else { + return context.get(scope, key, callback); + } }; obj.set = function(key, value, storage, callback) { var context; if (!storage && !callback) { - context = externalContexts["_"]; + context = stores["_"]; } else { if (typeof storage === 'function') { callback = storage; @@ -175,7 +225,7 @@ function createContext(id,seed) { obj.keys = function(storage, callback) { var context; if (!storage && !callback) { - context = externalContexts["_"]; + context = stores["_"]; } else { if (typeof storage === 'function') { callback = storage; @@ -186,7 +236,18 @@ function createContext(id,seed) { } context = getContextStorage(storage); } - return context.keys(scope, callback); + if (seed) { + if (callback) { + context.keys(scope, function(err,keys) { + callback(err,Array.from(new Set(seedKeys.concat(keys)).keys())); + }); + } else { + var keys = context.keys(scope); + return Array.from(new Set(seedKeys.concat(keys)).keys()) + } + } else { + return context.keys(scope, callback); + } }; return obj; } @@ -203,21 +264,20 @@ function getContext(localId,flowId) { if (flowId) { newContext.flow = getContext(flowId); } - if (globalContext) { - newContext.global = globalContext; - } + newContext.global = contexts['global']; contexts[contextId] = newContext; return newContext; } function deleteContext(id,flowId) { - if(!hasExternalContext){ + if(!hasConfiguredStore){ + // only delete context if there's no configured storage. var contextId = id; if (flowId) { contextId = id+":"+flowId; } delete contexts[contextId]; - return externalContexts["_"].delete(contextId); + return stores["_"].delete(contextId); }else{ return Promise.resolve(); } @@ -225,9 +285,9 @@ function deleteContext(id,flowId) { function clean(flowConfig) { var promises = []; - for(var plugin in externalContexts){ - if(externalContexts.hasOwnProperty(plugin)){ - promises.push(externalContexts[plugin].clean(Object.keys(flowConfig.allNodes))); + for(var plugin in stores){ + if(stores.hasOwnProperty(plugin)){ + promises.push(stores[plugin].clean(Object.keys(flowConfig.allNodes))); } } for (var id in contexts) { @@ -243,9 +303,9 @@ function clean(flowConfig) { function close() { var promises = []; - for(var plugin in externalContexts){ - if(externalContexts.hasOwnProperty(plugin)){ - promises.push(externalContexts[plugin].close()); + for(var plugin in stores){ + if(stores.hasOwnProperty(plugin)){ + promises.push(stores[plugin].close()); } } return Promise.all(promises); diff --git a/red/runtime/nodes/context/memory.js b/red/runtime/nodes/context/memory.js index b6f687f96..93c213403 100644 --- a/red/runtime/nodes/context/memory.js +++ b/red/runtime/nodes/context/memory.js @@ -109,10 +109,6 @@ Memory.prototype.clean = function(activeNodes){ return Promise.resolve(); } -Memory.prototype.setGlobalContext= function(seed){ - this.data["global"] = seed; -}; - module.exports = function(config){ return new Memory(config); -}; \ No newline at end of file +}; diff --git a/test/red/runtime/nodes/context/index_spec.js b/test/red/runtime/nodes/context/index_spec.js index fa1332f20..eda70395a 100644 --- a/test/red/runtime/nodes/context/index_spec.js +++ b/test/red/runtime/nodes/context/index_spec.js @@ -124,7 +124,7 @@ describe('context', function() { }); }); - it('enumerates context keys', function() { + it('enumerates context keys - sync', function() { var context = Context.get("1","flowA"); var keys = context.keys(); @@ -142,15 +142,63 @@ describe('context', function() { keys[1].should.equal("abc"); }); - it('should enumerate only context keys when GlobalContext was given', function() { + it('enumerates context keys - async', function(done) { + var context = Context.get("1","flowA"); + + var keys = context.keys(function(err,keys) { + keys.should.be.an.Array(); + keys.should.be.empty(); + context.set("foo","bar"); + keys = context.keys(function(err,keys) { + keys.should.have.length(1); + keys[0].should.equal("foo"); + + context.set("abc.def","bar"); + keys = context.keys(function(err,keys) { + keys.should.have.length(2); + keys[1].should.equal("abc"); + done(); + }); + }); + }); + }); + + it('should enumerate only context keys when GlobalContext was given - sync', function() { Context.init({functionGlobalContext: {foo:"bar"}}); return Context.load().then(function(){ var context = Context.get("1","flowA"); + context.global.set("foo2","bar2"); var keys = context.global.keys(); - keys.should.have.length(1); + keys.should.have.length(2); keys[0].should.equal("foo"); + keys[1].should.equal("foo2"); }); }); + + it('should enumerate only context keys when GlobalContext was given - async', function(done) { + Context.init({functionGlobalContext: {foo:"bar"}}); + return Context.load().then(function(){ + var context = Context.get("1","flowA"); + context.global.set("foo2","bar2"); + context.global.keys(function(err,keys) { + keys.should.have.length(2); + keys[0].should.equal("foo"); + keys[1].should.equal("foo2"); + done(); + }); + }).catch(done); + }); + + + + it('returns functionGlobalContext value if store value undefined', function() { + Context.init({functionGlobalContext: {foo:"bar"}}); + return Context.load().then(function(){ + var context = Context.get("1","flowA"); + var v = context.global.get('foo'); + v.should.equal('bar'); + }); + }) }); describe('external context storage',function() { @@ -215,6 +263,11 @@ describe('context', function() { config:{} } }; + var memoryStorage ={ + memory:{ + module: "memory" + } + }; afterEach(function() { sandbox.reset(); @@ -257,7 +310,7 @@ describe('context', function() { stubSet.calledWithExactly("1:flow","file","file2",cb).should.be.true(); stubSet.calledWithExactly("1:flow","num","num3",cb).should.be.true(); done(); - }); + }).catch(done); }); it('should ignore reserved storage name `_`', function(done) { Context.init({contextStorage:{_:{module:testPlugin}}}); @@ -271,7 +324,7 @@ describe('context', function() { stubGet.called.should.be.false(); stubKeys.called.should.be.false(); done(); - }); + }).catch(done); }); it('should fail when using invalid default context', function(done) { Context.init({contextStorage:{default:"noexist"}}); @@ -300,14 +353,15 @@ describe('context', function() { }); describe('close modules',function(){ - it('should call close()', function() { + it('should call close()', function(done) { Context.init({contextStorage:contextDefaultStorage}); return Context.load().then(function(){ return Context.close().then(function(){ stubClose.called.should.be.true(); stubClose2.called.should.be.true(); + done(); }); - }); + }).catch(done); }); }); @@ -324,7 +378,7 @@ describe('context', function() { stubGet.calledWithExactly("1:flow","foo",cb).should.be.true(); stubKeys.calledWithExactly("1:flow",cb).should.be.true(); done(); - }); + }).catch(done); }); it('should store flow property to external context storage',function(done) { Context.init({contextStorage:contextStorage}); @@ -338,7 +392,7 @@ describe('context', function() { stubGet.calledWithExactly("flow","foo",cb).should.be.true(); stubKeys.calledWithExactly("flow",cb).should.be.true(); done(); - }); + }).catch(done); }); it('should store global property to external context storage',function(done) { Context.init({contextStorage:contextStorage}); @@ -349,10 +403,10 @@ describe('context', function() { context.global.get("foo","test",cb); context.global.keys("test",cb); stubSet.calledWithExactly("global","foo","bar",cb).should.be.true(); - stubGet.calledWithExactly("global","foo",cb).should.be.true(); - stubKeys.calledWithExactly("global",cb).should.be.true(); + stubGet.calledWith("global","foo").should.be.true(); + stubKeys.calledWith("global").should.be.true(); done(); - }); + }).catch(done); }); it('should store data to the default context when non-existent context storage was specified', function(done) { Context.init({contextStorage:contextDefaultStorage}); @@ -369,7 +423,7 @@ describe('context', function() { stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true(); stubKeys2.calledWithExactly("1:flow",cb).should.be.true(); done(); - }); + }).catch(done); }); it('should use the default context', function(done) { Context.init({contextStorage:contextDefaultStorage}); @@ -386,7 +440,7 @@ describe('context', function() { stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true(); stubKeys2.calledWithExactly("1:flow",cb).should.be.true(); done(); - }); + }).catch(done); }); it('should use the alias of default context', function(done) { Context.init({contextStorage:contextDefaultStorage}); @@ -403,7 +457,7 @@ describe('context', function() { stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true(); stubKeys2.calledWithExactly("1:flow",cb).should.be.true(); done(); - }); + }).catch(done); }); it('should use default as the alias of other context', function(done) { Context.init({contextStorage:contextAlias}); @@ -417,22 +471,16 @@ describe('context', function() { stubGet.calledWithExactly("1:flow","foo",cb).should.be.true(); stubKeys.calledWithExactly("1:flow",cb).should.be.true(); done(); - }); + }).catch(done); }); - it('should throw an error using undefined storage for local context', function(done) { + it('should not throw an error using undefined storage for local context', function(done) { Context.init({contextStorage:contextStorage}); Context.load().then(function(){ var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.get("local","nonexist",cb); - should.fail(null, null, "An error was not thrown using undefined storage for local context"); - }).catch(function(err) { - if (err.name === "ContextError") { - done(); - } else { - done(err); - } - }); + done() + }).catch(done); }); it('should throw an error using undefined storage for flow context', function(done) { Context.init({contextStorage:contextStorage}); @@ -440,39 +488,89 @@ describe('context', function() { var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.flow.get("flow","nonexist",cb); - should.fail(null, null, "An error was not thrown using undefined storage for flow context"); - }).catch(function(err) { - if (err.name === "ContextError") { - done(); - } else { - done(err); - } - }); + done(); + }).catch(done); }); + + it('should return functionGlobalContext value as a default - synchronous', function(done) { + var fGC = { "foo": 456 }; + Context.init({contextStorage:memoryStorage, functionGlobalContext:fGC }); + Context.load().then(function() { + var context = Context.get("1","flow"); + // Get foo - should be value from fGC + var v = context.global.get("foo"); + v.should.equal(456); + + // Update foo - should not touch fGC object + context.global.set("foo","new value"); + fGC.foo.should.equal(456); + + // Get foo - should be the updated value + v = context.global.get("foo"); + v.should.equal("new value"); + done(); + }).catch(done); + }) + + it('should return functionGlobalContext value as a default - async', function(done) { + var fGC = { "foo": 456 }; + Context.init({contextStorage:memoryStorage, functionGlobalContext:fGC }); + Context.load().then(function() { + var context = Context.get("1","flow"); + // Get foo - should be value from fGC + context.global.get("foo", function(err, v) { + if (err) { + done(err) + } else { + v.should.equal(456); + // Update foo - should not touch fGC object + context.global.set("foo","new value", function(err) { + if (err) { + done(err) + } else { + fGC.foo.should.equal(456); + // Get foo - should be the updated value + context.global.get("foo", function(err, v) { + if (err) { + done(err) + } else { + v.should.equal("new value"); + done(); + } + }); + } + }); + } + }); + }).catch(done); + }) + }); describe('delete context',function(){ - it('should not call delete() when external context storage is used', function() { + it('should not call delete() when external context storage is used', function(done) { Context.init({contextStorage:contextDefaultStorage}); return Context.load().then(function(){ Context.get("flowA"); return Context.delete("flowA").then(function(){ stubDelete.called.should.be.false(); stubDelete2.called.should.be.false(); + done(); }); - }); + }).catch(done); }); }); describe('clean context',function(){ - it('should call clean()', function() { + it('should call clean()', function(done) { Context.init({contextStorage:contextDefaultStorage}); return Context.load().then(function(){ return Context.clean({allNodes:{}}).then(function(){ stubClean.calledWithExactly([]).should.be.true(); stubClean2.calledWithExactly([]).should.be.true(); + done(); }); - }); + }).catch(done); }); }); }); diff --git a/test/red/runtime/nodes/context/memory_spec.js b/test/red/runtime/nodes/context/memory_spec.js index 090c46b41..56a36a9f2 100644 --- a/test/red/runtime/nodes/context/memory_spec.js +++ b/test/red/runtime/nodes/context/memory_spec.js @@ -103,19 +103,6 @@ describe('memory',function() { keysY[0].should.equal("hoge"); }); - it('should enumerate only context keys when GlobalContext was given', function() { - var keys = context.keys("global"); - keys.should.be.an.Array(); - keys.should.be.empty(); - - var data = { - foo: "bar" - } - context.setGlobalContext(data); - keys = context.keys("global"); - keys.should.have.length(1); - keys[0].should.equal("foo"); - }); }); describe('#delete',function() { @@ -163,17 +150,4 @@ describe('memory',function() { }); }); - describe('#setGlobalContext',function() { - it('should initialize global context with argument', function() { - var keys = context.keys("global"); - keys.should.be.an.Array(); - keys.should.be.empty(); - - var data = { - foo: "bar" - } - context.setGlobalContext(data); - context.get("global","foo").should.equal("bar"); - }); - }); -}); \ No newline at end of file +}); From 08b0838f9ac186ac5531b5715ee959b5e295e3cc Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 2 Jul 2018 15:32:29 +0100 Subject: [PATCH 02/34] Fix linting in view.js --- editor/js/ui/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js index 129817285..4559c5ec4 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -491,7 +491,7 @@ RED.view = (function() { var midX = Math.floor(destX-dx/2); var midY = Math.floor(destY-dy/2); // - if (dy == 0) { + if (dy === 0) { midY = destY + node_height; } var cp_height = node_height/2; From 742358350886a3ba0978ceaa8f5764b8fef921c0 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 2 Jul 2018 15:47:47 +0100 Subject: [PATCH 03/34] Create default store for node tests to use --- red/runtime/nodes/context/index.js | 1 + test/red/runtime/nodes/context/index_spec.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index bd336d371..b5ae720eb 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -36,6 +36,7 @@ function init(_settings) { stores = {}; var seed = settings.functionGlobalContext || {}; contexts['global'] = createContext("global",seed); + stores["_"] = new memory(); } function load() { diff --git a/test/red/runtime/nodes/context/index_spec.js b/test/red/runtime/nodes/context/index_spec.js index eda70395a..04a30a8f2 100644 --- a/test/red/runtime/nodes/context/index_spec.js +++ b/test/red/runtime/nodes/context/index_spec.js @@ -23,7 +23,7 @@ describe('context', function() { describe('local memory',function() { beforeEach(function() { Context.init({}); - return Context.load(); + Context.load(); }); afterEach(function() { Context.clean({allNodes:{}}); @@ -165,7 +165,7 @@ describe('context', function() { it('should enumerate only context keys when GlobalContext was given - sync', function() { Context.init({functionGlobalContext: {foo:"bar"}}); - return Context.load().then(function(){ + Context.load().then(function(){ var context = Context.get("1","flowA"); context.global.set("foo2","bar2"); var keys = context.global.keys(); @@ -177,7 +177,7 @@ describe('context', function() { it('should enumerate only context keys when GlobalContext was given - async', function(done) { Context.init({functionGlobalContext: {foo:"bar"}}); - return Context.load().then(function(){ + Context.load().then(function(){ var context = Context.get("1","flowA"); context.global.set("foo2","bar2"); context.global.keys(function(err,keys) { @@ -193,7 +193,7 @@ describe('context', function() { it('returns functionGlobalContext value if store value undefined', function() { Context.init({functionGlobalContext: {foo:"bar"}}); - return Context.load().then(function(){ + Context.load().then(function(){ var context = Context.get("1","flowA"); var v = context.global.get('foo'); v.should.equal('bar'); @@ -279,18 +279,18 @@ describe('context', function() { describe('load modules',function(){ it('should call open()', function() { Context.init({contextStorage:contextDefaultStorage}); - return Context.load().then(function(){ + Context.load().then(function(){ stubOpen.called.should.be.true(); stubOpen2.called.should.be.true(); }); }); it('should load memory module', function() { Context.init({contextStorage:{memory:{module:"memory"}}}); - return Context.load(); + Context.load(); }); it('should load localfilesystem module', function() { Context.init({contextStorage:{file:{module:"localfilesystem",config:{dir:resourcesDir}}}}); - return Context.load(); + Context.load(); }); it('should accept special storage name', function(done) { Context.init({ @@ -355,7 +355,7 @@ describe('context', function() { describe('close modules',function(){ it('should call close()', function(done) { Context.init({contextStorage:contextDefaultStorage}); - return Context.load().then(function(){ + Context.load().then(function(){ return Context.close().then(function(){ stubClose.called.should.be.true(); stubClose2.called.should.be.true(); @@ -550,7 +550,7 @@ describe('context', function() { describe('delete context',function(){ it('should not call delete() when external context storage is used', function(done) { Context.init({contextStorage:contextDefaultStorage}); - return Context.load().then(function(){ + Context.load().then(function(){ Context.get("flowA"); return Context.delete("flowA").then(function(){ stubDelete.called.should.be.false(); @@ -564,7 +564,7 @@ describe('context', function() { describe('clean context',function(){ it('should call clean()', function(done) { Context.init({contextStorage:contextDefaultStorage}); - return Context.load().then(function(){ + Context.load().then(function(){ return Context.clean({allNodes:{}}).then(function(){ stubClean.calledWithExactly([]).should.be.true(); stubClean2.calledWithExactly([]).should.be.true(); From 43d7c8d48cdb352835755ff2693a1cc3014f4f04 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 2 Jul 2018 22:32:20 +0100 Subject: [PATCH 04/34] Add caching to localfilesystem context --- red/api/admin/context.js | 32 +++-- red/runtime/nodes/context/localfilesystem.js | 134 +++++++++++++++--- .../nodes/context/localfilesystem_spec.js | 6 +- 3 files changed, 140 insertions(+), 32 deletions(-) diff --git a/red/api/admin/context.js b/red/api/admin/context.js index 37c6462cb..66aedcdcb 100644 --- a/red/api/admin/context.js +++ b/red/api/admin/context.js @@ -29,7 +29,6 @@ module.exports = { var scope = req.params.scope; var id = req.params.id; var key = req.params[0]; - var result = {}; var ctx; if (scope === 'global') { ctx = redNodes.getContext('global'); @@ -43,19 +42,28 @@ module.exports = { } if (ctx) { if (key) { - result = util.encodeObject({msg:ctx.get(key)}); + ctx.get(key,function(err, v) { + console.log(key,v); + res.json(util.encodeObject({msg:v})); + }); + return; } else { - var keys = ctx.keys(); - - var i = 0; - var l = keys.length; - while(i < l) { - var k = keys[i]; - result[k] = util.encodeObject({msg:ctx.get(k)}); - i++; - } + ctx.keys(function(err, keys) { + var result = {}; + var c = keys.length; + keys.forEach(function(key) { + ctx.get(key,function(err, v) { + result[key] = util.encodeObject({msg:v}); + c--; + if (c === 0) { + res.json(result); + } + }); + }); + }); } + } else { + res.json({}); } - res.json(result); } } diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index fed8ae959..5cf37e4a6 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -14,10 +14,39 @@ * limitations under the License. **/ +/** + * Local file-system based context storage + * + * Configuration options: + * { + * base: "contexts", // the base directory to use + * // default: "contexts" + * dir: "/path/to/storage", // the directory to create the base directory in + * // default: settings.userDir + * cache: true // whether to cache contents in memory + * // default: true + * } + * + * + * $HOME/.node-red/contexts + * ├── global + * │ └── global_context.json + * ├── + * │ ├── flow_context.json + * │ ├── .json + * │ └── .json + * └── + * ├── flow_context.json + * ├── .json + * └── .json + */ + var fs = require('fs-extra'); var path = require("path"); var util = require("../../util"); +var MemoryStore = require("./memory"); + function getStoragePath(storageBaseDir, scope) { if(scope.indexOf(":") === -1){ if(scope === "global"){ @@ -76,10 +105,48 @@ function loadFile(storagePath){ function LocalFileSystem(config){ this.config = config; this.storageBaseDir = getBasePath(this.config); + if (config.hasOwnProperty('cache')?config.cache:true) { + this.cache = MemoryStore({}); + } } LocalFileSystem.prototype.open = function(){ - return Promise.resolve(); + var self = this; + if (this.cache) { + var scopes = []; + var promises = []; + var subdirs = []; + var subdirPromises = []; + return fs.readdir(self.storageBaseDir).then(function(dirs){ + dirs.forEach(function(fn) { + var p = getStoragePath(self.storageBaseDir ,fn)+".json"; + scopes.push(fn); + promises.push(loadFile(p)); + subdirs.push(path.join(self.storageBaseDir,fn)); + subdirPromises.push(fs.readdir(path.join(self.storageBaseDir,fn))); + }) + return Promise.all(subdirPromises); + }).then(function(dirs) { + dirs.forEach(function(files,i) { + files.forEach(function(fn) { + if (fn !== 'flow.json' && fn !== 'global.json') { + scopes.push(fn.substring(0,fn.length-5)+":"+scopes[i]); + promises.push(loadFile(path.join(subdirs[i],fn))) + } + }); + }) + return Promise.all(promises); + }).then(function(res) { + scopes.forEach(function(scope,i) { + var data = res[i]?JSON.parse(res[i]):{}; + Object.keys(data).forEach(function(key) { + self.cache.set(scope,key,data[key]); + }) + }); + }) + } else { + return Promise.resolve(); + } } LocalFileSystem.prototype.close = function(){ @@ -87,6 +154,9 @@ LocalFileSystem.prototype.close = function(){ } LocalFileSystem.prototype.get = function(scope, key, callback) { + if (this.cache) { + return this.cache.get(scope,key,callback); + } if(typeof callback !== "function"){ throw new Error("Callback must be a function"); } @@ -102,7 +172,7 @@ LocalFileSystem.prototype.get = function(scope, key, callback) { }); }; -LocalFileSystem.prototype.set =function(scope, key, value, callback) { +LocalFileSystem.prototype._set = function(scope, key, value, callback) { var storagePath = getStoragePath(this.storageBaseDir ,scope); loadFile(storagePath + ".json").then(function(data){ var obj = data ? JSON.parse(data) : {} @@ -117,9 +187,23 @@ LocalFileSystem.prototype.set =function(scope, key, value, callback) { callback(err); } }); +} + +LocalFileSystem.prototype.set = function(scope, key, value, callback) { + if (this.cache) { + this.cache.set(scope,key,value,callback); + this._set(scope,key,value, function(err) { + // TODO: log any errors + }); + } else { + this._set(scope,key,value,callback); + } }; LocalFileSystem.prototype.keys = function(scope, callback){ + if (this.cache) { + return this.cache.keys(scope,callback); + } if(typeof callback !== "function"){ throw new Error("Callback must be a function"); } @@ -136,29 +220,45 @@ LocalFileSystem.prototype.keys = function(scope, callback){ }; LocalFileSystem.prototype.delete = function(scope){ - var storagePath = getStoragePath(this.storageBaseDir ,scope); - return fs.remove(storagePath + ".json"); + var cachePromise; + if (this.cache) { + cachePromise = this.cache.delete(scope); + } else { + cachePromise = Promise.resolve(); + } + var that = this; + return cachePromise.then(function() { + var storagePath = getStoragePath(that.storageBaseDir,scope); + return fs.remove(storagePath + ".json"); + }); } LocalFileSystem.prototype.clean = function(activeNodes){ var self = this; - return fs.readdir(self.storageBaseDir).then(function(dirs){ - return Promise.all(dirs.reduce(function(result, item){ - if(item !== "global" && activeNodes.indexOf(item) === -1){ - result.push(fs.remove(path.join(self.storageBaseDir,item))); + var cachePromise; + if (this.cache) { + cachePromise = this.cache.clean(activeNodes); + } else { + cachePromise = Promise.resolve(); + } + return cachePromise.then(function() { + return fs.readdir(self.storageBaseDir).then(function(dirs){ + return Promise.all(dirs.reduce(function(result, item){ + if(item !== "global" && activeNodes.indexOf(item) === -1){ + result.push(fs.remove(path.join(self.storageBaseDir,item))); + } + return result; + },[])); + }).catch(function(err){ + if(err.code == 'ENOENT') { + return Promise.resolve(); + }else{ + return Promise.reject(err); } - return result; - },[])); - }).catch(function(err){ - if(err.code == 'ENOENT') { - return Promise.resolve(); - }else{ - return Promise.reject(err); - } + }); }); } module.exports = function(config){ return new LocalFileSystem(config); }; - diff --git a/test/red/runtime/nodes/context/localfilesystem_spec.js b/test/red/runtime/nodes/context/localfilesystem_spec.js index a03d5b24a..61d591b07 100644 --- a/test/red/runtime/nodes/context/localfilesystem_spec.js +++ b/test/red/runtime/nodes/context/localfilesystem_spec.js @@ -29,7 +29,7 @@ describe('localfilesystem',function() { }); beforeEach(function() { - context = LocalFileSystem({dir: resourcesDir}); + context = LocalFileSystem({dir: resourcesDir, cache: false}); return context.open(); }); @@ -308,7 +308,7 @@ describe('localfilesystem',function() { done(); }); }); - }); + }).catch(done); }); }); }); @@ -375,4 +375,4 @@ describe('localfilesystem',function() { }); }); }); -}); \ No newline at end of file +}); From 7d702e8332ff50dc9fe4f545c20639e8522cd4b1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 2 Jul 2018 22:38:37 +0100 Subject: [PATCH 05/34] Remove console.log --- red/api/admin/context.js | 1 - 1 file changed, 1 deletion(-) diff --git a/red/api/admin/context.js b/red/api/admin/context.js index 66aedcdcb..ef23a26ea 100644 --- a/red/api/admin/context.js +++ b/red/api/admin/context.js @@ -43,7 +43,6 @@ module.exports = { if (ctx) { if (key) { ctx.get(key,function(err, v) { - console.log(key,v); res.json(util.encodeObject({msg:v})); }); return; From a1251371d784d628d20727350719ea3807173969 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 3 Jul 2018 11:29:45 +0100 Subject: [PATCH 06/34] Avoid unnecessary re-reading of file context when caching is enabled --- red/runtime/nodes/context/localfilesystem.js | 6 ++++-- red/runtime/nodes/context/memory.js | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index 5cf37e4a6..28e1c4cce 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -192,8 +192,10 @@ LocalFileSystem.prototype._set = function(scope, key, value, callback) { LocalFileSystem.prototype.set = function(scope, key, value, callback) { if (this.cache) { this.cache.set(scope,key,value,callback); - this._set(scope,key,value, function(err) { - // TODO: log any errors + // With cache enabled, no need to re-read the file prior to writing. + var newContext = this.cache._export()[scope]; + var storagePath = getStoragePath(this.storageBaseDir ,scope); + fs.outputFile(storagePath + ".json", JSON.stringify(newContext, undefined, 4), "utf8").catch(function(err) { }); } else { this._set(scope,key,value,callback); diff --git a/red/runtime/nodes/context/memory.js b/red/runtime/nodes/context/memory.js index 93c213403..cace896c5 100644 --- a/red/runtime/nodes/context/memory.js +++ b/red/runtime/nodes/context/memory.js @@ -109,6 +109,11 @@ Memory.prototype.clean = function(activeNodes){ return Promise.resolve(); } +Memory.prototype._export = function() { + return this.data; +} + + module.exports = function(config){ return new Memory(config); }; From c440a4c73052cf4482bcecbb6dc55dde12dab596 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 3 Jul 2018 14:17:42 +0100 Subject: [PATCH 07/34] Expose list of context stores to the editor --- red/api/editor/settings.js | 2 + red/runtime/nodes/context/index.js | 61 +++++++++++++++++------------- red/runtime/nodes/index.js | 3 +- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/red/api/editor/settings.js b/red/api/editor/settings.js index 8929f08de..3faec405e 100644 --- a/red/api/editor/settings.js +++ b/red/api/editor/settings.js @@ -40,6 +40,8 @@ module.exports = { }) } + safeSettings.context = runtime.nodes.listContextStores(); + var themeSettings = theme.settings(); if (themeSettings) { safeSettings.editorTheme = themeSettings; diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index b5ae720eb..e926d8c7d 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -25,11 +25,12 @@ var contexts = {}; // A map of store name to instance var stores = {}; +var storeList = []; +var defaultStore; // Whether there context storage has been configured or left as default var hasConfiguredStore = false; -var defaultStore = "_"; function init(_settings) { settings = _settings; @@ -37,15 +38,16 @@ function init(_settings) { var seed = settings.functionGlobalContext || {}; contexts['global'] = createContext("global",seed); stores["_"] = new memory(); + defaultStore = "memory"; } function load() { return new Promise(function(resolve,reject) { // load & init plugins in settings.contextStorage - var plugins = settings.contextStorage; + var plugins = settings.contextStorage || {}; var defaultIsAlias = false; var promises = []; - if (plugins) { + if (plugins && Object.keys(plugins).length > 0) { var hasDefault = plugins.hasOwnProperty('default'); var defaultName; for (var pluginName in plugins) { @@ -104,34 +106,36 @@ function load() { promises.push(stores[plugin].open()); } } - // There is a 'default' listed in the configuration if (hasDefault) { // If 'default' is an alias, point it at the right module - we have already // checked that it exists. If it isn't an alias, then it will // already be set to a configured store if (defaultIsAlias) { - stores["default"] = stores[plugins["default"]]; + stores["_"] = stores[plugins["default"]]; + defaultStore = plugins["default"]; + } else { + stores["_"] = stores["default"]; + defaultStore = "default"; } - stores["_"] = stores["default"]; } else if (defaultName) { // No 'default' listed, so pick first in list as the default - stores["default"] = stores[defaultName]; - stores["_"] = stores["default"]; - } // else there were no stores list the config object - fall through - // to below where we default to a memory store - } - - if (promises.length === 0) { - // No stores have been configured. Setup the default as an instance - // of memory storage - stores["_"] = memory(); - stores["default"] = stores["_"]; - promises.push(stores["_"].open()) - } else { - // if there's configured storage then the lifecycle is slightly different - // - specifically, we don't delete node context on redeploy + stores["_"] = stores[defaultName]; + defaultStore = defaultName; + defaultIsAlias = true; + } else { + // else there were no stores list the config object - fall through + // to below where we default to a memory store + storeList = ["memory"]; + defaultStore = "memory"; + } hasConfiguredStore = true; + storeList = Object.keys(stores).filter(n=>!(defaultIsAlias && n==="default") && n!== "_"); + } else { + // No configured plugins + promises.push(stores["_"].open()) + storeList = ["memory"]; + defaultStore = "memory"; } return resolve(Promise.all(promises)); }); @@ -149,9 +153,9 @@ function getContextStorage(storage) { if (stores.hasOwnProperty(storage)) { // A known context return stores[storage]; - } else if (stores.hasOwnProperty("default")) { + } else if (stores.hasOwnProperty("_")) { // Not known, but we have a default to fall back to - return stores["default"]; + return stores["_"]; } else { // Not known and no default configured var contextError = new Error(log._("context.error-use-undefined-storage", {storage:storage})); @@ -176,7 +180,7 @@ function createContext(id,seed) { } else { if (typeof storage === 'function') { callback = storage; - storage = "default"; + storage = "_"; } if (typeof callback !== 'function'){ throw new Error("Callback must be a function"); @@ -214,7 +218,7 @@ function createContext(id,seed) { } else { if (typeof storage === 'function') { callback = storage; - storage = "default"; + storage = "_"; } if (callback && typeof callback !== 'function') { throw new Error("Callback must be a function"); @@ -230,7 +234,7 @@ function createContext(id,seed) { } else { if (typeof storage === 'function') { callback = storage; - storage = "default"; + storage = "_"; } if (typeof callback !== 'function') { throw new Error("Callback must be a function"); @@ -312,9 +316,14 @@ function close() { return Promise.all(promises); } +function listStores() { + return {default:defaultStore,stores:storeList}; +} + module.exports = { init: init, load: load, + listStores: listStores, get: getContext, delete: deleteContext, clean: clean, diff --git a/red/runtime/nodes/index.js b/red/runtime/nodes/index.js index cbb78784e..4e89acc2d 100644 --- a/red/runtime/nodes/index.js +++ b/red/runtime/nodes/index.js @@ -222,5 +222,6 @@ module.exports = { // Contexts loadContextsPlugin: context.load, - closeContextsPlugin: context.close + closeContextsPlugin: context.close, + listContextStores: context.listStores }; From 9bbe405cd0999adabdda2265cacc02e33a6724fb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 3 Jul 2018 21:18:15 +0100 Subject: [PATCH 08/34] Do not show blank popovers --- editor/js/ui/common/popover.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/editor/js/ui/common/popover.js b/editor/js/ui/common/popover.js index d0253a8cc..4e2a0b0ef 100644 --- a/editor/js/ui/common/popover.js +++ b/editor/js/ui/common/popover.js @@ -48,12 +48,15 @@ RED.popover = (function() { var openPopup = function(instant) { if (active) { - div = $('
').appendTo("body"); + div = $('
'); if (size !== "default") { div.addClass("red-ui-popover-size-"+size); } if (typeof content === 'function') { var result = content.call(res); + if (result === null) { + return; + } if (typeof result === 'string') { div.text(result); } else { @@ -65,7 +68,7 @@ RED.popover = (function() { if (width !== "auto") { div.width(width); } - + div.appendTo("body"); var targetPos = target.offset(); var targetWidth = target.outerWidth(); From 4e4a1f11e6a363eab9097967b75574ce71e31f3f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 3 Jul 2018 21:18:43 +0100 Subject: [PATCH 09/34] Fix context admin api for empty contexts --- red/api/admin/context.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/red/api/admin/context.js b/red/api/admin/context.js index ef23a26ea..b99221dc4 100644 --- a/red/api/admin/context.js +++ b/red/api/admin/context.js @@ -50,15 +50,19 @@ module.exports = { ctx.keys(function(err, keys) { var result = {}; var c = keys.length; - keys.forEach(function(key) { - ctx.get(key,function(err, v) { - result[key] = util.encodeObject({msg:v}); - c--; - if (c === 0) { - res.json(result); - } + if (c === 0) { + res.json(result); + } else { + keys.forEach(function(key) { + ctx.get(key,function(err, v) { + result[key] = util.encodeObject({msg:v}); + c--; + if (c === 0) { + res.json(result); + } + }); }); - }); + } }); } } else { From 80873e4ea976dac17d2863aa6e96fb520c6556bd Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 3 Jul 2018 21:27:55 +0100 Subject: [PATCH 10/34] fix settings api test for context stores --- test/red/api/editor/settings_spec.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/red/api/editor/settings_spec.js b/test/red/api/editor/settings_spec.js index 142d5731a..631d0907a 100644 --- a/test/red/api/editor/settings_spec.js +++ b/test/red/api/editor/settings_spec.js @@ -59,7 +59,8 @@ describe("api/editor/settings", function() { }, nodes: { paletteEditorEnabled: function() { return true; }, - getCredentialKeyType: function() { return "test-key-type"} + getCredentialKeyType: function() { return "test-key-type"}, + listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}} }, log: { error: console.error }, storage: {} @@ -73,6 +74,7 @@ describe("api/editor/settings", function() { } res.body.should.have.property("httpNodeRoot","testHttpNodeRoot"); res.body.should.have.property("version","testVersion"); + res.body.should.have.property("context",{default: "foo", stores: ["foo","bar"]}); res.body.should.have.property("paletteCategories",["red","blue","green"]); res.body.should.have.property("editorTheme",{test:456}); res.body.should.have.property("testNodeSetting","helloWorld"); @@ -95,7 +97,8 @@ describe("api/editor/settings", function() { }, nodes: { paletteEditorEnabled: function() { return true; }, - getCredentialKeyType: function() { return "test-key-type"} + getCredentialKeyType: function() { return "test-key-type"}, + listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}} }, log: { error: console.error }, storage: {} @@ -130,7 +133,8 @@ describe("api/editor/settings", function() { }, nodes: { paletteEditorEnabled: function() { return true; }, - getCredentialKeyType: function() { return "test-key-type"} + getCredentialKeyType: function() { return "test-key-type"}, + listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}} }, log: { error: console.error }, storage: { @@ -169,7 +173,8 @@ describe("api/editor/settings", function() { }, nodes: { paletteEditorEnabled: function() { return true; }, - getCredentialKeyType: function() { return "test-key-type"} + getCredentialKeyType: function() { return "test-key-type"}, + listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}} }, log: { error: console.error }, storage: { @@ -211,7 +216,8 @@ describe("api/editor/settings", function() { }, nodes: { paletteEditorEnabled: function() { return true; }, - getCredentialKeyType: function() { return "test-key-type"} + getCredentialKeyType: function() { return "test-key-type"}, + listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}} }, log: { error: console.error }, storage: { @@ -248,8 +254,8 @@ describe("api/editor/settings", function() { }, nodes: { paletteEditorEnabled: function() { return false; }, - getCredentialKeyType: function() { return "test-key-type"} - + getCredentialKeyType: function() { return "test-key-type"}, + listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}} }, log: { error: console.error }, storage: {} From c2434814329429004d89bcb549393b4120910fa2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 25 Jun 2018 10:38:34 +0100 Subject: [PATCH 11/34] Add sub options to Inject node --- editor/js/ui/common/typedInput.js | 34 +++++++++++++++++++++++++++++-- nodes/core/core/20-inject.html | 11 ++++++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 0f9e2a0d9..27c11e108 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -14,10 +14,40 @@ * limitations under the License. **/ (function($) { + var contextParse = function(v) { + var parts = {}; + var m = /^#:\((\S+?)\)::(.*)$/.exec(v); + if (m) { + parts.option = m[1]; + parts.value = m[2]; + } else { + parts.value = v; + } + return parts; + } + var contextExport = function(v,opt) { + return "#:("+((typeof opt === "string")?opt:opt.value)+")::"+v; + } var allOptions = { msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression}, - flow: {value:"flow",label:"flow.",validate:RED.utils.validatePropertyExpression}, - global: {value:"global",label:"global.",validate:RED.utils.validatePropertyExpression}, + flow: {value:"flow",label:"flow.",hasValue:true, + options:[ + {value:"memory",label: "memory", icon:''}//, + // {value:"redis",label:"redis",icon:''} + ], + validate:RED.utils.validatePropertyExpression, + parse: contextParse, + export: contextExport + }, + global: {value:"global",label:"global.",hasValue:true, + options:[ + {value:"memory",label: "memory", icon:''}, + {value:"redis",label:"redis",icon:''} + ], + validate:RED.utils.validatePropertyExpression, + parse: contextParse, + export: contextExport + }, str: {value:"str",label:"string",icon:"red/images/typedInput/az.png"}, num: {value:"num",label:"number",icon:"red/images/typedInput/09.png",validate:/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/}, bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.png",options:["true","false"]}, diff --git a/nodes/core/core/20-inject.html b/nodes/core/core/20-inject.html index 6f8ebcf69..87bb807c6 100644 --- a/nodes/core/core/20-inject.html +++ b/nodes/core/core/20-inject.html @@ -237,10 +237,13 @@ If you want every 20 minutes from now - use the "interval" option.

} else { return this._("inject.timestamp")+suffix; } - } else if (this.payloadType === 'flow' && this.payload.length < 19) { - return 'flow.'+this.payload+suffix; - } else if (this.payloadType === 'global' && this.payload.length < 17) { - return 'global.'+this.payload+suffix; + } else if (this.payloadType === 'flow' || this.payloadType === 'global') { + var key = this.payload; + var m = /^#:\((\S+?)\)::(.*)$/.exec(key); + if (m) { + key = m[2]; + } + return 'flow.'+key+suffix; } else { return this._("inject.inject")+suffix; } From 33b4774c497f0cd41f838f737ed60da5c9039346 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 3 Jul 2018 21:17:15 +0100 Subject: [PATCH 12/34] Load typedinput context list from settings --- editor/js/ui/common/typedInput.js | 34 +++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 27c11e108..ca13961b6 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -22,28 +22,28 @@ parts.value = m[2]; } else { parts.value = v; + parts.option = RED.settings.context.default; } return parts; } var contextExport = function(v,opt) { - return "#:("+((typeof opt === "string")?opt:opt.value)+")::"+v; + var store = ((typeof opt === "string")?opt:opt.value) + if (store !== RED.settings.context.default) { + return "#:("+store+")::"+v; + } else { + return v; + } } var allOptions = { msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression}, flow: {value:"flow",label:"flow.",hasValue:true, - options:[ - {value:"memory",label: "memory", icon:''}//, - // {value:"redis",label:"redis",icon:''} - ], + options:[], validate:RED.utils.validatePropertyExpression, parse: contextParse, export: contextExport }, global: {value:"global",label:"global.",hasValue:true, - options:[ - {value:"memory",label: "memory", icon:''}, - {value:"redis",label:"redis",icon:''} - ], + options:[], validate:RED.utils.validatePropertyExpression, parse: contextParse, export: contextExport @@ -117,12 +117,19 @@ $.widget( "nodered.typedInput", { _create: function() { + try { if (!nlsd && RED && RED._) { for (var i in allOptions) { if (allOptions.hasOwnProperty(i)) { allOptions[i].label = RED._("typedInput.type."+i,{defaultValue:allOptions[i].label}); } } + var contextStores = RED.settings.context.stores; + var contextOptions = contextStores.map(function(store) { + return {value:store,label: store, icon:''} + }) + allOptions.flow.options = contextOptions; + allOptions.global.options = contextOptions; } nlsd = true; var that = this; @@ -200,6 +207,9 @@ // explicitly set optionSelectTrigger display to inline-block otherwise jQ sets it to 'inline' this.optionSelectTrigger = $('').appendTo(this.uiSelect); this.optionSelectLabel = $('').prependTo(this.optionSelectTrigger); + RED.popover.tooltip(this.optionSelectLabel,function() { + return that.optionValue; + }); this.optionSelectTrigger.click(function(event) { event.preventDefault(); that._showOptionSelectMenu(); @@ -216,6 +226,9 @@ this.optionExpandButton = $('').appendTo(this.uiSelect); this.type(this.options.default||this.typeList[0].value); + }catch(err) { + console.log(err.stack); + } }, _showTypeMenu: function() { if (this.typeList.length > 1) { @@ -578,6 +591,9 @@ if (typeof selectedOption === "string") { this.optionValue = selectedOption; + if (!this.activeOptions.hasOwnProperty(selectedOption)) { + selectedOption = Object.keys(this.activeOptions)[0]; + } this._updateOptionSelectLabel(this.activeOptions[selectedOption]); } else { this.optionValue = selectedOption.value; From 7ca153abd0840fd846321d129eab4cd9de80822b Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Wed, 4 Jul 2018 20:50:33 +0900 Subject: [PATCH 13/34] fix error on typedInput initialization --- editor/js/ui/common/typedInput.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 0f9e2a0d9..81b3a8d6e 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -467,7 +467,9 @@ var opt = this.typeMap[type]; if (opt && this.propertyType !== type) { this.propertyType = type; - this.typeField.val(type); + if (this.typeField) { + this.typeField.val(type); + } this.selectLabel.empty(); var image; if (opt.icon) { @@ -574,15 +576,17 @@ } this.elementDiv.show(); } - if (opt.expand && typeof opt.expand === 'function') { - this.optionExpandButton.show(); - this.optionExpandButton.off('click'); - this.optionExpandButton.on('click',function(evt) { - evt.preventDefault(); - opt.expand.call(that); - }) - } else { - this.optionExpandButton.hide(); + if (this.optionExpandButton) { + if (opt.expand && typeof opt.expand === 'function') { + this.optionExpandButton.show(); + this.optionExpandButton.off('click'); + this.optionExpandButton.on('click',function(evt) { + evt.preventDefault(); + opt.expand.call(that); + }) + } else { + this.optionExpandButton.hide(); + } } this.input.trigger('change',this.propertyType,this.value()); } From e6c5cfb703844ae337ca390c8fe76f516dfaefe2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 4 Jul 2018 13:36:23 +0100 Subject: [PATCH 14/34] Do not show TypedInput context options if there's only one available --- editor/js/ui/common/typedInput.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index ca13961b6..77b94bbc3 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -27,6 +27,9 @@ return parts; } var contextExport = function(v,opt) { + if (!opt) { + return v; + } var store = ((typeof opt === "string")?opt:opt.value) if (store !== RED.settings.context.default) { return "#:("+store+")::"+v; @@ -128,8 +131,13 @@ var contextOptions = contextStores.map(function(store) { return {value:store,label: store, icon:''} }) - allOptions.flow.options = contextOptions; - allOptions.global.options = contextOptions; + if (contextOptions.length < 2) { + delete allOptions.flow.options; + delete allOptions.global.options + } else { + allOptions.flow.options = contextOptions; + allOptions.global.options = contextOptions; + } } nlsd = true; var that = this; From 372c213c2cec7781d413248a44765c6b8a6e0e6b Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 4 Jul 2018 14:23:18 +0100 Subject: [PATCH 15/34] Still parse/export typedInput values even when no options set --- editor/js/ui/common/typedInput.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 492a9dcc0..74bbeb9ac 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -132,8 +132,8 @@ return {value:store,label: store, icon:''} }) if (contextOptions.length < 2) { - delete allOptions.flow.options; - delete allOptions.global.options + allOptions.flow.options = []; + allOptions.global.options = []; } else { allOptions.flow.options = contextOptions; allOptions.global.options = contextOptions; @@ -592,22 +592,31 @@ var parts = opt.parse(this.input.val()); if (parts.option) { selectedOption = parts.option; + if (!this.activeOptions.hasOwnProperty(selectedOption)) { + parts.option = Object.keys(this.activeOptions)[0]; + selectedOption = parts.option + } } this.input.val(parts.value); if (opt.export) { this.element.val(opt.export(parts.value,parts.option||selectedOption)); } } - if (typeof selectedOption === "string") { this.optionValue = selectedOption; if (!this.activeOptions.hasOwnProperty(selectedOption)) { selectedOption = Object.keys(this.activeOptions)[0]; } - this._updateOptionSelectLabel(this.activeOptions[selectedOption]); - } else { + if (!selectedOption) { + this.optionSelectTrigger.hide(); + } else { + this._updateOptionSelectLabel(this.activeOptions[selectedOption]); + } + } else if (selectedOption) { this.optionValue = selectedOption.value; this._updateOptionSelectLabel(selectedOption); + } else { + this.optionSelectTrigger.hide(); } } } From 946a6d604160ce07b513dc7df182461505528755 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 5 Jul 2018 10:43:33 +0100 Subject: [PATCH 16/34] Update RED.util.evaluateNodeProperty to support context stores --- red/runtime/util.js | 51 +++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/red/runtime/util.js b/red/runtime/util.js index 1d49641df..7d6464174 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -322,35 +322,54 @@ function evaluteEnvProperty(value) { return value; } -function evaluateNodeProperty(value, type, node, msg) { +var parseContextStore = function(key) { + var parts = {}; + var m = /^#:\((\S+?)\)::(.*)$/.exec(key); + if (m) { + parts.store = m[1]; + parts.key = m[2]; + } else { + parts.key = key; + } + return parts; +} + +function evaluateNodeProperty(value, type, node, msg, callback) { + var result; if (type === 'str') { - return ""+value; + result = ""+value; } else if (type === 'num') { - return Number(value); + result = Number(value); } else if (type === 'json') { - return JSON.parse(value); + result = JSON.parse(value); } else if (type === 're') { - return new RegExp(value); + result = new RegExp(value); } else if (type === 'date') { - return Date.now(); + result = Date.now(); } else if (type === 'bin') { var data = JSON.parse(value); - return Buffer.from(data); + result = Buffer.from(data); } else if (type === 'msg' && msg) { - return getMessageProperty(msg,value); - } else if (type === 'flow' && node) { - return node.context().flow.get(value); - } else if (type === 'global' && node) { - return node.context().global.get(value); + result = getMessageProperty(msg,value); + } else if ((type === 'flow' || type === 'global') && node) { + var contextKey = parseContextStore(value); + result = node.context()[type].get(contextKey.key,contextKey.store,callback); + if (callback) { + return; + } } else if (type === 'bool') { - return /^true$/i.test(value); + result = /^true$/i.test(value); } else if (type === 'jsonata') { var expr = prepareJSONataExpression(value,node); - return evaluateJSONataExpression(expr,msg); + result = evaluateJSONataExpression(expr,msg); } else if (type === 'env') { - return evaluteEnvProperty(value); + result = evaluteEnvProperty(value); + } + if (callback) { + callback(result); + } else { + return value; } - return value; } function prepareJSONataExpression(value,node) { From 4bcf13cb58869902e3d62294af91eeece5c93497 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Sat, 7 Jul 2018 19:01:14 +0100 Subject: [PATCH 17/34] Let nrgpio code work with python 3 (just in case that becomes default) --- nodes/core/hardware/nrgpio.py | 40 ++++++++++++++++------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/nodes/core/hardware/nrgpio.py b/nodes/core/hardware/nrgpio.py index 6bbcddbbe..0cde0e4df 100755 --- a/nodes/core/hardware/nrgpio.py +++ b/nodes/core/hardware/nrgpio.py @@ -23,10 +23,6 @@ from time import sleep bounce = 25; -if sys.version_info >= (3,0): - print("Sorry - currently only configured to work with python 2.x") - sys.exit(1) - if len(sys.argv) > 2: cmd = sys.argv[1].lower() pin = int(sys.argv[2]) @@ -34,7 +30,7 @@ if len(sys.argv) > 2: GPIO.setwarnings(False) if cmd == "pwm": - #print "Initialised pin "+str(pin)+" to PWM" + #print("Initialised pin "+str(pin)+" to PWM") try: freq = int(sys.argv[3]) except: @@ -54,10 +50,10 @@ if len(sys.argv) > 2: GPIO.cleanup(pin) sys.exit(0) except Exception as ex: - print "bad data: "+data + print("bad data: "+data) elif cmd == "buzz": - #print "Initialised pin "+str(pin)+" to Buzz" + #print("Initialised pin "+str(pin)+" to Buzz") GPIO.setup(pin,GPIO.OUT) p = GPIO.PWM(pin, 100) p.stop() @@ -76,10 +72,10 @@ if len(sys.argv) > 2: GPIO.cleanup(pin) sys.exit(0) except Exception as ex: - print "bad data: "+data + print("bad data: "+data) elif cmd == "out": - #print "Initialised pin "+str(pin)+" to OUT" + #print("Initialised pin "+str(pin)+" to OUT") GPIO.setup(pin,GPIO.OUT) if len(sys.argv) == 4: GPIO.output(pin,int(sys.argv[3])) @@ -103,11 +99,11 @@ if len(sys.argv) > 2: GPIO.output(pin,data) elif cmd == "in": - #print "Initialised pin "+str(pin)+" to IN" + #print("Initialised pin "+str(pin)+" to IN") bounce = float(sys.argv[4]) def handle_callback(chan): sleep(bounce/1000.0) - print GPIO.input(chan) + print(GPIO.input(chan)) if sys.argv[3].lower() == "up": GPIO.setup(pin,GPIO.IN,GPIO.PUD_UP) @@ -116,7 +112,7 @@ if len(sys.argv) > 2: else: GPIO.setup(pin,GPIO.IN) - print GPIO.input(pin) + print(GPIO.input(pin)) GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=int(bounce)) while True: @@ -129,7 +125,7 @@ if len(sys.argv) > 2: sys.exit(0) elif cmd == "byte": - #print "Initialised BYTE mode - "+str(pin)+ + #print("Initialised BYTE mode - "+str(pin)+) list = [7,11,13,12,15,16,18,22] GPIO.setup(list,GPIO.OUT) @@ -152,7 +148,7 @@ if len(sys.argv) > 2: GPIO.output(list[bit], data & mask) elif cmd == "borg": - #print "Initialised BORG mode - "+str(pin)+ + #print("Initialised BORG mode - "+str(pin)+) GPIO.setup(11,GPIO.OUT) GPIO.setup(13,GPIO.OUT) GPIO.setup(15,GPIO.OUT) @@ -190,7 +186,7 @@ if len(sys.argv) > 2: button = ord( buf[0] ) & pin # mask out just the required button(s) if button != oldbutt: # only send if changed oldbutt = button - print button + print(button) while True: try: @@ -215,7 +211,7 @@ if len(sys.argv) > 2: # type,code,value print("%u,%u" % (code, value)) event = file.read(EVENT_SIZE) - print "0,0" + print("0,0") file.close() sys.exit(0) except: @@ -225,14 +221,14 @@ if len(sys.argv) > 2: elif len(sys.argv) > 1: cmd = sys.argv[1].lower() if cmd == "rev": - print GPIO.RPI_REVISION + print(GPIO.RPI_REVISION) elif cmd == "ver": - print GPIO.VERSION + print(GPIO.VERSION) elif cmd == "info": - print GPIO.RPI_INFO + print(GPIO.RPI_INFO) else: - print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}" - print " only ver (gpio version) and info (board information) accept no pin parameter." + print("Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}") + print(" only ver (gpio version) and info (board information) accept no pin parameter.") else: - print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}" + print("Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}") From f870e9ed3ef1db0c6bb4a1836cd4413bcc0048b6 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Sun, 8 Jul 2018 16:52:30 +0100 Subject: [PATCH 18/34] Let Join node accumulate top level properties Last in is still most significant --- nodes/core/logic/17-split.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/nodes/core/logic/17-split.js b/nodes/core/logic/17-split.js index 92b527d01..af16f5bbd 100644 --- a/nodes/core/logic/17-split.js +++ b/nodes/core/logic/17-split.js @@ -280,8 +280,8 @@ module.exports = function(RED) { msgs.sort(function(x,y) { var ix = x.parts.index; var iy = y.parts.index; - if (ix < iy) return -flag; - if (ix > iy) return flag; + if (ix < iy) { return -flag; } + if (ix > iy) { return flag; } return 0; }); for(var msg of msgs) { @@ -437,7 +437,8 @@ module.exports = function(RED) { newArray = newArray.concat(n); }) group.payload = newArray; - } else if (group.type === 'buffer') { + } + else if (group.type === 'buffer') { var buffers = []; var bufferLen = 0; if (group.joinChar !== undefined) { @@ -450,7 +451,8 @@ module.exports = function(RED) { buffers.push(group.payload[i]); bufferLen += group.payload[i].length; } - } else { + } + else { bufferLen = group.bufferLen; buffers = group.payload; } @@ -463,7 +465,8 @@ module.exports = function(RED) { groupJoinChar = group.joinChar.toString(); } RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar)); - } else { + } + else { if (node.propertyType === 'full') { group.msg = RED.util.cloneMessage(group.msg); } @@ -471,7 +474,8 @@ module.exports = function(RED) { } if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) { group.msg.parts = group.msg.parts.parts; - } else { + } + else { delete group.msg.parts; } delete group.msg.complete; @@ -525,7 +529,7 @@ module.exports = function(RED) { payloadType = node.build; targetCount = node.count; joinChar = node.joiner; - if (targetCount === 0 && msg.hasOwnProperty('parts')) { + if (n.count === "" && msg.hasOwnProperty('parts')) { targetCount = msg.parts.count || 0; } if (node.build === 'object') { @@ -554,7 +558,7 @@ module.exports = function(RED) { payload:{}, targetCount:targetCount, type:"object", - msg:msg + msg:RED.util.cloneMessage(msg) }; } else if (node.accumulate === true) { @@ -564,7 +568,7 @@ module.exports = function(RED) { payload:{}, targetCount:targetCount, type:payloadType, - msg:msg + msg:RED.util.cloneMessage(msg) } if (payloadType === 'string' || payloadType === 'array' || payloadType === 'buffer') { inflight[partId].payload = []; @@ -576,7 +580,7 @@ module.exports = function(RED) { payload:[], targetCount:targetCount, type:payloadType, - msg:msg + msg:RED.util.cloneMessage(msg) }; if (payloadType === 'string') { inflight[partId].joinChar = joinChar; @@ -624,14 +628,14 @@ module.exports = function(RED) { } group.currentCount++; } - // TODO: currently reuse the last received - add option to pick first received - group.msg = msg; + group.msg = Object.assign(group.msg, msg); var tcnt = group.targetCount; if (msg.hasOwnProperty("parts")) { tcnt = group.targetCount || msg.parts.count; } if ((tcnt > 0 && group.currentCount >= tcnt) || msg.hasOwnProperty('complete')) { completeSend(partId); } - } catch(err) { + } + catch(err) { console.log(err.stack); } }); From afb566b6b4a4b2387ab5e300f35abd40fdcb1edf Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sat, 7 Jul 2018 22:09:55 +0100 Subject: [PATCH 19/34] Add async context support to Inject node --- nodes/core/core/20-inject.js | 40 +++++++++++++++++++++++------------- red/runtime/util.js | 2 +- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/nodes/core/core/20-inject.js b/nodes/core/core/20-inject.js index 9e6eb62a8..c0d9e0c2f 100644 --- a/nodes/core/core/20-inject.js +++ b/nodes/core/core/20-inject.js @@ -63,21 +63,33 @@ module.exports = function(RED) { } this.on("input",function(msg) { - try { - msg.topic = this.topic; - if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") { - msg.payload = Date.now(); - } else if (this.payloadType == null) { - msg.payload = this.payload; - } else if (this.payloadType === 'none') { - msg.payload = ""; - } else { - msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg); + msg.topic = this.topic; + if (this.payloadType !== 'flow' && this.payloadType !== 'global') { + try { + if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") { + msg.payload = Date.now(); + } else if (this.payloadType == null) { + msg.payload = this.payload; + } else if (this.payloadType === 'none') { + msg.payload = ""; + } else { + msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg); + } + this.send(msg); + msg = null; + } catch(err) { + this.error(err,msg); } - this.send(msg); - msg = null; - } catch(err) { - this.error(err,msg); + } else { + RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg, function(err,res) { + if (err) { + node.error(err,msg); + } else { + msg.payload = res; + node.send(msg); + } + + }); } }); } diff --git a/red/runtime/util.js b/red/runtime/util.js index 7d6464174..6279c416c 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -368,7 +368,7 @@ function evaluateNodeProperty(value, type, node, msg, callback) { if (callback) { callback(result); } else { - return value; + return result; } } From 1b693eed372633b3fdb7dff3a666154acc085ffe Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sat, 7 Jul 2018 22:23:26 +0100 Subject: [PATCH 20/34] Add async context support to Change node --- nodes/core/logic/15-change.js | 263 +++++++++++++----------- test/nodes/core/logic/15-change_spec.js | 23 +++ 2 files changed, 163 insertions(+), 123 deletions(-) diff --git a/nodes/core/logic/15-change.js b/nodes/core/logic/15-change.js index ab4f59cc3..4094f68e0 100644 --- a/nodes/core/logic/15-change.js +++ b/nodes/core/logic/15-change.js @@ -99,158 +99,175 @@ module.exports = function(RED) { } function applyRule(msg,rule) { - try { - var property = rule.p; - var value = rule.to; - if (rule.tot === 'json') { - value = JSON.parse(rule.to); - } else if (rule.tot === 'bin') { - value = Buffer.from(JSON.parse(rule.to)) - } - var current; - var fromValue; - var fromType; - var fromRE; - if (rule.tot === "msg") { - value = RED.util.getMessageProperty(msg,rule.to); - } else if (rule.tot === 'flow') { - value = node.context().flow.get(rule.to); - } else if (rule.tot === 'global') { - value = node.context().global.get(rule.to); - } else if (rule.tot === 'date') { - value = Date.now(); - } else if (rule.tot === 'jsonata') { - try{ - value = RED.util.evaluateJSONataExpression(rule.to,msg); - } catch(err) { - node.error(RED._("change.errors.invalid-expr",{error:err.message}),msg); - return; + return new Promise(function(resolve, reject){ + try { + var property = rule.p; + var value = rule.to; + if (rule.tot === 'json') { + value = JSON.parse(rule.to); + } else if (rule.tot === 'bin') { + value = Buffer.from(JSON.parse(rule.to)) } - } - if (rule.t === 'change') { - if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { - if (rule.fromt === "msg") { - fromValue = RED.util.getMessageProperty(msg,rule.from); - } else if (rule.fromt === 'flow') { - fromValue = node.context().flow.get(rule.from); - } else if (rule.fromt === 'global') { - fromValue = node.context().global.get(rule.from); + var current; + var fromValue; + var fromType; + var fromRE; + if (rule.tot === "msg") { + value = RED.util.getMessageProperty(msg,rule.to); + } else if (rule.tot === 'flow') { + value = node.context().flow.get(rule.to); + } else if (rule.tot === 'global') { + value = node.context().global.get(rule.to); + } else if (rule.tot === 'date') { + value = Date.now(); + } else if (rule.tot === 'jsonata') { + try{ + value = RED.util.evaluateJSONataExpression(rule.to,msg); + } catch(err) { + node.error(RED._("change.errors.invalid-expr",{error:err.message}),msg); + return; } - if (typeof fromValue === 'number' || fromValue instanceof Number) { - fromType = 'num'; - } else if (typeof fromValue === 'boolean') { - fromType = 'bool' - } else if (fromValue instanceof RegExp) { - fromType = 're'; - fromRE = fromValue; - } else if (typeof fromValue === 'string') { - fromType = 'str'; - fromRE = fromValue.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - try { - fromRE = new RegExp(fromRE, "g"); - } catch (e) { - valid = false; - node.error(RED._("change.errors.invalid-from",{error:e.message}),msg); - return; + } + if (rule.t === 'change') { + if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { + if (rule.fromt === "msg") { + fromValue = RED.util.getMessageProperty(msg,rule.from); + } else if (rule.fromt === 'flow') { + fromValue = node.context().flow.get(rule.from); + } else if (rule.fromt === 'global') { + fromValue = node.context().global.get(rule.from); + } + if (typeof fromValue === 'number' || fromValue instanceof Number) { + fromType = 'num'; + } else if (typeof fromValue === 'boolean') { + fromType = 'bool' + } else if (fromValue instanceof RegExp) { + fromType = 're'; + fromRE = fromValue; + } else if (typeof fromValue === 'string') { + fromType = 'str'; + fromRE = fromValue.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + try { + fromRE = new RegExp(fromRE, "g"); + } catch (e) { + valid = false; + reject(RED._("change.errors.invalid-from",{error:e.message})); + return; + } + } else { + reject(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)})); + return } } else { - node.error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)}),msg); - return - } - } else { - fromType = rule.fromt; - fromValue = rule.from; - fromRE = rule.fromRE; - } - } - if (rule.pt === 'msg') { - if (rule.t === 'delete') { - RED.util.setMessageProperty(msg,property,undefined); - } else if (rule.t === 'set') { - RED.util.setMessageProperty(msg,property,value); - } else if (rule.t === 'change') { - current = RED.util.getMessageProperty(msg,property); - if (typeof current === 'string') { - if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { - // str representation of exact from number/boolean - // only replace if they match exactly - RED.util.setMessageProperty(msg,property,value); - } else { - current = current.replace(fromRE,value); - RED.util.setMessageProperty(msg,property,current); - } - } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { - if (current == Number(fromValue)) { - RED.util.setMessageProperty(msg,property,value); - } - } else if (typeof current === 'boolean' && fromType === 'bool') { - if (current.toString() === fromValue) { - RED.util.setMessageProperty(msg,property,value); - } + fromType = rule.fromt; + fromValue = rule.from; + fromRE = rule.fromRE; } } - } - else { - var target; - if (rule.pt === 'flow') { - target = node.context().flow; - } else if (rule.pt === 'global') { - target = node.context().global; - } - if (target) { + if (rule.pt === 'msg') { if (rule.t === 'delete') { - target.set(property,undefined); + RED.util.setMessageProperty(msg,property,undefined); } else if (rule.t === 'set') { - target.set(property,value); + RED.util.setMessageProperty(msg,property,value); } else if (rule.t === 'change') { - current = target.get(property); + current = RED.util.getMessageProperty(msg,property); if (typeof current === 'string') { if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { // str representation of exact from number/boolean // only replace if they match exactly - target.set(property,value); + RED.util.setMessageProperty(msg,property,value); } else { current = current.replace(fromRE,value); - target.set(property,current); + RED.util.setMessageProperty(msg,property,current); } } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { if (current == Number(fromValue)) { - target.set(property,value); + RED.util.setMessageProperty(msg,property,value); } } else if (typeof current === 'boolean' && fromType === 'bool') { if (current.toString() === fromValue) { - target.set(property,value); + RED.util.setMessageProperty(msg,property,value); } } } } + else { + var target; + if (rule.pt === 'flow') { + target = node.context().flow; + } else if (rule.pt === 'global') { + target = node.context().global; + } + if (target) { + if (rule.t === 'delete') { + target.set(property,undefined); + } else if (rule.t === 'set') { + target.set(property,value); + } else if (rule.t === 'change') { + current = target.get(property); + if (typeof current === 'string') { + if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { + // str representation of exact from number/boolean + // only replace if they match exactly + target.set(property,value); + } else { + current = current.replace(fromRE,value); + target.set(property,current); + } + } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { + if (current == Number(fromValue)) { + target.set(property,value); + } + } else if (typeof current === 'boolean' && fromType === 'bool') { + if (current.toString() === fromValue) { + target.set(property,value); + } + } + } + } + } + } catch(err) {/*console.log(err.stack)*/} + resolve(msg); + }); + } + function applyRules(msg, currentRule) { + var r = node.rules[currentRule]; + var rulePromise; + if (r.t === "move") { + if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) { + rulePromise = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt}).then( + msg => applyRule(msg,{t:"delete", p:r.p, pt:r.pt}) + ); } - } catch(err) {/*console.log(err.stack)*/} - return msg; + else { // 2 step move if we are moving from a child + rulePromise = applyRule(msg,{t:"set", p:"_temp_move", pt:r.tot, to:r.p, tot:r.pt}).then( + msg => applyRule(msg,{t:"delete", p:r.p, pt:r.pt}) + ).then( + msg => applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:"_temp_move", tot:r.pt}) + ).then( + msg => applyRule(msg,{t:"delete", p:"_temp_move", pt:r.pt}) + ) + } + } else { + rulePromise = applyRule(msg,r); + } + return rulePromise.then( + msg => { + if (!msg) { + return + } else if (currentRule === node.rules.length - 1) { + return msg; + } else { + return applyRules(msg, currentRule+1); + } + } + ); } if (valid) { this.on('input', function(msg) { - for (var i=0; i { if (msg) { node.send(msg) }} ) + .catch( err => node.error(err, msg)) }); } } diff --git a/test/nodes/core/logic/15-change_spec.js b/test/nodes/core/logic/15-change_spec.js index e444b2663..f819c5d0d 100644 --- a/test/nodes/core/logic/15-change_spec.js +++ b/test/nodes/core/logic/15-change_spec.js @@ -15,6 +15,7 @@ **/ var should = require("should"); +var sinon = require("sinon"); var changeNode = require("../../../../nodes/core/logic/15-change.js"); var helper = require("node-red-node-test-helper"); @@ -454,6 +455,28 @@ describe('change Node', function() { }); }); + it('reports invalid jsonata expression', function(done) { + var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"set","p":"payload","to":"$invalid(payload)","tot":"jsonata"}],"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + sinon.spy(changeNode1,"error"); + helperNode1.on("input", function(msg) { + done("Invalid jsonata expression passed message through"); + }); + changeNode1.receive({payload:"Hello World!"}); + setTimeout(function() { + try { + changeNode1.error.called.should.be.true(); + done(); + } catch(err) { + done(err); + } + },50); + }); + }); + }); describe('#change', function() { it('changes the value of the message property', function(done) { From 1a6babd199db2a3c35594ac64f1f54e63f848e52 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 8 Jul 2018 22:13:36 +0100 Subject: [PATCH 21/34] Lint Switch code --- nodes/core/logic/10-switch.js | 113 ++++++++++++++++------------------ 1 file changed, 52 insertions(+), 61 deletions(-) diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index 919fe13ac..ee3b4ef12 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -59,19 +59,19 @@ module.exports = function(RED) { 'else': function(a) { return a === true; } }; - var _max_kept_msgs_count = undefined; + var _maxKeptCount; - function max_kept_msgs_count(node) { - if (_max_kept_msgs_count === undefined) { + function getMaxKeptCount() { + if (_maxKeptCount === undefined) { var name = "nodeMessageBufferMaxLength"; if (RED.settings.hasOwnProperty(name)) { - _max_kept_msgs_count = RED.settings[name]; + _maxKeptCount = RED.settings[name]; } else { - _max_kept_msgs_count = 0; + _maxKeptCount = 0; } } - return _max_kept_msgs_count; + return _maxKeptCount; } function SwitchNode(n) { @@ -94,10 +94,10 @@ module.exports = function(RED) { var node = this; var valid = true; var repair = n.repair; - var needs_count = repair; + var needsCount = repair; for (var i=0; i 0) && (pending_count > max_msgs)) { - clear_pending(); + pendingCount++; + var max_msgs = getMaxKeptCount(); + if ((max_msgs > 0) && (pendingCount > max_msgs)) { + clearPending(); node.error(RED._("switch.errors.too-many"), msg); } if (parts.hasOwnProperty("count")) { @@ -170,32 +170,29 @@ module.exports = function(RED) { return group; } - function del_group_in(id, group) { - pending_count -= group.msgs.length; - delete pending_in[id]; - } - function add2pending_in(msg) { + function addMessageToPending(msg) { var parts = msg.parts; if (parts.hasOwnProperty("id") && parts.hasOwnProperty("index")) { - var group = add2group_in(parts.id, msg, parts); + var group = addMessageToGroup(parts.id, msg, parts); var msgs = group.msgs; var count = group.count; if (count === msgs.length) { for (var i = 0; i < msgs.length; i++) { var msg = msgs[i]; msg.parts.count = count; - process_msg(msg, false); + processMessage(msg, false); } - del_group_in(parts.id, group); + pendingCount -= group.msgs.length; + delete pendingIn[parts.id]; } return true; } return false; } - function send_group(onwards, port_count) { + function sendGroup(onwards, port_count) { var counts = new Array(port_count).fill(0); for (var i = 0; i < onwards.length; i++) { var onward = onwards[i]; @@ -230,47 +227,41 @@ module.exports = function(RED) { } } - function send2ports(onward, msg) { + function sendMessages(onward, msg) { var parts = msg.parts; var gid = parts.id; received[gid] = ((gid in received) ? received[gid] : 0) +1; var send_ok = (received[gid] === parts.count); - if (!(gid in pending_out)) { - pending_out[gid] = { + if (!(gid in pendingOut)) { + pendingOut[gid] = { onwards: [] }; } - var group = pending_out[gid]; + var group = pendingOut[gid]; var onwards = group.onwards; onwards.push(onward); - pending_count++; + pendingCount++; if (send_ok) { - send_group(onwards, onward.length, msg); - pending_count -= onward.length; - delete pending_out[gid]; + sendGroup(onwards, onward.length, msg); + pendingCount -= onward.length; + delete pendingOut[gid]; delete received[gid]; } - var max_msgs = max_kept_msgs_count(node); - if ((max_msgs > 0) && (pending_count > max_msgs)) { - clear_pending(); + var max_msgs = getMaxKeptCount(); + if ((max_msgs > 0) && (pendingCount > max_msgs)) { + clearPending(); node.error(RED._("switch.errors.too-many"), msg); } } - function msg_has_parts(msg) { - if (msg.hasOwnProperty("parts")) { - var parts = msg.parts; - return (parts.hasOwnProperty("id") && - parts.hasOwnProperty("index")); - } - return false; - } + function processMessage(msg, check_parts) { + var has_parts = msg.hasOwnProperty("parts") && + msg.parts.hasOwnProperty("id") && + msg.parts.hasOwnProperty("index"); - function process_msg(msg, check_parts) { - var has_parts = msg_has_parts(msg); - if (needs_count && check_parts && has_parts && - add2pending_in(msg)) { + if (needsCount && check_parts && has_parts && + addMessageToPending(msg)) { return; } var onward = []; @@ -344,27 +335,27 @@ module.exports = function(RED) { node.send(onward); } else { - send2ports(onward, msg); + sendMessages(onward, msg); } } catch(err) { node.warn(err); } } - function clear_pending() { - pending_count = 0; - pending_id = 0; - pending_in = {}; - pending_out = {}; + function clearPending() { + pendingCount = 0; + pendingId = 0; + pendingIn = {}; + pendingOut = {}; received = {}; } this.on('input', function(msg) { - process_msg(msg, true); + processMessage(msg, true); }); this.on('close', function() { - clear_pending(); + clearPending(); }); } From 9c00492dc25922d846c1a82fee76045e594a2a4f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 8 Jul 2018 23:06:52 +0100 Subject: [PATCH 22/34] WIP: create async Switch node helper functions --- nodes/core/logic/10-switch.js | 103 +++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index ee3b4ef12..a58ac91ca 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -74,6 +74,81 @@ module.exports = function(RED) { return _maxKeptCount; } + function getProperty(node,msg) { + return new Promise((resolve,reject) => { + if (node.propertyType === 'jsonata') { + try { + resolve(RED.util.evaluateJSONataExpression(node.property,msg)); + } catch(err) { + // TODO: proper invalid expr message + reject(err); + } + } else { + resolve(RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg)); + } + }); + } + + function getV1(node,msg,rule,hasParts) { + return new Promise( (resolve,reject) => { + if (rule.vt === 'prev') { + resolve(node.previousValue); + } else if (rule.vt === 'jsonata') { + try { + var exp = rule.v; + if (rule.t === 'jsonata_exp') { + if (hasParts) { + exp.assign("I", msg.parts.index); + exp.assign("N", msg.parts.count); + } + } + resolve(RED.util.evaluateJSONataExpression(exp,msg)); + } catch(err) { + reject(RED._("switch.errors.invalid-expr",{error:err.message})); + } + } else if (rule.vt === 'json') { + resolve("json"); + } else if (rule.vt === 'null') { + resolve("null"); + } else { + RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) { + if (err) { + resolve(undefined); + } else { + resolve(value); + } + }); + } + }); + } + + function getV2(node,msg,rule) { + return new Promise((resolve,reject) => { + var v2 = rule.v2; + if (rule.v2t === 'prev') { + resolve(node.previousValue); + } else if (rule.v2t === 'jsonata') { + try { + resolve(RED.util.evaluateJSONataExpression(rule.v2,msg)); + } catch(err) { + reject(RED._("switch.errors.invalid-expr",{error:err.message})); + } + } else if (typeof v2 !== 'undefined') { + RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) { + if (err) { + resolve(undefined); + } else { + resolve(value); + } + }); + } else { + resolve(v2); + } + }) + } + + + function SwitchNode(n) { RED.nodes.createNode(this, n); this.rules = n.rules || []; @@ -227,7 +302,7 @@ module.exports = function(RED) { } } - function sendMessages(onward, msg) { + function sendGroupMessages(onward, msg) { var parts = msg.parts; var gid = parts.id; received[gid] = ((gid in received) ? received[gid] : 0) +1; @@ -255,35 +330,43 @@ module.exports = function(RED) { } } - function processMessage(msg, check_parts) { - var has_parts = msg.hasOwnProperty("parts") && + + + function processMessage(msg, checkParts) { + var hasParts = msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("id") && msg.parts.hasOwnProperty("index"); - if (needsCount && check_parts && has_parts && + if (needsCount && checkParts && hasParts && addMessageToPending(msg)) { return; } var onward = []; try { var prop; + + // getProperty if (node.propertyType === 'jsonata') { prop = RED.util.evaluateJSONataExpression(node.property,msg); } else { prop = RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg); } + // end getProperty + var elseflag = true; for (var i=0; i Date: Mon, 9 Jul 2018 11:30:53 +0100 Subject: [PATCH 23/34] Add async property handling to Switch node --- nodes/core/logic/10-switch.js | 219 +++++++++++++----------- red/runtime/util.js | 13 +- test/nodes/core/logic/10-switch_spec.js | 26 ++- 3 files changed, 149 insertions(+), 109 deletions(-) diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index a58ac91ca..d15be39de 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -84,7 +84,13 @@ module.exports = function(RED) { reject(err); } } else { - resolve(RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg)); + RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => { + if (err) { + resolve(undefined); + } else { + resolve(value); + } + }); } }); } @@ -147,6 +153,56 @@ module.exports = function(RED) { }) } + function applyRule(node, msg, property, state) { + return new Promise((resolve,reject) => { + + var rule = node.rules[state.currentRule]; + var v1,v2; + + getV1(node,msg,rule,state.hasParts).then(value => { + v1 = value; + }).then(()=>getV2(node,msg,rule)).then(value => { + v2 = value; + }).then(() => { + if (rule.t == "else") { + property = state.elseflag; + state.elseflag = true; + } + if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) { + state.onward.push(msg); + state.elseflag = false; + if (node.checkall == "false") { + return resolve(false); + } + } else { + state.onward.push(null); + } + resolve(state.currentRule < node.rules.length - 1); + }); + }) + } + + function applyRules(node, msg, property,state) { + if (!state) { + state = { + currentRule: 0, + elseflag: true, + onward: [], + hasParts: msg.hasOwnProperty("parts") && + msg.parts.hasOwnProperty("id") && + msg.parts.hasOwnProperty("index") + } + } + return applyRule(node,msg,property,state).then(hasMore => { + if (hasMore) { + state.currentRule++; + return applyRules(node,msg,property,state); + } else { + node.previousValue = property; + return state.onward; + } + }); + } function SwitchNode(n) { @@ -248,23 +304,23 @@ module.exports = function(RED) { function addMessageToPending(msg) { var parts = msg.parts; - if (parts.hasOwnProperty("id") && - parts.hasOwnProperty("index")) { - var group = addMessageToGroup(parts.id, msg, parts); - var msgs = group.msgs; - var count = group.count; - if (count === msgs.length) { - for (var i = 0; i < msgs.length; i++) { - var msg = msgs[i]; + // We've already checked the msg.parts has the require bits + var group = addMessageToGroup(parts.id, msg, parts); + var msgs = group.msgs; + var count = group.count; + if (count === msgs.length) { + // We have a complete group - send the individual parts + return msgs.reduce((promise, msg) => { + return promise.then((result) => { msg.parts.count = count; - processMessage(msg, false); - } + return processMessage(msg, false); + }) + }, Promise.resolve()).then( () => { pendingCount -= group.msgs.length; delete pendingIn[parts.id]; - } - return true; + }); } - return false; + return Promise.resolve(); } function sendGroup(onwards, port_count) { @@ -332,103 +388,28 @@ module.exports = function(RED) { + + function processMessage(msg, checkParts) { var hasParts = msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("id") && msg.parts.hasOwnProperty("index"); - if (needsCount && checkParts && hasParts && - addMessageToPending(msg)) { - return; + if (needsCount && checkParts && hasParts) { + return addMessageToPending(msg); } - var onward = []; - try { - var prop; - - // getProperty - if (node.propertyType === 'jsonata') { - prop = RED.util.evaluateJSONataExpression(node.property,msg); - } else { - prop = RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg); - } - // end getProperty - - var elseflag = true; - for (var i=0; i applyRules(node,msg,property)) + .then(onward => { + if (!repair || !hasParts) { + node.send(onward); } - } else if (rule.vt === 'json') { - v1 = "json"; - } else if (rule.vt === 'null') { - v1 = "null"; - } else { - try { - v1 = RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg); - } catch(err) { - v1 = undefined; + else { + sendGroupMessages(onward, msg); } - } - //// end getV1 - - //// getV2 - v2 = rule.v2; - if (rule.v2t === 'prev') { - v2 = node.previousValue; - } else if (rule.v2t === 'jsonata') { - try { - v2 = RED.util.evaluateJSONataExpression(rule.v2,msg); - } catch(err) { - node.error(RED._("switch.errors.invalid-expr",{error:err.message})); - return; - } - } else if (typeof v2 !== 'undefined') { - try { - v2 = RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg); - } catch(err) { - v2 = undefined; - } - } - //// end getV2 - - - if (rule.t == "else") { test = elseflag; elseflag = true; } - if (operators[rule.t](test,v1,v2,rule.case,msg.parts)) { - onward.push(msg); - elseflag = false; - if (node.checkall == "false") { break; } - } else { - onward.push(null); - } - } - node.previousValue = prop; - if (!repair || !hasParts) { - node.send(onward); - } - else { - sendGroupMessages(onward, msg); - } - } catch(err) { - node.warn(err); - } + }).catch(err => { + node.warn(err); + }); } function clearPending() { @@ -439,8 +420,38 @@ module.exports = function(RED) { received = {}; } + var pendingMessages = []; + var activeMessagePromise = null; + var processMessageQueue = function(msg) { + if (msg) { + // A new message has arrived - add it to the message queue + pendingMessages.push(msg); + if (activeMessagePromise !== null) { + // The node is currently processing a message, so do nothing + // more with this message + return; + } + } + if (pendingMessages.length === 0) { + // There are no more messages to process, clear the active flag + // and return + activeMessagePromise = null; + return; + } + + // There are more messages to process. Get the next message and + // start processing it. Recurse back in to check for any more + var nextMsg = pendingMessages.shift(); + activeMessagePromise = processMessage(nextMsg,true) + .then(processMessageQueue) + .catch((err) => { + node.error(err,nextMsg); + return processMessageQueue(); + }); + } + this.on('input', function(msg) { - processMessage(msg, true); + processMessageQueue(msg, true); }); this.on('close', function() { diff --git a/red/runtime/util.js b/red/runtime/util.js index 6279c416c..7ffe737b3 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -350,7 +350,16 @@ function evaluateNodeProperty(value, type, node, msg, callback) { var data = JSON.parse(value); result = Buffer.from(data); } else if (type === 'msg' && msg) { - result = getMessageProperty(msg,value); + try { + result = getMessageProperty(msg,value); + } catch(err) { + if (callback) { + callback(err); + } else { + throw err; + } + return; + } } else if ((type === 'flow' || type === 'global') && node) { var contextKey = parseContextStore(value); result = node.context()[type].get(contextKey.key,contextKey.store,callback); @@ -366,7 +375,7 @@ function evaluateNodeProperty(value, type, node, msg, callback) { result = evaluteEnvProperty(value); } if (callback) { - callback(result); + callback(null,result); } else { return result; } diff --git a/test/nodes/core/logic/10-switch_spec.js b/test/nodes/core/logic/10-switch_spec.js index e655a3fba..5159bf42e 100644 --- a/test/nodes/core/logic/10-switch_spec.js +++ b/test/nodes/core/logic/10-switch_spec.js @@ -460,7 +460,7 @@ describe('switch Node', function() { } catch(err) { done(err); } - },100) + },500) }); }); @@ -599,7 +599,7 @@ describe('switch Node', function() { it('should take head of message sequence (w. context)', function(done) { var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"head","v":"count",vt:"global"}],checkall:false,repair:true,outputs:1,wires:[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; - customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [0, 1, 2], true, + customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [0, 1, 2], true, function(node) { node.context().global.set("count", 3); }, done); @@ -642,7 +642,7 @@ describe('switch Node', function() { {id:"helperNode1", type:"helper", wires:[]}]; customFlowSwitchTest(flow, true, 9, done); }); - + it('should be able to use $I in JSONata expression', function(done) { var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"jsonata_exp","v":"$I % 2 = 1",vt:"jsonata"}],checkall:true,repair:true,outputs:1,wires:[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -821,4 +821,24 @@ describe('switch Node', function() { n1.receive({payload:1, parts:{index:0, count:4, id:222}}); }); }); + + it('should handle invalid jsonata expression', function(done) { + + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"$invalidExpression(payload)",propertyType:"jsonata",rules:[{"t":"btwn","v":"$sqrt(16)","vt":"jsonata","v2":"$sqrt(36)","v2t":"jsonata"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(switchNode, flow, function() { + var n1 = helper.getNode("switchNode1"); + setTimeout(function() { + var logEvents = helper.log().args.filter(function (evt) { + return evt[0].type == "switch"; + }); + var evt = logEvents[0][0]; + evt.should.have.property('id', "switchNode1"); + evt.should.have.property('type', "switch"); + done(); + }, 150); + n1.receive({payload:1}); + }); + }); + }); From b0d7e11d482378440600cba48557ed688f9cb988 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 9 Jul 2018 12:40:25 +0100 Subject: [PATCH 24/34] Fix evaluateNodeProperty handling of unknown types --- red/runtime/util.js | 2 +- test/red/runtime/util_spec.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/red/runtime/util.js b/red/runtime/util.js index 7ffe737b3..f4c0e1fa7 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -335,7 +335,7 @@ var parseContextStore = function(key) { } function evaluateNodeProperty(value, type, node, msg, callback) { - var result; + var result = value; if (type === 'str') { result = ""+value; } else if (type === 'num') { diff --git a/test/red/runtime/util_spec.js b/test/red/runtime/util_spec.js index 1b7438efd..f7bd3e950 100644 --- a/test/red/runtime/util_spec.js +++ b/test/red/runtime/util_spec.js @@ -307,6 +307,10 @@ describe("red/util", function() { },{}); result.should.eql("123"); }); + it('returns null', function() { + var result = util.evaluateNodeProperty(null,'null'); + (result === null).should.be.true(); + }) describe('environment variable', function() { before(function() { process.env.NR_TEST_A = "foo"; From d7adff9a654f1a74b2dd0c845cf3ea155620f76c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 9 Jul 2018 14:12:44 +0100 Subject: [PATCH 25/34] Add async message handling to Trigger node --- nodes/core/core/89-trigger.js | 187 ++++++++++++++++++------ nodes/core/logic/10-switch.js | 2 +- test/nodes/core/core/89-trigger_spec.js | 6 +- 3 files changed, 143 insertions(+), 52 deletions(-) diff --git a/nodes/core/core/89-trigger.js b/nodes/core/core/89-trigger.js index f5fd28b7f..48a595ccc 100644 --- a/nodes/core/core/89-trigger.js +++ b/nodes/core/core/89-trigger.js @@ -76,8 +76,43 @@ module.exports = function(RED) { var node = this; node.topics = {}; - this.on("input", function(msg) { + var pendingMessages = []; + var activeMessagePromise = null; + var processMessageQueue = function(msg) { + if (msg) { + // A new message has arrived - add it to the message queue + pendingMessages.push(msg); + if (activeMessagePromise !== null) { + // The node is currently processing a message, so do nothing + // more with this message + return; + } + } + if (pendingMessages.length === 0) { + // There are no more messages to process, clear the active flag + // and return + activeMessagePromise = null; + return; + } + + // There are more messages to process. Get the next message and + // start processing it. Recurse back in to check for any more + var nextMsg = pendingMessages.shift(); + activeMessagePromise = processMessage(nextMsg) + .then(processMessageQueue) + .catch((err) => { + node.error(err,nextMsg); + return processMessageQueue(); + }); + } + + this.on('input', function(msg) { + processMessageQueue(msg); + }); + + var processMessage = function(msg) { var topic = msg.topic || "_none"; + var promise; if (node.bytopic === "all") { topic = "_none"; } node.topics[topic] = node.topics[topic] || {}; if (msg.hasOwnProperty("reset") || ((node.reset !== '') && msg.hasOwnProperty("payload") && (msg.payload !== null) && msg.payload.toString && (msg.payload.toString() == node.reset)) ) { @@ -88,48 +123,88 @@ module.exports = function(RED) { } else { if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) { + promise = Promise.resolve(); if (node.op2type === "pay" || node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } else if (node.op2Templated) { node.topics[topic].m2 = mustache.render(node.op2,msg); } else if (node.op2type !== "nul") { - node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); - } - - if (node.op1type === "pay") { } - else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); } - else if (node.op1type !== "nul") { - msg.payload = RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg); - } - - if (node.duration === 0) { node.topics[topic].tout = 0; } - else if (node.loop === true) { - /* istanbul ignore else */ - if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); } - /* istanbul ignore else */ - if (node.op1type !== "nul") { - var msg2 = RED.util.cloneMessage(msg); - node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration); - } - } - else { - if (!node.topics[topic].tout) { - node.topics[topic].tout = setTimeout(function() { - var msg2 = null; - if (node.op2type !== "nul") { - msg2 = RED.util.cloneMessage(msg); - if (node.op2type === "flow" || node.op2type === "global") { - node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); - } - msg2.payload = node.topics[topic].m2; - delete node.topics[topic]; - node.send(msg2); + promise = new Promise((resolve,reject) => { + RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { + if (err) { + reject(err); + } else { + node.topics[topic].m2 = value; + resolve(); } - else { delete node.topics[topic]; } - node.status({}); - }, node.duration); - } + }); + }); } - node.status({fill:"blue",shape:"dot",text:" "}); - if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); } + + return promise.then(() => { + promise = Promise.resolve(); + if (node.op1type === "pay") { } + else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); } + else if (node.op1type !== "nul") { + promise = new Promise((resolve,reject) => { + RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg,(err,value) => { + if (err) { + reject(err); + } else { + msg.payload = value; + resolve(); + } + }); + }); + } + return promise.then(() => { + if (node.duration === 0) { node.topics[topic].tout = 0; } + else if (node.loop === true) { + /* istanbul ignore else */ + if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); } + /* istanbul ignore else */ + if (node.op1type !== "nul") { + var msg2 = RED.util.cloneMessage(msg); + node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration); + } + } + else { + if (!node.topics[topic].tout) { + node.topics[topic].tout = setTimeout(function() { + var msg2 = null; + if (node.op2type !== "nul") { + var promise = Promise.resolve(); + msg2 = RED.util.cloneMessage(msg); + if (node.op2type === "flow" || node.op2type === "global") { + promise = new Promise((resolve,reject) => { + RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { + if (err) { + reject(err); + } else { + node.topics[topic].m2 = value; + resolve(); + } + }); + }); + } + promise.then(() => { + msg2.payload = node.topics[topic].m2; + delete node.topics[topic]; + node.send(msg2); + node.status({}); + }).catch(err => { + node.error(err); + }); + } else { + delete node.topics[topic]; + node.status({}); + } + + }, node.duration); + } + } + node.status({fill:"blue",shape:"dot",text:" "}); + if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); } + }); + }); } else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) { /* istanbul ignore else */ @@ -138,25 +213,43 @@ module.exports = function(RED) { if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); } node.topics[topic].tout = setTimeout(function() { var msg2 = null; + var promise = Promise.resolve(); + if (node.op2type !== "nul") { if (node.op2type === "flow" || node.op2type === "global") { - node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); - } - if (node.topics[topic] !== undefined) { - msg2 = RED.util.cloneMessage(msg); - msg2.payload = node.topics[topic].m2; + promise = new Promise((resolve,reject) => { + RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { + if (err) { + reject(err); + } else { + node.topics[topic].m2 = value; + resolve(); + } + }); + }); } } - delete node.topics[topic]; - node.status({}); - node.send(msg2); + promise.then(() => { + if (node.op2type !== "nul") { + if (node.topics[topic] !== undefined) { + msg2 = RED.util.cloneMessage(msg); + msg2.payload = node.topics[topic].m2; + } + } + delete node.topics[topic]; + node.status({}); + node.send(msg2); + }).catch(err => { + node.error(err); + }); }, node.duration); } else { if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } } } - }); + return Promise.resolve(); + } this.on("close", function() { for (var t in node.topics) { /* istanbul ignore else */ diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index d15be39de..79b6e20b3 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -451,7 +451,7 @@ module.exports = function(RED) { } this.on('input', function(msg) { - processMessageQueue(msg, true); + processMessageQueue(msg); }); this.on('close', function() { diff --git a/test/nodes/core/core/89-trigger_spec.js b/test/nodes/core/core/89-trigger_spec.js index d07ab2a9b..f19bc42fe 100644 --- a/test/nodes/core/core/89-trigger_spec.js +++ b/test/nodes/core/core/89-trigger_spec.js @@ -288,7 +288,7 @@ describe('trigger node', function() { it('should be able to return things from flow and global context variables', function(done) { var spy = sinon.stub(RED.util, 'evaluateNodeProperty', - function(arg1, arg2, arg3, arg4) { return arg1; } + function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } } ); var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"flow", op2:"bar", op2type:"global", duration:"20", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; @@ -386,7 +386,7 @@ describe('trigger node', function() { it('should be able to extend the delay', function(done) { this.timeout(5000); // add extra time for flake var spy = sinon.stub(RED.util, 'evaluateNodeProperty', - function(arg1, arg2, arg3, arg4) { return arg1; } + function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } } ); var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"flow", op1:"foo", op2:"bar", op2type:"global", duration:"100", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; @@ -428,12 +428,10 @@ describe('trigger node', function() { n2.on("input", function(msg) { try { if (c === 0) { - console.log(c,Date.now() - ss,msg); msg.should.have.a.property("payload", "Hello"); c += 1; } else { - console.log(c,Date.now() - ss,msg); msg.should.have.a.property("payload", "World"); (Date.now() - ss).should.be.greaterThan(150); done(); From b2f06b6777acba3de5e70efe969c355450dfcbc1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 9 Jul 2018 15:12:09 +0100 Subject: [PATCH 26/34] Add async mode to evaluateJSONataExpression --- red/runtime/util.js | 33 +++++++++++++++++++++++++++++++-- test/red/runtime/util_spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/red/runtime/util.js b/red/runtime/util.js index f4c0e1fa7..b6a5a7e7b 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -394,15 +394,44 @@ function prepareJSONataExpression(value,node) { }) expr.registerFunction('clone', cloneMessage, '<(oa)-:o>'); expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value); + expr._node = node; return expr; } -function evaluateJSONataExpression(expr,msg) { +function evaluateJSONataExpression(expr,msg,callback) { var context = msg; if (expr._legacyMode) { context = {msg:msg}; } - return expr.evaluate(context); + var bindings = {}; + + if (callback) { + // If callback provided, need to override the pre-assigned sync + // context functions to be their async variants + bindings.flowContext = function(val) { + return new Promise((resolve,reject) => { + expr._node.context().flow.get(val, function(err,value) { + if (err) { + reject(err); + } else { + resolve(value); + } + }) + }); + } + bindings.globalContext = function(val) { + return new Promise((resolve,reject) => { + expr._node.context().global.get(val, function(err,value) { + if (err) { + reject(err); + } else { + resolve(value); + } + }) + }); + } + } + return expr.evaluate(context, bindings, callback); } diff --git a/test/red/runtime/util_spec.js b/test/red/runtime/util_spec.js index f7bd3e950..1be194f42 100644 --- a/test/red/runtime/util_spec.js +++ b/test/red/runtime/util_spec.js @@ -458,6 +458,30 @@ describe("red/util", function() { var result = util.evaluateJSONataExpression(expr,{payload:"hello"}); should.not.exist(result); }); + it('handles async flow context access', function(done) { + var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}}); + util.evaluateJSONataExpression(expr,{payload:"hello"},function(err,value) { + try { + should.not.exist(err); + value.should.eql("bar"); + done(); + } catch(err2) { + done(err2); + } + }); + }) + it('handles async global context access', function(done) { + var expr = util.prepareJSONataExpression('$globalContext("foo")',{context:function() { return {global:{get: function(key,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}}); + util.evaluateJSONataExpression(expr,{payload:"hello"},function(err,value) { + try { + should.not.exist(err); + value.should.eql("bar"); + done(); + } catch(err2) { + done(err2); + } + }); + }) }); From 807b512ef7feab6e2bef10de47a535f20005a9ac Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 9 Jul 2018 21:56:39 +0100 Subject: [PATCH 27/34] Add JSONata async support to Switch and Change nodes --- nodes/core/logic/10-switch.js | 47 +++--- nodes/core/logic/15-change.js | 293 ++++++++++++++++++++-------------- 2 files changed, 195 insertions(+), 145 deletions(-) diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index 79b6e20b3..df5a52023 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -77,12 +77,13 @@ module.exports = function(RED) { function getProperty(node,msg) { return new Promise((resolve,reject) => { if (node.propertyType === 'jsonata') { - try { - resolve(RED.util.evaluateJSONataExpression(node.property,msg)); - } catch(err) { - // TODO: proper invalid expr message - reject(err); - } + RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => { + if (err) { + reject(RED._("switch.errors.invalid-expr",{error:err.message})); + } else { + resolve(value); + } + }); } else { RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => { if (err) { @@ -100,18 +101,20 @@ module.exports = function(RED) { if (rule.vt === 'prev') { resolve(node.previousValue); } else if (rule.vt === 'jsonata') { - try { - var exp = rule.v; - if (rule.t === 'jsonata_exp') { - if (hasParts) { - exp.assign("I", msg.parts.index); - exp.assign("N", msg.parts.count); - } + var exp = rule.v; + if (rule.t === 'jsonata_exp') { + if (hasParts) { + exp.assign("I", msg.parts.index); + exp.assign("N", msg.parts.count); } - resolve(RED.util.evaluateJSONataExpression(exp,msg)); - } catch(err) { - reject(RED._("switch.errors.invalid-expr",{error:err.message})); } + RED.util.evaluateJSONataExpression(exp,msg,(err,value) => { + if (err) { + reject(RED._("switch.errors.invalid-expr",{error:err.message})); + } else { + resolve(value); + } + }); } else if (rule.vt === 'json') { resolve("json"); } else if (rule.vt === 'null') { @@ -134,11 +137,13 @@ module.exports = function(RED) { if (rule.v2t === 'prev') { resolve(node.previousValue); } else if (rule.v2t === 'jsonata') { - try { - resolve(RED.util.evaluateJSONataExpression(rule.v2,msg)); - } catch(err) { - reject(RED._("switch.errors.invalid-expr",{error:err.message})); - } + RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => { + if (err) { + reject(RED._("switch.errors.invalid-expr",{error:err.message})); + } else { + resolve(value); + } + }); } else if (typeof v2 !== 'undefined') { RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) { if (err) { diff --git a/nodes/core/logic/15-change.js b/nodes/core/logic/15-change.js index 4094f68e0..d98bf6bde 100644 --- a/nodes/core/logic/15-change.js +++ b/nodes/core/logic/15-change.js @@ -98,138 +98,183 @@ module.exports = function(RED) { } } - function applyRule(msg,rule) { - return new Promise(function(resolve, reject){ - try { - var property = rule.p; - var value = rule.to; - if (rule.tot === 'json') { - value = JSON.parse(rule.to); - } else if (rule.tot === 'bin') { - value = Buffer.from(JSON.parse(rule.to)) - } - var current; - var fromValue; - var fromType; - var fromRE; - if (rule.tot === "msg") { - value = RED.util.getMessageProperty(msg,rule.to); - } else if (rule.tot === 'flow') { - value = node.context().flow.get(rule.to); - } else if (rule.tot === 'global') { - value = node.context().global.get(rule.to); - } else if (rule.tot === 'date') { - value = Date.now(); - } else if (rule.tot === 'jsonata') { - try{ - value = RED.util.evaluateJSONataExpression(rule.to,msg); - } catch(err) { - node.error(RED._("change.errors.invalid-expr",{error:err.message}),msg); - return; + function getToValue(msg,rule) { + var value = rule.to; + if (rule.tot === 'json') { + value = JSON.parse(rule.to); + } else if (rule.tot === 'bin') { + value = Buffer.from(JSON.parse(rule.to)) + } + if (rule.tot === "msg") { + value = RED.util.getMessageProperty(msg,rule.to); + } else if (rule.tot === 'flow') { + value = node.context().flow.get(rule.to); + } else if (rule.tot === 'global') { + value = node.context().global.get(rule.to); + } else if (rule.tot === 'date') { + value = Date.now(); + } else if (rule.tot === 'jsonata') { + return new Promise((resolve,reject) => { + RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => { + if (err) { + reject(RED._("change.errors.invalid-expr",{error:err.message})) + } else { + resolve(value); } - } - if (rule.t === 'change') { - if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { - if (rule.fromt === "msg") { - fromValue = RED.util.getMessageProperty(msg,rule.from); - } else if (rule.fromt === 'flow') { - fromValue = node.context().flow.get(rule.from); - } else if (rule.fromt === 'global') { - fromValue = node.context().global.get(rule.from); - } - if (typeof fromValue === 'number' || fromValue instanceof Number) { - fromType = 'num'; - } else if (typeof fromValue === 'boolean') { - fromType = 'bool' - } else if (fromValue instanceof RegExp) { - fromType = 're'; - fromRE = fromValue; - } else if (typeof fromValue === 'string') { - fromType = 'str'; - fromRE = fromValue.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - try { - fromRE = new RegExp(fromRE, "g"); - } catch (e) { - valid = false; - reject(RED._("change.errors.invalid-from",{error:e.message})); - return; + }); + }); + } + return Promise.resolve(value); + } + function getFromValue(msg,rule) { + var fromValue; + var fromType; + var fromRE; + if (rule.t === 'change') { + if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { + return new Promise((resolve,reject) => { + if (rule.fromt === "msg") { + resolve(RED.util.getMessageProperty(msg,rule.from)); + } else if (rule.fromt === 'flow' || rule.fromt === 'global') { + node.context()[rule.fromt].get(rule.from,(err,fromValue) => { + if (err) { + reject(err); + } else { + resolve(fromValue); } - } else { - reject(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)})); - return + }); + } + }).then(fromValue => { + if (typeof fromValue === 'number' || fromValue instanceof Number) { + fromType = 'num'; + } else if (typeof fromValue === 'boolean') { + fromType = 'bool' + } else if (fromValue instanceof RegExp) { + fromType = 're'; + fromRE = fromValue; + } else if (typeof fromValue === 'string') { + fromType = 'str'; + fromRE = fromValue.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + try { + fromRE = new RegExp(fromRE, "g"); + } catch (e) { + reject(new Error(RED._("change.errors.invalid-from",{error:e.message}))); + return; } } else { - fromType = rule.fromt; - fromValue = rule.from; - fromRE = rule.fromRE; + reject(new Error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)}))); + return; } - } - if (rule.pt === 'msg') { - if (rule.t === 'delete') { - RED.util.setMessageProperty(msg,property,undefined); - } else if (rule.t === 'set') { - RED.util.setMessageProperty(msg,property,value); - } else if (rule.t === 'change') { - current = RED.util.getMessageProperty(msg,property); - if (typeof current === 'string') { - if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { - // str representation of exact from number/boolean - // only replace if they match exactly - RED.util.setMessageProperty(msg,property,value); - } else { - current = current.replace(fromRE,value); - RED.util.setMessageProperty(msg,property,current); - } - } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { - if (current == Number(fromValue)) { - RED.util.setMessageProperty(msg,property,value); - } - } else if (typeof current === 'boolean' && fromType === 'bool') { - if (current.toString() === fromValue) { - RED.util.setMessageProperty(msg,property,value); - } - } + return { + fromType, + fromValue, + fromRE } - } - else { - var target; - if (rule.pt === 'flow') { - target = node.context().flow; - } else if (rule.pt === 'global') { - target = node.context().global; - } - if (target) { - if (rule.t === 'delete') { - target.set(property,undefined); - } else if (rule.t === 'set') { - target.set(property,value); - } else if (rule.t === 'change') { - current = target.get(property); - if (typeof current === 'string') { - if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { - // str representation of exact from number/boolean - // only replace if they match exactly - target.set(property,value); - } else { - current = current.replace(fromRE,value); - target.set(property,current); - } - } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { - if (current == Number(fromValue)) { - target.set(property,value); - } - } else if (typeof current === 'boolean' && fromType === 'bool') { - if (current.toString() === fromValue) { - target.set(property,value); - } - } - } - } - } - } catch(err) {/*console.log(err.stack)*/} - resolve(msg); + }); + } else { + fromType = rule.fromt; + fromValue = rule.from; + fromRE = rule.fromRE; + } + } + return Promise.resolve({ + fromType, + fromValue, + fromRE }); } + function applyRule(msg,rule) { + var property = rule.p; + var current; + var fromValue; + var fromType; + var fromRE; + try { + return getToValue(msg,rule).then(value => { + return getFromValue(msg,rule).then(fromParts => { + fromValue = fromParts.fromValue; + fromType = fromParts.fromType; + fromRE = fromParts.fromRE; + if (rule.pt === 'msg') { + try { + if (rule.t === 'delete') { + RED.util.setMessageProperty(msg,property,undefined); + } else if (rule.t === 'set') { + RED.util.setMessageProperty(msg,property,value); + } else if (rule.t === 'change') { + current = RED.util.getMessageProperty(msg,property); + if (typeof current === 'string') { + if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { + // str representation of exact from number/boolean + // only replace if they match exactly + RED.util.setMessageProperty(msg,property,value); + } else { + current = current.replace(fromRE,value); + RED.util.setMessageProperty(msg,property,current); + } + } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { + if (current == Number(fromValue)) { + RED.util.setMessageProperty(msg,property,value); + } + } else if (typeof current === 'boolean' && fromType === 'bool') { + if (current.toString() === fromValue) { + RED.util.setMessageProperty(msg,property,value); + } + } + } + } catch(err) {} + return msg; + } else if (rule.pt === 'flow' || rule.pt === 'global') { + return new Promise((resolve,reject) => { + var target = node.context()[rule.pt]; + var callback = err => { + if (err) { + reject(err); + } else { + resolve(msg); + } + } + if (rule.t === 'delete') { + target.set(property,undefined,callback); + } else if (rule.t === 'set') { + target.set(property,value,callback); + } else if (rule.t === 'change') { + target.get(property,(err,current) => { + if (err) { + reject(err); + return; + } + if (typeof current === 'string') { + if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { + // str representation of exact from number/boolean + // only replace if they match exactly + target.set(property,value,callback); + } else { + current = current.replace(fromRE,value); + target.set(property,current,callback); + } + } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { + if (current == Number(fromValue)) { + target.set(property,value,callback); + } + } else if (typeof current === 'boolean' && fromType === 'bool') { + if (current.toString() === fromValue) { + target.set(property,value,callback); + } + } + }); + } + }); + } + }); + }).catch(err => { + node.error(err, msg); + return null; + }); + } catch(err) { + return Promise.resolve(msg); + } + } function applyRules(msg, currentRule) { var r = node.rules[currentRule]; var rulePromise; From d8d82e2ba3f617545fcc8d1fa38945d5e0575538 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 9 Jul 2018 23:06:51 +0100 Subject: [PATCH 28/34] Update sort node for async use of jsonata --- nodes/core/logic/18-sort.js | 216 ++++++++++++++++++++---------------- 1 file changed, 118 insertions(+), 98 deletions(-) diff --git a/nodes/core/logic/18-sort.js b/nodes/core/logic/18-sort.js index e30535dc1..a3023e5f1 100644 --- a/nodes/core/logic/18-sort.js +++ b/nodes/core/logic/18-sort.js @@ -17,7 +17,7 @@ module.exports = function(RED) { "use strict"; - var _max_kept_msgs_count = undefined; + var _max_kept_msgs_count; function max_kept_msgs_count(node) { if (_max_kept_msgs_count === undefined) { @@ -32,30 +32,20 @@ module.exports = function(RED) { return _max_kept_msgs_count; } - function eval_jsonata(node, code, val) { - try { - return RED.util.evaluateJSONataExpression(code, val); - } - catch (e) { - node.error(RED._("sort.invalid-exp")); - throw e; - } - } - - function get_context_val(node, name, dval) { - var context = node.context(); - var val = context.get(name); - if (val === undefined) { - context.set(name, dval); - return dval; - } - return val; - } + // function get_context_val(node, name, dval) { + // var context = node.context(); + // var val = context.get(name); + // if (val === undefined) { + // context.set(name, dval); + // return dval; + // } + // return val; + // } function SortNode(n) { RED.nodes.createNode(this, n); var node = this; - var pending = get_context_val(node, 'pending', {}) + var pending = {};//get_context_val(node, 'pending', {}) var pending_count = 0; var pending_id = 0; var order = n.order || "ascending"; @@ -76,11 +66,10 @@ module.exports = function(RED) { } } var dir = (order === "ascending") ? 1 : -1; - var conv = as_num - ? function(x) { return Number(x); } - : function(x) { return x; }; + var conv = as_num ? function(x) { return Number(x); } + : function(x) { return x; }; - function gen_comp(key) { + function generateComparisonFunction(key) { return function(x, y) { var xp = conv(key(x)); var yp = conv(key(y)); @@ -90,74 +79,97 @@ module.exports = function(RED) { }; } - function send_group(group) { - var key = key_is_exp - ? function(msg) { - return eval_jsonata(node, key_exp, msg); - } - : function(msg) { - return RED.util.getMessageProperty(msg, key_prop); - }; - var comp = gen_comp(key); + function sortMessageGroup(group) { + var promise; var msgs = group.msgs; - try { - msgs.sort(comp); - } - catch (e) { - return; // not send when error - } - for (var i = 0; i < msgs.length; i++) { - var msg = msgs[i]; - msg.parts.index = i; - node.send(msg); - } - } - - function sort_payload(msg) { - var data = RED.util.getMessageProperty(msg, target_prop); - if (Array.isArray(data)) { - var key = key_is_exp - ? function(elem) { - return eval_jsonata(node, key_exp, elem); - } - : function(elem) { return elem; }; - var comp = gen_comp(key); + if (key_is_exp) { + var evaluatedDataPromises = msgs.map(msg => { + return new Promise((resolve,reject) => { + RED.util.evaluateJSONataExpression(key_exp, msg, (err, result) => { + resolve({ + item: msg, + sortValue: result + }) + }); + }) + }); + promise = Promise.all(evaluatedDataPromises).then(evaluatedElements => { + // Once all of the sort keys are evaluated, sort by them + var comp = generateComparisonFunction(elem=>elem.sortValue); + return evaluatedElements.sort(comp).map(elem=>elem.item); + }); + } else { + var key = function(msg) { + return ; + } + var comp = generateComparisonFunction(msg => RED.util.getMessageProperty(msg, key_prop)); try { - data.sort(comp); + msgs.sort(comp); } catch (e) { - return false; + return; // not send when error } - return true; + promise = Promise.resolve(msgs); } - return false; + return promise.then(msgs => { + for (var i = 0; i < msgs.length; i++) { + var msg = msgs[i]; + msg.parts.index = i; + node.send(msg); + } + }); } - function check_parts(parts) { - if (parts.hasOwnProperty("id") && - parts.hasOwnProperty("index")) { - return true; + function sortMessageProperty(msg) { + var data = RED.util.getMessageProperty(msg, target_prop); + if (Array.isArray(data)) { + if (key_is_exp) { + // key is an expression. Evaluated the expression for each item + // to get its sort value. As this could be async, need to do + // it first. + var evaluatedDataPromises = data.map(elem => { + return new Promise((resolve,reject) => { + RED.util.evaluateJSONataExpression(key_exp, elem, (err, result) => { + resolve({ + item: elem, + sortValue: result + }) + }); + }) + }) + return Promise.all(evaluatedDataPromises).then(evaluatedElements => { + // Once all of the sort keys are evaluated, sort by them + // and reconstruct the original message item with the newly + // sorted values. + var comp = generateComparisonFunction(elem=>elem.sortValue); + data = evaluatedElements.sort(comp).map(elem=>elem.item); + RED.util.setMessageProperty(msg, target_prop,data); + return true; + }) + } else { + var comp = generateComparisonFunction(elem=>elem); + try { + data.sort(comp); + } catch (e) { + return Promise.resolve(false); + } + return Promise.resolve(true); + } } - return false; + return Promise.resolve(false); } - function clear_pending() { + function removeOldestPending() { + var oldest; + var oldest_key; for(var key in pending) { - node.log(RED._("sort.clear"), pending[key].msgs[0]); - delete pending[key]; - } - pending_count = 0; - } - - function remove_oldest_pending() { - var oldest = undefined; - var oldest_key = undefined; - for(var key in pending) { - var item = pending[key]; - if((oldest === undefined) || - (oldest.seq_no > item.seq_no)) { - oldest = item; - oldest_key = key; + if (pending.hasOwnProperty(key)) { + var item = pending[key]; + if((oldest === undefined) || + (oldest.seq_no > item.seq_no)) { + oldest = item; + oldest_key = key; + } } } if(oldest !== undefined) { @@ -166,16 +178,18 @@ module.exports = function(RED) { } return 0; } - - function process_msg(msg) { + + function processMessage(msg) { if (target_is_prop) { - if (sort_payload(msg)) { - node.send(msg); - } - return; + sortMessageProperty(msg).then(send => { + if (send) { + node.send(msg); + } + }).catch(err => { + }); } var parts = msg.parts; - if (!check_parts(parts)) { + if (!parts.hasOwnProperty("id") || !parts.hasOwnProperty("index")) { return; } var gid = parts.id; @@ -195,23 +209,29 @@ module.exports = function(RED) { pending_count++; if (group.count === msgs.length) { delete pending[gid] - send_group(group); + sortMessageGroup(group); pending_count -= msgs.length; - } - var max_msgs = max_kept_msgs_count(node); - if ((max_msgs > 0) && (pending_count > max_msgs)) { - pending_count -= remove_oldest_pending(); - node.error(RED._("sort.too-many"), msg); + } else { + var max_msgs = max_kept_msgs_count(node); + if ((max_msgs > 0) && (pending_count > max_msgs)) { + pending_count -= removeOldestPending(); + node.error(RED._("sort.too-many"), msg); + } } } - + this.on("input", function(msg) { - process_msg(msg); + processMessage(msg); }); this.on("close", function() { - clear_pending(); - }) + for(var key in pending) { + if (pending.hasOwnProperty(key)) { + node.log(RED._("sort.clear"), pending[key].msgs[0]); + delete pending[key]; + } + } + pending_count = 0; }) } RED.nodes.registerType("sort", SortNode); From 57c1524a9a5de597c5d94e8c67d4390229c4c61f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 10 Jul 2018 11:24:57 +0100 Subject: [PATCH 29/34] Add async jsonata support to join node --- nodes/core/logic/17-split.js | 201 +++++++++++++++++++++++------------ 1 file changed, 132 insertions(+), 69 deletions(-) diff --git a/nodes/core/logic/17-split.js b/nodes/core/logic/17-split.js index 92b527d01..73231335f 100644 --- a/nodes/core/logic/17-split.js +++ b/nodes/core/logic/17-split.js @@ -233,7 +233,7 @@ module.exports = function(RED) { RED.nodes.registerType("split",SplitNode); - var _max_kept_msgs_count = undefined; + var _max_kept_msgs_count; function max_kept_msgs_count(node) { if (_max_kept_msgs_count === undefined) { @@ -252,7 +252,15 @@ module.exports = function(RED) { exp.assign("I", index); exp.assign("N", count); exp.assign("A", accum); - return RED.util.evaluateJSONataExpression(exp, msg); + return new Promise((resolve,reject) => { + RED.util.evaluateJSONataExpression(exp, msg, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); } function apply_f(exp, accum, count) { @@ -269,32 +277,37 @@ module.exports = function(RED) { return exp } - function reduce_and_send_group(node, group) { + function reduceAndSendGroup(node, group) { var is_right = node.reduce_right; var flag = is_right ? -1 : 1; var msgs = group.msgs; - var accum = eval_exp(node, node.exp_init, node.exp_init_type); - var reduce_exp = node.reduce_exp; - var reduce_fixup = node.reduce_fixup; - var count = group.count; - msgs.sort(function(x,y) { - var ix = x.parts.index; - var iy = y.parts.index; - if (ix < iy) return -flag; - if (ix > iy) return flag; - return 0; + return getInitialReduceValue(node, node.exp_init, node.exp_init_type).then(accum => { + var reduce_exp = node.reduce_exp; + var reduce_fixup = node.reduce_fixup; + var count = group.count; + msgs.sort(function(x,y) { + var ix = x.parts.index; + var iy = y.parts.index; + if (ix < iy) {return -flag;} + if (ix > iy) {return flag;} + return 0; + }); + + return msgs.reduce((promise, msg) => promise.then(accum => apply_r(reduce_exp, accum, msg, msg.parts.index, count)), Promise.resolve(accum)) + .then(accum => { + if(reduce_fixup !== undefined) { + accum = apply_f(reduce_fixup, accum, count); + } + node.send({payload: accum}); + }); + }).catch(err => { + throw new Error(RED._("join.errors.invalid-expr",{error:e.message})); }); - for(var msg of msgs) { - accum = apply_r(reduce_exp, accum, msg, msg.parts.index, count); - } - if(reduce_fixup !== undefined) { - accum = apply_f(reduce_fixup, accum, count); - } - node.send({payload: accum}); } function reduce_msg(node, msg) { - if(msg.hasOwnProperty('parts')) { + var promise; + if (msg.hasOwnProperty('parts')) { var parts = msg.parts; var pending = node.pending; var pending_count = node.pending_count; @@ -312,65 +325,82 @@ module.exports = function(RED) { var group = pending[gid]; var msgs = group.msgs; if(parts.hasOwnProperty('count') && - (group.count === undefined)) { + (group.count === undefined)) { group.count = count; } msgs.push(msg); pending_count++; + var completeProcess = function() { + node.pending_count = pending_count; + var max_msgs = max_kept_msgs_count(node); + if ((max_msgs > 0) && (pending_count > max_msgs)) { + node.pending = {}; + node.pending_count = 0; + var promise = Promise.reject(RED._("join.too-many")); + promise.catch(()=>{}); + return promise; + } + return Promise.resolve(); + } if(msgs.length === group.count) { delete pending[gid]; - try { - pending_count -= msgs.length; - reduce_and_send_group(node, group); - } catch(e) { - node.error(RED._("join.errors.invalid-expr",{error:e.message})); } + pending_count -= msgs.length; + promise = reduceAndSendGroup(node, group).then(completeProcess); + } else { + promise = completeProcess(); } - node.pending_count = pending_count; - var max_msgs = max_kept_msgs_count(node); - if ((max_msgs > 0) && (pending_count > max_msgs)) { - node.pending = {}; - node.pending_count = 0; - node.error(RED._("join.too-many"), msg); - } - } - else { + } else { node.send(msg); } + if (!promise) { + promise = Promise.resolve(); + } + return promise; } - function eval_exp(node, exp, exp_type) { - if(exp_type === "flow") { - return node.context().flow.get(exp); - } - else if(exp_type === "global") { - return node.context().global.get(exp); - } - else if(exp_type === "str") { - return exp; - } - else if(exp_type === "num") { - return Number(exp); - } - else if(exp_type === "bool") { - if (exp === 'true') { - return true; + function getInitialReduceValue(node, exp, exp_type) { + return new Promise((resolve,reject) => { + if(exp_type === "flow" || exp_type === "global") { + node.context()[exp_type].get(exp,(err,value) => { + if (err) { + reject(err); + } else { + resolve(value); + } + }); + return; + } else if(exp_type === "jsonata") { + var jexp = RED.util.prepareJSONataExpression(exp, node); + RED.util.evaluateJSONataExpression(jexp, {},(err,value) => { + if (err) { + reject(err); + } else { + resolve(value); + } + }); + return; } - else if (exp === 'false') { - return false; + var result; + if(exp_type === "str") { + result = exp; + } else if(exp_type === "num") { + result = Number(exp); + } else if(exp_type === "bool") { + if (exp === 'true') { + result = true; + } else if (exp === 'false') { + result = false; + } + } else if ((exp_type === "bin") || (exp_type === "json")) { + result = JSON.parse(exp); + } else if(exp_type === "date") { + result = Date.now(); + } else { + reject(new Error("unexpected initial value type")); + return; } - } - else if ((exp_type === "bin") || - (exp_type === "json")) { - return JSON.parse(exp); - } - else if(exp_type === "date") { - return Date.now(); - } - else if(exp_type === "jsonata") { - var jexp = RED.util.prepareJSONataExpression(exp, node); - return RED.util.evaluateJSONataExpression(jexp, {}); - } - throw new Error("unexpected initial value type"); + resolve(result); + }); } function JoinNode(n) { @@ -478,6 +508,40 @@ module.exports = function(RED) { node.send(group.msg); } + var pendingMessages = []; + var activeMessagePromise = null; + // In reduce mode, we must process messages fully in order otherwise + // groups may overlap and cause unexpected results. The use of JSONata + // means some async processing *might* occur if flow/global context is + // accessed. + var processReduceMessageQueue = function(msg) { + if (msg) { + // A new message has arrived - add it to the message queue + pendingMessages.push(msg); + if (activeMessagePromise !== null) { + // The node is currently processing a message, so do nothing + // more with this message + return; + } + } + if (pendingMessages.length === 0) { + // There are no more messages to process, clear the active flag + // and return + activeMessagePromise = null; + return; + } + + // There are more messages to process. Get the next message and + // start processing it. Recurse back in to check for any more + var nextMsg = pendingMessages.shift(); + activeMessagePromise = reduce_msg(node, nextMsg) + .then(processReduceMessageQueue) + .catch((err) => { + node.error(err,nextMsg); + return processReduceMessageQueue(); + }); + } + this.on("input", function(msg) { try { var property; @@ -516,8 +580,7 @@ module.exports = function(RED) { propertyIndex = msg.parts.index; } else if (node.mode === 'reduce') { - reduce_msg(node, msg); - return; + return processReduceMessageQueue(msg); } else { // Use the node configuration to identify all of the group information From d8cf86fd6f200805e588c0730d66c008218786d5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 10 Jul 2018 11:41:46 +0100 Subject: [PATCH 30/34] Add RED.utils.parseContextKey --- editor/js/ui/common/typedInput.js | 13 ++++--------- editor/js/ui/utils.js | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 74bbeb9ac..59db274f7 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -15,16 +15,11 @@ **/ (function($) { var contextParse = function(v) { - var parts = {}; - var m = /^#:\((\S+?)\)::(.*)$/.exec(v); - if (m) { - parts.option = m[1]; - parts.value = m[2]; - } else { - parts.value = v; - parts.option = RED.settings.context.default; + var parts = RED.utils.parseContextKey(v); + return { + option: parts.store, + value: parts.key } - return parts; } var contextExport = function(v,opt) { if (!opt) { diff --git a/editor/js/ui/utils.js b/editor/js/ui/utils.js index e64a33996..7da56ab78 100644 --- a/editor/js/ui/utils.js +++ b/editor/js/ui/utils.js @@ -828,6 +828,20 @@ RED.utils = (function() { return payload; } + function parseContextKey(key) { + var parts = {}; + var m = /^#:\((\S+?)\)::(.*)$/.exec(key); + if (m) { + parts.store = m[1]; + parts.key = m[2]; + } else { + parts.key = key; + if (RED.settings.context) { + parts.store = RED.settings.context.default; + } + } + return parts; + } return { createObjectElement: buildMessageElement, @@ -839,6 +853,7 @@ RED.utils = (function() { getNodeIcon: getNodeIcon, getNodeLabel: getNodeLabel, addSpinnerOverlay: addSpinnerOverlay, - decodeObject: decodeObject + decodeObject: decodeObject, + parseContextKey: parseContextKey } })(); From 6e9fe3248a63ccda3fcb1e45f4380314f2f3e18c Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Tue, 10 Jul 2018 20:40:31 +0900 Subject: [PATCH 31/34] Fix appearrence of change node label for flow/global ref (#1792) * fix appearence of change node label for flow/global ref * use RED.utils.parseContextKey --- nodes/core/logic/15-change.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nodes/core/logic/15-change.html b/nodes/core/logic/15-change.html index 83bdecf9e..359b43fdd 100644 --- a/nodes/core/logic/15-change.html +++ b/nodes/core/logic/15-change.html @@ -54,6 +54,10 @@ outputs: 1, icon: "swap.png", label: function() { + function prop2name(type, key) { + var result = RED.utils.parseContextKey(key); + return type +"." +result.key; + } if (this.name) { return this.name; } @@ -70,13 +74,13 @@ } else { if (this.rules.length == 1) { if (this.rules[0].t === "set") { - return this._("change.label.set",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); + return this._("change.label.set",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)}); } else if (this.rules[0].t === "change") { - return this._("change.label.change",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); + return this._("change.label.change",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)}); } else if (this.rules[0].t === "move") { - return this._("change.label.move",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); + return this._("change.label.move",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)}); } else { - return this._("change.label.delete",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); + return this._("change.label.delete",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)}); } } else { return this._("change.label.changeCount",{count:this.rules.length}); From 407e16e9009db75f901e4b03f1cc1056cbaef9ed Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Tue, 10 Jul 2018 20:40:52 +0900 Subject: [PATCH 32/34] Fix appearrence of switch node port label for flow/global ref. (#1793) * fix appearrence of switch node port label for flow/global ref * use RED.utils.parseContextKey --- nodes/core/logic/10-switch.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nodes/core/logic/10-switch.html b/nodes/core/logic/10-switch.html index f4ffc7dea..a57ce2187 100644 --- a/nodes/core/logic/10-switch.html +++ b/nodes/core/logic/10-switch.html @@ -99,11 +99,17 @@ } return v; } + function prop2name(key) { + var result = RED.utils.parseContextKey(key); + return result.key; + } function getValueLabel(t,v) { if (t === 'str') { return '"'+clipValueLength(v)+'"'; - } else if (t === 'msg' || t==='flow' || t==='global') { + } else if (t === 'msg') { return t+"."+clipValueLength(v); + } else if (t === 'flow' || t === 'global') { + return t+"."+clipValueLength(prop2name(v)); } return clipValueLength(v); } From 1bf4addf638091c6b650667627e24a0b01d148c2 Mon Sep 17 00:00:00 2001 From: Hiroki Uchikawa <31908137+HirokiUchikawa@users.noreply.github.com> Date: Tue, 10 Jul 2018 20:41:16 +0900 Subject: [PATCH 33/34] Fix an error when initializing the cache (#1788) * Fix a error when initializing the cache * Make context directory if it is not there in initialization --- red/runtime/nodes/context/localfilesystem.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index 28e1c4cce..89c7e6760 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -143,7 +143,13 @@ LocalFileSystem.prototype.open = function(){ self.cache.set(scope,key,data[key]); }) }); - }) + }).catch(function(err){ + if(err.code == 'ENOENT') { + return fs.mkdir(self.storageBaseDir); + }else{ + return Promise.reject(err); + } + }); } else { return Promise.resolve(); } From 1a544b3b824079ac426f83efe81147737c08aecd Mon Sep 17 00:00:00 2001 From: YumaMatsuura <38545050+YumaMatsuura@users.noreply.github.com> Date: Tue, 10 Jul 2018 20:42:56 +0900 Subject: [PATCH 34/34] Headless option for ui test (#1784) --- Gruntfile.js | 4 ++++ test/editor/wdio.conf.js | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index faf68f100..508fadc58 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -24,6 +24,10 @@ module.exports = function(grunt) { nodemonArgs.push(flowFile); } + var nonHeadless = grunt.option('non-headless'); + if (nonHeadless) { + process.env.NODE_RED_NON_HEADLESS = 'true'; + } grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), paths: { diff --git a/test/editor/wdio.conf.js b/test/editor/wdio.conf.js index 08cc1e60f..eb23a9a2c 100644 --- a/test/editor/wdio.conf.js +++ b/test/editor/wdio.conf.js @@ -62,10 +62,11 @@ exports.config = { // browserName: 'chrome', chromeOptions: { - // Runs tests without opening a broser. - args: ['--headless', '--disable-gpu', 'window-size=1920,1080'], - // Runs tests with opening a broser. - // args: ['--disable-gpu'], + args: process.env.NODE_RED_NON_HEADLESS + // Runs tests with opening a browser. + ? ['--disable-gpu'] + // Runs tests without opening a browser. + : ['--headless', '--disable-gpu', 'window-size=1920,1080'] }, }], //