Initial implementation

This commit is contained in:
Miroslav Bajtoš 2016-07-28 15:40:19 +02:00
parent c8daeb5587
commit 9bd4afbe7e
7 changed files with 306 additions and 4 deletions

View File

@ -2,3 +2,33 @@
Current context for LoopBack applications, based on
node-continuation-local-storage.
## Usage
1) Add `per-request-context` middleware to your
`server/middleware-config.json`:
```json
{
"initial": {
"loopback-context-cls#per-request-context": {
}
}
}
```
2) Then you can access the context from your code:
```js
var ClsContext = require('loopback-context-cls');
// ...
MyModel.myMethod = function(cb) {
var ctx = ClsContext.getCurrentContext();
ctx.get('key');
ctx.set('key', { foo: 'bar' });
});
```
See also https://docs.strongloop.com/display/APIC/Using+current+context

View File

@ -0,0 +1,17 @@
// Copyright IBM Corp. 2015. All Rights Reserved.
// Node module: loopback-context-cls
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var ClsContext = module.exports;
ClsContext.getCurrentContext = function() {
return null;
};
ClsContext.runInContext =
ClsContext.createContext = function() {
throw new Error('Current context is not supported in the browser.');
};

View File

@ -11,19 +11,24 @@
"type": "git",
"url": "https://github.com/strongloop/loopback-context"
},
"main": "index.js",
"browser": "browser.js",
"main": "server/current-context.js",
"browser": "browser/current-context.js",
"scripts": {
"test": "mocha",
"posttest": "npm run lint",
"lint": "eslint ."
},
"license": "MIT",
"dependencies": {},
"dependencies": {
"continuation-local-storage": "^3.1.7"
},
"devDependencies": {
"chai": "^3.5.0",
"dirty-chai": "^1.2.2",
"eslint": "^2.13.1",
"eslint-config-loopback": "^4.0.0",
"loopback": "^3.0.0-alpha.1",
"mocha": "^2.5.3"
"mocha": "^2.5.3",
"supertest": "^1.2.0"
}
}

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

@ -0,0 +1,85 @@
// Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-context-cls
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var cls = require('continuation-local-storage');
var domain = require('domain');
var ClsContext = module.exports;
/**
* Get the current context object. The context is preserved
* across async calls, it behaves like a thread-local storage.
*
* @returns {Namespace} The context object or null.
*/
ClsContext.getCurrentContext = function() {
// A placeholder method, see CurrentContext.createContext() for the real version
return null;
};
/**
* Run the given function in such way that
* `CurrentContext.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 {Namespace} context An optional context object.
* When no value is provided, then the default global context is used.
*/
ClsContext.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 || ClsContext.createContext('loopback');
currentDomain.run(function() {
ns.run(function executeInContext(context) {
fn(ns, currentDomain);
});
});
};
/**
* Create a new LoopBackContext instance that can be used
* for `CurrentContext.runInContext`.
*
* **NOTES**
*
* At the moment, `CurrentContext.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 {Namespace} The new context object.
*/
ClsContext.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 CurrentContext.getCurrentContext()
ClsContext.getCurrentContext = function() {
return ns && ns.active ? ns : null;
};
}
return ns;
};

View File

@ -0,0 +1,59 @@
// Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-context-cls
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var ClsContext = require('../current-context');
module.exports = context;
var name = 'loopback';
/**
* Context middleware.
* ```js
* var app = loopback();
* app.use(loopback.context(options);
* app.use(loopback.rest());
* app.listen();
* ```
* @options {Object} [options] Options for context
* @property {String} name Context scope name.
* @property {Boolean} enableHttpContext Whether HTTP context is enabled. Default is false.
* @header loopback.context([options])
*/
function context(options) {
options = options || {};
var scope = options.name || name;
var enableHttpContext = options.enableHttpContext || false;
var ns = ClsContext.createContext(scope);
// Return the middleware
return function contextHandler(req, res, next) {
if (req.loopbackContext) {
return next();
}
ClsContext.runInContext(function processRequestInContext(ns, domain) {
req.loopbackContext = ns;
// Bind req/res event emitters to the given namespace
ns.bindEmitter(req);
ns.bindEmitter(res);
// 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();
});
};
}

6
test/helpers/expect.js Normal file
View File

@ -0,0 +1,6 @@
'use strict';
var chai = require('chai');
chai.use(require('dirty-chai'));
module.exports = chai.expect;

100
test/main.test.js Normal file
View File

@ -0,0 +1,100 @@
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-context-cls
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var ClsContext = require('..');
var Domain = require('domain');
var EventEmitter = require('events').EventEmitter;
var expect = require('./helpers/expect');
var loopback = require('loopback');
var request = require('supertest');
describe('CLS Context', function() {
var runInOtherDomain, runnerInterval;
before(function setupRunInOtherDomain() {
var emitterInOtherDomain = new EventEmitter();
Domain.create().add(emitterInOtherDomain);
runInOtherDomain = function(fn) {
emitterInOtherDomain.once('run', fn);
};
runnerInterval = setInterval(function() {
emitterInOtherDomain.emit('run');
}, 10);
});
after(function tearDownRunInOtherDomain() {
clearInterval(runnerInterval);
});
// See the following two items for more details:
// https://github.com/strongloop/loopback/issues/809
// https://github.com/strongloop/loopback/pull/337#issuecomment-61680577
it('preserves callback domain', function(done) {
var app = loopback({localRegistry: true, loadBuiltinModels: true});
app.set('remoting', {context: false});
app.use(require('../server/middleware/per-request-context')());
app.use(loopback.rest());
app.dataSource('db', {connector: 'memory'});
var TestModel = loopback.createModel({name: 'TestModel'});
app.model(TestModel, {dataSource: 'db', public: true});
// function for remote method
TestModel.test = function(inst, cb) {
var tmpCtx = ClsContext.getCurrentContext();
if (tmpCtx) tmpCtx.set('data', 'a value stored in context');
if (process.domain) cb = process.domain.bind(cb); // IMPORTANT
runInOtherDomain(cb);
};
// remote method
TestModel.remoteMethod('test', {
accepts: {arg: 'inst', type: 'TestModel'},
returns: {root: true},
http: {path: '/test', verb: 'get'},
});
// after remote hook
TestModel.afterRemote('**', function(ctxx, inst, next) {
var tmpCtx = ClsContext.getCurrentContext();
if (tmpCtx) {
ctxx.result.data = tmpCtx.get('data');
} else {
ctxx.result.data = 'context not available';
}
next();
});
request(app)
.get('/TestModels/test')
.end(function(err, res) {
if (err) return done(err);
expect(res.body.data).to.equal('a value stored in context');
done();
});
});
it('works outside REST middleware', function(done) {
ClsContext.runInContext(function() {
var ctx = ClsContext.getCurrentContext();
expect(ctx).is.an('object');
ctx.set('test-key', 'test-value');
process.nextTick(function() {
var ctx = ClsContext.getCurrentContext();
expect(ctx).is.an('object');
expect(ctx.get('test-key')).to.equal('test-value');
done();
});
});
});
});