Closed ritch closed 7 years ago
+1 for this method being a poor solution.
I refactored all of my code and tests after this popped up randomly only to discover that it does not work on related methods like contractor.prototype.__create__contract
.
It appears that the options
arg is not being persisted for related models. You can check method.accepts
during runtime and see that it does not have options
even after verifying that it is pushed on in this code.
app.remotes().methods().forEach(function(method) {
if(!hasOptions(method.accepts)) {
method.accepts.push({
arg: 'options',
description: '**Do not implement in clients**',
type: 'object',
injectCtx: true
});
}
});
For anyone who is interested here is an updated version of the hack that takes into account all of the previously listed errors, except for the related methods error.
/*
* Sets up options injection for passing accessToken and other properties around internally
*/
var setupOptionsInjection = function() {
function inject(ctx, next) {
var options = hasOptions(ctx.method.accepts) && (ctx.args.options || {});
if(options) {
options.accessToken = ctx.req.accessToken;
ctx.args.options = options;
}
next();
}
function hasOptions(accepts) {
for (var i = 0; i < accepts.length; i++) {
var argDesc = accepts[i];
if (argDesc.arg === 'options' && argDesc.injectCtx) {
return true;
}
}
}
if(!process.env.GENERATING_SDK) {
app.remotes().before('*.*', inject);
app.remotes().before('*.prototype.*', function(ctx, instance, next) {
if (typeof instance === 'function') {
next = instance;
}
inject(ctx, next);
});
var blacklist = ['login', 'logout', 'confirm', 'resetPassword'];
// unfortunately this requires us to add the options object
// to the remote method definition
app.remotes().methods().forEach(function(method) {
if(!hasOptions(method.accepts) && blacklist.indexOf(method.name) === -1) {
method.accepts.push({
arg: 'options',
description: '**Do not implement in clients**',
type: 'object',
injectCtx: true
});
}
});
}
};
This takes into account the bug pointed out by @wprater and a fix for it by @digitalsadhu.
It also adds a blacklist that will ignore certain functions such as login and logout.This fixes the issues noticed by @ryedin and others. The GENERATING_SDK
env var can be set before running lb-ng
to avoid having options
in the angular sdk.
@richardpringle is there any update on a fix for this issue?
As @hotaru355 @zanemcca mention to "model.prototype.createrelation" not work for @ritch 's solution. Because prototype.methods resolve by dynamically
https://github.com/strongloop/strong-remoting/blob/master/lib/shared-class.js#L130
My solution is walking around by acl.
module.exports = function(app) {
var Role = app.models.Role;
var merchantStaffRoles = ['owner', 'manager', 'cashier'];
function checkOptions(method) {
if(!hasOptions(method.accepts)) {
method.accepts.push({
arg: 'options',
type: 'object',
injectCtx: true
});
}
}
function hasOptions(accepts) {
for (var i = 0; i < accepts.length; i++) {
var argDesc = accepts[i];
if (argDesc.arg === 'options' && argDesc.injectCtx) {
return true;
}
}
}
function errorWithStatus(msg, status) {
var error = new Error(msg);
error.status = status;
return error;
}
// Inspire by https://github.com/strongloop/loopback/issues/1495
Role.registerResolver('merchantStaff', function (role, ctx, next) {
if(!ctx.accessToken.userId) return process.nextTick(()=>next(errorWithStatus('Forbidden anonymouse user', 401), false));
app.models.User.findById(ctx.accessToken.userId, function(err, user) {
if (err || !user) {
if(!user) err = errorWithStatus('Forbidden anonymouse user', 401);
return next(err, false);
}
checkOptions(ctx.sharedMethod);
var options = ctx.remotingContext.args.options||{};
options.currentUser = user;
ctx.remotingContext.args.options = options;
next(err, merchantStaffRoles.indexOf(user.role)>-1);
});
});
// Another poor solution, not work for model.prototype.__create__relation
// https://github.com/snowyu/loopback-component-remote-ctx.js
};
Are there any updates on this issue?
Why is this still a problem? We're about to go live with our app which has been in development for months and now this happens... Damn, this is not good at ALL...
In fact, I do not like this tricky. Why not to pass the current-context
as an optional argument to the model's event directly.
I agree that the each operation should have the remoting context when triggered by a user.
The scope of this change could be pretty big though and will have to be evaluated.
@raymondfeng @ritch @bajtos, what do you guys think?
How can I access the remote context in a middle ware, it just have (req, res, next)
?
I used monkey patching of strong remoting SharedMethod.prototype.invoke method to inject options, similar to callback. We can set req.contextOptions in middlewares, this gets passed to juggler and you can receive this in all observer hooks or user hooks.
var injectedOptions = ctx.req.contextOptions
formattedArgs.push(injectedOptions);
// add in the required callback
formattedArgs.push(callback);
@souluniversal
Can I lobby to have the issues with getCurrentContext() and this suggestion/issue page etc referenced in the loopback docs (e.g., https://docs.strongloop.com/display/public/LB/Using+current+context)?
Could save others some serious headaches.
Good idea, done.
~Since this issue is now referenced in the docs, I think it's good to reference yet another (completely different) alternative, even if not done (yet)~
~https://github.com/strongloop/loopback-context/pull/2~
~TL;DR; use the native-ish cls-hooked
(which is good for Node 6 and future Node releases) instead of continuation-local-storage
, and detect (unfortunately not at compile time) a very common source of bugs with cls
which were successfully reproduced (maybe only a subset of the possible bugs, so be warned).~ PR closed
FWIW: if you are writing a custom remote method and all you need is currentUserId
, then the following approach may work well for you:
MyModel.customFunc = function(data, currentUserId, cb) {
// ...
};
MyModel.remoteMethod('customFunc', {
accepts: [
// arguments supplied by the HTTP client
{ arg: 'data', type: 'object', required: true, http: { source: 'body' } },
// arguments inferred from the request context
{
arg: currentUserId, type: 'any',
// EDITED
http: function(ctx) { return ctx.req.accessToken && ctx.req.accessToken.userId; }
// doesn't work
// http: function(req) { return req.accessToken && req.accessToken.userId; }
},
],
// ...
});
@dongmai
How can I access the remote context in a middle ware, it just have
(req, res, next)
?
When using loopback-context#per-request
, you can use req.loopbackContext
to access the CLS container that will be returned by LoopBackContext.getCurrentContext()
.
@dongmai
I think I misunderstood what you are asking. I don't think there is an easy way how to access remoting context from a middleware. Middleware typically stores data on the req
object. Then you can create a remoting hook to process the data attached on the req
object and make any changes to the remoting context as needed.
Any updates on this issue?
I was also about to deploy to production when the context started to get lost. I have tried using the npm-shrinkwrap solution but it's not working for me.
Any particular version that I should stick to for continuation-local-storage?
Currently, my npm-shrinkwrap file contains the following cls version:
"continuation-local-storage": {
"version": "3.1.7",
"from": "continuation-local-storage@>=3.1.3 <4.0.0",
"resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.1.7.tgz",
"dependencies": {
"async-listener": {
"version": "0.6.3",
"from": "async-listener@>=0.6.0 <0.7.0",
"resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.3.tgz",
"dependencies": {
"shimmer": {
"version": "1.0.0",
"from": "shimmer@1.0.0",
"resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.0.0.tgz"
}
}
},
Are there any particular steps I should follow to make npm-shrinkwrap alleviate this problem? or are there any other potential workarounds? it's very furstrating because it seems that sometimes after re-deploying several times it ends up working.
Thanks!
@bajtos I have tried your solution. the following is my code.
ip and userId is undefined
Card.remoteMethod('preorder', {
isStatic: false,
accepts: [
{
arg: 'ip', type: 'any',
http: function (req) {return req.ip}
},
{
arg: 'userId', type: 'any',
http: function (req) {return req.accessToken && req.accessToken.userId}
}
],
returns: [
{arg: 'appId', type: 'string', description: '公众号名称'},
{arg: 'timeStamp', type: 'string'},
{arg: 'nonceStr', type: 'string'},
{arg: 'package', type: 'string'},
{arg: 'signType', type: 'string'},
{arg: 'paySign', type: 'string'}
],
http: {path: '/preorder', verb: 'post', errorStatus: 400}
});
Card.prototype.preorder = function (ip, userId, callback) {
console.log(ip);
console.log(userId);
}
@JoeShi
Same thing happened to me when trying to return the accessToken. One thing I've noted in the past is that for some reason req doesn't seem to have attached the accessToken object. At least it's not always available. I'm not sure why.
I'm thinking on having to parse the query to get the access_token and then just query the DB to get the userId corresponding to that token and use that instead. Unfortunately it will add another query for each call.
Disclaimer: I plan to do the same for options injection too soon. I'm not trying to hijack the original intent of this issue.
Here's an idea for those who are still sticking with CLS:
nodejs
, npm
and the output file called npm-shrinkwrap.json
after running npm shrinkwrap
then I can look into setting up all of the permutations on Docker and publish a table of results around which ones work and which ones don't.
cls
... if I guessed wrong, my apologies.apache-ab
or something to test the server. If someone has unit tests, or functional test so that I don't have to build a test harness, even better!I'm volunteering for running a large-scale experiment to narrow down the issue of what works most easily, therefore, anything you can pitch in to the trial will help.
Fwiw we have stopped using currentContext as well as operation hooks and never looked back. Have yet to come across a situation where this has been a problem for us. (We have a half dozen medium to large loopback apps in production)
You can always get ahold of req inside custom remote methods and before and after remote hooks and use that to pass state or get access to the user and token. On Thu, 18 Aug 2016 at 21:03, Pulkit Singhal notifications@github.com wrote:
Here's an idea for those who are still sticking with CLS:
- If the following people can share their exact versions for nodejs, npm and the output file called npm-shrinkwrap.json after running npm shrinkwrap then I can look into setting up all of the permutations on Docker and publish a table of results around which ones work and which ones don't.
- @Saganus https://github.com/Saganus, @skyserpent https://github.com/skyserpent , @pongstr https://github.com/pongstr , @snowyu https://github.com/snowyu,
- Any simplified code (based on your usecases) that you can share for quickly testing if CLS is working, would also be appreciated! I will volume-mount such code into all the docker containers and then maybe use apache-ab or something to test the server. If someone has unit tests, or functional test so that I don't have to build a test harness, even better!
- I understand that connectors are a big part of the puzzle so I hope to also test across mongo and memory. If you want me to test additional ones then you may have to help me out a bit by giving me a simple sandbox env that's ready to go. For example, I prepared ones for connectors that I'm familiar with:
- mongo - https://github.com/ShoppinPal/loopback-mongo-sandbox/blob/master/docker-compose.yml
- elasticsearch - https://github.com/strongloop-community/loopback-connector-elastic-search/blob/master/docker-compose-for-tests.yml
I'm volunteering for running a large-scale experiment to narrow down the issue of what works most easily, therefore, anything you can pitch in to the trial will help.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/strongloop/loopback/issues/1495#issuecomment-240823473, or mute the thread https://github.com/notifications/unsubscribe-auth/ABH2CjIvCZRVQ3Jl5jD2a2IR2zilxWbqks5qhKxrgaJpZM4FQCkF .
Hi @pulkitsinghal,
Thanks for offering to do this.
Regarding the code I'm not sure I'll be able to extract a small working example since in my case I've only experienced CLS issues when deploying to Heroku. I can however share my npm-shrinkwrap.json with the versions I'm using.
By the way, I was still having issues even after using npm-shrinkwrap, however I was not clearing the cache dir in Heroku so I guess that could've prevented it from working, since after I purged the cache dir and redeployed everything then things seem to started working ok (however not sure for how long or if we'll have the issue again or not).
I think I'm going to do as @digitalsadhu and remove all use of the currentContext and see if we can get rid of the issues.
One question for @digitalsadhu: you say you also got rid of operationHooks. Are they also causing context problems or were they removed for different reasons?
I ask because I also use operationsHooks and I'm not yet aware of any issues but I also am not looking very hard, however you just made me worry a bit :)
Thanks!
Btw, my shrinkwrap file is here:
Thanks for the help and hard work!
No issues with operation hooks as such it's just that they are afaik the only place where you can't get at the req without current context. In our experience before and after remote hooks are enough. On Fri, 19 Aug 2016 at 16:11, Akram Shehadi notifications@github.com wrote:
Hi @pulkitsinghal https://github.com/pulkitsinghal,
Thanks for offering to do this.
Regarding the code I'm not sure I'll be able to extract a small working example since in my case I've only experienced CLS issues when deploying to Heroku. I can however share my npm-shrinkwrap.json with the versions I'm using.
By the way, I was still having issues even after using npm-shrinkwrap, however I was not clearing the cache dir in Heroku so I guess that could've prevented it from working, since after I purged the cache dir and redeployed everything then things seem to started working ok (however not sure for how long or if we'll have the issue again or not).
I think I'm going to do as @digitalsadhu https://github.com/digitalsadhu and remove all use of the currentContext and see if we can get rid of the issues.
One question for @digitalsadhu https://github.com/digitalsadhu: you say you also got rid of operationHooks. Are they also causing context problems or were they removed for different reasons?
I ask because I also use operationsHooks and I'm not yet aware of any issues but I also am not looking very hard, however you just made me worry a bit :)
Thanks!
Btw, my shrinkwrap file is here:
Thanks for the help and hard work!
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/strongloop/loopback/issues/1495#issuecomment-241028843, or mute the thread https://github.com/notifications/unsubscribe-auth/ABH2CvOCySjvD4aOO93Xv3PNyksvcTMaks5qhbl8gaJpZM4FQCkF .
Is there any plan or a fix branch for this, this is really frustrating two days before the launch.
@pulkitsinghal
cls-hooked
instead of continuation-local-storage
, and that you'll need to clone my forked repo and checkout the branch cls-hooked
and install Node >= 4.
https://github.com/josieusa/loopback-context/blob/bf874aa3376b3edc4a6ee64d93855e7420bc71c3/test/main.test.js#L115cls-hooked
I don't know if it would be useful to test connectors. In fact, it uses async-hook
which patches these functions: setTimeout, clearTimeout, setInterval, clearInterval, process.nextTick, promise.prototype.then, as you can see here.
https://github.com/AndreasMadsen/async-hook/tree/9c2d1ee927233ec1ad31cbf286029c1c98bd9885/patches
In the case of cls-hooked
, I think that when it fails it's because they are not patched in time, before the connectors use these functions. That's why I wrote the test the way I did.@pulkitsinghal @Saganus @josieusa please open a new issue to discuss this, or perhaps use https://gitter.im/strongloop/loopback instead, so that we can keep this discussion focused on the original proposal, which is to allow an alternative way for injecting context into remote methods, one that does not rely on CLS.
Hello everybody watching this issue, I have just submitted a patch proposing a solution:
https://github.com/strongloop/loopback/pull/2762
I am encouraging you to take a look and let us know what do you think about my proposal.
Cons:
The written remote method must meet the specification, or crash the server(maybe use the Promise only to solve it). eg,
Note.remoteMethod('greet', {
isStatic: true,
accepts: [
{ arg: 'msg', type: 'string', http: { source: 'query' } },
{ arg: 'options', type: 'object', http: loopback.optionsFromContext },
],
returns: { arg: 'greeting', type: String },
http: { verb: 'get'}
});
Note.greet = function(msg, cb) {
process.nextTick(function() {
msg = msg || 'world';
cb(null, 'Hello ' + msg); //crash here.
});
};
On Aug 19 @digitalsadhu said:
Fwiw we have stopped using currentContext as well as operation hooks and never looked back. Have yet to come across a situation where this has been a problem for us. (We have a half dozen medium to large loopback apps in production)
@digitalsadhu, could you elaborate on the final form of your approach to replace currentContext?
I see about 6 variations on the solution discussed above, and as a SL noob I would appreciate a simple code sample, a "with CLS" and "no CLS" kind of thing... ; )
Hello again,
I came to this page from: https://docs.strongloop.com/display/public/LB/Using+current+context, which links to this page with the following (emphasis mine):
Refer to loopback issue #1495 updates and an alternative solution.
As I mentioned in my effort to contact @digitalsadhu, I'm trying to sort through all of the "alternative solutions" mentioned above in this thread to understand what the fix will entail.
I see @ritch commented on Sep 17, 2015
Will link the PR when I have something concrete. Thanks for all the feedback.
And @bajtos commented 7 days ago
Hello everybody watching this issue, I have just submitted a patch proposing a solution:
2762
I know everyone's busy and this is OSS, but if the SL docs send me to this page for an alternative solution, which one is it?
As a front-end dev covering for our back-end dev (away on holidays) I'm really reluctant to jump in and start re-inventing yet another fix to this - is there a recommended solution from SL, and if so, where is it?
@chris-hydra
Our early apps used current context, we fell prey to CLS buggyness and did some rewriting to use a solution similar to what @ritch proposed in the description of this issue. We only did this 1x as we found it just caused us too much hassle trying to pass around the context all the time.
In the end what we settled on was to never use operation hooks which to my knowledge are the only place you can't just get at the context as you please. This was actually a double win for us as operation hooks we find tend to introduce side effects throughout your codebase that can be easy to forget about. (we use remote hooks instead)
So heres how we essentially share our state throughout a request lifecycle.
There are various ways you can access the request early enough to set desired state. This sort of thing could be via middleware in server.js for example:
app.use((req, res, next) => {
req.user.name = 'bob'
next()
})
Though you can set state in components, mixins and boot scripts. Anywhere you can hook into the request lifecycle.
A boot script or component example
module.exports = function (app) {
app.remotes().before('**', (ctx, cb) => {
req.user.name = 'bob'
cb()
})
}
A Mixin example
module.exports = function (MyModel) {
MyModel.beforeRemote('**', (ctx, instance, cb) => {
req.user.name = 'bob'
cb()
})
}
MyModel.beforeRemote('create', (ctx, instance, cb) => {
// ctx.req.user.name
cb()
})
MyModel.myMethod = (ctx, cb) => {
// ctx.req.user.name
cb()
}
MyModel.remoteMethod('myMethod', {
accepts: [
{arg: 'ctx', type: 'object', http: {source: 'ctx'}}
],
returns: {root: true},
http: {path: '/my-method', verb: 'get'}
})
I realise that this likely won't accommodate everyones use cases but for us, this has worked out great. We have some pretty decent size loopback apps now and have absolutely zero need for getCurrentContext. Hopefully this helps someone out.
@digitalsadhu, thank you for taking the time to write this up - I hope other folks will get some use out of this as well.
@digitalsadhu, your approach seems to be natural and simple. How can it be used to access the state from the model's code, for example from the overrided CRUD method?
@aorlic We typically disable the CRUD endpoint in question and implement our own as a new remote method injecting ctx as needed.
eg. replacing find would look something like this
MyModel.disableRemoteMethod('find', true)
MyModel.customFind = (filter, ctx, cb) => {
// ctx.req.someState is now available
MyModel.find(filter, cb)
}
MyModel.remoteMethod('customFind', {
accepts: [
{arg: 'filter', type: 'object', http: {source: 'query'}}
{arg: 'ctx', type: 'object', http: {source: 'ctx'}}
],
returns: {root: true},
http: {path: '/', verb: 'get'}
})
Please excuse any mistakes, code above is untested and mostly going from memory.
I especially like this approach as the behaviour of the ORM's find method stays the same, only the endpoint is overridden.
@digitalsadhu, I get it, thank you. Seems like a valid workaround, as the external REST endpoints remain just the same.
However, I just cannot believe that a serious API Server does not permit to legally override a CRUD method (without this kind of hacking) and access the context from it.
What else a developer is expected to do in an API Server, if no to play around with CRUDs?
@digitalsadhu, one more question for you. In case of "overriding" a findById method this way, what should go here?
http: {path: '/', verb: 'get'}
What does it mean returns: {root: true}?
I can't make it work for this case...
Thank you very much!
@aorlic looking at the loopback source code, the definition for findById here: https://github.com/strongloop/loopback/blob/master/lib/persisted-model.js#L733 looks like:
{
description: 'Find a model instance by {{id}} from the data source.',
accessType: 'READ',
accepts: [
{ arg: 'id', type: 'any', description: 'Model id', required: true,
http: { source: 'path' }},
{ arg: 'filter', type: 'object',
description: 'Filter defining fields and include' },
],
returns: { arg: 'data', type: typeName, root: true },
http: { verb: 'get', path: '/:id' },
rest: { after: convertNullToNotFoundError },
}
{root: true}
just means that whatever the remote method returns is returned at the root of returned payload.
See the "argument descriptions" section of the remote methods docs here: https://docs.strongloop.com/display/public/LB/Remote+methods
For those looking for a workaround and details, here is a nice resource on the subject : http://blog.digitopia.com/tokens-sessions-users/
I am struggling to understand, exactly how to utilize any of the above solutions with remote operations like loaded, and access.
We have middleware that extracts a JWT, and in the past has stored data from the jwt in the current context. Then in remote operations such as loaded
, or access
we do some work, depending on values in the jwt.
getCurrentContext is seemingly randomly returning null now and no amount of shrinkwrap, or reverting library versions seems to have resolved it.
@fperks can you extract the jwt info and stick it on the req object in your middleware then use beforeRemote and afterRemote hooks instead of access and loaded operation hooks?
@digitalsadhu i feel that would require a significant amount of time and effort given the size of our project. Not to mention the amount of testing and other requirements that we would need to go through again.
I am more looking for a simple and clean solution to this problem.
@fperks Ah, fair enough. I'm afraid I'm not sure such a solution exists. :(
I've posted my workaround and some observations here: https://github.com/strongloop/loopback/issues/1676
@digitalsadhu Thank you for your suggestion! This is the great solution, we never getCurrentContext and never got currentContext bug. It's really cool.
Just a small updating on step Access state in a remote method
MyModel.remoteMethod('myMethod', {
accepts: [
{arg: 'ctx', type: 'object', http: {source: 'req'}}
],
returns: {root: true},
http: {path: '/my-method', verb: 'get'}
})
http.source can be req or context is also OK. More info http://loopback.io/doc/en/lb2/Remote-methods.html#http-mapping-of-input-arguments
hi all, there is already a ctx.options
object referenced here for operation hooks that
enables hooks to access any options provided by the caller of the specific model method (operation)
also when you look at the ctx context available for exemple at role resolving stage, there is no such ctx.options
object, but there is a ctx.remoteContext.options
. Anything you put in this later object is then already available in ctx.options
in remote method hooks
.
Couldn't we not just 'bridge' the ctx.remoteContext.options
object with the operation hooks ctx.options
object to make the ctx.options
behavior consistent all along the workflow ?
This way, one could choose to pass any options in this object and retrieve them in any related piece of code through the app.
One one my regular concern is indeed passing information from custom role resolver to any possible methods and related hooks so that i don't need to do again the role resolving (db requests consuming) in the models when needed for advanced CRUD access control.
It seems @guanbo is trying similar things here by going through the options
req argument
Keep this fresh, we are using the "Loopback Component Group Access"-module to enable group access to our resources. Of course this module requires the getCurrentContext which forced us to downgrade multiple modules to make things work. Such a basic thing shouldn't become an complicated puzzle of module versions to make it work.
@Undrium please check the solution I managed to set up here, gathering a lot of individual contributions https://github.com/strongloop/loopback/issues/1676#issuecomment-260088138
Hi everyone, following my comment here on Aug 10, I closed that PR and deleted that branch (in favor of the newer strongloop/loopback-context/pull/11 which works for me and is only waiting for review). EDIT Please use #2728 for comments about it, and not this, thank you
Hello, the patch allowing remote methods to receive remoting context via options
argument has been landed to both master
(3.x) and 2.x
.
Remaining tasks:
Please note that I am still intending to land (and release) the solution proposed by @josieusa in strongloop/loopback-context#11 too, as it offers an important alternative to our "official" approach.
Ouch, I pressed "comment" button too soon. The pull request for 2.x is waiting for CI right now (see #3048).
@bajtos
as it offers an important alternative to our "official" approach
which do you consider as the target approach ?
UPDATE 2016-12-22 We ended up implementing a different solution as described in https://github.com/strongloop/loopback/pull/3023.
Tasks
master
branch - see https://github.com/strongloop/loopback/pull/3023 (released inloopback@3.2.0
)2.x
- see https://github.com/strongloop/loopback/issues/3048 (released inloopback@2.37.0
)Original description for posterity
The example below allows you to inject the remote context object (from strong remoting) into all methods that accept an
options
argument.Why? So you can use the remote context in remote methods, operation hooks, connector implementation, etc.
This approach is specifically designed to allow you to do what is possible with
loopback.getCurrentContext()
but without the dependency oncls
. The injection approach is much simpler and should be quite a bit faster sincecls
adds some significant overhead.@bajtos @fabien @raymondfeng