From a907bda36b55ecfeb0df67d92d4972c33f7b2d2a Mon Sep 17 00:00:00 2001 From: Andres Correa Casablanca Date: Sat, 5 Feb 2022 00:46:32 +0100 Subject: [PATCH 1/2] feat: split sync & async resolution --- lambda-ioc/deno/combinators.ts | 61 ++-- lambda-ioc/deno/container.ts | 285 +++++++++++++++---- lambda-ioc/src/__tests__/constructor.test.ts | 14 +- lambda-ioc/src/__tests__/container.test.ts | 35 ++- lambda-ioc/src/__tests__/func.test.ts | 14 +- lambda-ioc/src/__tests__/singleton.test.ts | 12 +- lambda-ioc/src/combinators.ts | 61 ++-- lambda-ioc/src/container.ts | 285 +++++++++++++++---- yarn.lock | 120 +++++--- 9 files changed, 636 insertions(+), 251 deletions(-) diff --git a/lambda-ioc/deno/combinators.ts b/lambda-ioc/deno/combinators.ts index b79f061..e8f8507 100644 --- a/lambda-ioc/deno/combinators.ts +++ b/lambda-ioc/deno/combinators.ts @@ -1,7 +1,7 @@ import { ContainerKey, - DependencyFactory, ReadableContainer, + SyncDependencyFactory, } from './container.ts'; import { ParamsToResolverKeys, TupleO, Zip } from './util.ts'; @@ -13,13 +13,15 @@ export function singleton< TVal, TDependencies extends Record, >( - factory: DependencyFactory>, -): DependencyFactory> { - let result: TVal | undefined + // eslint-disable-next-line @typescript-eslint/ban-types + factory: SyncDependencyFactory>, + // eslint-disable-next-line @typescript-eslint/ban-types +): SyncDependencyFactory> { + let result: Awaited | undefined - return async (container) => { + return (container) => { if (!result) { - result = await factory(container) + result = factory(container) } return result } @@ -37,17 +39,19 @@ export function func< >( fn: (...args: TParams) => Awaited, ...args: TDependencies -): DependencyFactory<() => TReturn, FuncContainer> { - return async (container: FuncContainer) => { - const argPromises = args.map((arg) => +): SyncDependencyFactory< + () => TReturn, + SyncFuncContainer +> { + return (container: SyncFuncContainer) => { + const resolvedArgs = args.map((arg) => container.resolve( // This is ugly as hell, but I did not want to apply ts-ignore - arg as Parameters['resolve']>[0], + arg as Parameters< + SyncFuncContainer['resolve'] + >[0], ), - ) - const resolvedArgs = (await Promise.all( - argPromises as Promise[], - )) as unknown as TParams + ) as unknown as TParams return () => fn(...resolvedArgs) } @@ -62,19 +66,18 @@ export function constructor< TClass, TDependencies extends ParamsToResolverKeys, >( - constructor: new (...args: TParams) => TClass, + constructor: new (...args: TParams) => Awaited, ...args: TDependencies -): DependencyFactory> { - return async (container: FuncContainer) => { - const argPromises = args.map((arg) => +): SyncDependencyFactory> { + return (container: SyncFuncContainer) => { + const resolvedArgs = args.map((arg) => container.resolve( // This is ugly as hell, but I did not want to apply ts-ignore - arg as Parameters['resolve']>[0], + arg as Parameters< + SyncFuncContainer['resolve'] + >[0], ), - ) - const resolvedArgs = (await Promise.all( - argPromises as Promise[], - )) as unknown as TParams + ) as unknown as TParams return new constructor(...resolvedArgs) } @@ -83,10 +86,14 @@ export function constructor< // ----------------------------------------------------------------------------- // Private Types // ----------------------------------------------------------------------------- -type FuncContainer< +type SyncFuncContainer< TParams extends readonly unknown[], - TDependencies extends ParamsToResolverKeys, + TSyncDependencies extends ParamsToResolverKeys, > = ReadableContainer< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TupleO, readonly [ContainerKey, any][]>> + TupleO< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Extract, readonly [ContainerKey, any][]> + >, + // eslint-disable-next-line @typescript-eslint/ban-types + {} > diff --git a/lambda-ioc/deno/container.ts b/lambda-ioc/deno/container.ts index ac2eca5..c570d76 100644 --- a/lambda-ioc/deno/container.ts +++ b/lambda-ioc/deno/container.ts @@ -6,26 +6,61 @@ export type ContainerKey = string | symbol * Represents a dependency factory: a function that, given an IoC container, it * is able to instantiate a specific dependency. */ -export type DependencyFactory< + +export interface SyncDependencyFactory< + T, + TContainer extends ReadableContainer, {}>, +> { + (container: TContainer): Awaited +} + +export interface AsyncDependencyFactory< + T, + TContainer extends ReadableContainer< + Record, + Record + >, +> { + // It might seem counterintuitive that we allow T as return type, but the + // factory might also "become async" because of its dependencies, not just + // because of its return type. + (container: TContainer): Promise +} + +export interface DependencyFactory< T, - TContainer extends ReadableContainer>, -> = (container: TContainer) => T | Promise + TContainer extends ReadableContainer< + Record, + Record + >, +> extends SyncDependencyFactory, + AsyncDependencyFactory {} /** * Represents a read-only version of a type-safe IoC container with "auto-wired" * dependencies resolution. */ export interface ReadableContainer< - TDependencies extends Record, + TSyncDependencies extends Record, + TAsyncDependencies extends Record, > { /** - * Resolve a dependency from the container. + * Resolve a "synchronous" dependency from the container. * * @param name The "name" of the dependency (can be a symbol). */ - resolve( + resolve( name: TName, - ): Promise + ): TSyncDependencies[TName] + + /** + * Resolve an "asynchronous" dependency from the container. + * + * @param name The "name" of the dependency (can be a symbol). + */ + resolveAsync( + name: TName, + ): Promise } /** @@ -33,27 +68,58 @@ export interface ReadableContainer< * "auto-wired" dependencies resolution. */ export interface WritableContainer< - TDependencies extends Record, + TSyncDependencies extends Record, + TAsyncDependencies extends Record, > { /** - * Register a new dependency factory. + * Register a new synchronous dependency factory. * * @param name The "name" of the dependency (can be a symbol). * @param dependency A dependency factory. */ register( name: TName, - dependency: DependencyFactory< + dependency: SyncDependencyFactory< TDependency, - ReadableContainer + ReadableContainer >, - ): Container<{ - [TK in keyof TDependencies | TName]: TK extends keyof TDependencies - ? TName extends TK - ? TDependency - : TDependencies[TK] - : TDependency - }> + ): Container< + { + [TK in + | keyof TSyncDependencies + | TName]: TK extends keyof TSyncDependencies + ? TName extends TK + ? TDependency + : TSyncDependencies[TK] + : TDependency + }, + TAsyncDependencies + > + + /** + * Register a new asynchronous dependency factory. + * + * @param name The "name" of the dependency (can be a symbol). + * @param dependency A dependency factory. + */ + registerAsync( + name: TName, + dependency: AsyncDependencyFactory< + TDependency, + ReadableContainer + >, + ): Container< + TSyncDependencies, + { + [TK in + | keyof TAsyncDependencies + | TName]: TK extends keyof TAsyncDependencies + ? TName extends TK + ? TDependency + : TAsyncDependencies[TK] + : TDependency + } + > /** * Register an already instantiated dependency. @@ -64,44 +130,70 @@ export interface WritableContainer< registerValue( name: TName, dependency: TDependency, - ): Container<{ - [TK in keyof TDependencies | TName]: TK extends keyof TDependencies - ? TName extends TK - ? TDependency - : TDependencies[TK] - : TDependency - }> + ): Container< + { + [TK in + | keyof TSyncDependencies + | TName]: TK extends keyof TSyncDependencies + ? TName extends TK + ? TDependency + : TSyncDependencies[TK] + : TDependency + }, + TAsyncDependencies + > } /** * Represents a type-safe IoC container with "auto-wired" dependencies * resolution */ -export interface Container> - extends ReadableContainer, - WritableContainer {} +export interface Container< + TSyncDependencies extends Record, + TAsyncDependencies extends Record, +> extends ReadableContainer, + WritableContainer {} /** * Creates a new type-safe IoC container. */ -export function createContainer(): Container<{}> { - return __createContainer({}) +export function createContainer(): Container<{}, {}> { + return __createContainer({}, {}) } // ----------------------------------------------------------------------------- // Private Types // ----------------------------------------------------------------------------- -type FactoriesToValues< +type SyncFactoriesToValues< + TDependencyFactories extends Record< + ContainerKey, + SyncDependencyFactory, {}>> + >, +> = {} extends TDependencyFactories + ? {} + : { + [name in keyof TDependencyFactories]: TDependencyFactories[name] extends SyncDependencyFactory< + infer T, + Container, {}> + > + ? T + : never + } + +type AsyncFactoriesToValues< TDependencyFactories extends Record< ContainerKey, - DependencyFactory>> + AsyncDependencyFactory< + unknown, + Container, Record> + > >, > = {} extends TDependencyFactories ? {} : { - [name in keyof TDependencyFactories]: TDependencyFactories[name] extends DependencyFactory< + [name in keyof TDependencyFactories]: TDependencyFactories[name] extends AsyncDependencyFactory< infer T, - Container> + Container, Record> > ? Awaited : never @@ -111,52 +203,121 @@ type FactoriesToValues< // Private Functions // ----------------------------------------------------------------------------- function __createContainer< - TDependencyFactories extends Record< + TSyncDependencyFactories extends Record< + ContainerKey, + SyncDependencyFactory< + unknown, + Container, Record> + > + >, + TAsyncDependencyFactories extends Record< ContainerKey, - DependencyFactory>> + AsyncDependencyFactory< + unknown, + Container, Record> + > >, >( - dependencies: TDependencyFactories, -): Container> { + syncDependencies: TSyncDependencyFactories, + asyncDependencies: TAsyncDependencyFactories, +): Container< + SyncFactoriesToValues, + AsyncFactoriesToValues +> { // These are "local" types, useful for the type inference - type TDependencies = FactoriesToValues - type NewContainerType = Container<{ - [TK in keyof TDependencies | TName]: TK extends keyof TDependencies - ? TName extends TK - ? TDependency - : TDependencies[TK] - : TDependency - }> + type TSyncDependencies = SyncFactoriesToValues + type TAsyncDependencies = AsyncFactoriesToValues + type ContainerWithNewSyncDep< + TName extends ContainerKey, + TDependency, + > = Container< + { + [TK in + | keyof TSyncDependencies + | TName]: TK extends keyof TSyncDependencies + ? TName extends TK + ? TDependency + : TSyncDependencies[TK] + : TDependency + }, + AsyncFactoriesToValues + > + type ContainerWithNewAsyncDep< + TName extends ContainerKey, + TDependency, + > = Container< + SyncFactoriesToValues, + { + [TK in + | keyof TAsyncDependencies + | TName]: TK extends keyof TAsyncDependencies + ? TName extends TK + ? TDependency + : TAsyncDependencies[TK] + : TDependency + } + > return { register( name: TName, - dependency: DependencyFactory>, - ): NewContainerType { - return __createContainer({ - ...dependencies, + dependency: SyncDependencyFactory< + TDependency, + Container + >, + ): ContainerWithNewSyncDep { + return __createContainer( + { + ...syncDependencies, + [name]: dependency, + }, + asyncDependencies, + ) as ContainerWithNewSyncDep + }, + + registerAsync( + name: TName, + dependency: AsyncDependencyFactory< + TDependency, + Container + >, + ): ContainerWithNewAsyncDep { + return __createContainer(syncDependencies, { + ...asyncDependencies, [name]: dependency, - }) as NewContainerType + }) as ContainerWithNewAsyncDep }, registerValue( name: TName, dependency: TDependency, - ): NewContainerType { - return __createContainer({ - ...dependencies, - [name]: () => dependency, - }) as NewContainerType + ): ContainerWithNewSyncDep { + return __createContainer( + { + ...syncDependencies, + [name]: () => dependency, + }, + asyncDependencies, + ) as ContainerWithNewSyncDep + }, + + resolve( + name: TName, + ): TSyncDependencies[TName] { + return ( + syncDependencies[ + name as keyof TSyncDependencyFactories + ] as SyncDependencyFactory + )(this) }, - async resolve( + async resolveAsync( name: TName, - ): Promise { + ): Promise { return await ( - dependencies[name as keyof TDependencyFactories] as DependencyFactory< - TDependencies[TName], - typeof this - > + asyncDependencies[ + name as keyof TAsyncDependencyFactories + ] as AsyncDependencyFactory )(this) }, } diff --git a/lambda-ioc/src/__tests__/constructor.test.ts b/lambda-ioc/src/__tests__/constructor.test.ts index eb2d8c4..5a137fd 100644 --- a/lambda-ioc/src/__tests__/constructor.test.ts +++ b/lambda-ioc/src/__tests__/constructor.test.ts @@ -1,14 +1,14 @@ import { constructor, createContainer, singleton } from '..' describe('constructor', () => { - it('can be registered without parameters', async () => { + it('can be registered without parameters', () => { class A {} const container = createContainer().register('A', constructor(A)) - const a = await container.resolve('A') + const a = container.resolve('A') expect(a).toBeInstanceOf(A) }) - it('can be registered with parameters', async () => { + it('can be registered with parameters', () => { class A {} class B { constructor(readonly a: A) {} @@ -24,15 +24,15 @@ describe('constructor', () => { // We abuse a bit this test to verify other tangential properties, like // uniqueness of instances. - const b1 = await container.resolve('B') - const b2 = await container.resolve('B') + const b1 = container.resolve('B') + const b2 = container.resolve('B') expect(b1).toBeInstanceOf(B) // That's the real test. expect(b2).toBeInstanceOf(B) expect(b1).not.toBe(b2) - const c1 = await container.resolve('C') - const c2 = await container.resolve('C') + const c1 = container.resolve('C') + const c2 = container.resolve('C') expect(c1).toBeInstanceOf(C) expect(c2).toBeInstanceOf(C) diff --git a/lambda-ioc/src/__tests__/container.test.ts b/lambda-ioc/src/__tests__/container.test.ts index 1093325..7d6ab6a 100644 --- a/lambda-ioc/src/__tests__/container.test.ts +++ b/lambda-ioc/src/__tests__/container.test.ts @@ -1,21 +1,44 @@ import { createContainer } from '..' describe('container', () => { - it('can register simple values', async () => { + it('can register simple values', () => { const container = createContainer() .registerValue('foo', 'bar') .registerValue('theanswer', 42) - expect(await container.resolve('foo')).toBe('bar') - expect(await container.resolve('theanswer')).toBe(42) + expect(container.resolve('foo')).toBe('bar') + expect(container.resolve('theanswer')).toBe(42) }) - it('can register simple factories', async () => { + it('can register simple factories', () => { const container = createContainer() .register('foo', () => 'bar') .register('theanswer', () => 42) - expect(await container.resolve('foo')).toBe('bar') - expect(await container.resolve('theanswer')).toBe(42) + expect(container.resolve('foo')).toBe('bar') + expect(container.resolve('theanswer')).toBe(42) + }) + + it('can register async factories', async () => { + const container = createContainer() + .registerAsync('foo', async () => Promise.resolve('bar')) + .registerAsync('theanswer', async () => Promise.resolve(42)) + + expect(await container.resolveAsync('foo')).toBe('bar') + expect(await container.resolveAsync('theanswer')).toBe(42) + }) + + it('can register async factories depending on sync & async dependencies', async () => { + const container = createContainer() + .register('a', () => 3) + .registerAsync('b', async () => await Promise.resolve(5)) + .registerAsync( + 'ab', + // In real life we would use a helper/combinator to avoid writing this + // kind of ugly code + async (c) => c.resolve('a') * (await c.resolveAsync('b')), + ) + + expect(await container.resolveAsync('ab')).toBe(15) }) }) diff --git a/lambda-ioc/src/__tests__/func.test.ts b/lambda-ioc/src/__tests__/func.test.ts index 67f4514..c86dc5f 100644 --- a/lambda-ioc/src/__tests__/func.test.ts +++ b/lambda-ioc/src/__tests__/func.test.ts @@ -1,15 +1,15 @@ import { createContainer, func } from '..' describe('func', () => { - it('can be registered without parameters', async () => { + it('can be registered without parameters', () => { const container = createContainer().register( 'foo', func(() => 'result'), ) - expect((await container.resolve('foo'))()).toBe('result') + expect(container.resolve('foo')()).toBe('result') }) - it('can be registered with parameters', async () => { + it('can be registered with parameters', () => { const container = createContainer() .registerValue('a', 3) .registerValue('b', 5) @@ -18,17 +18,17 @@ describe('func', () => { func((a: number, b: number) => a * b, 'a', 'b'), ) - expect((await container.resolve('f'))()).toBe(15) + expect(container.resolve('f')()).toBe(15) }) - it('resolves a new function each time', async () => { + it('resolves a new function each time', () => { const container = createContainer().register( 'foo', func(() => 'result'), ) - const f1 = await container.resolve('foo') - const f2 = await container.resolve('foo') + const f1 = container.resolve('foo') + const f2 = container.resolve('foo') expect(f1()).toBe('result') expect(f2()).toBe('result') diff --git a/lambda-ioc/src/__tests__/singleton.test.ts b/lambda-ioc/src/__tests__/singleton.test.ts index 61d1801..449a703 100644 --- a/lambda-ioc/src/__tests__/singleton.test.ts +++ b/lambda-ioc/src/__tests__/singleton.test.ts @@ -1,28 +1,28 @@ import { createContainer, func, singleton } from '..' describe('singleton', () => { - it('resolves the same instance each time (no dependencies)', async () => { + it('resolves the same instance each time (no dependencies)', () => { const container = createContainer().register( 'foo', singleton(func(() => 'result')), ) - const f1 = await container.resolve('foo') - const f2 = await container.resolve('foo') + const f1 = container.resolve('foo') + const f2 = container.resolve('foo') expect(f1()).toBe('result') expect(f2()).toBe('result') expect(f1).toBe(f2) }) - it('resolves the same instance each time (multiple dependencies)', async () => { + it('resolves the same instance each time (multiple dependencies)', () => { const container = createContainer() .registerValue('a', 3) .registerValue('b', 5) .register('f', singleton(func((a: number, b: number) => a * b, 'a', 'b'))) - const f1 = await container.resolve('f') - const f2 = await container.resolve('f') + const f1 = container.resolve('f') + const f2 = container.resolve('f') expect(f1()).toBe(15) expect(f2()).toBe(15) diff --git a/lambda-ioc/src/combinators.ts b/lambda-ioc/src/combinators.ts index d4e77b7..94b7653 100644 --- a/lambda-ioc/src/combinators.ts +++ b/lambda-ioc/src/combinators.ts @@ -1,7 +1,7 @@ import { ContainerKey, - DependencyFactory, ReadableContainer, + SyncDependencyFactory, } from './container' import { ParamsToResolverKeys, TupleO, Zip } from './util' @@ -13,13 +13,15 @@ export function singleton< TVal, TDependencies extends Record, >( - factory: DependencyFactory>, -): DependencyFactory> { - let result: TVal | undefined + // eslint-disable-next-line @typescript-eslint/ban-types + factory: SyncDependencyFactory>, + // eslint-disable-next-line @typescript-eslint/ban-types +): SyncDependencyFactory> { + let result: Awaited | undefined - return async (container) => { + return (container) => { if (!result) { - result = await factory(container) + result = factory(container) } return result } @@ -37,17 +39,19 @@ export function func< >( fn: (...args: TParams) => Awaited, ...args: TDependencies -): DependencyFactory<() => TReturn, FuncContainer> { - return async (container: FuncContainer) => { - const argPromises = args.map((arg) => +): SyncDependencyFactory< + () => TReturn, + SyncFuncContainer +> { + return (container: SyncFuncContainer) => { + const resolvedArgs = args.map((arg) => container.resolve( // This is ugly as hell, but I did not want to apply ts-ignore - arg as Parameters['resolve']>[0], + arg as Parameters< + SyncFuncContainer['resolve'] + >[0], ), - ) - const resolvedArgs = (await Promise.all( - argPromises as Promise[], - )) as unknown as TParams + ) as unknown as TParams return () => fn(...resolvedArgs) } @@ -62,19 +66,18 @@ export function constructor< TClass, TDependencies extends ParamsToResolverKeys, >( - constructor: new (...args: TParams) => TClass, + constructor: new (...args: TParams) => Awaited, ...args: TDependencies -): DependencyFactory> { - return async (container: FuncContainer) => { - const argPromises = args.map((arg) => +): SyncDependencyFactory> { + return (container: SyncFuncContainer) => { + const resolvedArgs = args.map((arg) => container.resolve( // This is ugly as hell, but I did not want to apply ts-ignore - arg as Parameters['resolve']>[0], + arg as Parameters< + SyncFuncContainer['resolve'] + >[0], ), - ) - const resolvedArgs = (await Promise.all( - argPromises as Promise[], - )) as unknown as TParams + ) as unknown as TParams return new constructor(...resolvedArgs) } @@ -83,10 +86,14 @@ export function constructor< // ----------------------------------------------------------------------------- // Private Types // ----------------------------------------------------------------------------- -type FuncContainer< +type SyncFuncContainer< TParams extends readonly unknown[], - TDependencies extends ParamsToResolverKeys, + TSyncDependencies extends ParamsToResolverKeys, > = ReadableContainer< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TupleO, readonly [ContainerKey, any][]>> + TupleO< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Extract, readonly [ContainerKey, any][]> + >, + // eslint-disable-next-line @typescript-eslint/ban-types + {} > diff --git a/lambda-ioc/src/container.ts b/lambda-ioc/src/container.ts index ac2eca5..c570d76 100644 --- a/lambda-ioc/src/container.ts +++ b/lambda-ioc/src/container.ts @@ -6,26 +6,61 @@ export type ContainerKey = string | symbol * Represents a dependency factory: a function that, given an IoC container, it * is able to instantiate a specific dependency. */ -export type DependencyFactory< + +export interface SyncDependencyFactory< + T, + TContainer extends ReadableContainer, {}>, +> { + (container: TContainer): Awaited +} + +export interface AsyncDependencyFactory< + T, + TContainer extends ReadableContainer< + Record, + Record + >, +> { + // It might seem counterintuitive that we allow T as return type, but the + // factory might also "become async" because of its dependencies, not just + // because of its return type. + (container: TContainer): Promise +} + +export interface DependencyFactory< T, - TContainer extends ReadableContainer>, -> = (container: TContainer) => T | Promise + TContainer extends ReadableContainer< + Record, + Record + >, +> extends SyncDependencyFactory, + AsyncDependencyFactory {} /** * Represents a read-only version of a type-safe IoC container with "auto-wired" * dependencies resolution. */ export interface ReadableContainer< - TDependencies extends Record, + TSyncDependencies extends Record, + TAsyncDependencies extends Record, > { /** - * Resolve a dependency from the container. + * Resolve a "synchronous" dependency from the container. * * @param name The "name" of the dependency (can be a symbol). */ - resolve( + resolve( name: TName, - ): Promise + ): TSyncDependencies[TName] + + /** + * Resolve an "asynchronous" dependency from the container. + * + * @param name The "name" of the dependency (can be a symbol). + */ + resolveAsync( + name: TName, + ): Promise } /** @@ -33,27 +68,58 @@ export interface ReadableContainer< * "auto-wired" dependencies resolution. */ export interface WritableContainer< - TDependencies extends Record, + TSyncDependencies extends Record, + TAsyncDependencies extends Record, > { /** - * Register a new dependency factory. + * Register a new synchronous dependency factory. * * @param name The "name" of the dependency (can be a symbol). * @param dependency A dependency factory. */ register( name: TName, - dependency: DependencyFactory< + dependency: SyncDependencyFactory< TDependency, - ReadableContainer + ReadableContainer >, - ): Container<{ - [TK in keyof TDependencies | TName]: TK extends keyof TDependencies - ? TName extends TK - ? TDependency - : TDependencies[TK] - : TDependency - }> + ): Container< + { + [TK in + | keyof TSyncDependencies + | TName]: TK extends keyof TSyncDependencies + ? TName extends TK + ? TDependency + : TSyncDependencies[TK] + : TDependency + }, + TAsyncDependencies + > + + /** + * Register a new asynchronous dependency factory. + * + * @param name The "name" of the dependency (can be a symbol). + * @param dependency A dependency factory. + */ + registerAsync( + name: TName, + dependency: AsyncDependencyFactory< + TDependency, + ReadableContainer + >, + ): Container< + TSyncDependencies, + { + [TK in + | keyof TAsyncDependencies + | TName]: TK extends keyof TAsyncDependencies + ? TName extends TK + ? TDependency + : TAsyncDependencies[TK] + : TDependency + } + > /** * Register an already instantiated dependency. @@ -64,44 +130,70 @@ export interface WritableContainer< registerValue( name: TName, dependency: TDependency, - ): Container<{ - [TK in keyof TDependencies | TName]: TK extends keyof TDependencies - ? TName extends TK - ? TDependency - : TDependencies[TK] - : TDependency - }> + ): Container< + { + [TK in + | keyof TSyncDependencies + | TName]: TK extends keyof TSyncDependencies + ? TName extends TK + ? TDependency + : TSyncDependencies[TK] + : TDependency + }, + TAsyncDependencies + > } /** * Represents a type-safe IoC container with "auto-wired" dependencies * resolution */ -export interface Container> - extends ReadableContainer, - WritableContainer {} +export interface Container< + TSyncDependencies extends Record, + TAsyncDependencies extends Record, +> extends ReadableContainer, + WritableContainer {} /** * Creates a new type-safe IoC container. */ -export function createContainer(): Container<{}> { - return __createContainer({}) +export function createContainer(): Container<{}, {}> { + return __createContainer({}, {}) } // ----------------------------------------------------------------------------- // Private Types // ----------------------------------------------------------------------------- -type FactoriesToValues< +type SyncFactoriesToValues< + TDependencyFactories extends Record< + ContainerKey, + SyncDependencyFactory, {}>> + >, +> = {} extends TDependencyFactories + ? {} + : { + [name in keyof TDependencyFactories]: TDependencyFactories[name] extends SyncDependencyFactory< + infer T, + Container, {}> + > + ? T + : never + } + +type AsyncFactoriesToValues< TDependencyFactories extends Record< ContainerKey, - DependencyFactory>> + AsyncDependencyFactory< + unknown, + Container, Record> + > >, > = {} extends TDependencyFactories ? {} : { - [name in keyof TDependencyFactories]: TDependencyFactories[name] extends DependencyFactory< + [name in keyof TDependencyFactories]: TDependencyFactories[name] extends AsyncDependencyFactory< infer T, - Container> + Container, Record> > ? Awaited : never @@ -111,52 +203,121 @@ type FactoriesToValues< // Private Functions // ----------------------------------------------------------------------------- function __createContainer< - TDependencyFactories extends Record< + TSyncDependencyFactories extends Record< + ContainerKey, + SyncDependencyFactory< + unknown, + Container, Record> + > + >, + TAsyncDependencyFactories extends Record< ContainerKey, - DependencyFactory>> + AsyncDependencyFactory< + unknown, + Container, Record> + > >, >( - dependencies: TDependencyFactories, -): Container> { + syncDependencies: TSyncDependencyFactories, + asyncDependencies: TAsyncDependencyFactories, +): Container< + SyncFactoriesToValues, + AsyncFactoriesToValues +> { // These are "local" types, useful for the type inference - type TDependencies = FactoriesToValues - type NewContainerType = Container<{ - [TK in keyof TDependencies | TName]: TK extends keyof TDependencies - ? TName extends TK - ? TDependency - : TDependencies[TK] - : TDependency - }> + type TSyncDependencies = SyncFactoriesToValues + type TAsyncDependencies = AsyncFactoriesToValues + type ContainerWithNewSyncDep< + TName extends ContainerKey, + TDependency, + > = Container< + { + [TK in + | keyof TSyncDependencies + | TName]: TK extends keyof TSyncDependencies + ? TName extends TK + ? TDependency + : TSyncDependencies[TK] + : TDependency + }, + AsyncFactoriesToValues + > + type ContainerWithNewAsyncDep< + TName extends ContainerKey, + TDependency, + > = Container< + SyncFactoriesToValues, + { + [TK in + | keyof TAsyncDependencies + | TName]: TK extends keyof TAsyncDependencies + ? TName extends TK + ? TDependency + : TAsyncDependencies[TK] + : TDependency + } + > return { register( name: TName, - dependency: DependencyFactory>, - ): NewContainerType { - return __createContainer({ - ...dependencies, + dependency: SyncDependencyFactory< + TDependency, + Container + >, + ): ContainerWithNewSyncDep { + return __createContainer( + { + ...syncDependencies, + [name]: dependency, + }, + asyncDependencies, + ) as ContainerWithNewSyncDep + }, + + registerAsync( + name: TName, + dependency: AsyncDependencyFactory< + TDependency, + Container + >, + ): ContainerWithNewAsyncDep { + return __createContainer(syncDependencies, { + ...asyncDependencies, [name]: dependency, - }) as NewContainerType + }) as ContainerWithNewAsyncDep }, registerValue( name: TName, dependency: TDependency, - ): NewContainerType { - return __createContainer({ - ...dependencies, - [name]: () => dependency, - }) as NewContainerType + ): ContainerWithNewSyncDep { + return __createContainer( + { + ...syncDependencies, + [name]: () => dependency, + }, + asyncDependencies, + ) as ContainerWithNewSyncDep + }, + + resolve( + name: TName, + ): TSyncDependencies[TName] { + return ( + syncDependencies[ + name as keyof TSyncDependencyFactories + ] as SyncDependencyFactory + )(this) }, - async resolve( + async resolveAsync( name: TName, - ): Promise { + ): Promise { return await ( - dependencies[name as keyof TDependencyFactories] as DependencyFactory< - TDependencies[TName], - typeof this - > + asyncDependencies[ + name as keyof TAsyncDependencyFactories + ] as AsyncDependencyFactory )(this) }, } diff --git a/yarn.lock b/yarn.lock index 5c6ebc4..980456d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@ampproject/remapping@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.0.2.tgz#f3d9760bf30588c51408dbe7c05ff2bb13069307" + integrity sha512-sE8Gx+qSDMLoJvb3QarJJlDQK7SSY4rK3hxp4XsiANeFOmjU46ZI7Y9adAQRJrmbz8zbtZkp3mJTT+rGxtF0XA== + dependencies: + "@jridgewell/trace-mapping" "^0.2.2" + sourcemap-codec "1.4.8" + "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" @@ -10,37 +18,37 @@ "@babel/highlight" "^7.16.7" "@babel/compat-data@^7.16.4": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.8.tgz#31560f9f29fdf1868de8cb55049538a1b9732a60" - integrity sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q== + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34" + integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng== "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": - version "7.16.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.12.tgz#5edc53c1b71e54881315923ae2aedea2522bb784" - integrity sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg== + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.0.tgz#16b8772b0a567f215839f689c5ded6bb20e864d5" + integrity sha512-x/5Ea+RO5MvF9ize5DeVICJoVrNv0Mi2RnIABrZEKYvPEpldXwauPkgvYA17cKa6WpU3LoYvYbuEMFtSNFsarA== dependencies: + "@ampproject/remapping" "^2.0.0" "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.16.8" + "@babel/generator" "^7.17.0" "@babel/helper-compilation-targets" "^7.16.7" "@babel/helper-module-transforms" "^7.16.7" - "@babel/helpers" "^7.16.7" - "@babel/parser" "^7.16.12" + "@babel/helpers" "^7.17.0" + "@babel/parser" "^7.17.0" "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.10" - "@babel/types" "^7.16.8" + "@babel/traverse" "^7.17.0" + "@babel/types" "^7.17.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.1.2" semver "^6.3.0" - source-map "^0.5.0" -"@babel/generator@^7.16.8", "@babel/generator@^7.7.2": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.8.tgz#359d44d966b8cd059d543250ce79596f792f2ebe" - integrity sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw== +"@babel/generator@^7.17.0", "@babel/generator@^7.7.2": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.0.tgz#7bd890ba706cd86d3e2f727322346ffdbf98f65e" + integrity sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw== dependencies: - "@babel/types" "^7.16.8" + "@babel/types" "^7.17.0" jsesc "^2.5.1" source-map "^0.5.0" @@ -134,14 +142,14 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== -"@babel/helpers@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.7.tgz#7e3504d708d50344112767c3542fc5e357fffefc" - integrity sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw== +"@babel/helpers@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.0.tgz#79cdf6c66a579f3a7b5e739371bc63ca0306886b" + integrity sha512-Xe/9NFxjPwELUvW2dsukcMZIp6XwPSbI4ojFBJuX5ramHuVE22SVcZIwqzdWo5uCgeTXW8qV97lMvSOjq+1+nQ== dependencies: "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.7" - "@babel/types" "^7.16.7" + "@babel/traverse" "^7.17.0" + "@babel/types" "^7.17.0" "@babel/highlight@^7.16.7": version "7.16.10" @@ -152,10 +160,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.10", "@babel/parser@^7.16.12", "@babel/parser@^7.16.7": - version "7.16.12" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.12.tgz#9474794f9a650cf5e2f892444227f98e28cdf8b6" - integrity sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c" + integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -257,26 +265,26 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/traverse@^7.16.10", "@babel/traverse@^7.16.7", "@babel/traverse@^7.7.2": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.10.tgz#448f940defbe95b5a8029975b051f75993e8239f" - integrity sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw== +"@babel/traverse@^7.16.7", "@babel/traverse@^7.17.0", "@babel/traverse@^7.7.2": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.0.tgz#3143e5066796408ccc880a33ecd3184f3e75cd30" + integrity sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg== dependencies: "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.16.8" + "@babel/generator" "^7.17.0" "@babel/helper-environment-visitor" "^7.16.7" "@babel/helper-function-name" "^7.16.7" "@babel/helper-hoist-variables" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.16.10" - "@babel/types" "^7.16.8" + "@babel/parser" "^7.17.0" + "@babel/types" "^7.17.0" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" - integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== +"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" + integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== dependencies: "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" @@ -512,6 +520,19 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz#b876e3feefb9c8d3aa84014da28b5e52a0640d72" + integrity sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg== + +"@jridgewell/trace-mapping@^0.2.2": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.2.6.tgz#5eac4bea1b56e073471c6f021582bdb986c4b8b7" + integrity sha512-rVJf5dSMEBxnDEwtAT5x8+p6tZ+xU6Ocm+cR1MYL2gMsRi4MMzVf9Pvq6JaxIsEeKAyYmo2U+yPQN4QfdTfFnA== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + sourcemap-codec "1.4.8" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1009,9 +1030,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001286: - version "1.0.30001305" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001305.tgz#02cd8031df07c4fcb117aa2ecc4899122681bd4c" - integrity sha512-p7d9YQMji8haf0f+5rbcv9WlQ+N5jMPfRAnUmZRlNxsNeBO3Yr7RYG6M2uTY1h9tCVdlkJg6YNNc4kiAiBLdWA== + version "1.0.30001307" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001307.tgz#27a67f13ebc4aa9c977e6b8256a11d5eafb30f27" + integrity sha512-+MXEMczJ4FuxJAUp0jvAl6Df0NI/OfW1RWEE61eSmzS7hw6lz4IKutbhbXendwq8BljfFuHtu26VWsg4afQ7Ng== chalk@^2.0.0: version "2.4.2" @@ -1216,9 +1237,9 @@ domexception@^2.0.1: webidl-conversions "^5.0.0" electron-to-chromium@^1.4.17: - version "1.4.62" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.62.tgz#f97b643d206813b9154f1700b3c5b5b828ddc9ac" - integrity sha512-fWc/zAThqZzl7fbuLzar+x6bqZBWHrsBXQOqv//yrgdTLY/G3JGTPOWhPKIhbhynJJhqE9QNzKzlpCINUmUMoA== + version "1.4.64" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.64.tgz#8b1b5372f77ca208f2c498c6490da0e51176bd81" + integrity sha512-8mec/99xgLUZCIZZq3wt61Tpxg55jnOSpxGYapE/1Ma9MpFEYYaz4QNYm0CM1rrnCo7i3FRHhbaWjeCLsveGjQ== emittery@^0.8.1: version "0.8.1" @@ -2737,9 +2758,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.6" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" - integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== sisteransi@^1.0.5: version "1.0.5" @@ -2774,6 +2795,11 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +sourcemap-codec@1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" From de6c673bedff8fd5c9bf6f0992ac2ecc7d54b601 Mon Sep 17 00:00:00 2001 From: Andres Correa Casablanca Date: Sat, 5 Feb 2022 01:01:52 +0100 Subject: [PATCH 2/2] docs: extend examples --- README.md | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bdc0a78..449b27d 100644 --- a/README.md +++ b/README.md @@ -58,19 +58,51 @@ import { ... } from 'https://deno.land/x/lambda_ioc@[VERSION]/lambda-ioc/deno/in ## Example ```ts -import { createContainer } from '@coderspirit/lambda-ioc' +import { + constructor, + createContainer, + func +} from '@coderspirit/lambda-ioc' function printNameAndAge(name: string, age: number) { console.log(`${name} is aged ${age}`) } + +class Person { + constructor( + public readonly age: number, + public readonly name: string + ) {} +} ​ const container = createContainer() .registerValue('someAge', 5) .registerValue('someName', 'Timmy') + // We can register functions .register('fn', func(printNameAndAge, 'someName', 'someAge')) + // And constructors too + .register('Person', constructor(Person, 'someAge', 'someName')) ​ -// For now it's always async, we'll improve its API to decide when to expose -// the registered dependencies synchronously or asynchronoyusly in a smart way. -const print = await container.resolve('fn') +const print = container.resolve('fn') print() // Prints "Timmy is aged 5" + +const person = container.resolve('Person') +console.print(person.age) // Prints "5" +console.print(person.name) // Prints "Timmy" ``` + +It is also possible to register and resolve asynchronous factories and +dependencies. They are not documented yet because some "helpers" are missing, +and therefore it's a bit more annoying to take advantage of that feature. + +If you are curious, just try out: +- `registerAsync` +- `resolveAsync` + +## Differences respect to Diddly + +- First-class support for Deno. +- First-class support for asynchronous dependency resolution. +- The container interface has been split into `ReaderContainer` and + `WriterContainer`, making it easier to use precise types. +- More extense documentation.