Merge pull request #1254 from strongloop/feature/run-in-context

Add `loopback.runInContext`
This commit is contained in:
Miroslav Bajtoš 2015-03-27 19:15:22 +01:00
commit 6640f8a082
8 changed files with 188 additions and 103 deletions

View File

@ -39,6 +39,9 @@ module.exports = function(grunt) {
common: { common: {
src: ['common/**/*.js'] src: ['common/**/*.js']
}, },
browser: {
src: ['browser/**/*.js']
},
server: { server: {
src: ['server/**/*.js'] src: ['server/**/*.js']
}, },
@ -51,6 +54,7 @@ module.exports = function(grunt) {
lib: ['lib/**/*.js'], lib: ['lib/**/*.js'],
common: ['common/**/*.js'], common: ['common/**/*.js'],
server: ['server/**/*.js'], server: ['server/**/*.js'],
browser: ['browser/**/*.js'],
test: ['test/**/*.js'] test: ['test/**/*.js']
}, },
watch: { watch: {

View File

@ -0,0 +1,10 @@
module.exports = function(loopback) {
loopback.getCurrentContext = function() {
return null;
};
loopback.runInContext =
loopback.createContext = function() {
throw new Error('Current context is not supported in the browser.');
};
};

View File

@ -5,6 +5,7 @@
"lib/server-app.js", "lib/server-app.js",
"lib/loopback.js", "lib/loopback.js",
"lib/registry.js", "lib/registry.js",
"server/current-context.js",
"lib/access-context.js", "lib/access-context.js",
{ "title": "Base models", "depth": 2 }, { "title": "Base models", "depth": 2 },
"lib/model.js", "lib/model.js",

View File

@ -189,10 +189,7 @@ loopback.template = function(file) {
return ejs.compile(str); return ejs.compile(str);
}; };
loopback.getCurrentContext = function() { require('../server/current-context')(loopback);
// A placeholder method, see lib/middleware/context.js for the real version
return null;
};
/*! /*!
* Built in models / services * Built in models / services

View File

@ -93,6 +93,7 @@
"browser": { "browser": {
"express": "./lib/browser-express.js", "express": "./lib/browser-express.js",
"./lib/server-app.js": "./lib/browser-express.js", "./lib/server-app.js": "./lib/browser-express.js",
"./server/current-context.js": "./browser/current-context.js",
"connect": false, "connect": false,
"nodemailer": false, "nodemailer": false,
"supertest": false, "supertest": false,

138
server/current-context.js Normal file
View File

@ -0,0 +1,138 @@
var juggler = require('loopback-datasource-juggler');
var remoting = require('strong-remoting');
var cls = require('continuation-local-storage');
var domain = require('domain');
module.exports = function(loopback) {
/**
* Get the current context object. The context is preserved
* across async calls, it behaves like a thread-local storage.
*
* @returns {ChainedContext} The context object or null.
*/
loopback.getCurrentContext = function() {
// A placeholder method, see loopback.createContext() for the real version
return null;
};
/**
* Run the given function in such way that
* `loopback.getCurrentContext` returns the
* provided context object.
*
* **NOTE**
*
* The method is supported on the server only, it does not work
* in the browser at the moment.
*
* @param {Function} fn The function to run, it will receive arguments
* (currentContext, currentDomain).
* @param {ChainedContext} context An optional context object.
* When no value is provided, then the default global context is used.
*/
loopback.runInContext = function(fn, context) {
var currentDomain = domain.create();
currentDomain.oldBind = currentDomain.bind;
currentDomain.bind = function(callback, context) {
return currentDomain.oldBind(ns.bind(callback, context), context);
};
var ns = context || loopback.createContext('loopback');
currentDomain.run(function() {
ns.run(function executeInContext(context) {
fn(ns, currentDomain);
});
});
};
/**
* Create a new LoopBackContext instance that can be used
* for `loopback.runInContext`.
*
* **NOTES**
*
* At the moment, `loopback.getCurrentContext` supports
* a single global context instance only. If you call `createContext()`
* multiple times, `getCurrentContext` will return the last context
* created.
*
* The method is supported on the server only, it does not work
* in the browser at the moment.
*
* @param {String} scopeName An optional scope name.
* @return {ChainedContext} The new context object.
*/
loopback.createContext = function(scopeName) {
// Make the namespace globally visible via the process.context property
process.context = process.context || {};
var ns = process.context[scopeName];
if (!ns) {
ns = cls.createNamespace(scopeName);
process.context[scopeName] = ns;
// Set up loopback.getCurrentContext()
loopback.getCurrentContext = function() {
return ns && ns.active ? ns : null;
};
chain(juggler);
chain(remoting);
}
return ns;
};
/**
* Create a chained context
* @param {Object} child The child context
* @param {Object} parent The parent context
* @private
* @constructor
*/
function ChainedContext(child, parent) {
this.child = child;
this.parent = parent;
}
/**
* Get the value by name from the context. If it doesn't exist in the child
* context, try the parent one
* @param {String} name Name of the context property
* @returns {*} Value of the context property
* @private
*/
ChainedContext.prototype.get = function(name) {
var val = this.child && this.child.get(name);
if (val === undefined) {
return this.parent && this.parent.get(name);
}
};
ChainedContext.prototype.set = function(name, val) {
if (this.child) {
return this.child.set(name, val);
} else {
return this.parent && this.parent.set(name, val);
}
};
ChainedContext.prototype.reset = function(name, val) {
if (this.child) {
return this.child.reset(name, val);
} else {
return this.parent && this.parent.reset(name, val);
}
};
function chain(child) {
if (typeof child.getCurrentContext === 'function') {
var childContext = new ChainedContext(child.getCurrentContext(),
loopback.getCurrentContext());
child.getCurrentContext = function() {
return childContext;
};
} else {
child.getCurrentContext = loopback.getCurrentContext;
}
}
};

