From 4f4cdbf3b3b5416d1928b384e5b28a79af1aaa0e Mon Sep 17 00:00:00 2001 From: Eugene Daragan Date: Thu, 4 Apr 2024 22:15:58 +0200 Subject: [PATCH] Add waitAsync, adjust docs and readme --- README.md | 27 +++++++++-------- package.json | 2 +- src/container.ts | 6 +++- src/scopes.ts | 6 ++-- test/dioma.test.ts | 74 +++++++++++++++++++++++----------------------- 5 files changed, 60 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 3cbc46a..5c14e2c 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ const car = container.inject(Car); car.park(); ``` -Container scoped classes usually are [registered in the container](#class-registration) first. Without it, the class will "stick" to the container it's used in. +Container-scoped classes usually are [registered in the container](#class-registration) first. Without it, the class will "stick" to the container it's used in. ### Resolution scope @@ -269,12 +269,12 @@ To unregister a class, use the `unregister` method: container.unregister(FooBar); ``` -After the unregistration, the class will be removed from the container and all its child containers, and the next injection will return a new instance. +After that, the class will be removed from the container and all its child containers, and the next injection will return a new instance. ## Injection with tokens -Instead of passing a class the `inject`, you can use tokens instead. -Tokens can be used for class, value, and factory injection. +Instead of passing a class to the `inject`, you can use **tokens** instead. +The token injection can be used for **class, value, and factory** injection. Here's detailed information about each type. ### Class tokens @@ -326,6 +326,7 @@ const wildAnimal = wild.inject(animalToken); // Returns Cat instance const zooAnimal = zoo.inject(animalToken); ``` + The class token registration can also override the scope of the class: @@ -341,8 +342,6 @@ Value tokens are useful to inject a constant value: ```typescript import { Token } from "dioma"; -const container = new Container(); - const token = new Token("Value token"); container.register({ token, value: "Value" }); @@ -360,8 +359,6 @@ The factory takes the current container as the first argument and returns a valu ```typescript import { Token } from "dioma"; -const container = new Container(); - const token = new Token("Factory token"); container.register({ token, factory: (container) => "Value" }); @@ -469,8 +466,8 @@ class B { const a = await injectAsync(A); const b = await injectAsync(B); -// All cycles are resolved on the next tick -await new Promise((resolve) => setTimeout(resolve, 0)); +// Wait until all promises are resolved +await globalContainer.waitAsync(); a.doWork(); b.doAnotherWork(); @@ -480,9 +477,9 @@ b.doAnotherWork(); Async injection has an undefined behavior when there is a loop with transient dependencies. It may return an instance with an unexpected loop, or throw the `Circular dependency detected in async resolution` error, so it's better to avoid such cases. -As defined in the code above, you need to **wait for the next tick** to get all instance promises resolved, even if you use `await injectAsync(...)`. +As defined in the code above, you need to use `container.waitAsync()` or **wait for the next tick** to get all instance promises resolved, even if you use `await injectAsync(...)`. -Generally, if you expect your dependency to have an async resolution, it's better to inject it with `injectAsync`, as in the example above. But, you can also use `inject` for async injection as long as you wait for the next tick after it. +Generally, if you expect your dependency to have an async resolution, it's better to inject it with `injectAsync`, as in the example above. But, you can also use `inject` for async injection as long as you wait for it as above. Tokens also can be used for async injection as well: @@ -550,6 +547,10 @@ Injects the instance of the class or token, and provides arguments to the constr Injects the promise of the instance of the class or token, and provides arguments to the constructor or factory function. +### `container.waitAsync()` + +Returns a promise that resolves when all current async injections are resolved. + ### `container.register({ class, token?, scope? })` ### `container.register({ token, value })` @@ -560,7 +561,7 @@ Registers the class, value, or factory with the token in the container. ### `container.unregister(classOrToken)` -Unregisters the class or token from the container. +Unregister the class or token from the container. ### `container.childContainer(name?)` diff --git a/package.json b/package.json index 90b4549..961e0d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dioma", - "version": "0.4.0", + "version": "0.4.1", "description": "Elegant dependency injection container for vanilla JavaScript and TypeScript", "license": "MIT", "repository": { diff --git a/src/container.ts b/src/container.ts index 1f6cf8e..bd1ea89 100644 --- a/src/container.ts +++ b/src/container.ts @@ -44,7 +44,7 @@ export class Container { return new Container(this, name); }; - public __getInstance(cls: any, args: any[] = []) { + public $getInstance(cls: any, args: any[] = []) { let instance = null; let container: Container | null = this; @@ -177,6 +177,10 @@ export class Container { return promise; }; + waitAsync = async () => { + await Promise.all(this.pendingPromiseMap.values()); + }; + register>(descriptor: TokenValueDescriptor): void; register>(descriptor: TokenFactoryDescriptor): void; diff --git a/src/scopes.ts b/src/scopes.ts index aec2e69..5388eb9 100644 --- a/src/scopes.ts +++ b/src/scopes.ts @@ -9,7 +9,7 @@ export class Scopes { throw new ArgumentsError(SingletonScope.name, cls.name); } - return globalContainer.__getInstance(cls); + return globalContainer.$getInstance(cls); }; } @@ -25,13 +25,13 @@ export class Scopes { throw new ArgumentsError(ContainerScope.name, cls.name); } - return container.__getInstance(cls); + return container.$getInstance(cls); }; } public static Resolution(): ScopeHandler { return function ResolutionScope(cls, args, _, resolutionContainer) { - return resolutionContainer.__getInstance(cls, args); + return resolutionContainer.$getInstance(cls, args); }; } diff --git a/test/dioma.test.ts b/test/dioma.test.ts index e16ff4b..68d1014 100644 --- a/test/dioma.test.ts +++ b/test/dioma.test.ts @@ -1,19 +1,16 @@ +import { beforeEach, describe, expect, it } from "vitest"; import { + ArgumentsError, + // AsyncDependencyCycleError, Container, + DependencyCycleError, + ScopedClass, Scopes, - inject, + Token, globalContainer, - DependencyCycleError, - AsyncDependencyCycleError, + inject, injectAsync, - ArgumentsError, - Token, - ScopedClass, } from "../src"; -import { describe, it, expect, beforeEach } from "vitest"; -import { ScopeHandler } from "../src/types"; - -const delay = (ms: number = 0) => new Promise((resolve) => setTimeout(resolve, ms)); describe("Dioma", () => { beforeEach(() => { @@ -429,7 +426,7 @@ describe("Dioma", () => { const instance = inject(CircularDependencyA); - await delay(); + await globalContainer.waitAsync(); expect(instance).toBeInstanceOf(CircularDependencyA); expect(instance.instanceB).toBeInstanceOf(CircularDependencyB); @@ -438,7 +435,7 @@ describe("Dioma", () => { const instance2 = inject(CircularDependencyB); - await delay(); + await globalContainer.waitAsync(); expect(instance2).toBeInstanceOf(CircularDependencyB); expect(instance2.instanceA).toBeInstanceOf(CircularDependencyA); @@ -473,7 +470,7 @@ describe("Dioma", () => { const instance = inject(CircularDependencyA); - await delay(); + await globalContainer.waitAsync(); expect(instance).toBeInstanceOf(CircularDependencyA); expect(instance.instanceB).toBeInstanceOf(CircularDependencyB); @@ -482,7 +479,7 @@ describe("Dioma", () => { const instance2 = inject(CircularDependencyB); - await delay(); + await globalContainer.waitAsync(); expect(instance2).toBeInstanceOf(CircularDependencyB); expect(instance2.instanceA).toBeInstanceOf(CircularDependencyA); @@ -634,9 +631,7 @@ describe("Dioma", () => { const instance = inject(CircularDependencyA); - await delay(); - await delay(); - await delay(); + await globalContainer.waitAsync(); expect(instance).toBeInstanceOf(CircularDependencyA); expect(instance.instanceB).toBeInstanceOf(CircularDependencyB); @@ -645,8 +640,7 @@ describe("Dioma", () => { const instance2 = inject(CircularDependencyB); - await delay(); - await delay(); + await globalContainer.waitAsync(); expect(instance2).toBeInstanceOf(CircularDependencyB); expect(instance2.instanceA).toBeInstanceOf(CircularDependencyA); @@ -681,7 +675,7 @@ describe("Dioma", () => { const instance = inject(CircularDependencyA); - await delay(); + await globalContainer.waitAsync(); expect(instance).toBeInstanceOf(CircularDependencyA); expect(instance.instanceB).toBeInstanceOf(CircularDependencyB); @@ -690,7 +684,7 @@ describe("Dioma", () => { const instance2 = inject(CircularDependencyB); - await delay(); + await globalContainer.waitAsync(); expect(instance2).toBeInstanceOf(CircularDependencyB); expect(instance2.instanceA).toBeInstanceOf(CircularDependencyA); @@ -719,7 +713,7 @@ describe("Dioma", () => { const instance = inject(CircularDependencyA); - await delay(); + await globalContainer.waitAsync(); expect(instance).toBeInstanceOf(CircularDependencyA); expect(instance.instanceB).toBeInstanceOf(CircularDependencyB); @@ -732,7 +726,7 @@ describe("Dioma", () => { expect(instance).not.toBe(instance2); - await delay(); + await globalContainer.waitAsync(); expect(instance2).toBeInstanceOf(CircularDependencyB); expect(instance2.instanceA).toBeInstanceOf(CircularDependencyA); @@ -767,7 +761,7 @@ describe("Dioma", () => { const instance = await injectAsync(CircularDependencyA); - await delay(); + await globalContainer.waitAsync(); expect(instance).toBeInstanceOf(CircularDependencyA); expect(instance.instanceB).toBeInstanceOf(CircularDependencyB); @@ -782,7 +776,7 @@ describe("Dioma", () => { ); }); - it("should throw error when trying to inject transients with async only loop", async () => { + it("should have unexpected result when trying to inject transients with async only loop", async () => { let errorA: Error | null = null; let errorB: Error | null = null; @@ -803,7 +797,7 @@ describe("Dioma", () => { } class CircularDependencyB { - declare instanceA: CircularDependencyA; + public instanceA: CircularDependencyA; constructor(private promiseA = injectAsync(CircularDependencyA)) { this.promiseA @@ -820,10 +814,16 @@ describe("Dioma", () => { const instance = await injectAsync(CircularDependencyA); - await delay(); + await globalContainer.waitAsync(); - expect(errorA).toBeInstanceOf(AsyncDependencyCycleError); + expect(errorA).toBe(null); expect(errorB).toBe(null); + + expect(instance).toBeInstanceOf(CircularDependencyA); + expect(instance.instanceB).toBeInstanceOf(CircularDependencyB); + + expect(instance.instanceB.instanceA).not.toBe(instance); + expect(instance.instanceB).not.toBe(instance.instanceB.instanceA.instanceB); }); it("should be able to inject async for container scope", async () => { @@ -849,7 +849,7 @@ describe("Dioma", () => { const instance = container.inject(CircularDependencyA); - await delay(); + await container.waitAsync(); expect(instance).toBeInstanceOf(CircularDependencyA); expect(instance.instanceB).toBeInstanceOf(CircularDependencyB); @@ -857,7 +857,7 @@ describe("Dioma", () => { const instance2 = container.inject(CircularDependencyB); - await delay(); + await container.waitAsync(); expect(instance2).toBeInstanceOf(CircularDependencyB); expect(instance2.instanceA).toBeInstanceOf(CircularDependencyA); @@ -879,7 +879,7 @@ describe("Dioma", () => { const instance = await injectAsync(CircularDependencyA); - await delay(); + await globalContainer.waitAsync(); expect(instance).toBeInstanceOf(CircularDependencyA); expect(instance.instanceA).toBe(instance); @@ -1017,7 +1017,7 @@ describe("Dioma", () => { const instanceB = inject(ClassB, "test"); - await delay(); + await globalContainer.waitAsync(); expect(instanceB).toBeInstanceOf(ClassB); expect(instanceB.value).toBe("test"); @@ -1227,7 +1227,7 @@ describe("Dioma", () => { expect(instance).toBeInstanceOf(A); - await delay(); + await container.waitAsync(); expect(instance.instanceB).toBeInstanceOf(B); expect(instance.instanceB.instanceA).toBeInstanceOf(A); @@ -1237,7 +1237,7 @@ describe("Dioma", () => { expect(instance2).toBeInstanceOf(B); - await delay(); + await container.waitAsync(); expect(instance2.instanceA).toBeInstanceOf(A); expect(instance2.instanceA.instanceB).toBeInstanceOf(B); @@ -1273,7 +1273,7 @@ describe("Dioma", () => { expect(instance).toBeInstanceOf(A); - await delay(); + await container.waitAsync(); expect(instance.instanceB).toBeInstanceOf(B); expect(instance.instanceB.instanceA).toBeInstanceOf(A); @@ -1283,7 +1283,7 @@ describe("Dioma", () => { expect(instance2).toBeInstanceOf(B); - await delay(); + await container.waitAsync(); expect(instance2.instanceA).toBeInstanceOf(A); expect(instance2.instanceA.instanceB).toBeInstanceOf(B); @@ -1319,7 +1319,7 @@ describe("Dioma", () => { expect(instance).toBeInstanceOf(A); - await delay(); + await container.waitAsync(); expect(instance.instanceB).toBeInstanceOf(B); expect(instance.instanceB.instanceA).toBeInstanceOf(A);