1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Apply fGC to all global contexts for default values

This commit is contained in:
Nick O'Leary 2018-07-02 15:21:13 +01:00
parent 14882bda78
commit 038d821a7c
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
4 changed files with 239 additions and 111 deletions

View File

@ -19,21 +19,23 @@ var log = require("../../log");
var memory = require("./memory"); var memory = require("./memory");
var settings; var settings;
// A map of scope id to context instance
var contexts = {}; var contexts = {};
var globalContext = null;
var externalContexts = {}; // A map of store name to instance
var hasExternalContext = false; var stores = {};
// Whether there context storage has been configured or left as default
var hasConfiguredStore = false;
var defaultStore = "_";
function init(_settings) { function init(_settings) {
settings = _settings; settings = _settings;
externalContexts = {}; stores = {};
// init memory plugin
var seed = settings.functionGlobalContext || {}; var seed = settings.functionGlobalContext || {};
externalContexts["_"] = memory(); contexts['global'] = createContext("global",seed);
externalContexts["_"].setGlobalContext(seed);
globalContext = createContext("global",seed);
contexts['global'] = globalContext;
} }
function load() { function load() {
@ -43,6 +45,8 @@ function load() {
var defaultIsAlias = false; var defaultIsAlias = false;
var promises = []; var promises = [];
if (plugins) { if (plugins) {
var hasDefault = plugins.hasOwnProperty('default');
var defaultName;
for (var pluginName in plugins) { for (var pluginName in plugins) {
if (plugins.hasOwnProperty(pluginName)) { if (plugins.hasOwnProperty(pluginName)) {
// "_" is a reserved name - do not allow it to be overridden // "_" is a reserved name - do not allow it to be overridden
@ -59,6 +63,9 @@ function load() {
defaultIsAlias = true; defaultIsAlias = true;
continue; continue;
} }
if (!hasDefault && !defaultName) {
defaultName = pluginName;
}
var plugin; var plugin;
if (plugins[pluginName].hasOwnProperty("module")) { if (plugins[pluginName].hasOwnProperty("module")) {
// Get the provided config and copy in the 'approved' top-level settings (eg userDir) // Get the provided config and copy in the 'approved' top-level settings (eg userDir)
@ -79,7 +86,7 @@ function load() {
} }
try { try {
// Create a new instance of the plugin by calling its module function // Create a new instance of the plugin by calling its module function
externalContexts[pluginName] = plugin(config); stores[pluginName] = plugin(config);
} catch(err) { } catch(err) {
return reject(new Error(log._("context.error-loading-module",{module:pluginName,message:err.toString()}))); 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 // Open all of the configured contexts
for (var plugin in externalContexts) { for (var plugin in stores) {
if (externalContexts.hasOwnProperty(plugin)) { if (stores.hasOwnProperty(plugin)) {
promises.push(externalContexts[plugin].open()); promises.push(stores[plugin].open());
} }
} }
// If 'default' is an alias, point it at the right module - we have already // There is a 'default' listed in the configuration
// checked that it exists if (hasDefault) {
if (defaultIsAlias) { // If 'default' is an alias, point it at the right module - we have already
externalContexts["default"] = externalContexts[plugins["default"]]; // 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) { 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 { } 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)); return resolve(Promise.all(promises));
}); });
@ -122,12 +145,12 @@ function copySettings(config, settings){
} }
function getContextStorage(storage) { function getContextStorage(storage) {
if (externalContexts.hasOwnProperty(storage)) { if (stores.hasOwnProperty(storage)) {
// A known context // A known context
return externalContexts[storage]; return stores[storage];
} else if (externalContexts.hasOwnProperty("default")) { } else if (stores.hasOwnProperty("default")) {
// Not known, but we have a default to fall back to // Not known, but we have a default to fall back to
return externalContexts["default"]; return stores["default"];
} else { } else {
// Not known and no default configured // Not known and no default configured
var contextError = new Error(log._("context.error-use-undefined-storage", {storage:storage})); var contextError = new Error(log._("context.error-use-undefined-storage", {storage:storage}));
@ -136,14 +159,19 @@ function getContextStorage(storage) {
} }
} }
function createContext(id,seed) { function createContext(id,seed) {
// Seed is only set for global context - sourced from functionGlobalContext
var scope = id; var scope = id;
var obj = seed || {}; var obj = seed || {};
var seedKeys;
if (seed) {
seedKeys = Object.keys(seed);
}
obj.get = function(key, storage, callback) { obj.get = function(key, storage, callback) {
var context; var context;
if (!storage && !callback) { if (!storage && !callback) {
context = externalContexts["_"]; context = stores["_"];
} else { } else {
if (typeof storage === 'function') { if (typeof storage === 'function') {
callback = storage; callback = storage;
@ -154,12 +182,34 @@ function createContext(id,seed) {
} }
context = getContextStorage(storage); 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) { obj.set = function(key, value, storage, callback) {
var context; var context;
if (!storage && !callback) { if (!storage && !callback) {
context = externalContexts["_"]; context = stores["_"];
} else { } else {
if (typeof storage === 'function') { if (typeof storage === 'function') {
callback = storage; callback = storage;
@ -175,7 +225,7 @@ function createContext(id,seed) {
obj.keys = function(storage, callback) { obj.keys = function(storage, callback) {
var context; var context;
if (!storage && !callback) { if (!storage && !callback) {
context = externalContexts["_"]; context = stores["_"];
} else { } else {
if (typeof storage === 'function') { if (typeof storage === 'function') {
callback = storage; callback = storage;
@ -186,7 +236,18 @@ function createContext(id,seed) {
} }
context = getContextStorage(storage); 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; return obj;
} }
@ -203,21 +264,20 @@ function getContext(localId,flowId) {
if (flowId) { if (flowId) {
newContext.flow = getContext(flowId); newContext.flow = getContext(flowId);
} }
if (globalContext) { newContext.global = contexts['global'];
newContext.global = globalContext;
}
contexts[contextId] = newContext; contexts[contextId] = newContext;
return newContext; return newContext;
} }
function deleteContext(id,flowId) { function deleteContext(id,flowId) {
if(!hasExternalContext){ if(!hasConfiguredStore){
// only delete context if there's no configured storage.
var contextId = id; var contextId = id;
if (flowId) { if (flowId) {
contextId = id+":"+flowId; contextId = id+":"+flowId;
} }
delete contexts[contextId]; delete contexts[contextId];
return externalContexts["_"].delete(contextId); return stores["_"].delete(contextId);
}else{ }else{
return Promise.resolve(); return Promise.resolve();
} }
@ -225,9 +285,9 @@ function deleteContext(id,flowId) {
function clean(flowConfig) { function clean(flowConfig) {
var promises = []; var promises = [];
for(var plugin in externalContexts){ for(var plugin in stores){
if(externalContexts.hasOwnProperty(plugin)){ if(stores.hasOwnProperty(plugin)){
promises.push(externalContexts[plugin].clean(Object.keys(flowConfig.allNodes))); promises.push(stores[plugin].clean(Object.keys(flowConfig.allNodes)));
} }
} }
for (var id in contexts) { for (var id in contexts) {
@ -243,9 +303,9 @@ function clean(flowConfig) {
function close() { function close() {
var promises = []; var promises = [];
for(var plugin in externalContexts){ for(var plugin in stores){
if(externalContexts.hasOwnProperty(plugin)){ if(stores.hasOwnProperty(plugin)){
promises.push(externalContexts[plugin].close()); promises.push(stores[plugin].close());
} }
} }
return Promise.all(promises); return Promise.all(promises);

View File

@ -109,10 +109,6 @@ Memory.prototype.clean = function(activeNodes){
return Promise.resolve(); return Promise.resolve();
} }
Memory.prototype.setGlobalContext= function(seed){
this.data["global"] = seed;
};
module.exports = function(config){ module.exports = function(config){
return new Memory(config); return new Memory(config);
}; };

View File

@ -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 context = Context.get("1","flowA");
var keys = context.keys(); var keys = context.keys();
@ -142,15 +142,63 @@ describe('context', function() {
keys[1].should.equal("abc"); 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"}}); Context.init({functionGlobalContext: {foo:"bar"}});
return Context.load().then(function(){ return Context.load().then(function(){
var context = Context.get("1","flowA"); var context = Context.get("1","flowA");
context.global.set("foo2","bar2");
var keys = context.global.keys(); var keys = context.global.keys();
keys.should.have.length(1); keys.should.have.length(2);
keys[0].should.equal("foo"); 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() { describe('external context storage',function() {
@ -215,6 +263,11 @@ describe('context', function() {
config:{} config:{}
} }
}; };
var memoryStorage ={
memory:{
module: "memory"
}
};
afterEach(function() { afterEach(function() {
sandbox.reset(); sandbox.reset();
@ -257,7 +310,7 @@ describe('context', function() {
stubSet.calledWithExactly("1:flow","file","file2",cb).should.be.true(); stubSet.calledWithExactly("1:flow","file","file2",cb).should.be.true();
stubSet.calledWithExactly("1:flow","num","num3",cb).should.be.true(); stubSet.calledWithExactly("1:flow","num","num3",cb).should.be.true();
done(); done();
}); }).catch(done);
}); });
it('should ignore reserved storage name `_`', function(done) { it('should ignore reserved storage name `_`', function(done) {
Context.init({contextStorage:{_:{module:testPlugin}}}); Context.init({contextStorage:{_:{module:testPlugin}}});
@ -271,7 +324,7 @@ describe('context', function() {
stubGet.called.should.be.false(); stubGet.called.should.be.false();
stubKeys.called.should.be.false(); stubKeys.called.should.be.false();
done(); done();
}); }).catch(done);
}); });
it('should fail when using invalid default context', function(done) { it('should fail when using invalid default context', function(done) {
Context.init({contextStorage:{default:"noexist"}}); Context.init({contextStorage:{default:"noexist"}});
@ -300,14 +353,15 @@ describe('context', function() {
}); });
describe('close modules',function(){ describe('close modules',function(){
it('should call close()', function() { it('should call close()', function(done) {
Context.init({contextStorage:contextDefaultStorage}); Context.init({contextStorage:contextDefaultStorage});
return Context.load().then(function(){ return Context.load().then(function(){
return Context.close().then(function(){ return Context.close().then(function(){
stubClose.called.should.be.true(); stubClose.called.should.be.true();
stubClose2.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(); stubGet.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys.calledWithExactly("1:flow",cb).should.be.true(); stubKeys.calledWithExactly("1:flow",cb).should.be.true();
done(); done();
}); }).catch(done);
}); });
it('should store flow property to external context storage',function(done) { it('should store flow property to external context storage',function(done) {
Context.init({contextStorage:contextStorage}); Context.init({contextStorage:contextStorage});
@ -338,7 +392,7 @@ describe('context', function() {
stubGet.calledWithExactly("flow","foo",cb).should.be.true(); stubGet.calledWithExactly("flow","foo",cb).should.be.true();
stubKeys.calledWithExactly("flow",cb).should.be.true(); stubKeys.calledWithExactly("flow",cb).should.be.true();
done(); done();
}); }).catch(done);
}); });
it('should store global property to external context storage',function(done) { it('should store global property to external context storage',function(done) {
Context.init({contextStorage:contextStorage}); Context.init({contextStorage:contextStorage});
@ -349,10 +403,10 @@ describe('context', function() {
context.global.get("foo","test",cb); context.global.get("foo","test",cb);
context.global.keys("test",cb); context.global.keys("test",cb);
stubSet.calledWithExactly("global","foo","bar",cb).should.be.true(); stubSet.calledWithExactly("global","foo","bar",cb).should.be.true();
stubGet.calledWithExactly("global","foo",cb).should.be.true(); stubGet.calledWith("global","foo").should.be.true();
stubKeys.calledWithExactly("global",cb).should.be.true(); stubKeys.calledWith("global").should.be.true();
done(); done();
}); }).catch(done);
}); });
it('should store data to the default context when non-existent context storage was specified', function(done) { it('should store data to the default context when non-existent context storage was specified', function(done) {
Context.init({contextStorage:contextDefaultStorage}); Context.init({contextStorage:contextDefaultStorage});
@ -369,7 +423,7 @@ describe('context', function() {
stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true(); stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys2.calledWithExactly("1:flow",cb).should.be.true(); stubKeys2.calledWithExactly("1:flow",cb).should.be.true();
done(); done();
}); }).catch(done);
}); });
it('should use the default context', function(done) { it('should use the default context', function(done) {
Context.init({contextStorage:contextDefaultStorage}); Context.init({contextStorage:contextDefaultStorage});
@ -386,7 +440,7 @@ describe('context', function() {
stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true(); stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys2.calledWithExactly("1:flow",cb).should.be.true(); stubKeys2.calledWithExactly("1:flow",cb).should.be.true();
done(); done();
}); }).catch(done);
}); });
it('should use the alias of default context', function(done) { it('should use the alias of default context', function(done) {
Context.init({contextStorage:contextDefaultStorage}); Context.init({contextStorage:contextDefaultStorage});
@ -403,7 +457,7 @@ describe('context', function() {
stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true(); stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys2.calledWithExactly("1:flow",cb).should.be.true(); stubKeys2.calledWithExactly("1:flow",cb).should.be.true();
done(); done();
}); }).catch(done);
}); });
it('should use default as the alias of other context', function(done) { it('should use default as the alias of other context', function(done) {
Context.init({contextStorage:contextAlias}); Context.init({contextStorage:contextAlias});
@ -417,22 +471,16 @@ describe('context', function() {
stubGet.calledWithExactly("1:flow","foo",cb).should.be.true(); stubGet.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys.calledWithExactly("1:flow",cb).should.be.true(); stubKeys.calledWithExactly("1:flow",cb).should.be.true();
done(); 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.init({contextStorage:contextStorage});
Context.load().then(function(){ Context.load().then(function(){
var context = Context.get("1","flow"); var context = Context.get("1","flow");
var cb = function(){done("An error occurred")} var cb = function(){done("An error occurred")}
context.get("local","nonexist",cb); context.get("local","nonexist",cb);
should.fail(null, null, "An error was not thrown using undefined storage for local context"); done()
}).catch(function(err) { }).catch(done);
if (err.name === "ContextError") {
done();
} else {
done(err);
}
});
}); });
it('should throw an error using undefined storage for flow context', function(done) { it('should throw an error using undefined storage for flow context', function(done) {
Context.init({contextStorage:contextStorage}); Context.init({contextStorage:contextStorage});
@ -440,39 +488,89 @@ describe('context', function() {
var context = Context.get("1","flow"); var context = Context.get("1","flow");
var cb = function(){done("An error occurred")} var cb = function(){done("An error occurred")}
context.flow.get("flow","nonexist",cb); context.flow.get("flow","nonexist",cb);
should.fail(null, null, "An error was not thrown using undefined storage for flow context"); done();
}).catch(function(err) { }).catch(done);
if (err.name === "ContextError") {
done();
} else {
done(err);
}
});
}); });
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(){ 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}); Context.init({contextStorage:contextDefaultStorage});
return Context.load().then(function(){ return Context.load().then(function(){
Context.get("flowA"); Context.get("flowA");
return Context.delete("flowA").then(function(){ return Context.delete("flowA").then(function(){
stubDelete.called.should.be.false(); stubDelete.called.should.be.false();
stubDelete2.called.should.be.false(); stubDelete2.called.should.be.false();
done();
}); });
}); }).catch(done);
}); });
}); });
describe('clean context',function(){ describe('clean context',function(){
it('should call clean()', function() { it('should call clean()', function(done) {
Context.init({contextStorage:contextDefaultStorage}); Context.init({contextStorage:contextDefaultStorage});
return Context.load().then(function(){ return Context.load().then(function(){
return Context.clean({allNodes:{}}).then(function(){ return Context.clean({allNodes:{}}).then(function(){
stubClean.calledWithExactly([]).should.be.true(); stubClean.calledWithExactly([]).should.be.true();
stubClean2.calledWithExactly([]).should.be.true(); stubClean2.calledWithExactly([]).should.be.true();
done();
}); });
}); }).catch(done);
}); });
}); });
}); });

View File

@ -103,19 +103,6 @@ describe('memory',function() {
keysY[0].should.equal("hoge"); 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() { 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");
});
});
});