diff --git a/src/container.test.ts b/src/container.test.ts index cbe1a60..8baec6b 100644 --- a/src/container.test.ts +++ b/src/container.test.ts @@ -50,7 +50,7 @@ describe("Container API", () => { .bind({ provide: otherToken, async: true, - useFactory: () => new Promise((resolve) => resolve("foo")), + useFactory: () => Promise.resolve("foo"), }) .bind({ provide: token, diff --git a/src/container.ts b/src/container.ts index 937ccc5..9cb7d64 100644 --- a/src/container.ts +++ b/src/container.ts @@ -109,10 +109,19 @@ export class Container { superClass = Object.getPrototypeOf(superClass) } } else if (isInjectionToken(token) && token.options?.factory) { - this.bind({ - provide: token, - useFactory: token.options.factory, - }); + if (!token.options.async) { + this.bind({ + provide: token, + async: false, + useFactory: token.options.factory, + }); + } else if (token.options.async) { + this.bind({ + provide: token, + async: true, + useFactory: token.options.factory, + }); + } } } } diff --git a/src/examples.test.ts b/src/examples.test.ts index fa10734..d2a5a86 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -132,10 +132,7 @@ describe("Container", () => { .bind({ provide: tokenProvidedAsync, async: true, - useFactory: () => - new Promise((resolve) => { - resolve({ foo: "async" }); - }), + useFactory: () => Promise.resolve({ foo: "async" }), }); expect(factoryFn).not.toHaveBeenCalled(); diff --git a/src/providers.test.ts b/src/providers.test.ts index 7f22bfe..e93224d 100644 --- a/src/providers.test.ts +++ b/src/providers.test.ts @@ -1,12 +1,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { Container } from "./container.js"; import { InjectionToken } from "./tokens.js"; -import { injectable } from './decorators.js'; +import { injectable } from "./decorators.js"; const myServiceConstructorSpy = vi.fn(); class MyService { - constructor(public name = 'MyService') { + constructor(public name = "MyService") { myServiceConstructorSpy(); } } @@ -106,7 +106,7 @@ describe("Providers", () => { container.bind({ provide: MyService, async: true, - useFactory: () => new Promise(resolve => resolve(new MyService())) + useFactory: () => Promise.resolve(new MyService()), }); expect(myServiceConstructorSpy).not.toHaveBeenCalled(); @@ -126,7 +126,7 @@ describe("Providers", () => { expect(() => container.get(MyService)).toThrowError(); expect(container.get(MyService, { optional: true })).toBeUndefined(); - const OTHER_TOKEN = new InjectionToken('MyService'); + const OTHER_TOKEN = new InjectionToken("MyService"); container.bind(MyService); container.bind({ @@ -144,16 +144,51 @@ describe("Providers", () => { expect(myServiceConstructorSpy).toHaveBeenCalledTimes(1); }); - describe('abstract classes and inheritance', () => { + it("Token factories should be provided once", () => { + const container = new Container(); + + const TOKEN = new InjectionToken("MyService", { + factory: () => new MyService(), + }); + + expect(myServiceConstructorSpy).not.toHaveBeenCalled(); + + const myService = container.get(TOKEN); + + expect(myService).toBeInstanceOf(MyService); + expect(container.get(TOKEN)).toBe(myService); + expect(container.get(TOKEN, { optional: true })).toBe(myService); + expect(myServiceConstructorSpy).toHaveBeenCalledTimes(1); + }); + + it("Token async factories should be provided once", async () => { + const container = new Container(); + + const TOKEN = new InjectionToken("MyService", { + async: true, + factory: () => Promise.resolve(new MyService()), + }); + + expect(myServiceConstructorSpy).not.toHaveBeenCalled(); + + const myService = await container.getAsync(TOKEN); + + expect(myService).toBeInstanceOf(MyService); + expect(await container.getAsync(TOKEN)).toBe(myService); + expect(await container.getAsync(TOKEN, { optional: true })).toBe(myService); + expect(myServiceConstructorSpy).toHaveBeenCalledTimes(1); + }); + + describe("abstract classes and inheritance", () => { abstract class AbstractService { - protected constructor(public name = 'AbstractService') {} + protected constructor(public name = "AbstractService") {} } - it('should support annotated subclasses', () => { + it("should support annotated subclasses", () => { @injectable() class FooService extends AbstractService { - constructor(public fooProp = 'foo') { - super('FooService') + constructor(public fooProp = "foo") { + super("FooService"); } } @@ -165,34 +200,34 @@ describe("Providers", () => { expect(container.get(AbstractService)).toBeInstanceOf(FooService); }); - it('should support binding subclasses', () => { + it("should support binding subclasses", () => { class FooService extends AbstractService { - constructor(public fooProp = 'foo') { - super('FooService') + constructor(public fooProp = "foo") { + super("FooService"); } } class BarService extends AbstractService { - constructor(public fooProp = 'bar') { - super('BarService') + constructor(public fooProp = "bar") { + super("BarService"); } } const container = new Container(); container - .bind({ - provide: FooService, - useClass: FooService - }) - .bind({ - provide: BarService, - useClass: BarService - }) - .bind({ - provide: AbstractService, - useExisting: FooService - }) + .bind({ + provide: FooService, + useClass: FooService, + }) + .bind({ + provide: BarService, + useClass: BarService, + }) + .bind({ + provide: AbstractService, + useExisting: FooService, + }); expect(container.get(FooService)).toBeInstanceOf(FooService); expect(container.get(FooService)).toBeInstanceOf(AbstractService); @@ -201,7 +236,5 @@ describe("Providers", () => { expect(container.get(AbstractService)).toBeInstanceOf(FooService); }); - - }); -}); \ No newline at end of file +}); diff --git a/src/tokens.ts b/src/tokens.ts index c90e4eb..94180f2 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -7,10 +7,20 @@ export type Token = | symbol | InjectionToken; +interface InjectionTokenOptions { + async?: false, + factory: () => T +} + +interface AsyncInjectionTokenOptions { + async: true, + factory: () => Promise +} + export class InjectionToken { constructor( private description: string | symbol, - public options?: { factory: () => T }, + public options?: InjectionTokenOptions> | AsyncInjectionTokenOptions>, ) {} public toString(): string {