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 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());
}
}
// 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
// checked that it exists. If it isn't an alias, then it will
// already be set to a configured store
if (defaultIsAlias) {
externalContexts["default"] = externalContexts[plugins["default"]];
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);
}
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);
}
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);

View File

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

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 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();
}).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 {
done(err);
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);
});
});
});

View File

@ -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");
});
});
});