From 4374506981139e4988d1d6b099b47b3344642ff7 Mon Sep 17 00:00:00 2001 From: cclauss Date: Wed, 15 Aug 2018 16:25:58 +0200 Subject: [PATCH 01/12] Define raw_input() in Python 3 & fix time.sleep() * __raw_input()__ was removed in Python 3 in favor of __input()__ * Fix __sleep()__ to match the import on line 22 [flake8](http://flake8.pycqa.org) testing of https://github.com/node-red/node-red on Python 3.7.0 $ __flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics__ ``` ./nodes/core/hardware/nrgpio.py:45:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:63:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:85:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:120:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:134:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:164:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:201:17: F821 undefined name 'time' time.sleep(10) ^ 7 F821 undefined name 'raw_input' 7 ``` @dceejay --- nodes/core/hardware/nrgpio.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nodes/core/hardware/nrgpio.py b/nodes/core/hardware/nrgpio.py index 0cde0e4df..7908ca117 100755 --- a/nodes/core/hardware/nrgpio.py +++ b/nodes/core/hardware/nrgpio.py @@ -21,7 +21,12 @@ import os import subprocess from time import sleep -bounce = 25; +try: + raw_input # Python 2 +except NameError: + raw_input = input # Python 3 + +bounce = 25 if len(sys.argv) > 2: cmd = sys.argv[1].lower() @@ -198,7 +203,7 @@ if len(sys.argv) > 2: elif cmd == "kbd": # catch keyboard button events try: while not os.path.isdir("/dev/input/by-path"): - time.sleep(10) + sleep(10) infile = subprocess.check_output("ls /dev/input/by-path/ | grep -m 1 'kbd'", shell=True).strip() infile_path = "/dev/input/by-path/" + infile EVENT_SIZE = struct.calcsize('llHHI') From 048f9c02943f7c8f47b844a024ef0bef3529ff35 Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Sun, 9 Sep 2018 06:41:38 +0900 Subject: [PATCH 02/12] Fix node color bug (#1877) * Fix node color bug * Add color property into sample node * Revert view.js * Add color handling into getNodeColor() --- editor/js/ui/utils.js | 6 +++++- nodes/99-sample.html.demo | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/editor/js/ui/utils.js b/editor/js/ui/utils.js index 8e6654a7a..c944f3504 100644 --- a/editor/js/ui/utils.js +++ b/editor/js/ui/utils.js @@ -826,7 +826,11 @@ RED.utils = (function() { } result = nodeColorCache[type]; } - return result; + if (result) { + return result; + } else { + return "#ddd"; + } } function addSpinnerOverlay(container,contain) { diff --git a/nodes/99-sample.html.demo b/nodes/99-sample.html.demo index 94e049e31..c7548b032 100644 --- a/nodes/99-sample.html.demo +++ b/nodes/99-sample.html.demo @@ -67,6 +67,7 @@ }, inputs:1, // set the number of inputs - only 0 or 1 outputs:1, // set the number of outputs - 0 to n + color: "#ddd", // set icon color // set the icon (held in icons dir below where you save the node) icon: "myicon.png", // saved in icons/myicon.png label: function() { // sets the default label contents From 0f4d46671f5cefe00d306eefd3e9f208986ff210 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 9 Sep 2018 11:07:44 +0100 Subject: [PATCH 03/12] Fix global.get("foo.bar") for functionGlobalContext set values --- red/runtime/nodes/context/index.js | 7 +++--- test/red/runtime/nodes/context/index_spec.js | 23 +++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index be79eb00e..c85f18d80 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -17,6 +17,7 @@ var clone = require("clone"); var log = require("../../log"); var memory = require("./memory"); +var util = require("../../util"); var settings; @@ -209,12 +210,12 @@ function createContext(id,seed) { insertSeedValues = function(keys,values) { if (!Array.isArray(keys)) { if (values[0] === undefined) { - values[0] = seed[keys]; + values[0] = util.getObjectProperty(seed,keys); } } else { for (var i=0;i Date: Sun, 9 Sep 2018 23:47:31 +0100 Subject: [PATCH 04/12] Handle context.get with multiple levels of unknown key Fixes #1883 --- red/runtime/nodes/context/localfilesystem.js | 21 +++++++++-- red/runtime/nodes/context/memory.js | 9 ++++- red/runtime/util.js | 36 +++++++++++-------- .../nodes/context/localfilesystem_spec.js | 2 +- test/red/runtime/nodes/context/memory_spec.js | 2 +- 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index dc25b6290..548c98c60 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -230,14 +230,31 @@ LocalFileSystem.prototype.get = function(scope, key, callback) { } var storagePath = getStoragePath(this.storageBaseDir ,scope); loadFile(storagePath + ".json").then(function(data){ + var value; if(data){ data = JSON.parse(data); if (!Array.isArray(key)) { - callback(null, util.getObjectProperty(data,key)); + try { + value = util.getObjectProperty(data,key); + } catch(err) { + if (err.code === "INVALID_EXPR") { + throw err; + } + value = undefined; + } + callback(null, value); } else { var results = [undefined]; for (var i=0;i Date: Mon, 10 Sep 2018 22:30:51 +0100 Subject: [PATCH 05/12] Ensure context.flow/global cannot be deleted or enumerated --- red/runtime/nodes/context/index.js | 181 ++++++++++--------- test/red/runtime/nodes/context/index_spec.js | 15 ++ 2 files changed, 111 insertions(+), 85 deletions(-) diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index c85f18d80..f048947ad 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -221,93 +221,100 @@ function createContext(id,seed) { } } } - - obj.get = function(key, storage, callback) { - var context; - if (!storage && !callback) { - context = stores["_"]; - } else { - if (typeof storage === 'function') { - callback = storage; - storage = "_"; - } - if (callback && typeof callback !== 'function'){ - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); - } - if (callback) { - if (!seed) { - context.get(scope,key,callback); - } else { - context.get(scope,key,function() { - if (arguments[0]) { - callback(arguments[0]); - return; + Object.defineProperties(obj, { + get: { + value: function(key, storage, callback) { + var context; + if (!storage && !callback) { + context = stores["_"]; + } else { + if (typeof storage === 'function') { + callback = storage; + storage = "_"; } - var results = Array.prototype.slice.call(arguments,[1]); - insertSeedValues(key,results); - // Put the err arg back - results.unshift(undefined); - callback.apply(null,results); - }); - } - } else { - // No callback, attempt to do this synchronously - var results = context.get(scope,key); - if (seed) { - if (Array.isArray(key)) { - insertSeedValues(key,results); - } else if (results === undefined){ - results = util.getObjectProperty(seed,key); + if (callback && typeof callback !== 'function'){ + throw new Error("Callback must be a function"); + } + context = getContextStorage(storage); + } + if (callback) { + if (!seed) { + context.get(scope,key,callback); + } else { + context.get(scope,key,function() { + if (arguments[0]) { + callback(arguments[0]); + return; + } + var results = Array.prototype.slice.call(arguments,[1]); + insertSeedValues(key,results); + // Put the err arg back + results.unshift(undefined); + callback.apply(null,results); + }); + } + } else { + // No callback, attempt to do this synchronously + var results = context.get(scope,key); + if (seed) { + if (Array.isArray(key)) { + insertSeedValues(key,results); + } else if (results === undefined){ + results = util.getObjectProperty(seed,key); + } + } + return results; + } + } + }, + set: { + value: function(key, value, storage, callback) { + var context; + if (!storage && !callback) { + context = stores["_"]; + } else { + if (typeof storage === 'function') { + callback = storage; + storage = "_"; + } + if (callback && typeof callback !== 'function') { + throw new Error("Callback must be a function"); + } + context = getContextStorage(storage); + } + context.set(scope, key, value, callback); + } + }, + keys: { + value: function(storage, callback) { + var context; + if (!storage && !callback) { + context = stores["_"]; + } else { + if (typeof storage === 'function') { + callback = storage; + storage = "_"; + } + if (callback && typeof callback !== 'function') { + throw new Error("Callback must be a function"); + } + context = getContextStorage(storage); + } + 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 results; } - }; - obj.set = function(key, value, storage, callback) { - var context; - if (!storage && !callback) { - context = stores["_"]; - } else { - if (typeof storage === 'function') { - callback = storage; - storage = "_"; - } - if (callback && typeof callback !== 'function') { - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); - } - context.set(scope, key, value, callback); - }; - obj.keys = function(storage, callback) { - var context; - if (!storage && !callback) { - context = stores["_"]; - } else { - if (typeof storage === 'function') { - callback = storage; - storage = "_"; - } - if (callback && typeof callback !== 'function') { - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); - } - 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; } @@ -321,9 +328,13 @@ function getContext(localId,flowId) { } var newContext = createContext(contextId); if (flowId) { - newContext.flow = getContext(flowId); + Object.defineProperty(newContext, 'flow', { + value: getContext(flowId) + }); } - newContext.global = contexts['global']; + Object.defineProperty(newContext, 'global', { + value: contexts['global'] + }) contexts[contextId] = newContext; return newContext; } diff --git a/test/red/runtime/nodes/context/index_spec.js b/test/red/runtime/nodes/context/index_spec.js index 6f5a73908..3beb9b201 100644 --- a/test/red/runtime/nodes/context/index_spec.js +++ b/test/red/runtime/nodes/context/index_spec.js @@ -113,6 +113,21 @@ describe('context', function() { context2.global.get("foo").should.equal("test"); }); + it('context.flow/global are not enumerable', function() { + var context1 = Context.get("1","flowA"); + Object.keys(context1).length.should.equal(0); + Object.keys(context1.flow).length.should.equal(0); + Object.keys(context1.global).length.should.equal(0); + }) + + it('context.flow/global cannot be deleted', function() { + var context1 = Context.get("1","flowA"); + delete context1.flow; + should.exist(context1.flow); + delete context1.global; + should.exist(context1.global); + }) + it('deletes context',function() { var context = Context.get("1","flowA"); should.not.exist(context.get("foo")); From 17a737ca889d58366eebea6e66dfb6596b1d32c5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 14 Sep 2018 11:09:56 +0100 Subject: [PATCH 06/12] Fix dbl-click handling on webkit-based browsers d3.event.buttons is not as widely supported as I thought. Can change this one instance as it is inside a click handler so d3.event.button will be defined instead --- 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 4688c15cd..f3928351f 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -1766,7 +1766,7 @@ RED.view = (function() { clickTime = now; dblClickPrimed = (lastClickNode == mousedown_node && - d3.event.buttons === 1 && + d3.event.button === 0 && !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey); lastClickNode = mousedown_node; From 66ee27c5fa054d5f1269d91f0bbaf6b6e0e4c41d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 14 Sep 2018 14:03:36 +0100 Subject: [PATCH 07/12] Switch node: only use promises when absolutely necessary Fixes a significant performance regression introduced when the node was made async-only with the persistent context work. --- nodes/core/logic/10-switch.js | 321 ++++++++++++++++++++++++---------- 1 file changed, 230 insertions(+), 91 deletions(-) diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index 89593047e..1d9c52971 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -92,31 +92,80 @@ module.exports = function(RED) { } function getProperty(node,msg) { - return new Promise((resolve,reject) => { + if (node.useAsyncRules) { + return new Promise((resolve,reject) => { + if (node.propertyType === 'jsonata') { + 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) { + resolve(undefined); + } else { + resolve(value); + } + }); + } + }); + } else { if (node.propertyType === 'jsonata') { - RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => { - if (err) { - reject(RED._("switch.errors.invalid-expr",{error:err.message})); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateJSONataExpression(node.property,msg); + } catch(err) { + throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) + } } else { - RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => { - if (err) { - resolve(undefined); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg); + } catch(err) { + return undefined; + } } - }); + } } function getV1(node,msg,rule,hasParts) { - return new Promise( (resolve,reject) => { + if (node.useAsyncRules) { + return new Promise( (resolve,reject) => { + if (rule.vt === 'prev') { + resolve(node.previousValue); + } else if (rule.vt === 'jsonata') { + var exp = rule.v; + if (rule.t === 'jsonata_exp') { + if (hasParts) { + exp.assign("I", msg.parts.index); + exp.assign("N", msg.parts.count); + } + } + 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"); // TODO: ?! invalid case + } 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); + } + }); + } + }); + } else { if (rule.vt === 'prev') { - resolve(node.previousValue); + return node.previousValue; } else if (rule.vt === 'jsonata') { var exp = rule.v; if (rule.t === 'jsonata_exp') { @@ -125,83 +174,120 @@ module.exports = function(RED) { exp.assign("N", msg.parts.count); } } - RED.util.evaluateJSONataExpression(exp,msg,(err,value) => { - if (err) { - reject(RED._("switch.errors.invalid-expr",{error:err.message})); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateJSONataExpression(exp,msg); + } catch(err) { + throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) + } } else if (rule.vt === 'json') { - resolve("json"); + return "json"; // TODO: ?! invalid case } else if (rule.vt === 'null') { - resolve("null"); + return "null"; } else { - RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) { - if (err) { - resolve(undefined); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg); + } catch(err) { + return undefined; + } } - }); + } } function getV2(node,msg,rule) { - return new Promise((resolve,reject) => { + if (node.useAsyncRules) { + return new Promise((resolve,reject) => { + var v2 = rule.v2; + if (rule.v2t === 'prev') { + resolve(node.previousValue); + } else if (rule.v2t === 'jsonata') { + 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) { + resolve(undefined); + } else { + resolve(value); + } + }); + } else { + resolve(v2); + } + }) + } else { var v2 = rule.v2; if (rule.v2t === 'prev') { - resolve(node.previousValue); + return node.previousValue; } else if (rule.v2t === 'jsonata') { - RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => { - if (err) { - reject(RED._("switch.errors.invalid-expr",{error:err.message})); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateJSONataExpression(rule.v2,msg); + } catch(err) { + throw new Error(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); - } - }); + try { + return RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg); + } catch(err) { + return undefined; + } } else { - resolve(v2); + return v2; } - }) + } } function applyRule(node, msg, property, state) { - return new Promise((resolve,reject) => { + if (node.useAsyncRules) { + return new Promise((resolve,reject) => { - var rule = node.rules[state.currentRule]; - var v1,v2; + 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); + 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; } - } else { - state.onward.push(null); + 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); + }); + }) + } else { + var rule = node.rules[state.currentRule]; + var v1 = getV1(node,msg,rule,state.hasParts); + var v2 = getV2(node,msg,rule); + 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 false; } - resolve(state.currentRule < node.rules.length - 1); - }); - }) + } else { + state.onward.push(null); + } + return state.currentRule < node.rules.length - 1 + } } function applyRules(node, msg, property,state) { @@ -215,7 +301,18 @@ module.exports = function(RED) { msg.parts.hasOwnProperty("index") } } - return applyRule(node,msg,property,state).then(hasMore => { + if (node.useAsyncRules) { + 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; + } + }); + } else { + var hasMore = applyRule(node,msg,property,state); if (hasMore) { state.currentRule++; return applyRules(node,msg,property,state); @@ -223,7 +320,7 @@ module.exports = function(RED) { node.previousValue = property; return state.onward; } - }); + } } @@ -248,6 +345,14 @@ module.exports = function(RED) { var valid = true; var repair = n.repair; var needsCount = repair; + this.useAsyncRules = ( + this.propertyType === 'flow' || + this.propertyType === 'global' || ( + this.propertyType === 'jsonata' && + /\$(flow|global)Context/.test(this.property) + ) + ); + for (var i=0; i applyRules(node,msg,property)) - .then(onward => { - if (!repair || !hasParts) { - node.send(onward); - } - else { - sendGroupMessages(onward, msg); - } - }).catch(err => { - node.warn(err); - }); + if (node.useAsyncRules) { + return getProperty(node,msg) + .then(property => applyRules(node,msg,property)) + .then(onward => { + if (!repair || !hasParts) { + node.send(onward); + } + else { + sendGroupMessages(onward, msg); + } + }).catch(err => { + node.warn(err); + }); + } else { + try { + var property = getProperty(node,msg); + var onward = applyRules(node,msg,property); + if (!repair || !hasParts) { + node.send(onward); + } else { + sendGroupMessages(onward, msg); + } + } catch(err) { + node.warn(err); + } + } } function clearPending() { @@ -473,7 +608,11 @@ module.exports = function(RED) { } this.on('input', function(msg) { - processMessageQueue(msg); + if (node.useAsyncRules) { + processMessageQueue(msg); + } else { + processMessage(msg,true); + } }); this.on('close', function() { From a8ec032553cc4fa51803b84fff411507c1b4dfa7 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 14 Sep 2018 23:21:05 +0100 Subject: [PATCH 08/12] Allow context store name to be provided in the key For nodes that get/set context, when multiple stores are configured they will not know to parse the store name from the key. So they will pass the store name in the key, such as #:(store)::key. Currently that will cause that full string to be used as the key and the default context store used - which is wrong. The code now parses out the store name from the key if it is set - athough if the call to get/set does include the store argument, it will take precedence. This only applies when the key is a string - it doesn't apply when an array of keys is provided. --- red/runtime/nodes/context/index.js | 52 ++++++++++++++------ test/red/runtime/nodes/context/index_spec.js | 17 +++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index f048947ad..d08734a2f 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -225,18 +225,28 @@ function createContext(id,seed) { get: { value: function(key, storage, callback) { var context; - if (!storage && !callback) { - context = stores["_"]; + + if (!callback && typeof storage === 'function') { + callback = storage; + storage = undefined; + } + if (callback && typeof callback !== 'function'){ + throw new Error("Callback must be a function"); + } + + if (!Array.isArray(key)) { + var keyParts = util.parseContextStore(key); + key = keyParts.key; + if (!storage) { + storage = keyParts.store || "_"; + } } else { - if (typeof storage === 'function') { - callback = storage; + if (!storage) { storage = "_"; } - if (callback && typeof callback !== 'function'){ - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); } + context = getContextStorage(storage); + if (callback) { if (!seed) { context.get(scope,key,callback); @@ -270,18 +280,28 @@ function createContext(id,seed) { set: { value: function(key, value, storage, callback) { var context; - if (!storage && !callback) { - context = stores["_"]; + + if (!callback && typeof storage === 'function') { + callback = storage; + storage = undefined; + } + if (callback && typeof callback !== 'function'){ + throw new Error("Callback must be a function"); + } + + if (!Array.isArray(key)) { + var keyParts = util.parseContextStore(key); + key = keyParts.key; + if (!storage) { + storage = keyParts.store || "_"; + } } else { - if (typeof storage === 'function') { - callback = storage; + if (!storage) { storage = "_"; } - if (callback && typeof callback !== 'function') { - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); } + context = getContextStorage(storage); + context.set(scope, key, value, callback); } }, diff --git a/test/red/runtime/nodes/context/index_spec.js b/test/red/runtime/nodes/context/index_spec.js index 3beb9b201..c723a0a6e 100644 --- a/test/red/runtime/nodes/context/index_spec.js +++ b/test/red/runtime/nodes/context/index_spec.js @@ -499,6 +499,23 @@ describe('context', function() { done(); }).catch(done); }); + + it('should allow the store name to be provide in the key', function(done) { + Context.init({contextStorage:contextDefaultStorage}); + Context.load().then(function(){ + var context = Context.get("1","flow"); + var cb = function(){done("An error occurred")} + context.set("#:(test)::foo","bar"); + context.get("#:(test)::foo"); + stubGet2.called.should.be.false(); + stubSet2.called.should.be.false(); + stubSet.calledWithExactly("1:flow","foo","bar",undefined).should.be.true(); + stubGet.calledWith("1:flow","foo").should.be.true(); + done(); + }).catch(done); + }); + + it('should use default as the alias of other context', function(done) { Context.init({contextStorage:contextAlias}); Context.load().then(function(){ From fd86035865b0da7bf92fee2e9872a4039cc6054e Mon Sep 17 00:00:00 2001 From: Hiroki Uchikawa <31908137+HirokiUchikawa@users.noreply.github.com> Date: Mon, 17 Sep 2018 05:15:23 +0900 Subject: [PATCH 09/12] Prevent race condition (#1889) * Make pending Flag to be deleted after write process complete. * Prevent executing write process until the previous process is completed * Fix to prevent file write race condition when closing file context * Make flushing rerun if pendingWrites was added --- red/runtime/nodes/context/localfilesystem.js | 35 ++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index 548c98c60..ee07dd26b 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -142,6 +142,7 @@ function LocalFileSystem(config){ this.storageBaseDir = getBasePath(this.config); if (config.hasOwnProperty('cache')?config.cache:true) { this.cache = MemoryStore({}); + this.writePromise = Promise.resolve(); } this.pendingWrites = {}; this.knownCircularRefs = {}; @@ -203,8 +204,22 @@ LocalFileSystem.prototype.open = function(){ log.debug("Flushing localfilesystem context scope "+scope); promises.push(fs.outputFile(storagePath + ".json", stringifiedContext.json, "utf8")); }); - delete self._pendingWriteTimeout; - return Promise.all(promises); + return Promise.all(promises).then(function(){ + if(Object.keys(self.pendingWrites).length > 0){ + // Rerun flushing if pendingWrites was added when the promise was running + return new Promise(function(resolve, reject) { + setTimeout(function() { + self._flushPendingWrites.call(self).then(function(){ + resolve(); + }).catch(function(err) { + reject(err); + }); + }, self.flushInterval); + }); + } else { + delete self._pendingWriteTimeout; + } + }); } }); } else { @@ -213,10 +228,16 @@ LocalFileSystem.prototype.open = function(){ } LocalFileSystem.prototype.close = function(){ - if (this.cache && this._flushPendingWrites) { + var self = this; + if (this.cache && this._pendingWriteTimeout) { clearTimeout(this._pendingWriteTimeout); delete this._pendingWriteTimeout; - return this._flushPendingWrites(); + this.flushInterval = 0; + return this.writePromise.then(function(){ + if(Object.keys(self.pendingWrites).length > 0) { + return self._flushPendingWrites(); + } + }); } return Promise.resolve(); } @@ -277,8 +298,10 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) { return; } else { this._pendingWriteTimeout = setTimeout(function() { - self._flushPendingWrites.call(self).catch(function(err) { - log.error(log._("context.localfilesystem.error-write",{message:err.toString()})) + self.writePromise = self.writePromise.then(function(){ + return self._flushPendingWrites.call(self).catch(function(err) { + log.error(log._("context.localfilesystem.error-write",{message:err.toString()})); + }); }); }, this.flushInterval); } From 9777af7cb5f0eca4767cef8ad7820b6ce4b2b288 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 16 Sep 2018 22:04:09 +0100 Subject: [PATCH 10/12] LocalFileSystem Context: Remove extra flush code --- red/runtime/nodes/context/localfilesystem.js | 24 +++----------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index ee07dd26b..a507ce2b4 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -204,22 +204,8 @@ LocalFileSystem.prototype.open = function(){ log.debug("Flushing localfilesystem context scope "+scope); promises.push(fs.outputFile(storagePath + ".json", stringifiedContext.json, "utf8")); }); - return Promise.all(promises).then(function(){ - if(Object.keys(self.pendingWrites).length > 0){ - // Rerun flushing if pendingWrites was added when the promise was running - return new Promise(function(resolve, reject) { - setTimeout(function() { - self._flushPendingWrites.call(self).then(function(){ - resolve(); - }).catch(function(err) { - reject(err); - }); - }, self.flushInterval); - }); - } else { - delete self._pendingWriteTimeout; - } - }); + delete self._pendingWriteTimeout; + return Promise.all(promises); } }); } else { @@ -233,11 +219,7 @@ LocalFileSystem.prototype.close = function(){ clearTimeout(this._pendingWriteTimeout); delete this._pendingWriteTimeout; this.flushInterval = 0; - return this.writePromise.then(function(){ - if(Object.keys(self.pendingWrites).length > 0) { - return self._flushPendingWrites(); - } - }); + return this.writePromise; } return Promise.resolve(); } From c1d50e82e1bbf09f41626066f9fac90076a99cd4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 17 Sep 2018 10:31:00 +0100 Subject: [PATCH 11/12] Fix race condition in non-cache lfs context Fixes #1888 --- red/runtime/nodes/context/localfilesystem.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index a507ce2b4..0e6f8735b 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -140,9 +140,9 @@ function stringify(value) { function LocalFileSystem(config){ this.config = config; this.storageBaseDir = getBasePath(this.config); + this.writePromise = Promise.resolve(); if (config.hasOwnProperty('cache')?config.cache:true) { this.cache = MemoryStore({}); - this.writePromise = Promise.resolve(); } this.pendingWrites = {}; this.knownCircularRefs = {}; @@ -219,9 +219,8 @@ LocalFileSystem.prototype.close = function(){ clearTimeout(this._pendingWriteTimeout); delete this._pendingWriteTimeout; this.flushInterval = 0; - return this.writePromise; } - return Promise.resolve(); + return this.writePromise; } LocalFileSystem.prototype.get = function(scope, key, callback) { @@ -290,7 +289,7 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) { } else if (callback && typeof callback !== 'function') { throw new Error("Callback must be a function"); } else { - loadFile(storagePath + ".json").then(function(data){ + self.writePromise = self.writePromise.then(function() { return loadFile(storagePath + ".json") }).then(function(data){ var obj = data ? JSON.parse(data) : {} if (!Array.isArray(key)) { key = [key]; From 08fccc4e775121cb4eba2526666e010dd6e85b72 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 17 Sep 2018 11:26:06 +0100 Subject: [PATCH 12/12] Update for 0.19.4 --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6770f3158..29b8dabb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +#### 0.19.4: Maintenance Release + + - Fix race condition in non-cache lfs context Fixes #1888 + - LocalFileSystem Context: Remove extra flush code + - Prevent race condition in caching mode of lfs context (#1889) + - Allow context store name to be provided in the key + - Switch node: only use promises when absolutely necessary + - Fix dbl-click handling on webkit-based browsers + - Ensure context.flow/global cannot be deleted or enumerated + - Handle context.get with multiple levels of unknown key Fixes #1883 + - Fix global.get("foo.bar") for functionGlobalContext set values + - Fix node color bug (#1877) + - Merge pull request #1857 from cclauss/patch-1 + - Define raw_input() in Python 3 & fix time.sleep() + #### 0.19.3: Maintenance Release - Split node - fix complete to send msg for k/v object diff --git a/package.json b/package.json index 57d3cc9d8..6c025db08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "0.19.3", + "version": "0.19.4", "description": "A visual tool for wiring the Internet of Things", "homepage": "http://nodered.org", "license": "Apache-2.0",