Merge pull request #1254 from strongloop/feature/run-in-context
Add `loopback.runInContext`
This commit is contained in:
commit
6640f8a082
|
@ -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: {
|
||||||
|
|
|
@ -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.');
|
||||||
|
};
|
||||||
|
};
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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();
|
||||||
}
|
}
|
||||||
req.loopbackContext = ns;
|
|
||||||
// Bind req/res event emitters to the given namespace
|
|
||||||
ns.bindEmitter(req);
|
|
||||||
ns.bindEmitter(res);
|
|
||||||
|
|
||||||
var currentDomain = domain.create();
|
loopback.runInContext(function processRequestInContext(ns, domain) {
|
||||||
currentDomain.oldBind = currentDomain.bind;
|
req.loopbackContext = ns;
|
||||||
currentDomain.bind = function(callback, context) {
|
|
||||||
return currentDomain.oldBind(ns.bind(callback, context), context);
|
|
||||||
};
|
|
||||||
|
|
||||||
currentDomain.add(req);
|
// Bind req/res event emitters to the given namespace
|
||||||
currentDomain.add(res);
|
ns.bindEmitter(req);
|
||||||
|
ns.bindEmitter(res);
|
||||||
|
|
||||||
// Create namespace for the request context
|
// Add req/res event emitters to the current domain
|
||||||
currentDomain.run(function() {
|
domain.add(req);
|
||||||
ns.run(function processRequestInContext(context) {
|
domain.add(res);
|
||||||
// Run the code in the context of the namespace
|
|
||||||
if (enableHttpContext) {
|
// Run the code in the context of the namespace
|
||||||
ns.set('http', {req: req, res: res}); // Set up the transport context
|
if (enableHttpContext) {
|
||||||
}
|
// Set up the transport context
|
||||||
next();
|
ns.set('http', {req: req, res: res});
|
||||||
});
|
}
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue