Skip to content

Commit 2aa09ba

Browse files
author
Miroslav Bajtoš
committed
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
1 parent 548cb6e commit 2aa09ba

File tree

8 files changed

+188
-103
lines changed

8 files changed

+188
-103
lines changed

Gruntfile.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ module.exports = function(grunt) {
3939
common: {
4040
src: ['common/**/*.js']
4141
},
42+
browser: {
43+
src: ['browser/**/*.js']
44+
},
4245
server: {
4346
src: ['server/**/*.js']
4447
},
@@ -51,6 +54,7 @@ module.exports = function(grunt) {
5154
lib: ['lib/**/*.js'],
5255
common: ['common/**/*.js'],
5356
server: ['server/**/*.js'],
57+
browser: ['browser/**/*.js'],
5458
test: ['test/**/*.js']
5559
},
5660
watch: {

browser/current-context.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = function(loopback) {
2+
loopback.getCurrentContext = function() {
3+
return null;
4+
};
5+
6+
loopback.runInContext =
7+
loopback.createContext = function() {
8+
throw new Error('Current context is not supported in the browser.');
9+
};
10+
};

docs.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"lib/server-app.js",
66
"lib/loopback.js",
77
"lib/registry.js",
8+
"server/current-context.js",
89
"lib/access-context.js",
910
{ "title": "Base models", "depth": 2 },
1011
"lib/model.js",
@@ -13,19 +14,19 @@
1314
"server/middleware/context.js",
1415
"server/middleware/favicon.js",
1516
"server/middleware/rest.js",
16-
"server/middleware/static.js",
17+
"server/middleware/static.js",
1718
"server/middleware/status.js",
1819
"server/middleware/token.js",
1920
"server/middleware/url-not-found.js",
2021
{ "title": "Built-in models", "depth": 2 },
2122
"common/models/access-token.js",
2223
"common/models/acl.js",
2324
"common/models/application.js",
24-
"common/models/change.js",
25+
"common/models/change.js",
2526
"common/models/email.js",
2627
"common/models/role.js",
2728
"common/models/role-mapping.js",
28-
"common/models/scope.js",
29+
"common/models/scope.js",
2930
"common/models/user.js"
3031
],
3132
"assets": "/docs/assets"

lib/loopback.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,7 @@ loopback.template = function(file) {
189189
return ejs.compile(str);
190190
};
191191

192-
loopback.getCurrentContext = function() {
193-
// A placeholder method, see lib/middleware/context.js for the real version
194-
return null;
195-
};
192+
require('../server/current-context')(loopback);
196193

197194
/*!
198195
* Built in models / services

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"browser": {
9494
"express": "./lib/browser-express.js",
9595
"./lib/server-app.js": "./lib/browser-express.js",
96+
"./server/current-context.js": "./browser/current-context.js",
9697
"connect": false,
9798
"nodemailer": false,
9899
"supertest": false,

server/current-context.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
var juggler = require('loopback-datasource-juggler');
2+
var remoting = require('strong-remoting');
3+
var cls = require('continuation-local-storage');
4+
var domain = require('domain');
5+
6+
module.exports = function(loopback) {
7+
8+
/**
9+
* Get the current context object. The context is preserved
10+
* across async calls, it behaves like a thread-local storage.
11+
*
12+
* @returns {ChainedContext} The context object or null.
13+
*/
14+
loopback.getCurrentContext = function() {
15+
// A placeholder method, see loopback.createContext() for the real version
16+
return null;
17+
};
18+
19+
/**
20+
* Run the given function in such way that
21+
* `loopback.getCurrentContext` returns the
22+
* provided context object.
23+
*
24+
* **NOTE**
25+
*
26+
* The method is supported on the server only, it does not work
27+
* in the browser at the moment.
28+
*
29+
* @param {Function} fn The function to run, it will receive arguments
30+
* (currentContext, currentDomain).
31+
* @param {ChainedContext} context An optional context object.
32+
* When no value is provided, then the default global context is used.
33+
*/
34+
loopback.runInContext = function(fn, context) {
35+
var currentDomain = domain.create();
36+
currentDomain.oldBind = currentDomain.bind;
37+
currentDomain.bind = function(callback, context) {
38+
return currentDomain.oldBind(ns.bind(callback, context), context);
39+
};
40+
41+
var ns = context || loopback.createContext('loopback');
42+
43+
currentDomain.run(function() {
44+
ns.run(function executeInContext(context) {
45+
fn(ns, currentDomain);
46+
});
47+
});
48+
};
49+
50+
/**
51+
* Create a new LoopBackContext instance that can be used
52+
* for `loopback.runInContext`.
53+
*
54+
* **NOTES**
55+
*
56+
* At the moment, `loopback.getCurrentContext` supports
57+
* a single global context instance only. If you call `createContext()`
58+
* multiple times, `getCurrentContext` will return the last context
59+
* created.
60+
*
61+
* The method is supported on the server only, it does not work
62+
* in the browser at the moment.
63+
*
64+
* @param {String} scopeName An optional scope name.
65+
* @return {ChainedContext} The new context object.
66+
*/
67+
loopback.createContext = function(scopeName) {
68+
// Make the namespace globally visible via the process.context property
69+
process.context = process.context || {};
70+
var ns = process.context[scopeName];
71+
if (!ns) {
72+
ns = cls.createNamespace(scopeName);
73+
process.context[scopeName] = ns;
74+
// Set up loopback.getCurrentContext()
75+
loopback.getCurrentContext = function() {
76+
return ns && ns.active ? ns : null;
77+
};
78+
79+
chain(juggler);
80+
chain(remoting);
81+
}
82+
return ns;
83+
};
84+
85+
/**
86+
* Create a chained context
87+
* @param {Object} child The child context
88+
* @param {Object} parent The parent context
89+
* @private
90+
* @constructor
91+
*/
92+
function ChainedContext(child, parent) {
93+
this.child = child;
94+
this.parent = parent;
95+
}
96+
97+
/**
98+
* Get the value by name from the context. If it doesn't exist in the child
99+
* context, try the parent one
100+
* @param {String} name Name of the context property
101+
* @returns {*} Value of the context property
102+
* @private
103+
*/
104+
ChainedContext.prototype.get = function(name) {
105+
var val = this.child && this.child.get(name);
106+
if (val === undefined) {
107+
return this.parent && this.parent.get(name);
108+
}
109+
};
110+
111+
ChainedContext.prototype.set = function(name, val) {
112+
if (this.child) {
113+
return this.child.set(name, val);
114+
} else {
115+
return this.parent && this.parent.set(name, val);
116+
}
117+
};
118+
119+
ChainedContext.prototype.reset = function(name, val) {
120+
if (this.child) {
121+
return this.child.reset(name, val);
122+
} else {
123+
return this.parent && this.parent.reset(name, val);
124+
}
125+
};
126+
127+
function chain(child) {
128+
if (typeof child.getCurrentContext === 'function') {
129+
var childContext = new ChainedContext(child.getCurrentContext(),
130+
loopback.getCurrentContext());
131+
child.getCurrentContext = function() {
132+
return childContext;
133+
};
134+
} else {
135+
child.getCurrentContext = loopback.getCurrentContext;
136+
}
137+
}
138+
};

