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 +});