-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for mixed sync/async bindings #193
Conversation
Making everything async seems to complicate the context so much. What about introducing class Context {
await: async function(key) {
return await this.get(key); // Return a promise regardless of the return value from get
}
... |
I agree async is adding extra complexity. However, consider this example in our codebase: this.find('applications.*').forEach(appBinding => {
debug('Registering app controllers for %j', appBinding.key);
const app = appBinding.getValue() as Application;
app.mountControllers(router);
}); Now let's say an application developer needs to run some async logic to build an app instance (it may be a bit silly, but let's use it for the sake of this example): this.bind('applications.async').toDynamicValue(() => {
// fetch some data, e.g. WSDL or Swagger
// or perhaps check with Consul to fetch updated config
const config = await appConfig();
return new AsyncApp(config);
}); Now the code in LoopBack's Again, this example may be not probable, but I feel there will be other, more probable places where we will expect context bindings to be synchronous and thus severely limiting extensibility. A real-world example: Socket.io session adapters (pre 1.x) - they are synchronous, therefore the only reliable implementation is the one storing data in memory (in-process), anything else is suffering from race conditions. See socketio/socket.io#952 and our own strongloop/strong-cluster-socket.io-store#8. Aren't we using |
Few more ideas:
|
2b38fb7
to
71c3939
Compare
packages/context/src/binding.ts
Outdated
return this; | ||
} | ||
|
||
toDynamicValue(factoryFn: () => BoundValue): this { | ||
this.getValue = factoryFn; | ||
// tslint:disable-next-line:promise-function-async |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I proposed a feature for tslint
that would make this comment unnecessary - see palantir/tslint#2637
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the second thought, let's remove promise-function-async
completely, it no longer serves the original purpose: #221
We had an offline discussion about this and we've agreed to implement the following: // Context is a container of async functions that resolve useful/parsed values from the request
const binding = ctx.findById('b')
binding.isSync() // true when bound to a value synchronously
binding.getSync() // returns value direct
ctx.getSync() // throws unless sync
ctx.get() // get the promise We should optimize |
71c3939
to
3fa3eac
Compare
Allow dynamic bindings to be created using an async function returning a promise. Modify `ctx.get` to always return a promise, regardless of whether the binding is sync or async. Modify dependency injection code to detect sync vs. async dependencies and optimise the code building the dependencies to prefer sync flow if allowed by dependencies.
3fa3eac
to
df42e70
Compare
@ritch I ended up with a simpler and more powerful design. There is no
@ritch @raymondfeng @superkhau please review |
A question to consider: should we rename |
LGTM Since you have the reference to the binding anyways its better to just resolve the value with that. 👍 |
packages/context/src/resolver.ts
Outdated
* @param ctor The class constructor to call. | ||
* @param ctx The context containing values for `@inject` resolution | ||
*/ | ||
export function createClassInstance<T>(ctor: Constructor<T>, ctx: Context): T | Promise<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name can be instantiate
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
This is required to allow injection of asynchronously-computed values into Controller constructors.
Allow dynamic bindings to be created using an async function returning
a promise.
Modify
ctx.get
to always return a promise, regardless of whetherthe binding is sync or async.
Modify dependency injection code to detect sync vs. async dependencies and optimise the code building the dependencies to prefer sync flow if allowed by dependencies.
Add new API
ctx.getAsync
that returns the bound value synchronously and throws if the factory function was async.Add new API
ctx.getBinding
to look up theBinding
object by a keyAdd new API
isPromise
that can be used by consumers ofctx.getBinding().getValue()
to detect whether the value was resolved sync or async.As we discussed elsewhere, most code should use
@inject
to resolve dependencies, therefore it should not be affected by sync/async nuances.When getting values from the context directly, people should default to asynchronous
ctx.get(key)
, because it's difficult to tell in advance which bindings may eventually become asynchronous due to 3rd party extensions.Having wrote that, the internal API for accessing either sync value or async Promise is public and available for advanced users.
This is a pre-requisite for #186 and #188 which I am implementing as part of the time allocated in strongloop-internal/scrum-asteroid#117.
cc @bajtos @raymondfeng @ritch @superkhau