View File

@ -1,31 +1,9 @@
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var juggler = require('loopback-datasource-juggler');
var remoting = require('strong-remoting');
var cls = require('continuation-local-storage');
var domain = require('domain');
module.exports = context; module.exports = context;
var name = 'loopback'; var name = 'loopback';
function createContext(scope) {
// Make the namespace globally visible via the process.context property
process.context = process.context || {};
var ns = process.context[scope];
if (!ns) {
ns = cls.createNamespace(scope);
process.context[scope] = ns;
// Set up loopback.getCurrentContext()
loopback.getCurrentContext = function() {
return ns && ns.active ? ns : null;
};
chain(juggler);
chain(remoting);
}
return ns;
}
/** /**
* Context middleware. * Context middleware.
* ```js * ```js
@ -44,89 +22,31 @@ function context(options) {
options = options || {}; options = options || {};
var scope = options.name || name; var scope = options.name || name;
var enableHttpContext = options.enableHttpContext || false; var enableHttpContext = options.enableHttpContext || false;
var ns = createContext(scope); var ns = loopback.createContext(scope);
// Return the middleware // Return the middleware
return function contextHandler(req, res, next) { return function contextHandler(req, res, next) {
if (req.loopbackContext) { if (req.loopbackContext) {
return next(); return next();
} }
loopback.runInContext(function processRequestInContext(ns, domain) {
req.loopbackContext = ns; req.loopbackContext = ns;
// Bind req/res event emitters to the given namespace // Bind req/res event emitters to the given namespace
ns.bindEmitter(req); ns.bindEmitter(req);
ns.bindEmitter(res); ns.bindEmitter(res);
var currentDomain = domain.create(); // Add req/res event emitters to the current domain
currentDomain.oldBind = currentDomain.bind; domain.add(req);
currentDomain.bind = function(callback, context) { domain.add(res);
return currentDomain.oldBind(ns.bind(callback, context), context);
};
currentDomain.add(req);
currentDomain.add(res);
// Create namespace for the request context
currentDomain.run(function() {
ns.run(function processRequestInContext(context) {
// Run the code in the context of the namespace // Run the code in the context of the namespace
if (enableHttpContext) { if (enableHttpContext) {
ns.set('http', {req: req, res: res}); // Set up the transport context // Set up the transport context
ns.set('http', {req: req, res: res});
} }
next(); next();
}); });
});
}; };
} }
/**
* Create a chained context
* @param {Object} child The child context
* @param {Object} parent The parent context
* @private
* @constructor
*/
function ChainedContext(child, parent) {
this.child = child;
this.parent = parent;
}
/*!
* Get the value by name from the context. If it doesn't exist in the child
* context, try the parent one
* @param {String} name Name of the context property
* @returns {*} Value of the context property
*/
ChainedContext.prototype.get = function(name) {
var val = this.child && this.child.get(name);
if (val === undefined) {
return this.parent && this.parent.get(name);
}
};
ChainedContext.prototype.set = function(name, val) {
if (this.child) {
return this.child.set(name, val);
} else {
return this.parent && this.parent.set(name, val);
}
};
ChainedContext.prototype.reset = function(name, val) {
if (this.child) {
return this.child.reset(name, val);
} else {
return this.parent && this.parent.reset(name, val);
}
};
function chain(child) {
if (typeof child.getCurrentContext === 'function') {
var childContext = new ChainedContext(child.getCurrentContext(),
loopback.getCurrentContext());
child.getCurrentContext = function() {
return childContext;
};
} else {
child.getCurrentContext = loopback.getCurrentContext;
}
}

View File

@ -462,5 +462,19 @@ describe('loopback', function() {
done(); done();
}); });
}); });
it('works outside REST middleware', function(done) {
loopback.runInContext(function() {
var ctx = loopback.getCurrentContext();
expect(ctx).is.an('object');
ctx.set('test-key', 'test-value');
process.nextTick(function() {
var ctx = loopback.getCurrentContext();
expect(ctx).is.an('object');
expect(ctx.get('test-key')).to.equal('test-value');
done();
});
});
});
}); });
}); });