server/middleware/context.js

Lines changed: 16 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,9 @@
11
var loopback = require('../../lib/loopback');
2-
var juggler = require('loopback-datasource-juggler');
3-
var remoting = require('strong-remoting');
4-
var cls = require('continuation-local-storage');
5-
var domain = require('domain');
62

73
module.exports = context;
84

95
var name = 'loopback';
106

11-
function createContext(scope) {
12-
// Make the namespace globally visible via the process.context property
13-
process.context = process.context || {};
14-
var ns = process.context[scope];
15-
if (!ns) {
16-
ns = cls.createNamespace(scope);
17-
process.context[scope] = ns;
18-
// Set up loopback.getCurrentContext()
19-
loopback.getCurrentContext = function() {
20-
return ns && ns.active ? ns : null;
21-
};
22-
23-
chain(juggler);
24-
chain(remoting);
25-
}
26-
return ns;
27-
}
28-
297
/**
308
* Context middleware.
319
* ```js
@@ -44,89 +22,31 @@ function context(options) {
4422
options = options || {};
4523
var scope = options.name || name;
4624
var enableHttpContext = options.enableHttpContext || false;
47-
var ns = createContext(scope);
25+
var ns = loopback.createContext(scope);
4826

4927
// Return the middleware
5028
return function contextHandler(req, res, next) {
5129
if (req.loopbackContext) {
5230
return next();
5331
}
54-
req.loopbackContext = ns;
55-
// Bind req/res event emitters to the given namespace
56-
ns.bindEmitter(req);
57-
ns.bindEmitter(res);
5832

59-
var currentDomain = domain.create();
60-
currentDomain.oldBind = currentDomain.bind;
61-
currentDomain.bind = function(callback, context) {
62-
return currentDomain.oldBind(ns.bind(callback, context), context);
63-
};
33+
loopback.runInContext(function processRequestInContext(ns, domain) {
34+
req.loopbackContext = ns;
35+
36+
// Bind req/res event emitters to the given namespace
37+
ns.bindEmitter(req);
38+
ns.bindEmitter(res);
6439

65-
currentDomain.add(req);
66-
currentDomain.add(res);
40+
// Add req/res event emitters to the current domain
41+
domain.add(req);
42+
domain.add(res);
6743

68-
// Create namespace for the request context
69-
currentDomain.run(function() {
70-
ns.run(function processRequestInContext(context) {
71-
// Run the code in the context of the namespace
72-
if (enableHttpContext) {
73-
ns.set('http', {req: req, res: res}); // Set up the transport context
74-
}
75-
next();
76-
});
44+
// Run the code in the context of the namespace
45+
if (enableHttpContext) {
46+
// Set up the transport context
47+
ns.set('http', {req: req, res: res});
48+
}
49+
next();
7750
});
7851
};
7952
}
80-
81-
/**
82-
* Create a chained context
83-
* @param {Object} child The child context
84-
* @param {Object} parent The parent context
85-
* @private
86-
* @constructor
87-
*/
88-
function ChainedContext(child, parent) {
89-
this.child = child;
90-
this.parent = parent;
91-
}
92-
93-
/*!
94-
* Get the value by name from the context. If it doesn't exist in the child
95-
* context, try the parent one
96-
* @param {String} name Name of the context property
97-
* @returns {*} Value of the context property
98-
*/
99-
ChainedContext.prototype.get = function(name) {
100-
var val = this.child && this.child.get(name);
101-
if (val === undefined) {
102-
return this.parent && this.parent.get(name);
103-
}
104-
};
105-
106-
ChainedContext.prototype.set = function(name, val) {
107-
if (this.child) {
108-
return this.child.set(name, val);
109-
} else {
110-
return this.parent && this.parent.set(name, val);
111-
}
112-
};
113-
114-
ChainedContext.prototype.reset = function(name, val) {
115-
if (this.child) {
116-
return this.child.reset(name, val);
117-
} else {
118-
return this.parent && this.parent.reset(name, val);
119-
}
120-
};
121-
122-
function chain(child) {
123-
if (typeof child.getCurrentContext === 'function') {
124-
var childContext = new ChainedContext(child.getCurrentContext(),
125-
loopback.getCurrentContext());
126-
child.getCurrentContext = function() {
127-
return childContext;
128-
};
129-
} else {
130-
child.getCurrentContext = loopback.getCurrentContext;
131-
}
132-
}

test/loopback.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,5 +462,19 @@ describe('loopback', function() {
462462
done();
463463
});
464464
});
465+
466+
it('works outside REST middleware', function(done) {
467+
loopback.runInContext(function() {
468+
var ctx = loopback.getCurrentContext();
469+
expect(ctx).is.an('object');
470+
ctx.set('test-key', 'test-value');
471+
process.nextTick(function() {
472+
var ctx = loopback.getCurrentContext();
473+
expect(ctx).is.an('object');
474+
expect(ctx.get('test-key')).to.equal('test-value');
475+
done();
476+
});
477+
});
478+
});
465479
});
466480
});

0 commit comments

Comments
 (0)