From 2aa09ba5742f2d07eab7984711a20466a91eb814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 27 Mar 2015 14:45:37 +0100 Subject: [PATCH] Add `loopback.runInContext` Refactor the core implementation of current context from server/middleware/context.js into server/current-context.js. Expose new public API: - loopback.runInContext - loopback.createContext --- Gruntfile.js | 4 + browser/current-context.js | 10 +++ docs.json | 7 +- lib/loopback.js | 5 +- package.json | 1 + server/current-context.js | 138 +++++++++++++++++++++++++++++++++++ server/middleware/context.js | 112 ++++------------------------ test/loopback.test.js | 14 ++++ 8 files changed, 188 insertions(+), 103 deletions(-) create mode 100644 browser/current-context.js create mode 100644 server/current-context.js diff --git a/Gruntfile.js b/Gruntfile.js index ff145b65..e1720f6b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -39,6 +39,9 @@ module.exports = function(grunt) { common: { src: ['common/**/*.js'] }, + browser: { + src: ['browser/**/*.js'] + }, server: { src: ['server/**/*.js'] }, @@ -51,6 +54,7 @@ module.exports = function(grunt) { lib: ['lib/**/*.js'], common: ['common/**/*.js'], server: ['server/**/*.js'], + browser: ['browser/**/*.js'], test: ['test/**/*.js'] }, watch: { diff --git a/browser/current-context.js b/browser/current-context.js new file mode 100644 index 00000000..cdf1d8a2 --- /dev/null +++ b/browser/current-context.js @@ -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.'); + }; +}; diff --git a/docs.json b/docs.json index 94373a92..c99680b2 100644 --- a/docs.json +++ b/docs.json @@ -5,6 +5,7 @@ "lib/server-app.js", "lib/loopback.js", "lib/registry.js", + "server/current-context.js", "lib/access-context.js", { "title": "Base models", "depth": 2 }, "lib/model.js", @@ -13,7 +14,7 @@ "server/middleware/context.js", "server/middleware/favicon.js", "server/middleware/rest.js", - "server/middleware/static.js", + "server/middleware/static.js", "server/middleware/status.js", "server/middleware/token.js", "server/middleware/url-not-found.js", @@ -21,11 +22,11 @@ "common/models/access-token.js", "common/models/acl.js", "common/models/application.js", - "common/models/change.js", + "common/models/change.js", "common/models/email.js", "common/models/role.js", "common/models/role-mapping.js", - "common/models/scope.js", + "common/models/scope.js", "common/models/user.js" ], "assets": "/docs/assets" diff --git a/lib/loopback.js b/lib/loopback.js index fae80bb2..e98fb986 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -189,10 +189,7 @@ loopback.template = function(file) { return ejs.compile(str); }; -loopback.getCurrentContext = function() { - // A placeholder method, see lib/middleware/context.js for the real version - return null; -}; +require('../server/current-context')(loopback); /*! * Built in models / services diff --git a/package.json b/package.json index 2e065bad..535ffd20 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "browser": { "express": "./lib/browser-express.js", "./lib/server-app.js": "./lib/browser-express.js", + "./server/current-context.js": "./browser/current-context.js", "connect": false, "nodemailer": false, "supertest": false, diff --git a/server/current-context.js b/server/current-context.js new file mode 100644 index 00000000..6b8304e2 --- /dev/null +++ b/server/current-context.js @@ -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; + } + } +}; diff --git a/server/middleware/context.js b/server/middleware/context.js index 3890aa7a..95352018 100644 --- a/server/middleware/context.js +++ b/server/middleware/context.js @@ -1,31 +1,9 @@ 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; 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. * ```js @@ -44,89 +22,31 @@ function context(options) { options = options || {}; var scope = options.name || name; var enableHttpContext = options.enableHttpContext || false; - var ns = createContext(scope); + var ns = loopback.createContext(scope); // Return the middleware return function contextHandler(req, res, next) { if (req.loopbackContext) { return next(); } - req.loopbackContext = ns; - // Bind req/res event emitters to the given namespace - ns.bindEmitter(req); - ns.bindEmitter(res); - var currentDomain = domain.create(); - currentDomain.oldBind = currentDomain.bind; - currentDomain.bind = function(callback, context) { - return currentDomain.oldBind(ns.bind(callback, context), context); - }; + loopback.runInContext(function processRequestInContext(ns, domain) { + req.loopbackContext = ns; - currentDomain.add(req); - currentDomain.add(res); + // Bind req/res event emitters to the given namespace + ns.bindEmitter(req); + ns.bindEmitter(res); - // Create namespace for the request context - currentDomain.run(function() { - ns.run(function processRequestInContext(context) { - // Run the code in the context of the namespace - if (enableHttpContext) { - ns.set('http', {req: req, res: res}); // Set up the transport context - } - next(); - }); + // Add req/res event emitters to the current domain + domain.add(req); + domain.add(res); + + // Run the code in the context of the namespace + if (enableHttpContext) { + // Set up the transport context + 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; - } -} diff --git a/test/loopback.test.js b/test/loopback.test.js index bb42763a..c8e9853f 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -462,5 +462,19 @@ describe('loopback', function() { 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(); + }); + }); + }); }); });