-
Notifications
You must be signed in to change notification settings - Fork 169
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
feat: support asynchronous initialization of services #126
base: develop
Are you sure you want to change the base?
Conversation
Hi, do you know when this PR will get merged? I'm looking for this feature or some workaround. |
I wonder if the automatic calling of an initializing function should be introduced to sync version as well. I like the PR but I am not experienced with this project so cannot decide whether the implementation is a good approach for the problem or not. PS: Kudos for the well-written description, make understanding rationale a lot easier. |
I think that the user of the service shouldn't be aware about whether the service is synchronous or asynchronous (otherwise it would break the encapsulation and changing the service behavior would be impossible in the future). I believe, if we want to support asynchronous services, we should change the entire API interface to be fully asynchronous. Regarding the concurrency issues, the DIC should initialize the promise on the first request and then to return the same promise to all callers, this would prevent the service from being initialized multiple times and all callers will eventually receive the same instance. |
That has a big performance overhead compared to using the sync API. That would mean serious perf regression when injecting services in loops with a few thousands of elements. |
@NoNameProvided I'm not sure that I understand your point, could you elaborate on this please? If you are not using real async calls to initialize your services than all promises will be resolved synchronously. This will add some overhead due to indirection and additional function calls, but I think that performance hit will be almost unnoticeable. |
In the above comment, you mentioned that we should change the entire TypeDI API to async, which means everything needs to return a Creating promises is not cheap, in fact, there is like a 1:10 performance hit with async functions. If someone requests an instance from TypeDI in a loop or for example for every request on a web server, that will definitely add up. Once we started down this path, the user will have to create extra sync functions just to be able to await the Service, so ther performance hit will be rather A very bacic perf test: let counter = 0;
async function asyncTest() { return 1 + 1; }
function test() { return 1 + 1; }
// call both of them a lot of time
// counter = asyncTest() vs counter = test() |
This comment has been minimized.
This comment has been minimized.
Perhaps another interesting and useful addition could be |
825137a
to
ee97141
Compare
ee97141
to
724a6d4
Compare
Something about it feels off. But I have no reasonable argument. I would personally avoid having this kind of logic in my container, as an alternative, you could do: class A {
ready: Promise<boolean>;
constructor() {
this.ready = new Promise((resolve) => {
await this.init();
resolve();
});
}
async init() { ... }
}
const a = container.get(A);
await a.ready; |
I am thinking in something like // pseudo code not real names
Container.set({ type: MyAsyncClass, id: MyAsyncClass, async: true })
Container.set({ type: SyncDependingOnAbove, id: SyncDependingOnAbove })
await Container.waitForClassInit() // or Container.set also returns a promise which can be awaited This " |
There are some very common use-cases requiring asynchronous initialization of class instances, e.g. database connection on a repository. This PR enables this via opt-in non-breaking features, without having to do said initialization outside of typedi.
This PR:
AsyncInitializedService
class to be extended by client codeContainer.getAsync()
andContainer.getManyAsync()
methodsasyncInitialization: boolean
option to the@Service
decorator's options objectUsage:
As long as you extend
AsyncInitializedService
and implementinitialize()
, this will automatically be resolved when usingContainer.getAsync()
andContainer.getManyAsync()
.If you would rather not extend this and want more control, you can set the
asyncInitialization
option on the@Service
decorator totrue
and make sure that you setYourClass#initialized
to aPromise
from within your constructor. The following is equivalent to the above example, using this option instead:Considerations / Potential Problems:
Now that this can be called asynchronously and may defer execution at multiple places within the code, it is possible to get into weird states, especially if you don't
await
when calling the new methods. It is, for example, possible to instantiate a global more than once. Given that the use-cases are aimed at start-up initialization, this should be mostly a non-issue, however it's clearly better if we can avoid users shooting themselves in the foot. One way to mitigate this is through maintaining a mutex on eachContainerInstance
, and throwing an error if a container attempts to be used while another method is still in progress.Another idea is to opt-in to asynchronous
Container
instances, either on the global instance or any scoped instance. This way, the differentgetAsync
andgetManyAsync
naming could revert to use the originalget
andgetMany
. Then, if we implement the mutex, we could actually wait until the container is free instead of throwing an error (without having to worry about dealing with the sync methods).TODO:
ContainerInstance
but I don't currently see straightforward refactor strategy yet. Would like maintainer feedback here.